Skip to content

RFC: Authorizer as Full OIDC Provider #514

@lakhansamani

Description

@lakhansamani

RFC: Authorizer as Full OIDC Provider

Phase: 3 — Enterprise SSO & Federation
Priority: P2 — High
Estimated Effort: High
Depends on: M2M Auth (#509), Fine-Grained Permissions (#508)


Problem Statement

Authorizer can consume OIDC (social login providers) but cannot act as an OIDC Provider. Third-party applications cannot use "Sign in with Authorizer" for SSO. WorkOS Connect, Clerk as IdP, and Keycloak as IdP all support this. This is essential for organizations that want Authorizer to be their central identity provider for all internal and external applications.


Current Architecture Context

  • Partial OAuth endpoints exist: /authorize, /oauth/token (authorization_code + refresh_token grants), /userinfo
  • /.well-known/openid-configuration endpoint exists but is minimal
  • /.well-known/jwks.json endpoint exists and works
  • Token generation supports HS/RS/ES algorithm families
  • PKCE support exists (RFC 7636)
  • No client registration mechanism — single ClientID/ClientSecret per instance
  • No consent screen — authorization is implicit
  • No codetoken exchange with proper client authentication for third-party apps

Current /authorize flow (internal/http_handlers/authorize.go):

  • Generates authorization code or tokens
  • Validates redirect_uri against configured allowed origins
  • Stores state in memory store
  • Redirects to login page if no session

Current /.well-known/openid-configuration (internal/http_handlers/openid_config.go):

  • Returns basic OIDC discovery document
  • References /authorize, /oauth/token, /userinfo, JWKS endpoint

Proposed Solution

1. OIDC Client Registration

Third-party applications register with Authorizer to authenticate users.

New schema: internal/storage/schemas/oidc_client.go

type OIDCClient struct {
    ID                  string `json:"id" gorm:"primaryKey;type:char(36)"`
    Name                string `json:"name" gorm:"type:varchar(256)"`
    ClientID            string `json:"client_id" gorm:"type:varchar(64);uniqueIndex"`
    ClientSecretHash    string `json:"-" gorm:"type:varchar(256)"`                      // for confidential clients
    ClientType          string `json:"client_type" gorm:"type:varchar(20)"`             // "confidential" or "public"
    RedirectURIs        string `json:"redirect_uris" gorm:"type:text"`                  // JSON array of allowed redirect URIs
    PostLogoutRedirects string `json:"post_logout_redirects" gorm:"type:text"`          // JSON array
    AllowedScopes       string `json:"allowed_scopes" gorm:"type:text"`                 // comma-separated: "openid,profile,email,roles"
    AllowedGrantTypes   string `json:"allowed_grant_types" gorm:"type:text"`            // comma-separated: "authorization_code,refresh_token"
    LogoURL             string `json:"logo_url" gorm:"type:text"`
    Description         string `json:"description" gorm:"type:text"`
    OrganizationID      string `json:"organization_id" gorm:"type:char(36);index"`
    RequirePKCE         bool   `json:"require_pkce" gorm:"type:bool;default:true"`
    RequireConsent      bool   `json:"require_consent" gorm:"type:bool;default:true"`
    TokenEndpointAuth   string `json:"token_endpoint_auth" gorm:"type:varchar(50);default:client_secret_basic"` 
    AccessTokenTTL      int64  `json:"access_token_ttl" gorm:"default:1800"`            // seconds
    RefreshTokenTTL     int64  `json:"refresh_token_ttl" gorm:"default:2592000"`        // 30 days
    IsActive            bool   `json:"is_active" gorm:"type:bool;default:true"`
    CreatedBy           string `json:"created_by" gorm:"type:char(36)"`
    CreatedAt           int64  `json:"created_at" gorm:"autoCreateTime"`
    UpdatedAt           int64  `json:"updated_at" gorm:"autoUpdateTime"`
}

Client types:

  • Confidential: Server-side apps with a client secret (traditional web apps, backend services)
  • Public: SPAs, mobile apps, CLIs — no client secret, PKCE mandatory

2. Enhanced Authorization Endpoint

Update internal/http_handlers/authorize.go to support third-party OIDC clients:

func (h *httpProvider) AuthorizeHandler() gin.HandlerFunc {
    return func(c *gin.Context) {
        clientID := c.Query("client_id")
        redirectURI := c.Query("redirect_uri")
        responseType := c.Query("response_type")
        scope := c.Query("scope")
        state := c.Query("state")
        nonce := c.Query("nonce")
        codeChallenge := c.Query("code_challenge")
        codeChallengeMethod := c.Query("code_challenge_method")
        
        // 1. Look up OIDC client (or fall back to instance client_id for backward compat)
        client, err := store.GetOIDCClientByClientID(ctx, clientID)
        if client == nil {
            // Check if it's the instance's own client_id (backward compatibility)
            if clientID == cfg.ClientID {
                // Existing behavior — no consent needed
                handleLegacyAuthorize(c)
                return
            }
            return oauthError(c, "invalid_client")
        }
        
        // 2. Validate redirect_uri against registered URIs
        if !isValidRedirectURI(redirectURI, client.RedirectURIs) {
            return oauthError(c, "invalid_redirect_uri")
        }
        
        // 3. Validate scopes
        requestedScopes := parseScopes(scope)
        if !areScopesAllowed(requestedScopes, client.AllowedScopes) {
            return oauthError(c, "invalid_scope")
        }
        
        // 4. PKCE validation
        if client.RequirePKCE && codeChallenge == "" {
            return oauthError(c, "invalid_request", "code_challenge required")
        }
        
        // 5. Check if user is authenticated
        user := getAuthenticatedUser(c)
        if user == nil {
            // Redirect to login page with return URL
            redirectToLogin(c, buildReturnURL(c))
            return
        }
        
        // 6. Consent screen (if required and not previously granted)
        if client.RequireConsent {
            existingConsent := getExistingConsent(ctx, user.ID, client.ID)
            if existingConsent == nil || !scopesCovered(existingConsent.Scopes, requestedScopes) {
                // Show consent screen
                renderConsentPage(c, client, requestedScopes, user)
                return
            }
        }
        
        // 7. Generate authorization code
        code := generateAuthCode(user, client, requestedScopes, nonce, codeChallenge)
        
        // 8. Redirect with code
        redirectWithCode(c, redirectURI, code, state)
    }
}

3. Consent Screen

New endpoint: POST /oauth/consent — handles consent form submission

Consent schema:

type OIDCConsent struct {
    ID        string `json:"id" gorm:"primaryKey;type:char(36)"`
    UserID    string `json:"user_id" gorm:"type:char(36);index:idx_consent_user_client,unique"`
    ClientID  string `json:"client_id" gorm:"type:char(36);index:idx_consent_user_client,unique"`
    Scopes    string `json:"scopes" gorm:"type:text"`
    GrantedAt int64  `json:"granted_at" gorm:"autoCreateTime"`
}

Consent page shows:

  • Application name and logo (from OIDCClient)
  • Requested scopes in human-readable form:
    • openid → "Verify your identity"
    • profile → "Access your name and profile picture"
    • email → "Access your email address"
    • roles → "Access your role information"
    • permissions → "Access your permission list"
    • org → "Access your organization information"
  • "Allow" / "Deny" buttons

Consent page rendered from web/app/ — new React component, or a minimal server-rendered HTML page.

4. Enhanced Token Endpoint

Update /oauth/token to handle third-party OIDC clients:

  • Client authentication: Validate client_id/client_secret for confidential clients

    • client_secret_basic: HTTP Basic auth header
    • client_secret_post: Body parameters
    • none: Public clients (PKCE required)
  • Authorization code exchange: Validate code was issued to this client, verify PKCE code_verifier

  • Refresh token: Issue refresh tokens only if offline_access scope requested and client supports refresh_token grant type

  • Refresh token rotation: When refreshing, invalidate old refresh token and issue new one (security best practice for OIDC)

5. Standard OIDC Scopes

Scope Claims Included
openid sub, iss, aud, iat, exp, nonce
profile given_name, family_name, middle_name, nickname, picture, gender, birthdate
email email, email_verified
phone phone_number, phone_number_verified
roles roles (string array)
permissions permissions (string array, requires #508)
org org_id, org_slug, org_role (requires #511)
offline_access Refresh token issued

6. Enhanced Discovery Document

Update /.well-known/openid-configuration:

{
    "issuer": "https://auth.example.com",
    "authorization_endpoint": "https://auth.example.com/authorize",
    "token_endpoint": "https://auth.example.com/oauth/token",
    "userinfo_endpoint": "https://auth.example.com/userinfo",
    "jwks_uri": "https://auth.example.com/.well-known/jwks.json",
    "revocation_endpoint": "https://auth.example.com/oauth/revoke",
    "end_session_endpoint": "https://auth.example.com/logout",
    "registration_endpoint": "https://auth.example.com/oauth/register",
    "scopes_supported": ["openid", "profile", "email", "phone", "roles", "permissions", "org", "offline_access"],
    "response_types_supported": ["code", "id_token", "code id_token"],
    "grant_types_supported": ["authorization_code", "refresh_token", "client_credentials"],
    "subject_types_supported": ["public"],
    "id_token_signing_alg_values_supported": ["RS256", "ES256", "HS256"],
    "token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post", "none"],
    "code_challenge_methods_supported": ["S256"],
    "claims_supported": ["sub", "iss", "aud", "exp", "iat", "nonce", "email", "email_verified", "given_name", "family_name", "picture", "roles", "permissions"]
}

7. GraphQL Admin API for Client Management

type OIDCClient {
    id: ID!
    name: String!
    client_id: String!
    client_type: String!
    redirect_uris: [String!]!
    allowed_scopes: [String!]!
    allowed_grant_types: [String!]!
    logo_url: String
    require_pkce: Boolean!
    require_consent: Boolean!
    is_active: Boolean!
    created_at: Int64!
}

type OIDCClientWithSecret {
    client: OIDCClient!
    client_secret: String!              # Shown only once
}

type Mutation {
    _create_oidc_client(params: CreateOIDCClientInput!): OIDCClientWithSecret!
    _update_oidc_client(params: UpdateOIDCClientInput!): OIDCClient!
    _delete_oidc_client(id: ID!): Response!
    _rotate_oidc_client_secret(id: ID!): OIDCClientWithSecret!
}

type Query {
    _oidc_clients(params: PaginatedInput): OIDCClients!
    _oidc_client(id: ID!): OIDCClient!
}

User-facing query (list authorized applications):

type Query {
    authorized_apps: [AuthorizedApp!]!    # Apps the user has granted consent to
}

type Mutation {
    revoke_app_consent(client_id: ID!): Response!  # Revoke consent for an app
}

Backward Compatibility

  • Existing /authorize flow with instance ClientID continues to work unchanged
  • Existing /oauth/token with authorization_code and refresh_token grants unchanged
  • New OIDC client system is additive — only activated when third-party clients are registered
  • Discovery document enhanced but remains backward compatible

OpenID Certification Path

This implementation targets compliance with:

  • OpenID Connect Core 1.0
  • OpenID Connect Discovery 1.0
  • OpenID Connect Dynamic Client Registration 1.0 (if DCR enabled)
  • OAuth 2.0 Multiple Response Types
  • PKCE (RFC 7636)

Future: Submit for official OpenID Certification (Phase 6.3).


Testing Plan

  • Integration test: full authorization_code flow with PKCE
  • Test consent screen display and grant persistence
  • Test token exchange with confidential and public clients
  • Test redirect_uri validation (exact match, no open redirect)
  • Test scope enforcement (only allowed scopes granted)
  • Test refresh token rotation
  • Test OIDC discovery document compliance
  • Test UserInfo endpoint with scoped claims
  • Test backward compatibility with instance ClientID

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions