How OIDC ID-Token JWKS Verification Integrates with Login and Tenant Provisioning in WeKnora
WeKnora cryptographically verifies OIDC ID tokens against provider JWKS endpoints during the authentication callback before automatically provisioning or linking tenant accounts, ensuring only validated identities receive organizational access.
WeKnora, Tencent's open-source cloud-native platform, implements a robust OpenID Connect (OIDC) authentication flow that tightly couples JSON Web Key Set (JWKS) verification with tenant lifecycle management. This integration ensures that cryptographic validation of identity tokens occurs immediately before tenant provisioning logic executes, creating a secure boundary between unverified authentication attempts and resource allocation.
The Complete OIDC Authentication Flow
The authentication process spans multiple layers of the WeKnora architecture, from configuration loading through final tenant association.
Discovery and Configuration Loading
The flow begins when the client retrieves OIDC provider metadata via GET /api/v1/auth/oidc/config. This endpoint returns the discovery document containing the JWKS URL essential for subsequent token verification. The configuration structure is defined in [internal/config/config.go](https://github.com/Tencent/WeKnora/blob/main/internal/config/config.go#L26) within the OIDCAuthConfig struct, with validation logic residing in the same file (approximately lines 620-630).
// Client-side configuration retrieval
fetch('/api/v1/auth/oidc/config')
.then(r => r.json())
.then(cfg => {
// cfg.jwks_uri used server-side for verification
console.log('JWKS URL:', cfg.jwks_uri);
});
Login Initiation and Nonce Generation
The client requests authentication through either GET /api/v1/auth/oidc/url (JSON response) or GET /api/v1/auth/oidc/start (HTTP 302 redirect). Both handlers reside in [internal/handler/auth.go](https://github.com/Tencent/WeKnora/blob/main/internal/handler/auth.go#L309-L367) and perform critical security setup:
- Generate a cryptographically secure nonce
- Store the nonce in an HttpOnly cookie named
weknora_oidc_nonce - Build the provider authorization URL via
userService.GetOIDCAuthorizationURL
// Client-side login initiation
fetch('/api/v1/auth/oidc/url?redirect_uri=https://app.example.com')
.then(r => r.json())
.then(res => {
// Navigate to provider login page
window.location = res.authorization_url;
});
Callback Handling and Token Exchange
After provider authentication, the OIDC server redirects to GET /api/v1/auth/oidc/callback, handled in [internal/handler/auth.go](https://github.com/Tencent/WeKnora/blob/main/internal/handler/auth.go#L385-L460). This handler:
- Extracts the authorization
codefrom query parameters - Exchanges the code for an ID token and access token via the provider's token endpoint
- Invokes
userService.verifyOIDCIDTokento perform cryptographic validation - Redirects to the frontend with either
oidc_result=<payload>oroidc_error=<message>fragments
Cryptographic Verification of ID Tokens
The JWKS verification implementation in [internal/application/service/user.go](https://github.com/Tencent/WeKnora/blob/main/internal/application/service/user.go#L1993-L2008) serves as the security gatekeeper before tenant provisioning occurs.
JWKS Retrieval and Caching
The verifyOIDCIDToken method retrieves the JWKS URL from the cached OIDCAuthConfig. To prevent repeated HTTP requests, WeKnora implements oidcJWKCache within the user service, caching the JSON Web Key Set for a configurable duration. If the cache misses or expires, the system downloads the JWKS set using net/http.
Signature Validation Logic
The verification process extracts the kid (Key ID) claim from the ID token header and locates the matching JWK in the retrieved set. The system constructs the appropriate public key (RSA or ECDSA) and validates the JWT signature:
func (s *userService) verifyOIDCIDToken(ctx context.Context, cfg *config.OIDCAuthConfig, rawToken string) (*oidc.IDToken, error) {
// 1. Fetch or reuse cached JWKS
jwks, err := s.jwksFetcher.Fetch(ctx, cfg.JWKSURL)
if err != nil { return nil, err }
// 2. Parse token header to extract Key ID
parsed, _ := jwt.ParseSigned(rawToken)
kid := parsed.Headers[0].KeyID
// 3. Locate matching JWK
key := jwks.Key(kid)
if key == nil {
return nil, fmt.Errorf("no matching JWK for kid %s", kid)
}
// 4. Verify signature and extract claims
var claims map[string]interface{}
if err := parsed.Claims(key, &claims); err != nil {
return nil, err
}
// Additional validation: iss, aud, exp, iat, nbf...
return &oidc.IDToken{Claims: claims}, nil
}
Standard Claims Verification
Beyond signature validation, the implementation verifies standard OIDC claims including iss (issuer), aud (audience), exp (expiration), iat (issued at), and nbf (not before). If any validation fails, the function returns an error immediately, aborting the login process before tenant logic executes.
Tenant Provisioning Integration
Successful JWKS verification triggers the tenant lifecycle management phase, tightly coupled to the authentication result.
User Identity Resolution
Following successful verification, resolveOIDCIDUserInfo (lines approximately 2030-2060 in the same service file) decodes the verified token payload into a oidcUserInfo struct. This extracts the unique subject identifier (sub), email address, and display name from the cryptographically validated claims.
Automatic Tenant Creation and Association
The SwitchTenantForUser method (implemented around lines 1647-1705 in [internal/application/service/user.go](https://github.com/Tencent/WeKnora/blob/main/internal/application/service/user.go#L1647-L1705)) executes the provisioning logic:
- Checks the
tenantstable (defined in [internal/types/tenant.go](https://github.com/Tencent/WeKnora/blob/main/internal/types/tenant.go)) for existing tenant associations - If no tenant exists, invokes
tenantService.CreateTenantto provision a default tenant - Links the verified user (identified by
subclaim) to the tenant - Returns the tenant context for session establishment
This coupling ensures tenant creation only occurs after cryptographic proof of identity. The handler's final redirect includes the tenant context in the oidc_result fragment, allowing the frontend to establish a session with proper organizational scoping.
Security Mechanisms
Replay Attack Prevention via Nonce Validation
The system protects against replay attacks through nonce validation implemented in decodeOIDCState within [internal/handler/auth.go](https://github.com/Tencent/WeKnora/blob/main/internal/handler/auth.go#L471-L489). The nonce generated during login initiation and stored in the weknora_oidc_nonce HttpOnly cookie is compared against the nonce claim inside the ID token. Mismatches result in immediate authentication failure, preventing token reuse across sessions.
Key Rotation Handling
The JWKS caching strategy accommodates provider key rotation by respecting cache TTLs while allowing fresh fetches when keys change. If verification fails due to key mismatch (typically indicating rotation), the system can refresh the JWKS cache and retry validation, though the primary implementation returns errors for signature mismatches to maintain strict security posture.
Summary
- JWKS verification occurs inside the login callback at
GET /api/v1/auth/oidc/callbackininternal/handler/auth.go, not as a background process, ensuring immediate validation before resource allocation - Tenant provisioning is strictly dependent on successful
verifyOIDCIDTokencompletion, as implemented ininternal/application/service/user.golines 1647-1705 - Cryptographic validation includes signature verification against RSA/ECDSA keys from the provider JWKS endpoint, plus standard claim validation (
iss,aud,exp) - Security hardening includes nonce validation via HttpOnly cookies and configurable JWKS caching to balance performance with key rotation support
- Endpoint registration occurs in
internal/router/routes_auth_tenant.go(lines 220-224), mounting the OIDC handlers under/api/v1/auth/oidc/*
Frequently Asked Questions
What happens if the JWKS endpoint is unavailable during login?
If the JWKS endpoint returns an error or timeout during the verifyOIDCIDToken execution, the authentication flow aborts immediately. The callback handler redirects to the frontend with oidc_error=invalid_token or similar error indication, and no tenant provisioning occurs. The system relies on the cached oidcJWKCache to provide resilience, but verification fails if the cache lacks the required key identifier (kid).
How does WeKnora handle OIDC provider key rotation?
The oidcJWKCache implementation caches JWKS data for a configurable duration. When a token arrives signed with a new key (indicated by an unrecognized kid), the verification fails initially. The specific implementation in internal/application/service/user.go fetches fresh JWKS data when cache misses occur, allowing seamless key rotation without service restarts, provided the cache TTL is appropriately configured.
Can a user authenticate without triggering tenant provisioning?
No. According to the source code in internal/application/service/user.go, the SwitchTenantForUser call follows immediately after successful verifyOIDCIDToken and resolveOIDCUserInfo execution within the callback handler flow. The tenant check (existence or creation) is an integral part of the authentication completion process, though existing users simply link to their current tenant rather than creating new ones.
Where is the OIDC nonce validated in the WeKnora codebase?
The nonce validation occurs in the decodeOIDCState function within internal/handler/auth.go (approximately lines 471-489). This function compares the nonce stored in the weknora_oidc_nonce HttpOnly cookie against the nonce claim extracted from the ID token payload. This validation prevents CSRF and replay attacks by ensuring the token returned matches the specific authentication request initiated by the client.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →