Skip to content

Authentication & Authorization

Konfigo supports four authentication providers: OpenID Connect, JWT Bearer, SAML 2.0, and Local (username/password). The provider is selected by a single Authentication.Provider key — everything else is read from the corresponding configuration subsection.


How provider selection works

On startup the backend reads Authentication.Provider and registers one ASP.NET Core scheme:

Value Scheme Session storage
OpenId OpenID Connect (cookie) Cookie
Jwt JWT Bearer Stateless (token in Authorization header)
Saml SAML 2.0 (cookie) Cookie
Local Username/password (cookie) Cookie

If the key is not set, Saml is used (the hardcoded default in code).


Common settings

These fields live directly under the Authentication section and apply to every provider:

{
  "Authentication": {
    "Provider": "OpenId",
    "EmailClaimType": "email",
    "RoleClaimType": "groups",
    "IdClaimType": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"
  }
}
Field Default Description
Provider Saml Active provider: OpenId, Jwt, or Saml
EmailClaimType http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress Claim used to read the user's e-mail address
RoleClaimType http://schemas.microsoft.com/ws/2008/06/identity/claims/role Claim used to read roles. With Dex or SAML this is typically groups
IdClaimType http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier Claim used as the unique user identifier

OpenID Connect

Best choice for modern providers (Keycloak, Auth0, Dex, Azure AD, Google, Okta, etc.). Uses Authorization Code Flow with a server-side cookie session.

Configuration

{
  "Authentication": {
    "Provider": "OpenId",
    "RoleClaimType": "groups",
    "OpenId": {
      "Authority": "https://your-idp.example.com",
      "ClientId": "konfigo",
      "ClientSecret": "your-client-secret",
      "RequireHttpsMetadata": true,
      "ResponseType": "code",
      "Scope": [
        "openid",
        "profile",
        "email",
        "groups"
      ]
    }
  }
}
Field Required Description
Authority Yes OIDC provider URL. The backend fetches {Authority}/.well-known/openid-configuration for autodiscovery
ClientId Yes Client ID registered with the provider
ClientSecret Yes Client secret
RequireHttpsMetadata Set to false to allow HTTP for autodiscovery (local development only)
ResponseType code — Authorization Code Flow (recommended)
Scope Requested scopes. Add any provider-specific scope that carries roles (e.g. groups)

Local development with Dex

docker-compose.yml includes Dex — a lightweight OIDC provider. Its configuration lives in apps/backend/docker/dex/config.yaml.

Start the infrastructure:

cd apps/backend
docker compose up -d

Configure appsettings.Local.json:

{
  "Authentication": {
    "Provider": "OpenId",
    "RoleClaimType": "groups",
    "OpenId": {
      "Authority": "http://localhost:3000/dex",
      "ClientId": "konfigo",
      "ClientSecret": "konfigo-local-secret",
      "RequireHttpsMetadata": false,
      "ResponseType": "code",
      "Scope": ["openid", "profile", "email", "groups"]
    }
  }
}

Dex issues a groups claim with values from docker/dex/config.yaml:

staticPasswords:
  - email: admin@konfigo.local
    username: admin
    groups:
      - admin
      - developer
  - email: developer@konfigo.local
    username: developer
    groups:
      - developer

This is why RoleClaimType must be set to groups — that is the claim Dex puts roles into.


JWT Bearer

Used when the frontend (or another service) obtains a JWT token from the provider independently and passes it in the Authorization: Bearer <token> header. No server-side session is created — every request is validated against the token.

Suitable for:

  • Machine-to-machine (service-to-service) access
  • Environments with an external auth proxy (e.g. Nginx auth_request)
  • SPAs using OAuth2 PKCE where the token is stored on the client

Configuration

{
  "Authentication": {
    "Provider": "Jwt",
    "RoleClaimType": "groups",
    "Jwt": {
      "Authority": "https://your-idp.example.com",
      "Audience": "konfigo",
      "RequireHttpsMetadata": true,
      "TokenValidationParameters": {
        "ValidateAudience": true,
        "ValidateIssuer": true
      }
    }
  }
}
Field Required Description
Authority Yes Provider URL used to load JWKS signing keys via {Authority}/.well-known/openid-configuration
Audience Yes Expected aud value in the token — must match what the provider issues
RequireHttpsMetadata Set to false to allow HTTP (development only)
TokenValidationParameters Extra validation options from Microsoft.IdentityModel.Tokens.TokenValidationParameters

You can also supply fields that the frontend uses for its OAuth2 login flow — they are exposed through GET /auth/config:

{
  "Authentication": {
    "Jwt": {
      "Authority": "https://your-idp.example.com",
      "ClientId": "konfigo",
      "AuthorizeUrl": "https://your-idp.example.com/oauth2/authorize",
      "TokenUrl": "https://your-idp.example.com/oauth2/token",
      "Scopes": "openid profile email",
      "Audience": "konfigo"
    }
  }
}
Field Description
ClientId Client ID — forwarded to the frontend for the OAuth2 flow
AuthorizeUrl Authorization endpoint URL. Needed when the provider does not support autodiscovery or uses a non-standard path
TokenUrl Token exchange endpoint URL
Scopes Space-separated scope string

Local development with mock-oauth2

docker-compose.yml includes mock-oauth2-server — an OAuth2 server with an interactive login form. Configuration: apps/backend/docker/mock-oauth2/config.json.

{
  "Authentication": {
    "Provider": "Jwt",
    "RoleClaimType": "groups",
    "Jwt": {
      "Authority": "http://localhost:8082/default",
      "ClientId": "konfigo",
      "AuthorizeUrl": "http://localhost:3000/mock-oauth2/default/authorize",
      "TokenUrl": "http://localhost:3000/mock-oauth2/default/token",
      "Scopes": "openid profile email",
      "Audience": "konfigo",
      "RequireHttpsMetadata": false,
      "TokenValidationParameters": {
        "ValidateAudience": false,
        "ValidateIssuer": false
      }
    }
  }
}

Users are defined in docker/mock-oauth2/config.json via requestMappings — the server matches the login username and injects a pre-configured set of claims, including groups, into the issued token.


SAML 2.0

The default provider. Implemented via Sustainsys.Saml2. Uses a cookie session. The backend acts as Service Provider (SP); the Identity Provider (IdP) is configured through a metadata URL.

Configuration

{
  "Authentication": {
    "Provider": "Saml",
    "EmailClaimType": "mail",
    "RoleClaimType": "groups",
    "Saml": {
      "SpOptionsEntityId": "https://your-app.example.com/saml2/Metadata",
      "SpOptionsModulePath": "/saml2",
      "IdentityProviderEntityId": "https://your-idp.example.com/saml2/idp/metadata",
      "IdentityProviderMetadataUrl": "https://your-idp.example.com/saml2/idp/metadata"
    }
  }
}
Field Required Description
SpOptionsEntityId Yes EntityID of your SP — a unique URI that identifies the application to the IdP. Typically the URL of the SP metadata endpoint
SpOptionsModulePath Yes Path prefix for SAML SP endpoints (ACS, SLO, Metadata). Defaults to /saml2
IdentityProviderEntityId Yes EntityID of the IdP as it appears in its metadata. Must match exactly — responses with a different entity ID are rejected
IdentityProviderMetadataUrl Yes URL from which the backend downloads IdP XML metadata on startup (LoadMetadata: true)

SAML endpoint routing. SpOptionsModulePath sets the prefix for paths that Sustainsys registers automatically: {ModulePath}/Acs, {ModulePath}/Metadata, {ModulePath}/Logout. When running behind nginx, make sure these paths are proxied to the backend and not to the IdP.

Local development with SimpleSAMLphp

docker-compose.yml includes a SimpleSAMLphp IdP. User accounts are defined in apps/backend/docker/saml-config/authsources.php.

{
  "Authentication": {
    "Provider": "Saml",
    "EmailClaimType": "mail",
    "RoleClaimType": "groups",
    "Saml": {
      "SpOptionsEntityId": "http://localhost:3000/saml2/Metadata",
      "SpOptionsModulePath": "/saml2",
      "IdentityProviderEntityId": "http://localhost:3000/saml2/simplesaml/saml2/idp/metadata.php",
      "IdentityProviderMetadataUrl": "http://localhost:3000/saml2/simplesaml/saml2/idp/metadata.php"
    }
  }
}

SimpleSAMLphp sends the mail attribute as the e-mail and groups as roles — hence EmailClaimType: "mail".

Nginx routes traffic as follows (from docker/nginx/default.conf):

location /saml2/simplesaml/ {
    proxy_pass http://saml-idp:8080/simplesaml/;   # IdP traffic
}

location /saml2/ {
    proxy_pass http://host.docker.internal:8080;    # SP endpoints to the backend
}

Local (username/password)

Self-hosted username/password authentication with no external identity provider. Users are stored in the public.local_users table and authenticated via a server-side cookie session, backed by PBKDF2 (SHA-256, 310k iterations) password hashing.

Configuration

{
  "Authentication": {
    "Provider": "Local",
    "Local": {
      "DefaultAdminUsername": "admin",
      "DefaultAdminPassword": "change-me"
    }
  }
}
Field Default Description
DefaultAdminUsername admin Username of the admin account seeded on first startup
DefaultAdminPassword admin Password for the seeded admin account. Only applied the first time the account is created — later password changes made through the UI are not overwritten by this setting on subsequent restarts

On every startup, if Provider is Local and no user with DefaultAdminUsername exists yet, the backend creates it with the admin role. Change DefaultAdminPassword before first deploying to production — leaving the default (admin) creates a predictable admin credential.

Managing users

Once signed in as an admin, open Users in the top navigation (only shown for admins when the Local provider is active) to create, edit, or delete accounts. Each user has:

  • Username and password (used to sign in)
  • Roleadmin (maps to the canAll policy) or developer (maps to canChange)

Admins cannot delete their own account through the UI, to avoid locking everyone out.

The management API lives at api/users (GET/POST/PUT {username}/DELETE {username}), gated by the canAll policy and only enabled while Provider is Local.


Authorization

Regardless of the provider, authorization works by mapping roles from identity claims to Konfigo policies.

{
  "Authorization": {
    "Policies": {
      "canAll": ["admin", "konfigo-admin"],
      "canChange": ["developer", "konfigo-dev"]
    }
  }
}
Policy Access
canAll Full administrative access: managing services, keys, and settings
canChange Editing configuration values. Automatically includes all roles from canAll

Roles are looked up across the user's claims. The backend checks several well-known claim types — role, roles, groups, name — as well as the type specified in RoleClaimType.

To inspect which claims and roles were received from the provider, call GET /auth/me after login. It returns the current user's id, email, name, full role set, and active policy list.


Switching providers in local development

All three providers start simultaneously with docker compose up -d. To switch, change Authentication.Provider in appsettings.Local.json and restart the backend:

{
  "Authentication": {
    "Provider": "OpenId",
    "RoleClaimType": "groups",
    "OpenId": {
      "Authority": "http://localhost:3000/dex",
      "ClientId": "konfigo",
      "ClientSecret": "konfigo-local-secret",
      "RequireHttpsMetadata": false,
      "ResponseType": "code",
      "Scope": ["openid", "profile", "email", "groups"]
    }
  }
}
{
  "Authentication": {
    "Provider": "Jwt",
    "RoleClaimType": "groups",
    "Jwt": {
      "Authority": "http://localhost:8082/default",
      "ClientId": "konfigo",
      "AuthorizeUrl": "http://localhost:3000/mock-oauth2/default/authorize",
      "TokenUrl": "http://localhost:3000/mock-oauth2/default/token",
      "Scopes": "openid profile email",
      "Audience": "konfigo",
      "RequireHttpsMetadata": false,
      "TokenValidationParameters": {
        "ValidateAudience": false,
        "ValidateIssuer": false
      }
    }
  }
}
{
  "Authentication": {
    "Provider": "Saml",
    "EmailClaimType": "mail",
    "RoleClaimType": "groups",
    "Saml": {
      "SpOptionsEntityId": "http://localhost:3000/saml2/Metadata",
      "SpOptionsModulePath": "/saml2",
      "IdentityProviderEntityId": "http://localhost:3000/saml2/simplesaml/saml2/idp/metadata.php",
      "IdentityProviderMetadataUrl": "http://localhost:3000/saml2/simplesaml/saml2/idp/metadata.php"
    }
  }
}
{
  "Authentication": {
    "Provider": "Local",
    "Local": {
      "DefaultAdminUsername": "admin",
      "DefaultAdminPassword": "admin"
    }
  }
}

No external IdP required. Sign in with admin / admin (or whatever DefaultAdminPassword is set to), then manage additional accounts from the Users page.

Test users are the same across the OpenID Connect, JWT, and SAML providers:

User Password Roles Konfigo policies
admin@konfigo.local admin admin, developer canAll, canChange
developer@konfigo.local developer developer canChange

Production: environment variables

All appsettings.json keys are available as environment variables using ASP.NET Core's double-underscore (__) nesting convention:

Authentication__Provider=OpenId
Authentication__RoleClaimType=role
Authentication__OpenId__Authority=https://your-idp.example.com
Authentication__OpenId__ClientId=konfigo
Authentication__OpenId__ClientSecret=your-secret
Authentication__OpenId__RequireHttpsMetadata=true
Authentication__OpenId__ResponseType=code
# Array scopes via index:
Authentication__OpenId__Scope__0=openid
Authentication__OpenId__Scope__1=profile
Authentication__OpenId__Scope__2=email
Authentication__OpenId__Scope__3=roles
Authentication__Provider=Jwt
Authentication__RoleClaimType=role
Authentication__Jwt__Authority=https://your-idp.example.com
Authentication__Jwt__Audience=konfigo
Authentication__Jwt__RequireHttpsMetadata=true
# Optional — for frontend OAuth2 flow:
Authentication__Jwt__ClientId=konfigo
Authentication__Jwt__AuthorizeUrl=https://your-idp.example.com/oauth2/authorize
Authentication__Jwt__TokenUrl=https://your-idp.example.com/oauth2/token
Authentication__Jwt__Scopes=openid profile email
Authentication__Provider=Saml
Authentication__EmailClaimType=mail
Authentication__RoleClaimType=groups
Authentication__Saml__SpOptionsEntityId=https://your-app.example.com/saml2/Metadata
Authentication__Saml__SpOptionsModulePath=/saml2
Authentication__Saml__IdentityProviderEntityId=https://your-idp.example.com/saml2/idp/metadata
Authentication__Saml__IdentityProviderMetadataUrl=https://your-idp.example.com/saml2/idp/metadata
Authentication__Provider=Local
Authentication__Local__DefaultAdminUsername=admin
Authentication__Local__DefaultAdminPassword=a-strong-password

Authorization policies via environment variables (roles use array index notation):

Authorization__Policies__canAll__0=admin
Authorization__Policies__canAll__1=konfigo-admin
Authorization__Policies__canChange__0=developer
Authorization__Policies__canChange__1=konfigo-dev