Commit 43cab48f authored by Eric Chiang's avatar Eric Chiang

*: expose KeySet, NewRemoteKeySet, and NewVerifier

Expose internal types to let users create IDTokenVerifiers without
using metadata discovery (/.well-known/openid-configuration). This
expands support to providers that don't implement discovery, and
lets users deliver verification keys out-of-band.
parent 731ffe63
...@@ -23,6 +23,20 @@ import ( ...@@ -23,6 +23,20 @@ import (
// updated. // updated.
const keysExpiryDelta = 30 * time.Second const keysExpiryDelta = 30 * time.Second
// NewRemoteKeySet returns a KeySet that can validate JSON web tokens by using HTTP
// GETs to fetch JSON web token sets hosted at a remote URL. This is automatically
// used by NewProvider using the URLs returned by OpenID Connect discovery, but is
// exposed for providers that don't support discovery or to prevent round trips to the
// discovery URL.
//
// The returned KeySet is a long lived verifier that caches keys based on cache-control
// headers. Reuse a common remote key set instead of creating new ones as needed.
//
// The behavior of the returned KeySet is undefined once the context is canceled.
func NewRemoteKeySet(ctx context.Context, jwksURL string) KeySet {
return newRemoteKeySet(ctx, jwksURL, time.Now)
}
func newRemoteKeySet(ctx context.Context, jwksURL string, now func() time.Time) *remoteKeySet { func newRemoteKeySet(ctx context.Context, jwksURL string, now func() time.Time) *remoteKeySet {
if now == nil { if now == nil {
now = time.Now now = time.Now
...@@ -79,6 +93,14 @@ func (i *inflight) result() ([]jose.JSONWebKey, error) { ...@@ -79,6 +93,14 @@ func (i *inflight) result() ([]jose.JSONWebKey, error) {
return i.keys, i.err return i.keys, i.err
} }
func (r *remoteKeySet) VerifySignature(ctx context.Context, jwt string) ([]byte, error) {
jws, err := jose.ParseSigned(jwt)
if err != nil {
return nil, fmt.Errorf("oidc: malformed jwt: %v", err)
}
return r.verify(ctx, jws)
}
func (r *remoteKeySet) verify(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) { func (r *remoteKeySet) verify(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) {
// We don't support JWTs signed with multiple signatures. // We don't support JWTs signed with multiple signatures.
keyID := "" keyID := ""
......
...@@ -64,7 +64,7 @@ type Provider struct { ...@@ -64,7 +64,7 @@ type Provider struct {
// Raw claims returned by the server. // Raw claims returned by the server.
rawClaims []byte rawClaims []byte
remoteKeySet *remoteKeySet remoteKeySet KeySet
} }
type cachedKeys struct { type cachedKeys struct {
...@@ -120,7 +120,7 @@ func NewProvider(ctx context.Context, issuer string) (*Provider, error) { ...@@ -120,7 +120,7 @@ func NewProvider(ctx context.Context, issuer string) (*Provider, error) {
tokenURL: p.TokenURL, tokenURL: p.TokenURL,
userInfoURL: p.UserInfoURL, userInfoURL: p.UserInfoURL,
rawClaims: body, rawClaims: body,
remoteKeySet: newRemoteKeySet(ctx, p.JWKSURL, time.Now), remoteKeySet: NewRemoteKeySet(ctx, p.JWKSURL),
}, nil }, nil
} }
......
...@@ -11,6 +11,6 @@ LINTABLE=$( go list -tags=golint -f ' ...@@ -11,6 +11,6 @@ LINTABLE=$( go list -tags=golint -f '
go test -v -i -race github.com/coreos/go-oidc/... go test -v -i -race github.com/coreos/go-oidc/...
go test -v -race github.com/coreos/go-oidc/... go test -v -race github.com/coreos/go-oidc/...
golint $LINTABLE golint -set_exit_status $LINTABLE
go vet github.com/coreos/go-oidc/... go vet github.com/coreos/go-oidc/...
go build -v ./example/... go build -v ./example/...
...@@ -19,19 +19,54 @@ const ( ...@@ -19,19 +19,54 @@ const (
issuerGoogleAccountsNoScheme = "accounts.google.com" issuerGoogleAccountsNoScheme = "accounts.google.com"
) )
// keySet is an interface that lets us stub out verification policies for // KeySet is a set of publc JSON Web Keys that can be used to validate the signature
// testing. Outside of testing, it's always backed by a remoteKeySet. // of JSON web tokens. This is expected to be backed by a remote key set through
type keySet interface { // provider metadata discovery or an in-memory set of keys delivered out-of-band.
verify(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) type KeySet interface {
// VerifySignature parses the JSON web token, verifies the signature, and returns
// the raw payload. Header and claim fields are validated by other parts of the
// package. For example, the KeySet does not need to check values such as signature
// algorithm, issuer, and audience since the IDTokenVerifier validates these values
// independently.
//
// If VerifySignature makes HTTP requests to verify the token, it's expected to
// use any HTTP client associated with the context through ClientContext.
VerifySignature(ctx context.Context, jwt string) (payload []byte, err error)
} }
// IDTokenVerifier provides verification for ID Tokens. // IDTokenVerifier provides verification for ID Tokens.
type IDTokenVerifier struct { type IDTokenVerifier struct {
keySet keySet keySet KeySet
config *Config config *Config
issuer string issuer string
} }
// NewVerifier returns a verifier manually constructed from a key set and issuer URL.
//
// It's easier to use provider discovery to construct an IDTokenVerifier than creating
// one directly. This method is intended to be used with provider that don't support
// metadata discovery, or avoiding round trips when the key set URL is already known.
//
// This constructor can be used to create a verifier directly using the issuer URL and
// JSON Web Key Set URL without using discovery:
//
// keySet := oidc.NewRemoteKeySet(ctx, "https://www.googleapis.com/oauth2/v3/certs")
// verifier := oidc.NewVerifier("https://accounts.google.com", keySet, config)
//
// Since KeySet is an interface, this constructor can also be used to supply custom
// public key sources. For example, if a user wanted to supply public keys out-of-band
// and hold them statically in-memory:
//
// // Custom KeySet implementation.
// keySet := newStatisKeySet(publicKeys...)
//
// // Verifier uses the custom KeySet implementation.
// verifier := oidc.NewVerifier("https://auth.example.com", keySet, config)
//
func NewVerifier(issuerURL string, keySet KeySet, config *Config) *IDTokenVerifier {
return &IDTokenVerifier{keySet: keySet, config: config, issuer: issuerURL}
}
// Config is the configuration for an IDTokenVerifier. // Config is the configuration for an IDTokenVerifier.
type Config struct { type Config struct {
// Expected audience of the token. For a majority of the cases this is expected to be // 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 { ...@@ -63,7 +98,7 @@ func (p *Provider) Verifier(config *Config) *IDTokenVerifier {
return newVerifier(p.remoteKeySet, config, p.issuer) return newVerifier(p.remoteKeySet, config, p.issuer)
} }
func newVerifier(keySet keySet, config *Config, issuer string) *IDTokenVerifier { func newVerifier(keySet KeySet, config *Config, issuer string) *IDTokenVerifier {
// If SupportedSigningAlgs is empty defaults to only support RS256. // If SupportedSigningAlgs is empty defaults to only support RS256.
if len(config.SupportedSigningAlgs) == 0 { if len(config.SupportedSigningAlgs) == 0 {
config.SupportedSigningAlgs = []string{RS256} config.SupportedSigningAlgs = []string{RS256}
...@@ -165,7 +200,7 @@ func (v *IDTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*IDTok ...@@ -165,7 +200,7 @@ func (v *IDTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*IDTok
return nil, fmt.Errorf("oidc: expected audience %q got %q", v.config.ClientID, t.Audience) return nil, fmt.Errorf("oidc: expected audience %q got %q", v.config.ClientID, t.Audience)
} }
} else { } else {
return nil, fmt.Errorf("oidc: Invalid configuration. ClientID must be provided or SkipClientIDCheck must be set.") return nil, fmt.Errorf("oidc: invalid configuration, clientID must be provided or SkipClientIDCheck must be set")
} }
} }
...@@ -194,7 +229,7 @@ func (v *IDTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*IDTok ...@@ -194,7 +229,7 @@ func (v *IDTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*IDTok
return nil, fmt.Errorf("oidc: id token signed with unsupported algorithm, expected %q got %q", v.config.SupportedSigningAlgs, sig.Header.Algorithm) return nil, fmt.Errorf("oidc: id token signed with unsupported algorithm, expected %q got %q", v.config.SupportedSigningAlgs, sig.Header.Algorithm)
} }
gotPayload, err := v.keySet.verify(ctx, jws) gotPayload, err := v.keySet.VerifySignature(ctx, rawIDToken)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to verify signature: %v", err) return nil, fmt.Errorf("failed to verify signature: %v", err)
} }
......
...@@ -2,6 +2,7 @@ package oidc ...@@ -2,6 +2,7 @@ package oidc
import ( import (
"context" "context"
"fmt"
"strconv" "strconv"
"testing" "testing"
"time" "time"
...@@ -13,7 +14,11 @@ type testVerifier struct { ...@@ -13,7 +14,11 @@ type testVerifier struct {
jwk jose.JSONWebKey jwk jose.JSONWebKey
} }
func (t *testVerifier) verify(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) { func (t *testVerifier) VerifySignature(ctx context.Context, jwt string) ([]byte, error) {
jws, err := jose.ParseSigned(jwt)
if err != nil {
return nil, fmt.Errorf("oidc: malformed jwt: %v", err)
}
return jws.Verify(&t.jwk) return jws.Verify(&t.jwk)
} }
...@@ -217,7 +222,7 @@ func (v verificationTest) run(t *testing.T) { ...@@ -217,7 +222,7 @@ func (v verificationTest) run(t *testing.T) {
if v.issuer != "" { if v.issuer != "" {
issuer = v.issuer issuer = v.issuer
} }
var ks keySet var ks KeySet
if v.verificationKey == nil { if v.verificationKey == nil {
ks = &testVerifier{v.signKey.jwk()} ks = &testVerifier{v.signKey.jwk()}
} else { } else {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment