Skip to content

Commit 0d50c93

Browse files
authored
feat(rp): optional authorized party check (#752)
This PR makes the default Authorized Party check in `rp.VerifyIDToken` optional by adding an options parameter for dynamic verification functions. This check is meant to be an optional validation requirement, so some providers (including GCP) do not adhere to it. See #405 for more context. Closes #405
1 parent 175edcf commit 0d50c93

File tree

5 files changed

+59
-5
lines changed

5 files changed

+59
-5
lines changed

pkg/client/rp/relying_party_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ func Test_verifyTokenResponse(t *testing.T) {
2222
KeySet: tu.KeySet{},
2323
MaxAge: 2 * time.Minute,
2424
ACR: tu.ACRVerify,
25+
AZP: oidc.DefaultAZPVerifier(tu.ValidClientID),
2526
Nonce: func(context.Context) string { return tu.ValidNonce },
2627
}
2728
tests := []struct {

pkg/client/rp/verifier.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ func VerifyIDToken[C oidc.Claims](ctx context.Context, token string, v *IDTokenV
5757
return nilClaims, err
5858
}
5959

60-
if err = oidc.CheckAuthorizedParty(claims, v.ClientID); err != nil {
60+
if err = oidc.CheckAZPVerifier(claims, v.AZP); err != nil {
6161
return nilClaims, err
6262
}
6363

@@ -86,6 +86,7 @@ func VerifyIDToken[C oidc.Claims](ctx context.Context, token string, v *IDTokenV
8686
if err = oidc.CheckAuthTime(claims, v.MaxAge); err != nil {
8787
return nilClaims, err
8888
}
89+
8990
return claims, nil
9091
}
9192

@@ -118,6 +119,7 @@ func NewIDTokenVerifier(issuer, clientID string, keySet oidc.KeySet, options ...
118119
Nonce: func(_ context.Context) string {
119120
return ""
120121
},
122+
AZP: oidc.DefaultAZPVerifier(clientID),
121123
}
122124

123125
for _, opts := range options {
@@ -159,6 +161,13 @@ func WithACRVerifier(verifier oidc.ACRVerifier) VerifierOption {
159161
}
160162
}
161163

164+
// WithAZPVerifier sets the verifier for the azp claim
165+
func WithAZPVerifier(verifier oidc.AZPVerifier) VerifierOption {
166+
return func(v *IDTokenVerifier) {
167+
v.AZP = verifier
168+
}
169+
}
170+
162171
// WithAuthTimeMaxAge provides the ability to define the maximum duration between auth_time and now
163172
func WithAuthTimeMaxAge(maxAge time.Duration) VerifierOption {
164173
return func(v *IDTokenVerifier) {

pkg/client/rp/verifier_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ func TestVerifyTokens(t *testing.T) {
2121
KeySet: tu.KeySet{},
2222
MaxAge: 2 * time.Minute,
2323
ACR: tu.ACRVerify,
24+
AZP: oidc.DefaultAZPVerifier(tu.ValidClientID),
2425
Nonce: func(context.Context) string { return tu.ValidNonce },
2526
ClientID: tu.ValidClientID,
2627
}
@@ -99,6 +100,7 @@ func TestVerifyIDToken(t *testing.T) {
99100
KeySet: tu.KeySet{},
100101
MaxAge: 2 * time.Minute,
101102
ACR: tu.ACRVerify,
103+
AZP: oidc.DefaultAZPVerifier(tu.ValidClientID),
102104
Nonce: func(context.Context) string { return tu.ValidNonce },
103105
ClientID: tu.ValidClientID,
104106
}
@@ -333,6 +335,7 @@ func TestNewIDTokenVerifier(t *testing.T) {
333335
WithIssuedAtMaxAge(time.Hour),
334336
WithNonce(nil), // otherwise assert.Equal will fail on the function
335337
WithACRVerifier(nil),
338+
WithAZPVerifier(nil),
336339
WithAuthTimeMaxAge(2 * time.Hour),
337340
WithSupportedSigningAlgorithms("ABC", "DEF"),
338341
},

pkg/oidc/verifier.go

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ type Verifier struct {
7272
SupportedSignAlgs []string
7373
MaxAge time.Duration
7474
ACR ACRVerifier
75+
AZP AZPVerifier
7576
KeySet KeySet
7677
Nonce func(ctx context.Context) string
7778
}
@@ -130,19 +131,43 @@ func CheckAudience(claims Claims, clientID string) error {
130131
return nil
131132
}
132133

134+
// AZPVerifier specifies the function to be used by the `DefaultVerifier` for validating the azp claim
135+
type AZPVerifier func(string) error
136+
137+
// DefaultAZPVerifier implements `AZPVerifier` returning an error
138+
// if the azp claim is set and doesn't match the clientID.
139+
func DefaultAZPVerifier(clientID string) AZPVerifier {
140+
return func(azp string) error {
141+
if azp != "" && azp != clientID {
142+
return fmt.Errorf("%w: azp %q must be equal to client_id %q", ErrAzpInvalid, azp, clientID)
143+
}
144+
return nil
145+
}
146+
}
147+
133148
// CheckAuthorizedParty checks azp (authorized party) claim requirements.
134149
//
135150
// If the ID Token contains multiple audiences, the Client SHOULD verify that an azp Claim is present.
136-
// If an azp Claim is present, the Client SHOULD verify that its client_id is the Claim Value.
151+
// If an azp Claim is present, the Client MAY verify that its client_id is the Claim Value.
137152
// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
138153
func CheckAuthorizedParty(claims Claims, clientID string) error {
154+
return CheckAZPVerifier(claims, DefaultAZPVerifier(clientID))
155+
}
156+
157+
// CheckAZPVerifier checks azp (authorized party) claim requirements.
158+
//
159+
// If the ID Token contains multiple audiences, the Client SHOULD verify that an azp Claim is present.
160+
// If an azp Claim is present, the Client MAY verify that its client_id is the Claim Value.
161+
// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
162+
func CheckAZPVerifier(claims Claims, azp AZPVerifier) error {
139163
if len(claims.GetAudience()) > 1 {
140164
if claims.GetAuthorizedParty() == "" {
141165
return ErrAzpMissing
142166
}
143167
}
144-
if claims.GetAuthorizedParty() != "" && claims.GetAuthorizedParty() != clientID {
145-
return fmt.Errorf("%w: azp %q must be equal to client_id %q", ErrAzpInvalid, claims.GetAuthorizedParty(), clientID)
168+
169+
if err := azp(claims.GetAuthorizedParty()); err != nil {
170+
return fmt.Errorf("%w: %v", ErrAzpInvalid, err)
146171
}
147172
return nil
148173
}

pkg/oidc/verifier_test.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ func TestCheckAuthorizedParty(t *testing.T) {
145145
tests := []struct {
146146
name string
147147
claims Claims
148+
azp AZPVerifier
148149
wantErr error
149150
}{
150151
{
@@ -174,6 +175,17 @@ func TestCheckAuthorizedParty(t *testing.T) {
174175
AuthorizedParty: clientID,
175176
},
176177
},
178+
{
179+
name: "custom azp",
180+
claims: &TokenClaims{
181+
Audience: []string{"not-client-id"},
182+
AuthorizedParty: clientID,
183+
},
184+
azp: func(s string) error {
185+
// skip check.
186+
return nil
187+
},
188+
},
177189
{
178190
name: "wrong azp",
179191
claims: &TokenClaims{
@@ -184,7 +196,11 @@ func TestCheckAuthorizedParty(t *testing.T) {
184196
}
185197
for _, tt := range tests {
186198
t.Run(tt.name, func(t *testing.T) {
187-
err := CheckAuthorizedParty(tt.claims, clientID)
199+
azp := tt.azp
200+
if azp == nil {
201+
azp = DefaultAZPVerifier(clientID)
202+
}
203+
err := CheckAZPVerifier(tt.claims, azp)
188204
assert.ErrorIs(t, err, tt.wantErr)
189205
})
190206
}

0 commit comments

Comments
 (0)