Skip to content

Commit 77e7f20

Browse files
authored
Merge pull request coreos#161 from ericchiang/out-of-band-verifiers
*: expose KeySet, NewRemoteKeySet, and NewVerifier
2 parents 731ffe6 + 43cab48 commit 77e7f20

File tree

5 files changed

+75
-13
lines changed

5 files changed

+75
-13
lines changed

jwks.go

+22
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,20 @@ import (
2323
// updated.
2424
const keysExpiryDelta = 30 * time.Second
2525

26+
// NewRemoteKeySet returns a KeySet that can validate JSON web tokens by using HTTP
27+
// GETs to fetch JSON web token sets hosted at a remote URL. This is automatically
28+
// used by NewProvider using the URLs returned by OpenID Connect discovery, but is
29+
// exposed for providers that don't support discovery or to prevent round trips to the
30+
// discovery URL.
31+
//
32+
// The returned KeySet is a long lived verifier that caches keys based on cache-control
33+
// headers. Reuse a common remote key set instead of creating new ones as needed.
34+
//
35+
// The behavior of the returned KeySet is undefined once the context is canceled.
36+
func NewRemoteKeySet(ctx context.Context, jwksURL string) KeySet {
37+
return newRemoteKeySet(ctx, jwksURL, time.Now)
38+
}
39+
2640
func newRemoteKeySet(ctx context.Context, jwksURL string, now func() time.Time) *remoteKeySet {
2741
if now == nil {
2842
now = time.Now
@@ -79,6 +93,14 @@ func (i *inflight) result() ([]jose.JSONWebKey, error) {
7993
return i.keys, i.err
8094
}
8195

96+
func (r *remoteKeySet) VerifySignature(ctx context.Context, jwt string) ([]byte, error) {
97+
jws, err := jose.ParseSigned(jwt)
98+
if err != nil {
99+
return nil, fmt.Errorf("oidc: malformed jwt: %v", err)
100+
}
101+
return r.verify(ctx, jws)
102+
}
103+
82104
func (r *remoteKeySet) verify(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) {
83105
// We don't support JWTs signed with multiple signatures.
84106
keyID := ""

oidc.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ type Provider struct {
6464
// Raw claims returned by the server.
6565
rawClaims []byte
6666

67-
remoteKeySet *remoteKeySet
67+
remoteKeySet KeySet
6868
}
6969

7070
type cachedKeys struct {
@@ -120,7 +120,7 @@ func NewProvider(ctx context.Context, issuer string) (*Provider, error) {
120120
tokenURL: p.TokenURL,
121121
userInfoURL: p.UserInfoURL,
122122
rawClaims: body,
123-
remoteKeySet: newRemoteKeySet(ctx, p.JWKSURL, time.Now),
123+
remoteKeySet: NewRemoteKeySet(ctx, p.JWKSURL),
124124
}, nil
125125
}
126126

test

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,6 @@ LINTABLE=$( go list -tags=golint -f '
1111

1212
go test -v -i -race github.com/coreos/go-oidc/...
1313
go test -v -race github.com/coreos/go-oidc/...
14-
golint $LINTABLE
14+
golint -set_exit_status $LINTABLE
1515
go vet github.com/coreos/go-oidc/...
1616
go build -v ./example/...

verify.go

+43-8
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,54 @@ const (
1919
issuerGoogleAccountsNoScheme = "accounts.google.com"
2020
)
2121

22-
// keySet is an interface that lets us stub out verification policies for
23-
// testing. Outside of testing, it's always backed by a remoteKeySet.
24-
type keySet interface {
25-
verify(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error)
22+
// KeySet is a set of publc JSON Web Keys that can be used to validate the signature
23+
// of JSON web tokens. This is expected to be backed by a remote key set through
24+
// provider metadata discovery or an in-memory set of keys delivered out-of-band.
25+
type KeySet interface {
26+
// VerifySignature parses the JSON web token, verifies the signature, and returns
27+
// the raw payload. Header and claim fields are validated by other parts of the
28+
// package. For example, the KeySet does not need to check values such as signature
29+
// algorithm, issuer, and audience since the IDTokenVerifier validates these values
30+
// independently.
31+
//
32+
// If VerifySignature makes HTTP requests to verify the token, it's expected to
33+
// use any HTTP client associated with the context through ClientContext.
34+
VerifySignature(ctx context.Context, jwt string) (payload []byte, err error)
2635
}
2736

2837
// IDTokenVerifier provides verification for ID Tokens.
2938
type IDTokenVerifier struct {
30-
keySet keySet
39+
keySet KeySet
3140
config *Config
3241
issuer string
3342
}
3443

44+
// NewVerifier returns a verifier manually constructed from a key set and issuer URL.
45+
//
46+
// It's easier to use provider discovery to construct an IDTokenVerifier than creating
47+
// one directly. This method is intended to be used with provider that don't support
48+
// metadata discovery, or avoiding round trips when the key set URL is already known.
49+
//
50+
// This constructor can be used to create a verifier directly using the issuer URL and
51+
// JSON Web Key Set URL without using discovery:
52+
//
53+
// keySet := oidc.NewRemoteKeySet(ctx, "https://www.googleapis.com/oauth2/v3/certs")
54+
// verifier := oidc.NewVerifier("https://accounts.google.com", keySet, config)
55+
//
56+
// Since KeySet is an interface, this constructor can also be used to supply custom
57+
// public key sources. For example, if a user wanted to supply public keys out-of-band
58+
// and hold them statically in-memory:
59+
//
60+
// // Custom KeySet implementation.
61+
// keySet := newStatisKeySet(publicKeys...)
62+
//
63+
// // Verifier uses the custom KeySet implementation.
64+
// verifier := oidc.NewVerifier("https://auth.example.com", keySet, config)
65+
//
66+
func NewVerifier(issuerURL string, keySet KeySet, config *Config) *IDTokenVerifier {
67+
return &IDTokenVerifier{keySet: keySet, config: config, issuer: issuerURL}
68+
}
69+
3570
// Config is the configuration for an IDTokenVerifier.
3671
type Config struct {
3772
// Expected audience of the token. For a majority of the cases this is expected to be
@@ -63,7 +98,7 @@ func (p *Provider) Verifier(config *Config) *IDTokenVerifier {
6398
return newVerifier(p.remoteKeySet, config, p.issuer)
6499
}
65100

66-
func newVerifier(keySet keySet, config *Config, issuer string) *IDTokenVerifier {
101+
func newVerifier(keySet KeySet, config *Config, issuer string) *IDTokenVerifier {
67102
// If SupportedSigningAlgs is empty defaults to only support RS256.
68103
if len(config.SupportedSigningAlgs) == 0 {
69104
config.SupportedSigningAlgs = []string{RS256}
@@ -165,7 +200,7 @@ func (v *IDTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*IDTok
165200
return nil, fmt.Errorf("oidc: expected audience %q got %q", v.config.ClientID, t.Audience)
166201
}
167202
} else {
168-
return nil, fmt.Errorf("oidc: Invalid configuration. ClientID must be provided or SkipClientIDCheck must be set.")
203+
return nil, fmt.Errorf("oidc: invalid configuration, clientID must be provided or SkipClientIDCheck must be set")
169204
}
170205
}
171206

@@ -194,7 +229,7 @@ func (v *IDTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*IDTok
194229
return nil, fmt.Errorf("oidc: id token signed with unsupported algorithm, expected %q got %q", v.config.SupportedSigningAlgs, sig.Header.Algorithm)
195230
}
196231

197-
gotPayload, err := v.keySet.verify(ctx, jws)
232+
gotPayload, err := v.keySet.VerifySignature(ctx, rawIDToken)
198233
if err != nil {
199234
return nil, fmt.Errorf("failed to verify signature: %v", err)
200235
}

verify_test.go

+7-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package oidc
22

33
import (
44
"context"
5+
"fmt"
56
"strconv"
67
"testing"
78
"time"
@@ -13,7 +14,11 @@ type testVerifier struct {
1314
jwk jose.JSONWebKey
1415
}
1516

16-
func (t *testVerifier) verify(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) {
17+
func (t *testVerifier) VerifySignature(ctx context.Context, jwt string) ([]byte, error) {
18+
jws, err := jose.ParseSigned(jwt)
19+
if err != nil {
20+
return nil, fmt.Errorf("oidc: malformed jwt: %v", err)
21+
}
1722
return jws.Verify(&t.jwk)
1823
}
1924

@@ -217,7 +222,7 @@ func (v verificationTest) run(t *testing.T) {
217222
if v.issuer != "" {
218223
issuer = v.issuer
219224
}
220-
var ks keySet
225+
var ks KeySet
221226
if v.verificationKey == nil {
222227
ks = &testVerifier{v.signKey.jwk()}
223228
} else {

0 commit comments

Comments
 (0)