Skip to content

Commit 6c38c68

Browse files
authored
Merge pull request #7 from Sovietaced/access-token
Add support for leeway
2 parents d865a6c + 167fb94 commit 6c38c68

File tree

3 files changed

+173
-45
lines changed

3 files changed

+173
-45
lines changed

metadata/okta/okta.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ import (
1111
"time"
1212
)
1313

14+
const (
15+
DefaultCacheTtl = 5 * time.Minute
16+
)
17+
1418
// Options are configurable options for the MetadataProvider.
1519
type Options struct {
1620
httpClient *http.Client
@@ -42,7 +46,7 @@ func defaultOptions() *Options {
4246
opts := &Options{}
4347
WithHttpClient(http.DefaultClient)(opts)
4448
withClock(clock.New())(opts)
45-
WithCacheTtl(5 * time.Minute)(opts)
49+
WithCacheTtl(DefaultCacheTtl)(opts)
4650
return opts
4751
}
4852

@@ -78,7 +82,12 @@ func NewMetadataProvider(issuer string, options ...Option) *MetadataProvider {
7882
}
7983

8084
metadataUrl := fmt.Sprintf("%s%s", issuer, "/.well-known/openid-configuration")
81-
return &MetadataProvider{metadataUrl: metadataUrl, httpClient: opts.httpClient, clock: opts.clock, cacheTtl: opts.cacheTtl}
85+
return &MetadataProvider{
86+
metadataUrl: metadataUrl,
87+
httpClient: opts.httpClient,
88+
clock: opts.clock,
89+
cacheTtl: opts.cacheTtl,
90+
}
8291
}
8392

8493
// GetMetadata gets metadata for the specified Okta issuer.

verifier.go

Lines changed: 26 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,17 @@ import (
77
"github.com/sovietaced/okta-jwt-verifier/keyfunc"
88
"github.com/sovietaced/okta-jwt-verifier/keyfunc/okta"
99
oktametadata "github.com/sovietaced/okta-jwt-verifier/metadata/okta"
10+
"time"
11+
)
12+
13+
const (
14+
DefaultLeeway = 0 // Default leeway that is configured for JWT validation
1015
)
1116

1217
// Options are configurable options for the Verifier.
1318
type Options struct {
1419
keyfuncProvider keyfunc.Provider
20+
leeway time.Duration
1521
}
1622

1723
// WithKeyfuncProvider allows for a configurable keyfunc.Provider, which may be useful if you want to customize
@@ -22,9 +28,17 @@ func WithKeyfuncProvider(keyfuncProvider keyfunc.Provider) Option {
2228
}
2329
}
2430

31+
// WithLeeway adds leeway to all time related validations.
32+
func WithLeeway(leeway time.Duration) Option {
33+
return func(mo *Options) {
34+
mo.leeway = leeway
35+
}
36+
}
37+
2538
func defaultOptions(issuer string) *Options {
2639
opts := &Options{}
2740
WithKeyfuncProvider(okta.NewKeyfuncProvider(oktametadata.NewMetadataProvider(issuer)))(opts)
41+
WithLeeway(DefaultLeeway)(opts)
2842
return opts
2943
}
3044

@@ -44,6 +58,7 @@ func newJwtFromToken(token *jwt.Token) *Jwt {
4458

4559
// Verifier is the implementation of the Okta JWT verification logic.
4660
type Verifier struct {
61+
parser *jwt.Parser
4762
keyfuncProvider keyfunc.Provider
4863
issuer string
4964
clientId string
@@ -56,7 +71,15 @@ func NewVerifier(issuer string, clientId string, options ...Option) *Verifier {
5671
option(opts)
5772
}
5873

59-
return &Verifier{issuer: issuer, clientId: clientId, keyfuncProvider: opts.keyfuncProvider}
74+
// Configure JWT parser
75+
parser := jwt.NewParser(
76+
jwt.WithLeeway(opts.leeway),
77+
jwt.WithIssuer(issuer),
78+
jwt.WithAudience(clientId),
79+
jwt.WithExpirationRequired(),
80+
)
81+
82+
return &Verifier{issuer: issuer, clientId: clientId, keyfuncProvider: opts.keyfuncProvider, parser: parser}
6083
}
6184

6285
// VerifyIdToken verifies an Okta ID token.
@@ -100,43 +123,18 @@ func (v *Verifier) parseToken(ctx context.Context, tokenString string) (*jwt.Tok
100123
return nil, fmt.Errorf("getting key function: %w", err)
101124
}
102125

103-
token, err := jwt.Parse(tokenString, keyfunc)
126+
token, err := v.parser.Parse(tokenString, keyfunc)
104127
if err != nil {
105128
return nil, fmt.Errorf("parsing token: %w", err)
106129
}
107130

108131
return token, err
109132
}
110133

134+
// validateCommonClaims validates claims that aren't validated natively by jwt.Parser
111135
func (v *Verifier) validateCommonClaims(ctx context.Context, jwt *jwt.Token) error {
112136
claims := jwt.Claims
113137

114-
jwtIssuer, err := claims.GetIssuer()
115-
if err != nil {
116-
return fmt.Errorf("verifying token issuer: %w", err)
117-
}
118-
119-
if jwtIssuer != v.issuer {
120-
return fmt.Errorf("verifying token issuer: issuer '%s' in token does not match '%s'", jwtIssuer, v.issuer)
121-
}
122-
123-
jwtAuds, err := claims.GetAudience()
124-
if err != nil {
125-
return fmt.Errorf("veriying token audience: %w", err)
126-
}
127-
128-
matchFound := false
129-
for _, jwtAud := range jwtAuds {
130-
if jwtAud == v.clientId {
131-
matchFound = true
132-
break
133-
}
134-
}
135-
136-
if !matchFound {
137-
return fmt.Errorf("verifying token audience: audience '%s' in token does not match '%s'", jwtAuds, v.clientId)
138-
}
139-
140138
jwtIat, err := claims.GetIssuedAt()
141139
if err != nil {
142140
return fmt.Errorf("verifying id token issued time: %w", err)
@@ -146,14 +144,5 @@ func (v *Verifier) validateCommonClaims(ctx context.Context, jwt *jwt.Token) err
146144
return fmt.Errorf("verifying token issued time: no issued time found")
147145
}
148146

149-
jwtExp, err := claims.GetExpirationTime()
150-
if err != nil {
151-
return fmt.Errorf("verifying token expriation time: %w", err)
152-
}
153-
154-
if jwtExp == nil {
155-
return fmt.Errorf("verifying token expiration time: no expiration time found")
156-
}
157-
158147
return nil
159148
}

verifier_test.go

Lines changed: 136 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,23 @@ func TestVerifierVerifyIdToken(t *testing.T) {
6262
require.NoError(t, err)
6363

6464
_, err = v.VerifyIdToken(ctx, idToken)
65-
require.ErrorContains(t, err, "verifying token issuer: issuer '' in token does not match 'https://test.okta.com'")
65+
require.ErrorContains(t, err, "verifying id token: parsing token: token has invalid claims: token is missing required claim: iss claim is required")
66+
})
67+
68+
t.Run("verify id token wrong issuer", func(t *testing.T) {
69+
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
70+
"iss": "wrong",
71+
"aud": clientId,
72+
"iat": time.Now().Unix(),
73+
"exp": time.Now().Add(24 * time.Hour).Unix(),
74+
"nonce": 456,
75+
})
76+
token.Header["kid"] = oktatest.KID
77+
idToken, err := token.SignedString(pk)
78+
require.NoError(t, err)
79+
80+
_, err = v.VerifyIdToken(ctx, idToken)
81+
require.ErrorContains(t, err, "verifying id token: parsing token: token has invalid claims: token has invalid issuer")
6682
})
6783

6884
t.Run("verify id token missing audience", func(t *testing.T) {
@@ -77,7 +93,23 @@ func TestVerifierVerifyIdToken(t *testing.T) {
7793
require.NoError(t, err)
7894

7995
_, err = v.VerifyIdToken(ctx, idToken)
80-
require.ErrorContains(t, err, "verifying token audience: audience '[]' in token does not match 'test'")
96+
require.ErrorContains(t, err, "verifying id token: parsing token: token has invalid claims: token is missing required claim: aud claim is required")
97+
})
98+
99+
t.Run("verify id token wrong audience", func(t *testing.T) {
100+
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
101+
"iss": issuer,
102+
"aud": "wrong",
103+
"iat": time.Now().Unix(),
104+
"exp": time.Now().Add(24 * time.Hour).Unix(),
105+
"nonce": 456,
106+
})
107+
token.Header["kid"] = oktatest.KID
108+
idToken, err := token.SignedString(pk)
109+
require.NoError(t, err)
110+
111+
_, err = v.VerifyIdToken(ctx, idToken)
112+
require.ErrorContains(t, err, "verifying id token: parsing token: token has invalid claims: token has invalid audience")
81113
})
82114

83115
t.Run("verify id token missing issued time", func(t *testing.T) {
@@ -107,7 +139,7 @@ func TestVerifierVerifyIdToken(t *testing.T) {
107139
require.NoError(t, err)
108140

109141
_, err = v.VerifyIdToken(ctx, idToken)
110-
require.ErrorContains(t, err, "verifying token expiration time: no expiration time found")
142+
require.ErrorContains(t, err, "verifying id token: parsing token: token has invalid claims: token is missing required claim: exp claim is required")
111143
})
112144

113145
t.Run("verify id token expired", func(t *testing.T) {
@@ -125,6 +157,39 @@ func TestVerifierVerifyIdToken(t *testing.T) {
125157
_, err = v.VerifyIdToken(ctx, idToken)
126158
require.ErrorContains(t, err, "verifying id token: parsing token: token has invalid claims: token is expired")
127159
})
160+
161+
t.Run("verify id token expiration with leeway", func(t *testing.T) {
162+
163+
lv := NewVerifier(issuer, clientId, WithKeyfuncProvider(kp), WithLeeway(time.Minute))
164+
165+
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
166+
"iss": issuer,
167+
"aud": clientId,
168+
"iat": time.Now().Unix(),
169+
"exp": time.Now().Add(-30 * time.Second).Unix(),
170+
"nonce": 456,
171+
})
172+
token.Header["kid"] = oktatest.KID
173+
idToken, err := token.SignedString(pk)
174+
require.NoError(t, err)
175+
176+
_, err = lv.VerifyIdToken(ctx, idToken)
177+
require.NoError(t, err)
178+
179+
token = jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
180+
"iss": issuer,
181+
"aud": clientId,
182+
"iat": time.Now().Unix(),
183+
"exp": time.Now().Add(-2 * time.Minute).Unix(),
184+
"nonce": 456,
185+
})
186+
token.Header["kid"] = oktatest.KID
187+
idToken, err = token.SignedString(pk)
188+
require.NoError(t, err)
189+
190+
_, err = lv.VerifyIdToken(ctx, idToken)
191+
require.ErrorContains(t, err, "verifying id token: parsing token: token has invalid claims: token is expired")
192+
})
128193
}
129194

130195
func TestVerifierVerifyAccessToken(t *testing.T) {
@@ -201,7 +266,23 @@ func TestVerifierVerifyAccessToken(t *testing.T) {
201266
require.NoError(t, err)
202267

203268
_, err = v.VerifyAccessToken(ctx, idToken)
204-
require.ErrorContains(t, err, "verifying token issuer: issuer '' in token does not match 'https://test.okta.com'")
269+
require.ErrorContains(t, err, "verifying access token: parsing token: token has invalid claims: token is missing required claim: iss claim is required")
270+
})
271+
272+
t.Run("verify access token wrong issuer", func(t *testing.T) {
273+
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
274+
"iss": "wrong",
275+
"aud": clientId,
276+
"iat": time.Now().Unix(),
277+
"exp": time.Now().Add(24 * time.Hour).Unix(),
278+
"nonce": 456,
279+
})
280+
token.Header["kid"] = oktatest.KID
281+
idToken, err := token.SignedString(pk)
282+
require.NoError(t, err)
283+
284+
_, err = v.VerifyAccessToken(ctx, idToken)
285+
require.ErrorContains(t, err, "verifying access token: parsing token: token has invalid claims: token has invalid issuer")
205286
})
206287

207288
t.Run("verify access token missing audience", func(t *testing.T) {
@@ -215,7 +296,23 @@ func TestVerifierVerifyAccessToken(t *testing.T) {
215296
require.NoError(t, err)
216297

217298
_, err = v.VerifyAccessToken(ctx, idToken)
218-
require.ErrorContains(t, err, "verifying token audience: audience '[]' in token does not match 'test'")
299+
require.ErrorContains(t, err, "verifying access token: parsing token: token has invalid claims: token is missing required claim: aud claim is required")
300+
})
301+
302+
t.Run("verify access token wrong audience", func(t *testing.T) {
303+
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
304+
"iss": issuer,
305+
"aud": "wrong",
306+
"iat": time.Now().Unix(),
307+
"exp": time.Now().Add(24 * time.Hour).Unix(),
308+
"nonce": 456,
309+
})
310+
token.Header["kid"] = oktatest.KID
311+
idToken, err := token.SignedString(pk)
312+
require.NoError(t, err)
313+
314+
_, err = v.VerifyAccessToken(ctx, idToken)
315+
require.ErrorContains(t, err, "verifying access token: parsing token: token has invalid claims: token has invalid audience")
219316
})
220317

221318
t.Run("verify access token missing issued time", func(t *testing.T) {
@@ -243,7 +340,7 @@ func TestVerifierVerifyAccessToken(t *testing.T) {
243340
require.NoError(t, err)
244341

245342
_, err = v.VerifyAccessToken(ctx, idToken)
246-
require.ErrorContains(t, err, "verifying token expiration time: no expiration time found")
343+
require.ErrorContains(t, err, "verifying access token: parsing token: token has invalid claims: token is missing required claim: exp claim is required")
247344
})
248345

249346
t.Run("verify access token expired", func(t *testing.T) {
@@ -260,4 +357,37 @@ func TestVerifierVerifyAccessToken(t *testing.T) {
260357
_, err = v.VerifyAccessToken(ctx, idToken)
261358
require.ErrorContains(t, err, "verifying access token: parsing token: token has invalid claims: token is expired")
262359
})
360+
361+
t.Run("verify access token expiration with leeway", func(t *testing.T) {
362+
363+
lv := NewVerifier(issuer, clientId, WithKeyfuncProvider(kp), WithLeeway(time.Minute))
364+
365+
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
366+
"iss": issuer,
367+
"aud": clientId,
368+
"iat": time.Now().Unix(),
369+
"exp": time.Now().Add(-30 * time.Second).Unix(),
370+
"nonce": 456,
371+
})
372+
token.Header["kid"] = oktatest.KID
373+
idToken, err := token.SignedString(pk)
374+
require.NoError(t, err)
375+
376+
_, err = lv.VerifyAccessToken(ctx, idToken)
377+
require.NoError(t, err)
378+
379+
token = jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
380+
"iss": issuer,
381+
"aud": clientId,
382+
"iat": time.Now().Unix(),
383+
"exp": time.Now().Add(-2 * time.Minute).Unix(),
384+
"nonce": 456,
385+
})
386+
token.Header["kid"] = oktatest.KID
387+
idToken, err = token.SignedString(pk)
388+
require.NoError(t, err)
389+
390+
_, err = lv.VerifyAccessToken(ctx, idToken)
391+
require.ErrorContains(t, err, "verifying access token: parsing token: token has invalid claims: token is expired")
392+
})
263393
}

0 commit comments

Comments
 (0)