Id RFC 004 oauth2 oidc service
RFC-004 — OAuth2/OIDC Service
- *tatus:*Draft
- *ate:*2026
0408 - *uthor:*Koder Team
- *epends on:*RFC
001, RFC002, RFC-003
Summary
The OAuth2OIDC service implements an OpenID Connect Provider (OP) compliant with [OpenID Connect Core 1.0](https:openid.netspecsopenidconnectcore-1_0.html) and [OAuth 2.1](https:datatracker.ietf.orgdochtmldraftietfoauthv21-07). It handles client registration, authorization flows, token issuance, and JWKS publication.
Supported Flows
MVP
| Flow | Use Case |
|---|---|
| *uthorization Code + PKCE* | Web apps, mobile apps, SPAs — the primary flow |
| *lient Credentials* | Machine |
| *efresh Token* | Token renewal without re-authentication |
Explicitly NOT Supported
| Flow | Reason |
|---|---|
| Implicit | Deprecated in OAuth 2.1, insecure |
| Resource Owner Password Credentials (ROPC) | Deprecated in OAuth 2.1 |
OIDC Discovery
GET /.well-known/openid-configuration
Auto-generated per tenant. Example for tenant koder:
{
"issuer": "https://koder.id.koder.dev",
"authorization_endpoint": "https://koder.id.koder.dev/oauth/v2/authorize",
"token_endpoint": "https://koder.id.koder.dev/oauth/v2/token",
"userinfo_endpoint": "https://koder.id.koder.dev/oidc/v1/userinfo",
"jwks_uri": "https://koder.id.koder.dev/oauth/v2/keys",
"revocation_endpoint": "https://koder.id.koder.dev/oauth/v2/revoke",
"introspection_endpoint": "https://koder.id.koder.dev/oauth/v2/introspect",
"end_session_endpoint": "https://koder.id.koder.dev/oidc/v1/end_session",
"registration_endpoint": "https://koder.id.koder.dev/oauth/v2/register",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "client_credentials", "refresh_token"],
"subject_types_supported": ["public"],
"id_token_signing_alg_values_supported": ["RS256", "ES256"],
"token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post", "none"],
"scopes_supported": ["openid", "profile", "email", "offline_access"],
"claims_supported": ["sub", "iss", "aud", "exp", "iat", "name", "email", "email_verified", "locale", "picture"],
"code_challenge_methods_supported": ["S256"],
"request_parameter_supported": false,
"request_uri_parameter_supported": false
}Authorization Code Flow + PKCE
Step 1 — Authorization Request
GET /oauth/v2/authorize
?response_type=code
&client_id=my-app
&redirect_uri=https://example.koder.dev/auth/callback
&scope=openid profile email
&state=random-csrf-token
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
&nonce=random-nonce*KCE is mandatory*for all clients (public and confidential). This follows OAuth 2.1.
*erver behavior:*
- Validate
client_idexists andredirect_urimatches registered URIs - If user is not authenticated → redirect to login page (Auth service)
- If user is authenticated → check consent
- If consent exists for requested scopes → generate code and redirect
- If no consent → show consent screen, then generate code and redirect
Step 2 — Token Exchange
POST /oauth/v2/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=AUTH_CODE
&redirect_uri=https://app.example.com/callback
&client_id=my-app
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk*erver behavior:*
- Validate authorization code exists, not used, not expired
- Validate
redirect_urimatches the one used in authorization request - Validate PKCE:
SHA256(code_verifier) == code_challenge - For confidential clients: validate
client_secret(via Basic auth or body) - Mark authorization code as used (single-use)
- Create session via Session service (gRPC) — the Session service generates the access token (JWT) and refresh token (see RFC-005)
- Return tokens to client:
*esponse:*
{
"access_token": "eyJhbGciOi...",
"token_type": "Bearer",
"expires_in": 900,
"refresh_token": "krt_...",
"id_token": "eyJhbGciOi...",
"scope": "openid profile email"
}Token Formats
Access Token (JWT)
{
"header": {
"alg": "RS256",
"typ": "at+jwt",
"kid": "key-2026-04"
},
"payload": {
"iss": "https://koder.id.koder.dev",
"sub": "01HXK...",
"aud": ["my-app"],
"exp": 1712577600,
"iat": 1712576700,
"nbf": 1712576700,
"jti": "01HXK...",
"client_id": "my-app",
"scope": "openid profile email",
"tenant_id": "koder"
}
}- *ifetime:*15 minutes (configurable per client: 5-60 min)
- *igning:*RS256 (default) or ES256
ID Token (JWT)
{
"header": {
"alg": "RS256",
"typ": "JWT",
"kid": "key-2026-04"
},
"payload": {
"iss": "https://koder.id.koder.dev",
"sub": "01HXK...",
"aud": "my-app",
"exp": 1712577600,
"iat": 1712576700,
"nonce": "random-nonce",
"at_hash": "base64url...",
"name": "Rodrigo Pereira",
"email": "rodrigo@koder.dev",
"email_verified": true,
"locale": "pt-BR",
"picture": "https://id.koder.dev/avatars/01HXK..."
}
}- *ifetime:*same as access token
- *ontains:*identity claims based on requested scopes
Refresh Token (opaque)
krt_01HXKa8b9c0d1e2f3g4h5i6j7k8l9m0n- *ormat:*prefix
krt_+ ULID (easily identifiable, revocable) - *ifetime:*30 days (configurable per client)
- *torage:*hashed in Session service database
- *otation:*new refresh token issued on each use (old one invalidated)
Key Rotation (JWKS)
GET /oauth/v2/keys
Returns the JSON Web Key Set with all active public keys:
{
"keys": [
{
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"kid": "key-2026-04",
"n": "base64url...",
"e": "AQAB"
}
]
}Rotation Strategy
- New key pair generated every *4 hours*(configurable)
- New key becomes active immediately for signing
- Old key remains in JWKS for *8 hours*(to validate tokens signed with it)
- After 48 hours, old key removed from JWKS and marked
rotatedin DB - Maximum 3 active keys in JWKS at any time
Token Revocation
POST /oauth/v2/revoke
POST /oauth/v2/revoke
Content-Type: application/x-www-form-urlencoded
token=krt_01HXK...
&token_type_hint=refresh_token- Revokes the refresh token and its associated session
- All access tokens from that session become invalid on next introspection
- Returns
200 OKalways (even if token doesn't exist — prevents enumeration)
Token Introspection
POST /oauth/v2/introspect
For resource servers to validate opaque tokens or check revocation status:
{
"active": true,
"sub": "01HXK...",
"client_id": "my-app",
"scope": "openid profile email",
"exp": 1712577600,
"iat": 1712576700,
"tenant_id": "koder"
}UserInfo Endpoint
GET /oidc/v1/userinfo
Authorization: Bearer eyJhbGciOi...*esponse:*
{
"sub": "01HXK...",
"name": "Rodrigo Pereira",
"email": "rodrigo@koder.dev",
"email_verified": true,
"locale": "pt-BR",
"picture": "https://id.koder.dev/avatars/01HXK..."
}Claims returned depend on the scopes granted in the access token.
Client Credentials Flow
For servicetoservice authentication (no user involved):
POST /oauth/v2/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret)
grant_type=client_credentials
&scope=admin:read admin:write*esponse:*access token only (no refresh token, no ID token).
gRPC Service Definition
syntax = "proto3";
package koder.id.oauth.v1;
service OAuthService {
// Authorization code operations
rpc CreateAuthorizationCode(CreateAuthCodeRequest) returns (AuthorizationCode);
rpc ExchangeAuthorizationCode(ExchangeCodeRequest) returns (TokenResponse);
// Token operations
rpc RefreshToken(RefreshTokenRequest) returns (TokenResponse);
rpc RevokeToken(RevokeTokenRequest) returns (RevokeTokenResponse);
rpc IntrospectToken(IntrospectRequest) returns (IntrospectResponse);
// Client management (called by Admin service)
rpc CreateClient(CreateClientRequest) returns (Client);
rpc GetClient(GetClientRequest) returns (Client);
rpc UpdateClient(UpdateClientRequest) returns (Client);
rpc DeleteClient(DeleteClientRequest) returns (DeleteClientResponse);
rpc ListClients(ListClientsRequest) returns (ListClientsResponse);
// Key management
rpc GetJWKS(GetJWKSRequest) returns (JWKS);
rpc RotateSigningKey(RotateKeyRequest) returns (SigningKey);
// Consent
rpc GetConsent(GetConsentRequest) returns (Consent);
rpc GrantConsent(GrantConsentRequest) returns (Consent);
rpc RevokeConsent(RevokeConsentRequest) returns (RevokeConsentResponse);
}Interaction with Other Services
┌────────┐ ┌──────────┐
│ OAuth2 │─── gRPC ──────▶│ Auth │ Trigger auth flow if user not logged in
│ /OIDC │ │ Service │
│Service │ └──────────┘
│ │
│ │─── gRPC ──────▶┌──────────┐
│ │ │ Identity │ Get user claims for ID token / userinfo
│ │ │ Service │
│ │ └──────────┘
│ │
│ │─── gRPC ──────▶┌──────────┐
│ │ │ Session │ Create/validate sessions, issue refresh tokens
└────────┘ │ Service │
└──────────┘Security Considerations
- *KCE mandatory*for all clients (OAuth 2.1 requirement)
- *uthorization codes*are single-use and expire in 30 seconds
- *efresh token rotation*— new token issued on each use, old one invalidated
- *edirect URI exact matching*— no wildcards, no partial matches
- *tate parameter*validated to prevent CSRF
- *oken binding*— access tokens are bound to the client that requested them
- *o implicit flow*— eliminated per OAuth 2.1
PAT Scopes
Personal Access Tokens (machinetoAPI auth, non-interactive) carry a distinct set of canonical scopes from the OAuth/OIDC consent scopes above (openid, profile, email, …). PAT scopes follow the GitHub convention verb:resource (lowercase, colon-separated) and never surface on a consent screen because PATs are issued outofband (CLI / settings page).
The canonical registry is pkg/scopes/pat.go::WellKnownPATScopes. The PAT issuance flow MUST reject any scope name not in the registry; the consuming resource service MUST gate the protected endpoint on the scope being present in the bearer's PAT.
Catalog
| Scope | Resource | Description | Consumer |
|---|---|---|---|
read:usage |
usage | Read your own usage data (rate-limit windows, token cost histograms, billing aggregates). | services/ai/gateway GET /v1/usage per RFC |
New PAT scopes register by adding a constant + map entry to pkg/scopes/pat.go and a row above; the pat_test.go constantvsmap back-ref test guards drift.
*tatus (2026
0517, #071 Wave 1):*the catalog is established; PAT issuance flow (CLIkoder-id-cli pat create --scope=read:usage, settings UI, validation gate at issue-time) is tracked as a separate follow-up — until it lands,read:usageis consumed only by gateway-side enforcement and rendered in this catalog for downstream reference.
Compliance
This service targets conformance with:
- OpenID Connect Core 1.0 — Authorization Code Flow
- OpenID Connect Discovery 1.0 — Provider metadata
- OAuth 2.1 Draft — Modern OAuth best practices
- RFC 7636 — PKCE
- RFC 7662 — Token Introspection
- RFC 7009 — Token Revocation