From 42671d3edb0d336794de1e164d147fb742364e11 Mon Sep 17 00:00:00 2001 From: James Elliott Date: Mon, 6 Mar 2023 13:35:58 +1100 Subject: [PATCH] feat(oidc): client_secret_jwt client auth (#5031) This theoretically adds support for client_secret_jwt. --- api/openapi.yml | 171 ++++++++++++++++++++++----------- internal/oidc/config.go | 12 +++ internal/oidc/const.go | 19 ++++ internal/oidc/discovery.go | 26 +++-- internal/oidc/provider_test.go | 56 +++++++---- internal/oidc/types.go | 8 ++ 6 files changed, 210 insertions(+), 82 deletions(-) diff --git a/api/openapi.yml b/api/openapi.yml index 1ed9e32c1..1468e1bdd 100644 --- a/api/openapi.yml +++ b/api/openapi.yml @@ -6,7 +6,7 @@ info: Authelia is an open-source authentication and authorization server providing 2-factor authentication and single sign-on (SSO) for your applications via a web portal. contact: - name: Authelia Support + name: Support url: https://www.authelia.com/contact/ email: team@authelia.com license: @@ -2940,9 +2940,9 @@ components: - "address" - "phone" openid.spec.IntrospectionRequest: - type: object required: - "token" + type: object properties: token: description: > @@ -2952,8 +2952,8 @@ components: this is the "refresh_token" value returned from the token endpoint as defined in OAuth 2.0 [RFC6749], Section 5.1. Other token types are outside the scope of this specification. - type: string example: "authelia_at_cr4i4EtTn2F4k6mX4XzxbsBewkxCGn" + type: string token_type_hint: description: > A hint about the type of the token submitted for @@ -2965,27 +2965,61 @@ components: is able to detect the token type automatically. Values for this field are defined in the "OAuth Token Type Hints" registry defined in OAuth Token Revocation [RFC7009]. - type: string - example: "access_token" enum: - "access_token" - "refresh_token" + example: "access_token" + type: string openid.spec.AccessRequest.ClientAuth: + oneOf: + - $ref: '#/components/schemas/openid.spec.AccessRequest.ClientAuth.Base' + - $ref: '#/components/schemas/openid.spec.AccessRequest.ClientAuth.Secret' + - $ref: '#/components/schemas/openid.spec.AccessRequest.ClientAuth.JWT' + openid.spec.AccessRequest.ClientAuth.Base: + required: + - "client_id" type: object properties: client_id: description: > - REQUIRED if the client is not authenticating with the - authorization server as described in Section 3.2.1. of [RFC6749]. - The client identifier as described in Section 2.2 of [RFC6749]. + REQUIRED if the client is not authenticating with the authorization server as described in + Section 3.2.1. of [RFC6749]. The client identifier as described in Section 2.2 of [RFC6749]. + example: "my_client" type: string - example: "authelia_dc_mn123kjn12kj3123njk" + openid.spec.AccessRequest.ClientAuth.Secret: + required: + - "client_secret" + type: object + properties: client_secret: description: > REQUIRED. The client secret. The client MAY omit the parameter if the client secret is an empty string. - type: string format: password + type: string + openid.spec.AccessRequest.ClientAuth.JWT: + allOf: + - $ref: '#/components/schemas/openid.spec.AccessRequest.ClientAuth.Base' + - type: object + required: + - "client_assertion" + - "client_assertion_type" + properties: + client_assertion: + description: > + The value of the client_assertion_type parameter MUST be + "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" + enum: + - "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" + example: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" + type: string + client_assertion_type: + description: > + A JWT signed with HS256 using the client secret value or RS256 using a registered public key. + Theoretically a properly formed JWT signed using HS256 with the client secret as the HMAC key should + work but this has not been tested. + format: password + type: string openid.spec.AccessRequest.AuthorizationCodeFlow: allOf: - $ref: '#/components/schemas/openid.spec.AccessRequest.ClientAuth' @@ -2995,22 +3029,22 @@ components: - "grant_type" properties: grant_type: - description: Value MUST be set to "urn:ietf:params:oauth:grant-type:device_code". - type: string + description: Value MUST be set to "code". enum: - "authorization_code" + type: string code: description: The Authorization Code. - type: string example: "authelia_ac_1j2kn3knj12n3kj12n" + type: string code_verifier: description: The Authorization Code Verifier (PKCE). - type: string example: "88a25754f7c0b3b3b88cf6cd4e29e8356b160524fdc1cb329a94471825628fd3" + type: string redirect_uri: description: The original Redirect URI used in the Authorization Request. - type: string example: "https://app.example.com/oidc/callback" + type: string openid.spec.AccessRequest.DeviceCodeFlow: allOf: - $ref: '#/components/schemas/openid.spec.AccessRequest.ClientAuth' @@ -3021,13 +3055,13 @@ components: properties: grant_type: description: Value MUST be set to "urn:ietf:params:oauth:grant-type:device_code". - type: string enum: - "urn:ietf:params:oauth:grant-type:device_code" + type: string device_code: description: The Device Authorization Code. - type: string example: "authelia_dc_mn123kjn12kj3123njk" + type: string openid.spec.AccessRequest.RefreshTokenFlow: allOf: - $ref: '#/components/schemas/openid.spec.AccessRequest.ClientAuth' @@ -3038,12 +3072,13 @@ components: properties: grant_type: description: Value MUST be set to "refresh_token". - type: string enum: - "refresh_token" + type: string refresh_token: description: The Refresh Token. example: "authelia_rt_1n2j3kihn12kj3n12k" + type: string scope: description: > The scope of the access request as described by @@ -3051,20 +3086,30 @@ components: not originally granted by the resource owner, and if omitted is treated as equal to the scope originally granted by the resource owner. + example: "openid profile groups" + type: string openid.spec.AccessResponse: type: object + required: + - "access_token" + - "token_type" + - "expires_in" properties: access_token: description: The access token issued by the authorization server. - type: string example: "authelia_at_cr4i4EtTn2F4k6mX4XzxbsBewkxCGn" - refresh_token: type: string + id_token: + description: The id token issued by the authorization server. + example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" + type: string + refresh_token: description: > The refresh token, which can be used to obtain new access tokens using the same authorization grant as described in Section 6. - token_type: + example: "authelia_rt_kGBoSMbfVGP2RR6Kvujv3Xg7uXV2i" type: string + token_type: description: > The access token type provides the client with the information required to successfully utilize the access token to make a protected @@ -3073,21 +3118,26 @@ components: type. enum: - "bearer" + example: "bearer" + type: string expires_in: - type: integer description: > The lifetime in seconds of the access token. For example, the value "3600" denotes that the access token will expire in one hour from the time the response was generated. If omitted, the authorization server SHOULD provide the expiration time via other means or document the default value. + example: 3600 + type: integer state: - type: string description: Exactly the state value passed in the authorization request if present. - scope: + example: "5dVZhNfri5XZS6wadskuzUk4MHYCvEcUgidjMeBjsktAhY7EKB" type: string + scope: description: > The scope of the access token as described by Section 3.3 if it differs from the requested scope. + example: "openid profile groups" + type: string openid.spec.AuthorizeRequest: type: object required: @@ -3098,14 +3148,14 @@ components: properties: scope: description: The requested scope. - type: string example: "openid profile groups" + type: string response_type: $ref: '#/components/schemas/openid.spec.ResponseType' client_id: description: The OAuth 2.0 client identifier. - type: string example: "app" + type: string redirect_uri: description: > Redirection URI to which the response will be sent. This URI MUST exactly match one of the @@ -3115,15 +3165,15 @@ components: that the Client Type is confidential, as defined in Section 2.1 of OAuth 2.0, and provided the OP allows the use of http Redirection URIs in this case. The Redirection URI MAY use an alternate scheme, such as one that is intended to identify a callback into a native application. - type: string example: "https://app.example.com" + type: string state: description: > Opaque value used to maintain state between the request and the callback. Typically, Cross-Site Request Forgery (CSRF, XSRF) mitigation is done by cryptographically binding the value of this parameter with a browser cookie. - type: string example: "oV84Vsy7wyCgRk2h4aZBmXZq4q3g2f" + type: string response_mode: $ref: '#/components/schemas/openid.spec.ResponseMode' nonce: @@ -3132,14 +3182,23 @@ components: The value is passed through unmodified from the Authentication Request to the ID Token. Sufficient entropy MUST be present in the nonce values used to prevent attackers from guessing values. For implementation notes, see Section 15.5.2. - type: string example: "TRMLqchoKGQNcooXvBvUy9PtmLdJGf" + type: string display: $ref: '#/components/schemas/openid.spec.DisplayType' prompt: description: > Not Supported: Space delimited, case sensitive list of ASCII string values that specifies whether the Authorization Server prompts the End-User for reauthentication and consent. + enum: + - "none" + - "login" + - "consent" + - "select_account" + - "login consent" + - "login select_account" + - "consent select_account" + example: "consent" type: string max_age: description: > @@ -3217,34 +3276,32 @@ components: description: > A Subject Identifier is a locally unique and never reassigned identifier within the Issuer for the End-User, which is intended to be consumed by the Client. - type: string enum: - "public" - "pairwise" + type: string openid.spec.ClientAuthMethod: description: The OAuth 2.0 / OpenID Connect 1.0 Client Authentication Method. - type: string enum: - "client_secret_basic" - "client_secret_post" - "client_secret_jwt" - "private_key_jwt" - "none" + type: string openid.spec.DisplayType: description: > ASCII string value that specifies how the Authorization Server displays the authentication and consent user interface pages to the End-User. - type: string - example: "page" enum: - "page" - "popup" - "touch" - "wap" + example: "page" + type: string openid.spec.ResponseType: description: The OAuth 2.0 / OpenID Connect 1.0 Response Type. - type: string - example: "code" enum: - "code" - "id_token" @@ -3254,21 +3311,21 @@ components: - "token id_token" - "code id_token token" - "none" + example: "code" + type: string openid.spec.ResponseMode: description: > Informs the Authorization Server of the mechanism to be used for returning parameters from the Authorization Endpoint. This use of this parameter is NOT RECOMMENDED when the Response Mode that would be requested is the default mode specified for the Response Type. - type: string - example: "query" enum: - "query" - "fragment" - "form_post" + example: "query" + type: string openid.spec.GrantType: description: The OAuth 2.0 / OpenID Connect 1.0 Grant Type. - type: string - example: "authorization_code" enum: - "authorization_code" - "refresh_token" @@ -3276,21 +3333,23 @@ components: - "password" - "client_credentials" - "urn:ietf:params:oauth:grant-type:device_code" + example: "authorization_code" + type: string openid.spec.CodeChallengeMethod: description: The RFC7636 Code Challenge Verifier Method. - type: string - example: "S256" enum: - "plain" - "S256" + example: "S256" + type: string openid.spec.ClaimType: description: The representation of claims. - type: string - example: "normal" enum: - "normal" - "aggregated" - "distributed" + example: "normal" + type: string jose.spec.None: description: The JSON Web Signature Algorithm type: string @@ -3298,13 +3357,12 @@ components: - "none" jose.spec.JWS.None: description: The JSON Web Signature Algorithm - type: string oneOf: - $ref: '#/components/schemas/jose.spec.None' - $ref: '#/components/schemas/jose.spec.jws' + type: string jose.spec.jws: description: The JSON Web Signature Algorithm - type: string enum: - "HS256" - "HS384" @@ -3318,9 +3376,9 @@ components: - "PS256" - "PS384" - "PS512" + type: string jose.spec.JWE.alg: description: The JSON Web Encryption Algorithm (CEK) - type: string enum: - "RSA1_5" - "RSA-OAEP" @@ -3339,9 +3397,9 @@ components: - "PBES2-HS256+A128KW" - "PBES2-HS384+A192KW" - "PBES2-HS512+A256KW" + type: string jose.spec.JWE.enc: description: The JSON Web Encryption Algorithm (Claims) - type: string enum: - "A128CBC-HS256" - "A192CBC-HS384" @@ -3350,6 +3408,7 @@ components: - "A256CBC" - "A128GCM" - "A256GCM" + type: string jose.spec.JWK.base: type: object properties: @@ -3359,21 +3418,20 @@ components: the public key. The "use" parameter is employed to indicate whether a public key is used for encrypting data or verifying the signature on data. - type: string - example: "sig" enum: - "sig" - "enc" + example: "sig" + type: string key_ops: description: > The "key_ops" (key operations) parameter identifies the operation(s) for which the key is intended to be used. The "key_ops" parameter is intended for use cases in which public, private, or symmetric keys may be present. - type: array example: ["sign"] + type: array items: - type: string enum: - "sign" - "verify" @@ -3383,6 +3441,7 @@ components: - "unwrapKey" - "deriveKey" - "deriveBits" + type: string kid: description: > The "kid" (key ID) parameter is used to match a specific key. This @@ -3427,8 +3486,8 @@ components: OPTIONAL. type: array items: - type: string format: byte + type: string x5t: description: > The "x5t" (X.509 certificate SHA-1 thumbprint) parameter is a @@ -3437,8 +3496,8 @@ components: thumbprints are also sometimes known as certificate fingerprints. The key in the certificate MUST match the public key represented by other members of the JWK. Use of this member is OPTIONAL. - type: string format: byte + type: string x5t#S256: description: > The "x5t#S256" (X.509 certificate SHA-256 thumbprint) parameter is a @@ -3447,17 +3506,17 @@ components: thumbprints are also sometimes known as certificate fingerprints. The key in the certificate MUST match the public key represented by other members of the JWK. Use of this member is OPTIONAL. - type: string format: byte + type: string jose.spec.JWK.RSA: description: RSA Public Key in JSON Web Key format as defined by RFC7517 and RFC7518. allOf: - $ref: '#/components/schemas/jose.spec.JWK.base' - - type: object - required: + - required: - "kty" - "n" - "e" + type: object properties: kty: description: > diff --git a/internal/oidc/config.go b/internal/oidc/config.go index 7a5110b09..695086c74 100644 --- a/internal/oidc/config.go +++ b/internal/oidc/config.go @@ -6,6 +6,7 @@ import ( "hash" "html/template" "net/url" + "path" "time" "github.com/hashicorp/go-retryablehttp" @@ -515,6 +516,17 @@ func (c *Config) GetFormPostHTMLTemplate(ctx context.Context) (tmpl *template.Te // GetTokenURL returns the token URL. func (c *Config) GetTokenURL(ctx context.Context) (tokenURL string) { + if ctx, ok := ctx.(OpenIDConnectContext); ok { + tokenURI, err := ctx.IssuerURL() + if err != nil { + return c.TokenURL + } + + tokenURI.Path = path.Join(tokenURI.Path, EndpointPathToken) + + return tokenURI.String() + } + return c.TokenURL } diff --git a/internal/oidc/const.go b/internal/oidc/const.go index 9c8fa6942..72598828b 100644 --- a/internal/oidc/const.go +++ b/internal/oidc/const.go @@ -73,6 +73,25 @@ const ( GrantTypeClientCredentials = "client_credentials" ) +// Client Auth Method strings. +const ( + ClientAuthMethodClientSecretBasic = "client_secret_basic" + ClientAuthMethodClientSecretPost = "client_secret_post" + ClientAuthMethodClientSecretJWT = "client_secret_jwt" + ClientAuthMethodNone = "none" +) + +// Response Type strings. +const ( + ResponseTypeAuthorizationCodeFlow = "code" + ResponseTypeImplicitFlowIDToken = "id_token" + ResponseTypeImplicitFlowToken = "token" + ResponseTypeImplicitFlowBoth = "id_token token" + ResponseTypeHybridFlowIDToken = "code id_token" + ResponseTypeHybridFlowToken = "code token" + ResponseTypeHybridFlowBoth = "code id_token token" +) + // Signing Algorithm strings. const ( SigningAlgorithmNone = none diff --git a/internal/oidc/discovery.go b/internal/oidc/discovery.go index 5f311c030..2890a71d9 100644 --- a/internal/oidc/discovery.go +++ b/internal/oidc/discovery.go @@ -8,14 +8,18 @@ func NewOpenIDConnectWellKnownConfiguration(enablePKCEPlainChallenge bool, clien SubjectTypePublic, }, ResponseTypesSupported: []string{ - "code", - "token", - "id_token", - "code token", - "code id_token", - "token id_token", - "code token id_token", - "none", + ResponseTypeAuthorizationCodeFlow, + ResponseTypeImplicitFlowIDToken, + ResponseTypeImplicitFlowToken, + ResponseTypeImplicitFlowBoth, + ResponseTypeHybridFlowIDToken, + ResponseTypeHybridFlowToken, + ResponseTypeHybridFlowBoth, + }, + GrantTypesSupported: []string{ + GrantTypeAuthorizationCode, + GrantTypeImplicit, + GrantTypeRefreshToken, }, ResponseModesSupported: []string{ ResponseModeFormPost, @@ -49,6 +53,12 @@ func NewOpenIDConnectWellKnownConfiguration(enablePKCEPlainChallenge bool, clien ClaimPreferredUsername, ClaimFullName, }, + TokenEndpointAuthMethodsSupported: []string{ + ClientAuthMethodClientSecretBasic, + ClientAuthMethodClientSecretPost, + ClientAuthMethodClientSecretJWT, + ClientAuthMethodNone, + }, }, OAuth2DiscoveryOptions: OAuth2DiscoveryOptions{ CodeChallengeMethodsSupported: []string{ diff --git a/internal/oidc/provider_test.go b/internal/oidc/provider_test.go index 85ab216ac..3045c6fc3 100644 --- a/internal/oidc/provider_test.go +++ b/internal/oidc/provider_test.go @@ -142,15 +142,25 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOpenIDConnectWellKnow assert.Len(t, disco.SubjectTypesSupported, 1) assert.Contains(t, disco.SubjectTypesSupported, SubjectTypePublic) - assert.Len(t, disco.ResponseTypesSupported, 8) - assert.Contains(t, disco.ResponseTypesSupported, "code") - assert.Contains(t, disco.ResponseTypesSupported, "token") - assert.Contains(t, disco.ResponseTypesSupported, "id_token") - assert.Contains(t, disco.ResponseTypesSupported, "code token") - assert.Contains(t, disco.ResponseTypesSupported, "code id_token") - assert.Contains(t, disco.ResponseTypesSupported, "token id_token") - assert.Contains(t, disco.ResponseTypesSupported, "code token id_token") - assert.Contains(t, disco.ResponseTypesSupported, "none") + assert.Len(t, disco.ResponseTypesSupported, 7) + assert.Contains(t, disco.ResponseTypesSupported, ResponseTypeAuthorizationCodeFlow) + assert.Contains(t, disco.ResponseTypesSupported, ResponseTypeImplicitFlowIDToken) + assert.Contains(t, disco.ResponseTypesSupported, ResponseTypeImplicitFlowToken) + assert.Contains(t, disco.ResponseTypesSupported, ResponseTypeImplicitFlowBoth) + assert.Contains(t, disco.ResponseTypesSupported, ResponseTypeHybridFlowIDToken) + assert.Contains(t, disco.ResponseTypesSupported, ResponseTypeHybridFlowToken) + assert.Contains(t, disco.ResponseTypesSupported, ResponseTypeHybridFlowBoth) + + assert.Len(t, disco.TokenEndpointAuthMethodsSupported, 4) + assert.Contains(t, disco.TokenEndpointAuthMethodsSupported, ClientAuthMethodClientSecretBasic) + assert.Contains(t, disco.TokenEndpointAuthMethodsSupported, ClientAuthMethodClientSecretPost) + assert.Contains(t, disco.TokenEndpointAuthMethodsSupported, ClientAuthMethodClientSecretJWT) + assert.Contains(t, disco.TokenEndpointAuthMethodsSupported, ClientAuthMethodNone) + + assert.Len(t, disco.GrantTypesSupported, 3) + assert.Contains(t, disco.GrantTypesSupported, GrantTypeAuthorizationCode) + assert.Contains(t, disco.GrantTypesSupported, GrantTypeRefreshToken) + assert.Contains(t, disco.GrantTypesSupported, GrantTypeImplicit) assert.Len(t, disco.IDTokenSigningAlgValuesSupported, 1) assert.Contains(t, disco.IDTokenSigningAlgValuesSupported, SigningAlgorithmRSAWithSHA256) @@ -231,15 +241,25 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOAuth2WellKnownConfig assert.Len(t, disco.SubjectTypesSupported, 1) assert.Contains(t, disco.SubjectTypesSupported, SubjectTypePublic) - assert.Len(t, disco.ResponseTypesSupported, 8) - assert.Contains(t, disco.ResponseTypesSupported, "code") - assert.Contains(t, disco.ResponseTypesSupported, "token") - assert.Contains(t, disco.ResponseTypesSupported, "id_token") - assert.Contains(t, disco.ResponseTypesSupported, "code token") - assert.Contains(t, disco.ResponseTypesSupported, "code id_token") - assert.Contains(t, disco.ResponseTypesSupported, "token id_token") - assert.Contains(t, disco.ResponseTypesSupported, "code token id_token") - assert.Contains(t, disco.ResponseTypesSupported, "none") + assert.Len(t, disco.ResponseTypesSupported, 7) + assert.Contains(t, disco.ResponseTypesSupported, ResponseTypeAuthorizationCodeFlow) + assert.Contains(t, disco.ResponseTypesSupported, ResponseTypeImplicitFlowIDToken) + assert.Contains(t, disco.ResponseTypesSupported, ResponseTypeImplicitFlowToken) + assert.Contains(t, disco.ResponseTypesSupported, ResponseTypeImplicitFlowBoth) + assert.Contains(t, disco.ResponseTypesSupported, ResponseTypeHybridFlowIDToken) + assert.Contains(t, disco.ResponseTypesSupported, ResponseTypeHybridFlowToken) + assert.Contains(t, disco.ResponseTypesSupported, ResponseTypeHybridFlowBoth) + + assert.Len(t, disco.TokenEndpointAuthMethodsSupported, 4) + assert.Contains(t, disco.TokenEndpointAuthMethodsSupported, ClientAuthMethodClientSecretBasic) + assert.Contains(t, disco.TokenEndpointAuthMethodsSupported, ClientAuthMethodClientSecretPost) + assert.Contains(t, disco.TokenEndpointAuthMethodsSupported, ClientAuthMethodClientSecretJWT) + assert.Contains(t, disco.TokenEndpointAuthMethodsSupported, ClientAuthMethodNone) + + assert.Len(t, disco.GrantTypesSupported, 3) + assert.Contains(t, disco.GrantTypesSupported, GrantTypeAuthorizationCode) + assert.Contains(t, disco.GrantTypesSupported, GrantTypeRefreshToken) + assert.Contains(t, disco.GrantTypesSupported, GrantTypeImplicit) assert.Len(t, disco.ClaimsSupported, 18) assert.Contains(t, disco.ClaimsSupported, ClaimAuthenticationMethodsReference) diff --git a/internal/oidc/types.go b/internal/oidc/types.go index d2782220c..471a4da14 100644 --- a/internal/oidc/types.go +++ b/internal/oidc/types.go @@ -1,6 +1,7 @@ package oidc import ( + "context" "net/url" "time" @@ -643,3 +644,10 @@ type OpenIDConnectWellKnownConfiguration struct { OpenIDConnectFrontChannelLogoutDiscoveryOptions OpenIDConnectBackChannelLogoutDiscoveryOptions } + +// OpenIDConnectContext represents the context implementation that is used by some OpenID Connect 1.0 implementations. +type OpenIDConnectContext interface { + context.Context + + IssuerURL() (issuerURL *url.URL, err error) +}