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 (
// updated.
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 {
if now == nil {
now = time.Now
......@@ -79,6 +93,14 @@ func (i *inflight) result() ([]jose.JSONWebKey, error) {
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) {
// We don't support JWTs signed with multiple signatures.
keyID := ""
......
......@@ -64,7 +64,7 @@ type Provider struct {
// Raw claims returned by the server.
rawClaims []byte
remoteKeySet *remoteKeySet
remoteKeySet KeySet
}
type cachedKeys struct {
......@@ -120,7 +120,7 @@ func NewProvider(ctx context.Context, issuer string) (*Provider, error) {
tokenURL: p.TokenURL,
userInfoURL: p.UserInfoURL,
rawClaims: body,
remoteKeySet: newRemoteKeySet(ctx, p.JWKSURL, time.Now),
remoteKeySet: NewRemoteKeySet(ctx, p.JWKSURL),
}, nil
}
......
......@@ -11,6 +11,6 @@ LINTABLE=$( go list -tags=golint -f '
go test -v -i -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 build -v ./example/...
......@@ -19,19 +19,54 @@ const (
issuerGoogleAccountsNoScheme = "accounts.google.com"
)
// keySet is an interface that lets us stub out verification policies for
// testing. Outside of testing, it's always backed by a remoteKeySet.
type keySet interface {
verify(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error)
// KeySet is a set of publc JSON Web Keys that can be used to validate the signature
// of JSON web tokens. This is expected to be backed by a remote key set through
// provider metadata discovery or an in-memory set of keys delivered out-of-band.
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.
type IDTokenVerifier struct {
keySet keySet
keySet KeySet
config *Config
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.
type Config struct {
// 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 {
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 len(config.SupportedSigningAlgs) == 0 {
config.SupportedSigningAlgs = []string{RS256}
......@@ -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)
}
} 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
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 {
return nil, fmt.Errorf("failed to verify signature: %v", err)
}
......
......@@ -2,6 +2,7 @@ package oidc
import (
"context"
"fmt"
"strconv"
"testing"
"time"
......@@ -13,7 +14,11 @@ type testVerifier struct {
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)
}
......@@ -217,7 +222,7 @@ func (v verificationTest) run(t *testing.T) {
if v.issuer != "" {
issuer = v.issuer
}
var ks keySet
var ks KeySet
if v.verificationKey == nil {
ks = &testVerifier{v.signKey.jwk()}
} 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