Skip to content

Commit

Permalink
feat(oidc): signed discovery (#6003)
Browse files Browse the repository at this point in the history
Optionally adds the signed_metadata value to the OAuth 2.0 Authorization Server Metadata and OpenID Connect Discovery 1.0 documents.

Signed-off-by: James Elliott <james-d-elliott@users.noreply.github.com>
  • Loading branch information
james-d-elliott committed Mar 6, 2024
1 parent ad05f22 commit 357ce8e
Show file tree
Hide file tree
Showing 10 changed files with 269 additions and 74 deletions.
12 changes: 12 additions & 0 deletions config.template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1301,6 +1301,18 @@ notifier:
## for security reasons.
# enforce_pkce: 'public_clients_only'

## SECURITY NOTICE: It's not recommended changing this option. We encourage you to read the documentation and fully
## understanding it before enabling this option.
# enable_jwt_access_token_stateless_introspection: false

## The signing algorithm used for signing the discovery and metadata responses. An issuer JWK with a matching
## algorithm must be available when configured. Most clients completely ignore this and it has a performance cost.
# discovery_signed_response_alg: 'none'

## The signing key id used for signing the discovery and metadata responses. An issuer JWK with a matching key id
## must be available when configured. Most clients completely ignore this and it has a performance cost.
# discovery_signed_response_key_id: ''

## Authorization Policies which can be utilized by clients. The 'policy_name' is an arbitrary value that you pick
## which is utilized as the value for the 'authorization_policy' on the client.
# authorization_policies:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ identity_providers:
minimum_parameter_entropy: 8
enforce_pkce: 'public_clients_only'
enable_pkce_plain_challenge: false
enable_jwt_access_token_stateless_introspection: false
discovery_signed_response_alg: 'none'
discovery_signed_response_key_id: ''
pushed_authorizations:
enforce: false
context_lifespan: '5m'
Expand Down Expand Up @@ -203,7 +206,6 @@ identity_providers:
-----END CERTIFICATE-----
```


#### key_id

{{< confkey type="string" default="<thumbprint of public key>" required="no" >}}
Expand Down Expand Up @@ -344,6 +346,43 @@ A client with an [access_token_signed_response_alg](clients.md#access_token_sign
[access_token_signed_response_key_id](clients.md#access_token_signed_response_key_id) must be configured for this option to
be enabled.

### discovery_signed_response_alg

{{< confkey type="string" default="none" required="no" >}}

_**Important Note:** Many clients do not support this option and it has a performance cost. It's therefore recommended
unless you have a specific need that you do not enable this option._

_**Note:** This value is completely ignored if the
[discovery_signed_response_key_id](#discovery_signed_response_key_id) is defined._

The algorithm used to sign the [OAuth 2.0 Authorization Server Metadata] and [OpenID Connect Discovery 1.0] responses.
Per the specifications this Signed JSON Web Token is stored in the `signed_metadata` value using the compact encoding.

See the response object section of the
[integration guide](../../../integration/openid-connect/introduction.md#response-object) for more information including
the algorithm column for supported values.

With the exclusion of `none` which excludes the `signed_metadata` value, the algorithm chosen must have a key
configured in the [jwks](#jwks) section to be considered valid.

See the response object section of the [integration guide](../../../integration/openid-connect/introduction.md#response-object)
for more information including the algorithm column for supported values.

### discovery_signed_response_key_id

{{< confkey type="string" required="no" >}}

_**Important Note:** Many clients do not support this option and it has a performance cost. It's therefore recommended
unless you have a specific need that you do not enable this option._

_**Note:** This value automatically configures the [discovery_signed_response_alg](#discovery_signed_response_alg)
value with the algorithm of the specified key._

The algorithm used to sign the [OAuth 2.0 Authorization Server Metadata] and [OpenID Connect Discovery 1.0] responses.
The value of this must one of those provided or calculated in the [jwks](#jwks). Per the specifications this Signed JSON
Web Token is stored in the `signed_metadata` value using the compact encoding.

### pushed_authorizations

Controls the behaviour of [Pushed Authorization Requests].
Expand Down Expand Up @@ -524,7 +563,7 @@ identity_providers:

### cors

Some [OpenID Connect 1.0] Endpoints need to allow cross-origin resource sharing, however some are optional. This section allows
Some [OpenID Connect 1.0] Endpoints need to allow cross-origin resource sharing; however, some are optional. This section allows
you to configure the optional parts. We reply with CORS headers when the request includes the Origin header.

#### endpoints
Expand Down Expand Up @@ -592,6 +631,8 @@ To integrate Authelia's [OpenID Connect 1.0] implementation with a relying party

[token lifespan]: https://docs.apigee.com/api-platform/antipatterns/oauth-long-expiration
[OpenID Connect 1.0]: https://openid.net/connect/
[OAuth 2.0 Authorization Server Metadata]: https://oauth.net/2/authorization-server-metadata/
[OpenID Connect Discovery 1.0]: https://openid.net/specs/openid-connect-discovery-1_0.html
[Token Endpoint]: https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint
[JWT]: https://datatracker.ietf.org/doc/html/rfc7519
[RFC6234]: https://datatracker.ietf.org/doc/html/rfc6234
Expand Down
12 changes: 12 additions & 0 deletions internal/configuration/config.template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1301,6 +1301,18 @@ notifier:
## for security reasons.
# enforce_pkce: 'public_clients_only'

## SECURITY NOTICE: It's not recommended changing this option. We encourage you to read the documentation and fully
## understanding it before enabling this option.
# enable_jwt_access_token_stateless_introspection: false

## The signing algorithm used for signing the discovery and metadata responses. An issuer JWK with a matching
## algorithm must be available when configured. Most clients completely ignore this and it has a performance cost.
# discovery_signed_response_alg: 'none'

## The signing key id used for signing the discovery and metadata responses. An issuer JWK with a matching key id
## must be available when configured. Most clients completely ignore this and it has a performance cost.
# discovery_signed_response_key_id: ''

## Authorization Policies which can be utilized by clients. The 'policy_name' is an arbitrary value that you pick
## which is utilized as the value for the 'authorization_policy' on the client.
# authorization_policies:
Expand Down
9 changes: 6 additions & 3 deletions internal/configuration/schema/identity_providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@ type IdentityProvidersOpenIDConnect struct {
HMACSecret string `koanf:"hmac_secret" json:"hmac_secret" jsonschema:"title=HMAC Secret" jsonschema_description:"The HMAC Secret used to sign Access Tokens."`
JSONWebKeys []JWK `koanf:"jwks" json:"jwks" jsonschema:"title=Issuer JSON Web Keys" jsonschema_description:"The JWK's which are to be used to sign various objects like ID Tokens."`

IssuerCertificateChain X509CertificateChain `koanf:"issuer_certificate_chain" json:"issuer_certificate_chain" jsonschema:"title=Issuer Certificate Chain,deprecated" jsonschema_description:"The Issuer Certificate Chain with an RSA Public Key used to sign ID Tokens."`
IssuerPrivateKey *rsa.PrivateKey `koanf:"issuer_private_key" json:"issuer_private_key" jsonschema:"title=Issuer Private Key,deprecated" jsonschema_description:"The Issuer Private Key with an RSA Private Key used to sign ID Tokens."`

EnableClientDebugMessages bool `koanf:"enable_client_debug_messages" json:"enable_client_debug_messages" jsonschema:"default=false,title=Enable Client Debug Messages" jsonschema_description:"Enables additional debug messages for clients."`
MinimumParameterEntropy int `koanf:"minimum_parameter_entropy" json:"minimum_parameter_entropy" jsonschema:"default=8,minimum=-1,title=Minimum Parameter Entropy" jsonschema_description:"The minimum entropy of the nonce parameter."`

Expand All @@ -27,6 +24,9 @@ type IdentityProvidersOpenIDConnect struct {

EnableJWTAccessTokenStatelessIntrospection bool `koanf:"enable_jwt_access_token_stateless_introspection" json:"enable_jwt_access_token_stateless_introspection" jsonschema:"title=Enable JWT Access Token Stateless Introspection" jsonschema_description:"Allows the use of stateless introspection of JWT Access Tokens which is not recommended."`

DiscoverySignedResponseAlg string `koanf:"discovery_signed_response_alg" json:"discovery_signed_response_alg" jsonschema:"default=none,enum=none,enum=RS256,enum=RS384,enum=RS512,enum=ES256,enum=ES384,enum=ES512,enum=PS256,enum=PS384,enum=PS512,title=Discovery Response Signing Algorithm" jsonschema_description:"The Algorithm this provider uses to sign the Discovery and Metadata Document responses."`
DiscoverySignedResponseKeyID string `koanf:"discovery_signed_response_key_id" json:"discovery_signed_response_key_id" jsonschema:"title=Discovery Response Signing Key ID" jsonschema_description:"The Key ID this provider uses to sign the Discovery and Metadata Document responses (overrides the 'discovery_signed_response_alg')."`

PAR IdentityProvidersOpenIDConnectPAR `koanf:"pushed_authorizations" json:"pushed_authorizations" jsonschema:"title=Pushed Authorizations" jsonschema_description:"Configuration options for Pushed Authorization Requests."`
CORS IdentityProvidersOpenIDConnectCORS `koanf:"cors" json:"cors" jsonschema:"title=CORS" jsonschema_description:"Configuration options for Cross-Origin Request Sharing."`

Expand All @@ -36,6 +36,9 @@ type IdentityProvidersOpenIDConnect struct {
Lifespans IdentityProvidersOpenIDConnectLifespans `koanf:"lifespans" json:"lifespans" jsonschema:"title=Lifespans" jsonschema_description:"Token lifespans configuration."`

Discovery IdentityProvidersOpenIDConnectDiscovery `json:"-"` // MetaData value. Not configurable by users.

IssuerCertificateChain X509CertificateChain `koanf:"issuer_certificate_chain" json:"issuer_certificate_chain" jsonschema:"title=Issuer Certificate Chain,deprecated" jsonschema_description:"The Issuer Certificate Chain with an RSA Public Key used to sign ID Tokens."`
IssuerPrivateKey *rsa.PrivateKey `koanf:"issuer_private_key" json:"issuer_private_key" jsonschema:"title=Issuer Private Key,deprecated" jsonschema_description:"The Issuer Private Key with an RSA Private Key used to sign ID Tokens."`
}

// IdentityProvidersOpenIDConnectPolicy configuration for OpenID Connect 1.0 authorization policies.
Expand Down
11 changes: 10 additions & 1 deletion internal/configuration/validator/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ const (
schemeHTTPS = "https"
)

// General fmt consts.
const (
errFmtMustBeOneOf = "'%s' must be one of %s but it's configured as '%s'"
)

// Notifier Error constants.
const (
errFmtNotifierMultipleConfigured = "notifier: please ensure only one of the 'smtp' or 'filesystem' notifier is configured"
Expand Down Expand Up @@ -179,6 +184,8 @@ const (
errFmtOIDCProviderPrivateKeysKeyCertificateMismatch = "identity_providers: oidc: jwks: key #%d with key id '%s': option 'certificate_chain' does not appear to contain the public key for the private key provided by option 'key'"
errFmtOIDCProviderPrivateKeysCertificateChainInvalid = "identity_providers: oidc: jwks: key #%d with key id '%s': option 'certificate_chain' produced an error during validation of the chain: %w"
errFmtOIDCProviderPrivateKeysNoRS256 = "identity_providers: oidc: jwks: keys: must at least have one key supporting the '%s' algorithm but only has %s"
errFmtOIDCProviderInvalidValue = "identity_providers: oidc: option " +
errFmtMustBeOneOf

errFmtOIDCCORSInvalidOrigin = "identity_providers: oidc: cors: option 'allowed_origins' contains an invalid value '%s' as it has a %s: origins must only be scheme, hostname, and an optional port"
errFmtOIDCCORSInvalidOriginWildcard = "identity_providers: oidc: cors: option 'allowed_origins' contains the wildcard origin '*' with more than one origin but the wildcard origin must be defined by itself"
Expand Down Expand Up @@ -236,7 +243,7 @@ const (
"%s however when utilizing the 'client_credentials' value for the 'grant_types' the values %s are not allowed"
errFmtOIDCClientInvalidEntryDuplicates = errFmtOIDCClientOption + "'%s' must have unique values but the values %s are duplicated"
errFmtOIDCClientInvalidValue = errFmtOIDCClientOption +
"'%s' must be one of %s but it's configured as '%s'"
errFmtMustBeOneOf
errFmtOIDCClientInvalidLifespan = errFmtOIDCClientOption +
"'lifespan' must not be configured when no custom lifespans are configured but it's configured as '%s'"
errFmtOIDCClientInvalidTokenEndpointAuthMethod = errFmtOIDCClientOption +
Expand Down Expand Up @@ -508,6 +515,8 @@ const (
attrOIDCGrantTypes = "grant_types"
attrOIDCRedirectURIs = "redirect_uris"
attrOIDCTokenAuthMethod = "token_endpoint_auth_method"
attrOIDCDiscoSigAlg = "discovery_signed_response_alg"
attrOIDCDiscoSigKID = "discovery_signed_response_key_id"
attrOIDCUsrSigAlg = "userinfo_signed_response_alg"
attrOIDCUsrSigKID = "userinfo_signed_response_key_id"
attrOIDCIntrospectionSigAlg = "introspection_signed_response_alg"
Expand Down
35 changes: 29 additions & 6 deletions internal/configuration/validator/identity_providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,34 @@ func validateOIDCIssuer(config *schema.IdentityProvidersOpenIDConnect, validator
fallthrough
case len(config.JSONWebKeys) != 0:
validateOIDCIssuerJSONWebKeys(config, validator)
validateOIDDIssuerSigningAlgsDiscovery(config, validator)
default:
validator.Push(fmt.Errorf(errFmtOIDCProviderNoPrivateKey))
}
}

func validateOIDDIssuerSigningAlgsDiscovery(config *schema.IdentityProvidersOpenIDConnect, validator *schema.StructValidator) {
config.DiscoverySignedResponseAlg, config.DiscoverySignedResponseKeyID = validateOIDCAlgKIDDefault(config, config.DiscoverySignedResponseAlg, config.DiscoverySignedResponseKeyID, schema.DefaultOpenIDConnectConfiguration.DiscoverySignedResponseAlg)

switch config.DiscoverySignedResponseKeyID {
case "":
switch config.DiscoverySignedResponseAlg {
case "", oidc.SigningAlgNone, oidc.SigningAlgRSAUsingSHA256:
break
default:
if !utils.IsStringInSlice(config.DiscoverySignedResponseAlg, config.Discovery.ResponseObjectSigningAlgs) {
validator.Push(fmt.Errorf(errFmtOIDCProviderInvalidValue, attrOIDCDiscoSigAlg, strJoinOr(append(config.Discovery.ResponseObjectSigningAlgs, oidc.SigningAlgNone)), config.DiscoverySignedResponseAlg))
}
}
default:
if !utils.IsStringInSlice(config.DiscoverySignedResponseKeyID, config.Discovery.ResponseObjectSigningKeyIDs) {
validator.Push(fmt.Errorf(errFmtOIDCProviderInvalidValue, attrOIDCDiscoSigKID, strJoinOr(config.Discovery.ResponseObjectSigningKeyIDs), config.DiscoverySignedResponseKeyID))
} else {
config.DiscoverySignedResponseAlg = getResponseObjectAlgFromKID(config, config.DiscoverySignedResponseKeyID, config.DiscoverySignedResponseAlg)
}
}
}

func validateOIDCIssuerPrivateKey(config *schema.IdentityProvidersOpenIDConnect) {
config.JSONWebKeys = append([]schema.JWK{{
Algorithm: oidc.SigningAlgRSAUsingSHA256,
Expand Down Expand Up @@ -1006,7 +1029,7 @@ func validateOIDDClientSigningAlgs(c int, config *schema.IdentityProvidersOpenID
}

func validateOIDDClientSigningAlgsIDToken(c int, config *schema.IdentityProvidersOpenIDConnect, validator *schema.StructValidator) {
config.Clients[c].IDTokenSignedResponseAlg, config.Clients[c].IDTokenSignedResponseKeyID = validateOIDCClientAlgKIDDefault(config, config.Clients[c].IDTokenSignedResponseAlg, config.Clients[c].IDTokenSignedResponseKeyID, schema.DefaultOpenIDConnectClientConfiguration.IDTokenSignedResponseAlg)
config.Clients[c].IDTokenSignedResponseAlg, config.Clients[c].IDTokenSignedResponseKeyID = validateOIDCAlgKIDDefault(config, config.Clients[c].IDTokenSignedResponseAlg, config.Clients[c].IDTokenSignedResponseKeyID, schema.DefaultOpenIDConnectClientConfiguration.IDTokenSignedResponseAlg)

switch config.Clients[c].IDTokenSignedResponseKeyID {
case "":
Expand All @@ -1030,7 +1053,7 @@ func validateOIDDClientSigningAlgsIDToken(c int, config *schema.IdentityProvider
}

func validateOIDDClientSigningAlgsAccessToken(c int, config *schema.IdentityProvidersOpenIDConnect, validator *schema.StructValidator) {
config.Clients[c].AccessTokenSignedResponseAlg, config.Clients[c].AccessTokenSignedResponseKeyID = validateOIDCClientAlgKIDDefault(config, config.Clients[c].AccessTokenSignedResponseAlg, config.Clients[c].AccessTokenSignedResponseKeyID, schema.DefaultOpenIDConnectClientConfiguration.AccessTokenSignedResponseAlg)
config.Clients[c].AccessTokenSignedResponseAlg, config.Clients[c].AccessTokenSignedResponseKeyID = validateOIDCAlgKIDDefault(config, config.Clients[c].AccessTokenSignedResponseAlg, config.Clients[c].AccessTokenSignedResponseKeyID, schema.DefaultOpenIDConnectClientConfiguration.AccessTokenSignedResponseAlg)

switch config.Clients[c].AccessTokenSignedResponseKeyID {
case "":
Expand Down Expand Up @@ -1058,7 +1081,7 @@ func validateOIDDClientSigningAlgsAccessToken(c int, config *schema.IdentityProv
}

func validateOIDDClientSigningAlgsUserInfo(c int, config *schema.IdentityProvidersOpenIDConnect, validator *schema.StructValidator) {
config.Clients[c].UserinfoSignedResponseAlg, config.Clients[c].UserinfoSignedResponseKeyID = validateOIDCClientAlgKIDDefault(config, config.Clients[c].UserinfoSignedResponseAlg, config.Clients[c].UserinfoSignedResponseKeyID, schema.DefaultOpenIDConnectClientConfiguration.UserinfoSignedResponseAlg)
config.Clients[c].UserinfoSignedResponseAlg, config.Clients[c].UserinfoSignedResponseKeyID = validateOIDCAlgKIDDefault(config, config.Clients[c].UserinfoSignedResponseAlg, config.Clients[c].UserinfoSignedResponseKeyID, schema.DefaultOpenIDConnectClientConfiguration.UserinfoSignedResponseAlg)

switch config.Clients[c].UserinfoSignedResponseKeyID {
case "":
Expand All @@ -1082,7 +1105,7 @@ func validateOIDDClientSigningAlgsUserInfo(c int, config *schema.IdentityProvide
}

func validateOIDDClientSigningAlgsIntrospection(c int, config *schema.IdentityProvidersOpenIDConnect, validator *schema.StructValidator) {
config.Clients[c].IntrospectionSignedResponseAlg, config.Clients[c].IntrospectionSignedResponseKeyID = validateOIDCClientAlgKIDDefault(config, config.Clients[c].IntrospectionSignedResponseAlg, config.Clients[c].IntrospectionSignedResponseKeyID, schema.DefaultOpenIDConnectClientConfiguration.IntrospectionSignedResponseAlg)
config.Clients[c].IntrospectionSignedResponseAlg, config.Clients[c].IntrospectionSignedResponseKeyID = validateOIDCAlgKIDDefault(config, config.Clients[c].IntrospectionSignedResponseAlg, config.Clients[c].IntrospectionSignedResponseKeyID, schema.DefaultOpenIDConnectClientConfiguration.IntrospectionSignedResponseAlg)

switch config.Clients[c].IntrospectionSignedResponseKeyID {
case "":
Expand All @@ -1106,7 +1129,7 @@ func validateOIDDClientSigningAlgsIntrospection(c int, config *schema.IdentityPr
}

func validateOIDDClientSigningAlgsJARM(c int, config *schema.IdentityProvidersOpenIDConnect, validator *schema.StructValidator) {
config.Clients[c].AuthorizationSignedResponseAlg, config.Clients[c].AuthorizationSignedResponseKeyID = validateOIDCClientAlgKIDDefault(config, config.Clients[c].AuthorizationSignedResponseAlg, config.Clients[c].AuthorizationSignedResponseKeyID, schema.DefaultOpenIDConnectClientConfiguration.AuthorizationSignedResponseAlg)
config.Clients[c].AuthorizationSignedResponseAlg, config.Clients[c].AuthorizationSignedResponseKeyID = validateOIDCAlgKIDDefault(config, config.Clients[c].AuthorizationSignedResponseAlg, config.Clients[c].AuthorizationSignedResponseKeyID, schema.DefaultOpenIDConnectClientConfiguration.AuthorizationSignedResponseAlg)

switch config.Clients[c].AuthorizationSignedResponseKeyID {
case "":
Expand All @@ -1129,7 +1152,7 @@ func validateOIDDClientSigningAlgsJARM(c int, config *schema.IdentityProvidersOp
}
}

func validateOIDCClientAlgKIDDefault(config *schema.IdentityProvidersOpenIDConnect, algCurrent, kidCurrent, algDefault string) (alg, kid string) {
func validateOIDCAlgKIDDefault(config *schema.IdentityProvidersOpenIDConnect, algCurrent, kidCurrent, algDefault string) (alg, kid string) {
alg, kid = algCurrent, kidCurrent

switch balg, bkid := len(alg) != 0, len(kid) != 0; {
Expand Down

0 comments on commit 357ce8e

Please sign in to comment.