Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow additional id token audiences #3616

Merged
merged 4 commits into from
Nov 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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") {
aeneasr marked this conversation as resolved.
Show resolved Hide resolved
// 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
}
Loading