Skip to content

OAuth 2.0 & OpenID Connect

Overview

Modgud is a full-fledged OAuth 2.0 authorization server and OpenID Connect provider. Implemented via OpenIddict 7 with its own Marten-based stores (MartenApplicationStore, MartenScopeStore, MartenAuthorizationStore, MartenTokenStore) — no Entity Framework.

Terminology (Client, Scope, API, Grant Type, token types) in the Glossary.

The three actors

ActorRoleExample
UserThe person signing inSomeone using your app
ClientThe application requesting accessSPA, mobile app, backend service
APIThe protected serviceA billing API, an order API

Modgud sits in the middle — it authenticates the user, issues tokens to the client, and the API verifies the tokens.

Supported flows

Authorization Code + PKCE (for user apps)

Standard for web apps, SPAs, mobile. PKCE (Proof Key for Code Exchange) is enforced (RequireProofKeyForCodeExchange).

Client Credentials (for services)

Machine-to-machine. The service authenticates directly with client ID + secret, no user involved:

http
POST /connect/token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
client_id=my-service
client_secret=...
scope=billing.read

Refresh Token

Enabled for clients that request offline_access. Refresh tokens are reference tokens, stored server-side in OpenIddictTokenDocument.

Device Code (RFC 8628)

For devices with no browser or limited input — CLIs, TVs, input-constrained appliances. The client polls /connect/token while the user completes the flow on a separate device. Endpoint shape + verification UI documented in Reference → OAuth API.

Dynamic Client Registration (DCR)

In addition to admin-created clients, Modgud supports Dynamic Client Registration (RFC 7591) — software registers itself against the IdP without an administrator pre-provisioning it. This is the protocol path MCP agents use to attach to MCP servers without per-agent onboarding.

DCR-registered clients are constrained to public PKCE + Authorization-Code/Refresh-Token only — no client_credentials, no secrets, no implicit/hybrid flows. The feature is off by default on every realm; turning it on is a triple opt-in (realm master + per-API + per-scope). See the concept page for the design rationale and the admin setup guide for the operational checklist.

No Implicit, no ROPC

Modgud rejects Implicit Flow and Resource Owner Password Credentials. Both are considered insecure — OAuth 2.1 deprecates them.

Token validation

How an API validates an access token depends on the configured token format (settable per client):

Token typeHow the API validates
Reference Token (default)Calls modgud's introspection endpoint — gets back user info, scopes, expiry. Can be revoked instantly.
JWTVerifies the signature locally with the signing key from the JWKS endpoint. No roundtrip needed, but revocation only works via expiry.

Which one when? See Glossary > Access token format.

Per-realm isolation

Every realm has its own OAuth configuration:

  • Clients from realm A cannot authenticate against realm B
  • Tokens from realm A are invalid in realm B (issuer check)
  • Each realm has its own discovery endpoint
  • The issuer claim in tokens contains the realm domain

Two realms can both have a client with client_id=my-app — those are different clients.

Implementation: RealmIssuerHandler (an OpenIddict pipeline hook) overrides the static issuer per request with BaseUri (= the realm domain).

Configurable per client:

Consent TypeBehaviour
implicitThe user never sees a consent page. Authorization runs through automatically.
explicitThe user must confirm every scope on the consent page. Previous approvals are remembered.

For explicit:

  1. AuthorizationController checks for existing permanent authorizations
  2. If none → redirect to /consent?returnUrl=...
  3. ConsentController shows scope details and processes the decision
  4. Approved scopes are stored as a permanent authorization
  5. With prompt=none and no existing consent → consent_required error

Scopes & API resources

Default scopes (seeded per realm at provisioning):

ScopePurpose
openidRequired for OIDC, returns the user ID
profileFirst name, last name
emailEmail address
rolesRole memberships
offline_accessEnables refresh tokens

Custom scopes can be created per realm by an admin, e.g. billing:read, repo:write. They can define UserClaims — when a token includes such a scope, the specified claims are packed into the token.

API resources represent protected APIs. Per API:

  • Identifier (audience claim)
  • List of supported scopes
  • UserClaims that should land in tokens for this API

Discovery privacy

scopes_supported in /.well-known/openid-configuration lists only the scopes a realm has explicitly published. Standard OIDC scopes default to public; custom App and API scopes default to private. Background:

  • RFC 8414 §3 declares scopes_supported as RECOMMENDED, not MUST — publishing every scope is allowed but not required.
  • In multi-tenant SaaS, leaking which APIs a tenant operates is information disclosure with no upside: clients learn the scopes they need from the resource server's integration docs, not from discovery.
  • The realm-DB scope validation is the access control. Hiding from discovery is defense-in-depth — an attacker can still guess scope names and probe /connect/token.

Admins toggle per scope via the Show in discovery document flag (see OAuth Scopes admin). Implementation: RealmScopesSupportedHandler (an OpenIddict pipeline hook in Modgud.Infrastructure/OpenIddict/) overrides the discovery handler so the realm-DB-backed scope set is filtered on this flag.

Token lifetimes

Configured in OpenIddictSettings (overridable per client):

TokenDefaultSetting key
Access Token60 minAccessTokenLifetimeMinutes
Refresh Token14 daysRefreshTokenLifetimeDays
Authorization Code5 minAuthorizationCodeLifetimeMinutes

Signing

ModeConfiguration
DevelopmentEphemeral signing/encryption keys (auto-generated, lost on restart)
ProductionX.509 certificate from file (SigningCertificatePath)

In dev mode every client app has to refresh its token validation after each modgud restart (JWKS changes). In production the certificate is persistent — a restart changes nothing.

Admin UI

The admin area (/admin/oauth/...) has list and detail views for:

  • Clients — application registrations with secrets, redirect URIs, grant types, per-client token settings
  • Scopes — permission definitions (built-in + custom) with UserClaim mappings
  • APIs — protected API resources with scopes and UserClaims

Gating: modgud:oauth-client:read/write/delete, modgud:oauth-scope:read/write/delete, modgud:oauth-api:read/write/delete. Per-resource admin bypass via modgud:oauth-client:admin etc.

Released under the Apache-2.0 License.