Skip to content

Commit

Permalink
feat(openid): harden id_token validation
Browse files Browse the repository at this point in the history
  • Loading branch information
tronghn committed Aug 15, 2023
1 parent f8d6633 commit e779920
Show file tree
Hide file tree
Showing 6 changed files with 429 additions and 8 deletions.
1 change: 1 addition & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ The following flags are available:
| `log-level` | string | Logging verbosity level. | `info` |
| `metrics-bind-address` | string | Listen address for metrics only. | `127.0.0.1:3001` |
| `openid.acr-values` | string | Space separated string that configures the default security level (`acr_values`) parameter for authorization requests. | |
| `openid.audiences` | strings | List of additional trusted audiences (other than the client_id) for OpenID Connect id_token validation. | |
| `openid.client-id` | string | Client ID for the OpenID client. | |
| `openid.client-jwk` | string | JWK containing the private key for the OpenID client in string format. | |
| `openid.post-logout-redirect-uri` | string | URI for redirecting the user after successful logout at the Identity Provider. | |
Expand Down
13 changes: 13 additions & 0 deletions pkg/config/openid.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

const (
OpenIDACRValues = "openid.acr-values"
OpenIDAudiences = "openid.audiences"
OpenIDClientID = "openid.client-id"
OpenIDClientJWK = "openid.client-jwk"
OpenIDPostLogoutRedirectURI = "openid.post-logout-redirect-uri"
Expand All @@ -18,6 +19,7 @@ const (

type OpenID struct {
ACRValues string `json:"acr-values"`
Audiences []string `json:"audiences"`
ClientID string `json:"client-id"`
ClientJWK string `json:"client-jwk"`
PostLogoutRedirectURI string `json:"post-logout-redirect-uri"`
Expand All @@ -28,6 +30,16 @@ type OpenID struct {
WellKnownURL string `json:"well-known-url"`
}

func (in OpenID) TrustedAudiences() map[string]bool {
m := make(map[string]bool)
m[in.ClientID] = true
for _, aud := range in.Audiences {
m[aud] = true
}

return m
}

type Provider string

const (
Expand All @@ -38,6 +50,7 @@ const (

func openIDFlags() {
flag.String(OpenIDACRValues, "", "Space separated string that configures the default security level (acr_values) parameter for authorization requests.")
flag.StringSlice(OpenIDAudiences, []string{}, "List of additional trusted audiences (other than the client_id) for OpenID Connect id_token validation.")
flag.String(OpenIDClientID, "", "Client ID for the OpenID client.")
flag.String(OpenIDClientJWK, "", "JWK containing the private key for the OpenID client in string format.")
flag.String(OpenIDPostLogoutRedirectURI, "", "URI for redirecting the user after successful logout at the Identity Provider.")
Expand Down
12 changes: 9 additions & 3 deletions pkg/mock/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,18 @@ import (

type TestClientConfiguration struct {
*config.Config
clientJwk jwk.Key
clientJwk jwk.Key
trustedAudiences map[string]bool
}

func (c *TestClientConfiguration) ACRValues() string {
return c.Config.OpenID.ACRValues
}

func (c *TestClientConfiguration) Audiences() map[string]bool {
return c.trustedAudiences
}

func (c *TestClientConfiguration) ClientID() string {
return c.Config.OpenID.ClientID
}
Expand Down Expand Up @@ -58,7 +63,8 @@ func clientConfiguration(cfg *config.Config) *TestClientConfiguration {
}

return &TestClientConfiguration{
Config: cfg,
clientJwk: key,
Config: cfg,
clientJwk: key,
trustedAudiences: cfg.OpenID.TrustedAudiences(),
}
}
13 changes: 10 additions & 3 deletions pkg/openid/config/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

type Client interface {
ACRValues() string
Audiences() map[string]bool
ClientID() string
ClientJWK() jwk.Key
PostLogoutRedirectURI() string
Expand All @@ -25,13 +26,18 @@ type Client interface {

type client struct {
wonderwallconfig.OpenID
clientJwk jwk.Key
clientJwk jwk.Key
trustedAudiences map[string]bool
}

func (in *client) ACRValues() string {
return in.OpenID.ACRValues
}

func (in *client) Audiences() map[string]bool {
return in.trustedAudiences
}

func (in *client) ClientID() string {
return in.OpenID.ClientID
}
Expand Down Expand Up @@ -83,8 +89,9 @@ func NewClientConfig(cfg *wonderwallconfig.Config) (Client, error) {
}

c := &client{
OpenID: cfg.OpenID,
clientJwk: clientJwk,
OpenID: cfg.OpenID,
clientJwk: clientJwk,
trustedAudiences: cfg.OpenID.TrustedAudiences(),
}

var clientConfig Client
Expand Down
43 changes: 41 additions & 2 deletions pkg/openid/tokens.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,31 @@ func (in *IDToken) Validate(cfg openidconfig.Config, cookie *LoginCookie) error
clientConfig := cfg.Client()

opts := []jwtlib.ValidateOption{
// OpenID Connect Core, section 2 - required claims.
jwtlib.WithRequiredClaim("iss"),
jwtlib.WithRequiredClaim("sub"),
jwtlib.WithRequiredClaim("aud"),
jwtlib.WithRequiredClaim("exp"),
jwtlib.WithRequiredClaim("iat"),
// OpenID Connect Core section 3.1.3.7, step 2.
// The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery) MUST exactly match the value of the `iss` (issuer) Claim.
jwtlib.WithIssuer(openIDconfig.Issuer()),
// OpenID Connect Core section 3.1.3.7, step 3.
// The Client MUST validate that the `aud` (audience) Claim contains its `client_id` value registered at the Issuer identified by the `iss` (issuer) Claim as an audience.
// The ID Token MUST be rejected if the ID Token does not list the Client as a valid audience
jwtlib.WithAudience(clientConfig.ClientID()),
// OpenID Connect Core section 3.1.3.7, step 11.
// If a nonce value was sent in the Authentication Request, a `nonce` Claim MUST be present and its value checked to verify that it is the same value as the one that was sent in the Authentication Request.
jwtlib.WithClaimValue("nonce", cookie.Nonce),
jwtlib.WithIssuer(openIDconfig.Issuer()),
jwtlib.WithAcceptableSkew(jwt.AcceptableClockSkew),
}

if openIDconfig.SidClaimRequired() {
opts = append(opts, jwtlib.WithRequiredClaim(jwt.SidClaim))
}

// OpenID Connect Core 3.1.3.7, step 12.
// If the `acr` Claim was requested, the Client SHOULD check that the asserted Claim Value is appropriate.
if len(clientConfig.ACRValues()) > 0 {
opts = append(opts, jwtlib.WithRequiredClaim(jwt.AcrClaim))

Expand All @@ -86,7 +101,31 @@ func (in *IDToken) Validate(cfg openidconfig.Config, cookie *LoginCookie) error
}
}

return jwtlib.Validate(in.GetToken(), opts...)
err := jwtlib.Validate(in.GetToken(), opts...)
if err != nil {
return err
}

// OpenID Connect Core 3.1.3.7, step 3.
// The `aud` (audience) Claim MAY contain an array with more than one element.
// The ID Token MUST be rejected if the ID Token [...] contains additional audiences not trusted by the Client.
audiences := in.GetToken().Audience()
if len(audiences) > 1 {
trusted := clientConfig.Audiences()
untrusted := make([]string, 0)

for _, audience := range audiences {
if !trusted[audience] {
untrusted = append(untrusted, audience)
}
}

if len(untrusted) > 0 {
return fmt.Errorf("'aud' not satisfied, untrusted audience(s) found: %q", untrusted)
}
}

return nil
}

func NewIDToken(raw string, jwtToken jwtlib.Token) *IDToken {
Expand Down
Loading

0 comments on commit e779920

Please sign in to comment.