From 18c8413ea384f13d4e90747eedd8e5d9baaf3a62 Mon Sep 17 00:00:00 2001 From: Scott McKendry Date: Mon, 13 Apr 2026 04:19:47 +1200 Subject: [PATCH] feat: support unsigned oidc request objects (#785) --- frontend/src/lib/hooks/oidc.ts | 36 ++++++++++++ internal/controller/well_known_controller.go | 56 ++++++++++--------- .../controller/well_known_controller_test.go | 28 +++++----- 3 files changed, 81 insertions(+), 39 deletions(-) diff --git a/frontend/src/lib/hooks/oidc.ts b/frontend/src/lib/hooks/oidc.ts index cc79a7e..1341e8c 100644 --- a/frontend/src/lib/hooks/oidc.ts +++ b/frontend/src/lib/hooks/oidc.ts @@ -11,6 +11,33 @@ export const oidcParamsSchema = z.object({ code_challenge_method: z.string().optional(), }); +function b64urlDecode(s: string): string { + const base64 = s.replace(/-/g, "+").replace(/_/g, "/"); + return atob(base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), "=")); +} + +function decodeRequestObject(jwt: string): Record { + try { + // Must have exactly 3 parts: header, payload, signature + const parts = jwt.split("."); + if (parts.length !== 3) return {}; + + // Header must specify "alg": "none" and signature must be empty string + const header = JSON.parse(b64urlDecode(parts[0])); + if (!header || typeof header !== "object" || header.alg !== "none" || parts[2] !== "") return {}; + + const payload = JSON.parse(b64urlDecode(parts[1])); + if (!payload || typeof payload !== "object" || Array.isArray(payload)) return {}; + const result: Record = {}; + for (const [k, v] of Object.entries(payload)) { + if (typeof v === "string") result[k] = v; + } + return result; + } catch { + return {}; + } +} + export const useOIDCParams = ( params: URLSearchParams, ): { @@ -20,6 +47,15 @@ export const useOIDCParams = ( compiled: string; } => { const obj = Object.fromEntries(params.entries()); + + // RFC 9101 / OIDC Core 6.1: if `request` param present, decode JWT payload + // and merge claims over top-level params (JWT claims take precedence) + const requestJwt = params.get("request"); + if (requestJwt) { + const claims = decodeRequestObject(requestJwt); + Object.assign(obj, claims); + } + const parsed = oidcParamsSchema.safeParse(obj); if (parsed.success) { diff --git a/internal/controller/well_known_controller.go b/internal/controller/well_known_controller.go index fea51cc..d09de0f 100644 --- a/internal/controller/well_known_controller.go +++ b/internal/controller/well_known_controller.go @@ -9,19 +9,21 @@ import ( ) type OpenIDConnectConfiguration struct { - Issuer string `json:"issuer"` - AuthorizationEndpoint string `json:"authorization_endpoint"` - TokenEndpoint string `json:"token_endpoint"` - UserinfoEndpoint string `json:"userinfo_endpoint"` - JwksUri string `json:"jwks_uri"` - ScopesSupported []string `json:"scopes_supported"` - ResponseTypesSupported []string `json:"response_types_supported"` - GrantTypesSupported []string `json:"grant_types_supported"` - SubjectTypesSupported []string `json:"subject_types_supported"` - IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"` - TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported"` - ClaimsSupported []string `json:"claims_supported"` - ServiceDocumentation string `json:"service_documentation"` + Issuer string `json:"issuer"` + AuthorizationEndpoint string `json:"authorization_endpoint"` + TokenEndpoint string `json:"token_endpoint"` + UserinfoEndpoint string `json:"userinfo_endpoint"` + JwksUri string `json:"jwks_uri"` + ScopesSupported []string `json:"scopes_supported"` + ResponseTypesSupported []string `json:"response_types_supported"` + GrantTypesSupported []string `json:"grant_types_supported"` + SubjectTypesSupported []string `json:"subject_types_supported"` + IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"` + TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported"` + ClaimsSupported []string `json:"claims_supported"` + ServiceDocumentation string `json:"service_documentation"` + RequestParameterSupported bool `json:"request_parameter_supported"` + RequestObjectSigningAlgValuesSupported []string `json:"request_object_signing_alg_values_supported"` } type WellKnownControllerConfig struct{} @@ -48,19 +50,21 @@ func (controller *WellKnownController) SetupRoutes() { func (controller *WellKnownController) OpenIDConnectConfiguration(c *gin.Context) { issuer := controller.oidc.GetIssuer() c.JSON(200, OpenIDConnectConfiguration{ - Issuer: issuer, - AuthorizationEndpoint: fmt.Sprintf("%s/authorize", issuer), - TokenEndpoint: fmt.Sprintf("%s/api/oidc/token", issuer), - UserinfoEndpoint: fmt.Sprintf("%s/api/oidc/userinfo", issuer), - JwksUri: fmt.Sprintf("%s/.well-known/jwks.json", issuer), - ScopesSupported: service.SupportedScopes, - ResponseTypesSupported: service.SupportedResponseTypes, - GrantTypesSupported: service.SupportedGrantTypes, - SubjectTypesSupported: []string{"pairwise"}, - IDTokenSigningAlgValuesSupported: []string{"RS256"}, - TokenEndpointAuthMethodsSupported: []string{"client_secret_basic", "client_secret_post"}, - ClaimsSupported: []string{"sub", "updated_at", "name", "preferred_username", "email", "email_verified", "groups"}, - ServiceDocumentation: "https://tinyauth.app/docs/guides/oidc", + Issuer: issuer, + AuthorizationEndpoint: fmt.Sprintf("%s/authorize", issuer), + TokenEndpoint: fmt.Sprintf("%s/api/oidc/token", issuer), + UserinfoEndpoint: fmt.Sprintf("%s/api/oidc/userinfo", issuer), + JwksUri: fmt.Sprintf("%s/.well-known/jwks.json", issuer), + ScopesSupported: service.SupportedScopes, + ResponseTypesSupported: service.SupportedResponseTypes, + GrantTypesSupported: service.SupportedGrantTypes, + SubjectTypesSupported: []string{"pairwise"}, + IDTokenSigningAlgValuesSupported: []string{"RS256"}, + TokenEndpointAuthMethodsSupported: []string{"client_secret_basic", "client_secret_post"}, + ClaimsSupported: []string{"sub", "updated_at", "name", "preferred_username", "email", "email_verified", "groups"}, + ServiceDocumentation: "https://tinyauth.app/docs/guides/oidc", + RequestParameterSupported: true, + RequestObjectSigningAlgValuesSupported: []string{"none"}, }) } diff --git a/internal/controller/well_known_controller_test.go b/internal/controller/well_known_controller_test.go index f956fda..47a0e7e 100644 --- a/internal/controller/well_known_controller_test.go +++ b/internal/controller/well_known_controller_test.go @@ -56,19 +56,21 @@ func TestWellKnownController(t *testing.T) { assert.NoError(t, err) expected := controller.OpenIDConnectConfiguration{ - Issuer: oidcServiceCfg.Issuer, - AuthorizationEndpoint: fmt.Sprintf("%s/authorize", oidcServiceCfg.Issuer), - TokenEndpoint: fmt.Sprintf("%s/api/oidc/token", oidcServiceCfg.Issuer), - UserinfoEndpoint: fmt.Sprintf("%s/api/oidc/userinfo", oidcServiceCfg.Issuer), - JwksUri: fmt.Sprintf("%s/.well-known/jwks.json", oidcServiceCfg.Issuer), - ScopesSupported: service.SupportedScopes, - ResponseTypesSupported: service.SupportedResponseTypes, - GrantTypesSupported: service.SupportedGrantTypes, - SubjectTypesSupported: []string{"pairwise"}, - IDTokenSigningAlgValuesSupported: []string{"RS256"}, - TokenEndpointAuthMethodsSupported: []string{"client_secret_basic", "client_secret_post"}, - ClaimsSupported: []string{"sub", "updated_at", "name", "preferred_username", "email", "email_verified", "groups"}, - ServiceDocumentation: "https://tinyauth.app/docs/guides/oidc", + Issuer: oidcServiceCfg.Issuer, + AuthorizationEndpoint: fmt.Sprintf("%s/authorize", oidcServiceCfg.Issuer), + TokenEndpoint: fmt.Sprintf("%s/api/oidc/token", oidcServiceCfg.Issuer), + UserinfoEndpoint: fmt.Sprintf("%s/api/oidc/userinfo", oidcServiceCfg.Issuer), + JwksUri: fmt.Sprintf("%s/.well-known/jwks.json", oidcServiceCfg.Issuer), + ScopesSupported: service.SupportedScopes, + ResponseTypesSupported: service.SupportedResponseTypes, + GrantTypesSupported: service.SupportedGrantTypes, + SubjectTypesSupported: []string{"pairwise"}, + IDTokenSigningAlgValuesSupported: []string{"RS256"}, + TokenEndpointAuthMethodsSupported: []string{"client_secret_basic", "client_secret_post"}, + ClaimsSupported: []string{"sub", "updated_at", "name", "preferred_username", "email", "email_verified", "groups"}, + ServiceDocumentation: "https://tinyauth.app/docs/guides/oidc", + RequestParameterSupported: true, + RequestObjectSigningAlgValuesSupported: []string{"none"}, } assert.Equal(t, expected, res)