Skip to content

Commit

Permalink
feat: allow additional id token audiences (#3616)
Browse files Browse the repository at this point in the history
  • Loading branch information
jonas-jonas authored Nov 13, 2023
1 parent 4364ba0 commit 0fa648d
Show file tree
Hide file tree
Showing 8 changed files with 102 additions and 41 deletions.
13 changes: 9 additions & 4 deletions embedx/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,14 @@
"description": "The ID of the organization that this provider belongs to. Only effective in the Ory Network.",
"type": "string",
"examples": ["12345678-1234-1234-1234-123456789012"]
},
"allowed_id_token_audiences": {
"title": "Additional client ids allowed when using ID token submission",
"type": "array",
"items": {
"type": "string",
"examples": ["12345678-1234-1234-1234-123456789012"]
}
}
},
"additionalProperties": false,
Expand Down Expand Up @@ -2004,10 +2012,7 @@
"type": "string",
"title": "Default Read Consistency Level",
"description": "The default consistency level to use when reading from the database. Defaults to `strong` to not break existing API contracts. Only set this to `eventual` if you can accept that other read APIs will suddenly return eventually consistent results. It is only effective in Ory Network.",
"enum": [
"strong",
"eventual"
],
"enum": ["strong", "eventual"],
"default": "strong"
}
}
Expand Down
17 changes: 5 additions & 12 deletions selfservice/strategy/oidc/provider_apple.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,20 +154,13 @@ func (a *ProviderApple) DecodeQuery(query url.Values, claims *Claims) {

var _ IDTokenVerifier = new(ProviderApple)

const issuerUrlApple = "https://appleid.apple.com"

func (a *ProviderApple) Verify(ctx context.Context, rawIDToken string) (*Claims, error) {
keySet := oidc.NewRemoteKeySet(ctx, a.JWKSUrl)
verifier := oidc.NewVerifier("https://appleid.apple.com", keySet, &oidc.Config{
ClientID: a.config.ClientID,
})
token, err := verifier.Verify(oidc.ClientContext(ctx, a.reg.HTTPClient(ctx).HTTPClient), rawIDToken)
if err != nil {
return nil, err
}
claims := &Claims{}
if err := token.Claims(claims); err != nil {
return nil, err
}
return claims, nil

ctx = oidc.ClientContext(ctx, a.reg.HTTPClient(ctx).HTTPClient)
return verifyToken(ctx, keySet, a.config, rawIDToken, issuerUrlApple)
}

var _ NonceValidationSkipper = new(ProviderApple)
Expand Down
22 changes: 17 additions & 5 deletions selfservice/strategy/oidc/provider_apple_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ func TestDecodeQuery(t *testing.T) {
assert.Empty(t, tc.claims.Email)
})
}

}

func TestAppleVerify(t *testing.T) {
Expand All @@ -64,7 +63,7 @@ func TestAppleVerify(t *testing.T) {
makeClaims := func(aud string) jwt.RegisteredClaims {
return jwt.RegisteredClaims{
Issuer: "https://appleid.apple.com",
Subject: "apple@ory.sh",
Subject: "acme@ory.sh",
Audience: jwt.ClaimStrings{aud},
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
}
Expand All @@ -79,8 +78,8 @@ func TestAppleVerify(t *testing.T) {

c, err := apple.Verify(context.Background(), token)
require.NoError(t, err)
assert.Equal(t, "apple@ory.sh", c.Email)
assert.Equal(t, "apple@ory.sh", c.Subject)
assert.Equal(t, "acme@ory.sh", c.Email)
assert.Equal(t, "acme@ory.sh", c.Subject)
assert.Equal(t, "https://appleid.apple.com", c.Issuer)
})

Expand All @@ -94,7 +93,7 @@ func TestAppleVerify(t *testing.T) {

_, err := apple.Verify(context.Background(), token)
require.Error(t, err)
assert.Equal(t, `oidc: expected audience "com.example.app" got ["com.different-example.app"]`, err.Error())
assert.Equal(t, `token audience didn't match allowed audiences: [com.example.app] oidc: expected audience "com.example.app" got ["com.different-example.app"]`, err.Error())
})

t.Run("case=fails due to jwks mismatch", func(t *testing.T) {
Expand All @@ -109,4 +108,17 @@ func TestAppleVerify(t *testing.T) {
require.Error(t, err)
assert.Equal(t, "failed to verify signature: failed to verify id token signature", err.Error())
})

t.Run("case=succeedes with additional id token audience", func(t *testing.T) {
_, reg := internal.NewFastRegistryWithMocks(t)
apple := oidc.NewProviderApple(&oidc.Configuration{
ClientID: "something.else.app",
AdditionalIDTokenAudiences: []string{"com.example.app"},
}, reg).(*oidc.ProviderApple)
apple.JWKSUrl = ts.URL
token := createIdToken(t, makeClaims("com.example.app"))

_, err := apple.Verify(context.Background(), token)
require.NoError(t, err)
})
}
6 changes: 5 additions & 1 deletion selfservice/strategy/oidc/provider_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ type Configuration struct {
// It can be either a URL (file://, http(s)://, base64://) or an inline JSONNet code snippet.
Mapper string `json:"mapper_url"`

// RequestedClaims string encoded json object that specifies claims and optionally their properties which should be
// RequestedClaims is a string encoded json object that specifies claims and optionally their properties that should be
// included in the id_token or returned from the UserInfo Endpoint.
//
// More information: https://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter
Expand All @@ -108,6 +108,10 @@ type Configuration struct {
// An optional organization ID that this provider belongs to.
// This parameter is only effective in the Ory Network.
OrganizationID string `json:"organization_id"`

// AdditionalIDTokenAudiences is a list of additional audiences allowed in the ID Token.
// This is only relevant in OIDC flows that submit an IDToken instead of using the callback from the OIDC provider.
AdditionalIDTokenAudiences []string `json:"additional_id_token_audiences"`
}

func (p Configuration) Redir(public *url.URL) string {
Expand Down
16 changes: 4 additions & 12 deletions selfservice/strategy/oidc/provider_google.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,20 +72,12 @@ func (g *ProviderGoogle) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption {

var _ IDTokenVerifier = new(ProviderGoogle)

const issuerUrlGoogle = "https://accounts.google.com"

func (p *ProviderGoogle) Verify(ctx context.Context, rawIDToken string) (*Claims, error) {
keySet := oidc.NewRemoteKeySet(ctx, p.JWKSUrl)
verifier := oidc.NewVerifier("https://accounts.google.com", keySet, &oidc.Config{
ClientID: p.config.ClientID,
})
token, err := verifier.Verify(oidc.ClientContext(ctx, p.reg.HTTPClient(ctx).HTTPClient), rawIDToken)
if err != nil {
return nil, err
}
claims := &Claims{}
if err := token.Claims(claims); err != nil {
return nil, err
}
return claims, nil
ctx = oidc.ClientContext(ctx, p.reg.HTTPClient(ctx).HTTPClient)
return verifyToken(ctx, keySet, p.config, rawIDToken, issuerUrlGoogle)
}

var _ NonceValidationSkipper = new(ProviderGoogle)
Expand Down
23 changes: 18 additions & 5 deletions selfservice/strategy/oidc/provider_google_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func TestProviderGoogle_AccessType(t *testing.T) {
assert.Contains(t, options, oauth2.AccessTypeOffline)
}

func TestVerify(t *testing.T) {
func TestGoogleVerify(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
w.Write(publicJWKS)
Expand All @@ -73,7 +73,7 @@ func TestVerify(t *testing.T) {
makeClaims := func(aud string) jwt.RegisteredClaims {
return jwt.RegisteredClaims{
Issuer: "https://accounts.google.com",
Subject: "apple@ory.sh",
Subject: "acme@ory.sh",
Audience: jwt.ClaimStrings{aud},
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
}
Expand All @@ -98,8 +98,8 @@ func TestVerify(t *testing.T) {

c, err := p.Verify(context.Background(), token)
require.NoError(t, err)
assert.Equal(t, "apple@ory.sh", c.Email)
assert.Equal(t, "apple@ory.sh", c.Subject)
assert.Equal(t, "acme@ory.sh", c.Email)
assert.Equal(t, "acme@ory.sh", c.Subject)
assert.Equal(t, "https://accounts.google.com", c.Issuer)
})

Expand All @@ -109,7 +109,7 @@ func TestVerify(t *testing.T) {

_, err := p.Verify(context.Background(), token)
require.Error(t, err)
assert.Equal(t, `oidc: expected audience "com.example.app" got ["com.different-example.app"]`, err.Error())
assert.Equal(t, `token audience didn't match allowed audiences: [com.example.app] oidc: expected audience "com.example.app" got ["com.different-example.app"]`, err.Error())
})

t.Run("case=fails due to jwks mismatch", func(t *testing.T) {
Expand All @@ -120,4 +120,17 @@ func TestVerify(t *testing.T) {
require.Error(t, err)
assert.Equal(t, "failed to verify signature: failed to verify id token signature", err.Error())
})

t.Run("case=succeedes with additional id token audience", func(t *testing.T) {
_, reg := internal.NewFastRegistryWithMocks(t)
google := oidc.NewProviderGoogle(&oidc.Configuration{
ClientID: "something.else.app",
AdditionalIDTokenAudiences: []string{"com.example.app"},
}, reg).(*oidc.ProviderGoogle)
google.JWKSUrl = ts.URL
token := createIdToken(t, makeClaims("com.example.app"))

_, err := google.Verify(context.Background(), token)
require.NoError(t, err)
})
}
4 changes: 2 additions & 2 deletions selfservice/strategy/oidc/strategy_helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ func newHydraIntegration(t *testing.T, remote *string, subject *string, claims *
GrantScope []string `json:"grant_scope,omitempty"`
}

var do = func(w http.ResponseWriter, r *http.Request, href string, payload io.Reader) {
do := func(w http.ResponseWriter, r *http.Request, href string, payload io.Reader) {
req, err := http.NewRequest("PUT", href, payload)
require.NoError(t, err)
req.Header.Set("Content-Type", "application/json")
Expand Down Expand Up @@ -370,7 +370,7 @@ func createIdToken(t *testing.T, cl jwt.RegisteredClaims) string {
require.NoError(t, json.Unmarshal(rawKey, key))
token := jwt.NewWithClaims(jwt.SigningMethodRS256, &claims{
RegisteredClaims: &cl,
Email: "apple@ory.sh",
Email: "acme@ory.sh",
})
token.Header["kid"] = key.KeyID
s, err := token.SignedString(key.Key)
Expand Down
42 changes: 42 additions & 0 deletions selfservice/strategy/oidc/token_verifier.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0

package oidc

import (
"context"
"fmt"
"strings"

"github.com/coreos/go-oidc"
)

func verifyToken(ctx context.Context, keySet oidc.KeySet, config *Configuration, rawIDToken, issuerURL string) (*Claims, error) {
tokenAudiences := append([]string{config.ClientID}, config.AdditionalIDTokenAudiences...)
var token *oidc.IDToken
err := fmt.Errorf("no audience matched the token's audience")
for _, aud := range tokenAudiences {
verifier := oidc.NewVerifier(issuerURL, keySet, &oidc.Config{
ClientID: aud,
})
token, err = verifier.Verify(ctx, rawIDToken)
if err != nil && strings.Contains(err.Error(), "oidc: expected audience") {
// The audience is not the one we expect, try the next one
continue
} else if err != nil {
// Something else went wrong
return nil, err
}
// The token was verified successfully
break
}
if err != nil {
// None of the allowed audiences matched the audience in the token
return nil, fmt.Errorf("token audience didn't match allowed audiences: %+v %w", tokenAudiences, err)
}
claims := &Claims{}
if err := token.Claims(claims); err != nil {
return nil, err
}
return claims, nil
}

0 comments on commit 0fa648d

Please sign in to comment.