Commit 7833b68a authored by Eric Chiang's avatar Eric Chiang

*: add a new OpenID Connect client

parent 16c5ecc5
language: go
go:
- 1.4.3
- 1.5.4
- 1.6.1
- 1.7
- 1.7.3
install:
- go get -v -t ./...
- go get -v -t github.com/coreos/go-oidc
- go get golang.org/x/tools/cmd/cover
script:
......
#!/bin/bash -e
GOBUILD="go build -a -installsuffix netgo -ldflags '-s'"
echo "building bin/oidc-example-app..."
${GOBUILD} -o bin/oidc-example-app github.com/coreos/go-oidc/example/app
echo "building bin/oidc-example-cli..."
${GOBUILD} -o bin/oidc-example-cli github.com/coreos/go-oidc/example/cli
echo "done"
/*
Package oidc implements OpenID Connect client logic for the golang.org/x/oauth2 package.
provider, err := oidc.NewProvider(ctx, "https://accounts.example.com")
if err != nil {
return err
}
// Configure an OpenID Connect aware OAuth2 client.
oauth2Config := oauth2.Config{
ClientID: clientID,
ClientSecret: clientSecret,
RedirectURL: redirectURL,
Endpoint: provider.Endpoint(),
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
}
OAuth2 redirects are unchanged.
func handleRedirect(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, oauth2Config.AuthCodeURL(state), http.StatusFound)
})
For callbacks the provider can be used to query for user information such as email.
func handleOAuth2Callback(w http.ResponseWriter, r *http.Request) {
// Verify state...
oauth2Token, err := oauth2Config.Exchange(ctx, r.URL.Query().Get("code"))
if err != nil {
http.Error(w, "Failed to exchange token: "+err.Error(), http.StatusInternalServerError)
return
}
userinfo, err := provider.UserInfo(ctx, oauth2.StaticTokenSource(oauth2Token))
if err != nil {
http.Error(w, "Failed to get userinfo: "+err.Error(), http.StatusInternalServerError)
return
}
// ...
})
The provider also has the ability to verify ID Tokens.
verifier := provider.Verifier()
The returned verifier can be used to perform basic validation on ID Token issued by the provider,
including verifying the JWT signature. It then returns the payload.
func handleOAuth2Callback(w http.ResponseWriter, r *http.Request) {
// Verify state...
oauth2Token, err := oauth2Config.Exchange(ctx, r.URL.Query().Get("code"))
if err != nil {
http.Error(w, "Failed to exchange token: "+err.Error(), http.StatusInternalServerError)
return
}
// Extract the ID Token from oauth2 token.
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
if !ok {
http.Error(w, "No ID Token found", http.StatusInternalServerError)
return
}
// Verify that the ID Token is signed by the provider.
idToken, err := verifier.Verify(rawIDToken)
if err != nil {
http.Error(w, "Failed to verify ID Token: "+err.Error(), http.StatusInternalServerError)
return
}
// Unmarshal ID Token for expected custom claims.
var claims struct {
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
}
if err := idToken.Claims(&claims); err != nil {
http.Error(w, "Failed to unmarshal ID Token custom claims: "+err.Error(), http.StatusInternalServerError)
return
}
// ...
})
ID Token nonces are supported.
First, provide a nonce source for nonce validation. This will then be used to wrap the existing
provider ID Token verifier.
// A verifier which boths verifies the ID Token signature and nonce.
nonceEnabledVerifier := provider.Verifier(oidc.VerifyNonce(nonceSource))
For the redirect provide a nonce auth code option. This will be placed as a URL parameter during
the client redirect.
func handleRedirect(w http.ResponseWriter, r *http.Request) {
nonce, err := newNonce()
if err != nil {
// ...
}
// Provide a nonce for the OpenID Connect ID Token.
http.Redirect(w, r, oauth2Config.AuthCodeURL(state, oidc.Nonce(nonce)), http.StatusFound)
})
The nonce enabled verifier can then be used to verify the nonce while unpacking the ID Token.
func handleOAuth2Callback(w http.ResponseWriter, r *http.Request) {
// Verify state...
oauth2Token, err := oauth2Config.Exchange(ctx, r.URL.Query().Get("code"))
if err != nil {
http.Error(w, "Failed to exchange token: "+err.Error(), http.StatusInternalServerError)
return
}
// Extract the ID Token from oauth2 token.
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
if !ok {
http.Error(w, "No ID Token found", http.StatusInternalServerError)
return
}
// Verify that the ID Token is signed by the provider and verify the nonce.
idToken, err := nonceEnabledVerifier.Verify(rawIDToken)
if err != nil {
http.Error(w, "Failed to verify ID Token: "+err.Error(), http.StatusInternalServerError)
return
}
// Continue as above...
})
This package uses contexts to derive HTTP clients in the same way as the oauth2 package. To configure
a custom client, use the oauth2 packages HTTPClient context key when constructing the context.
myClient := &http.Client{}
myCtx := context.WithValue(parentCtx, oauth2.HTTPClient, myClient)
// NewProvider will use myClient to make the request.
provider, err := oidc.NewProvider(myCtx, "https://accounts.example.com")
*/
package oidc
// +build !golint
// Don't lint this file. We don't want to have to add a comment to each constant.
package oidc
const (
// JOSE asymmetric signing algorithm values as defined by RFC 7518
//
// see: https://tools.ietf.org/html/rfc7518#section-3.1
RS256 = "RS256" // RSASSA-PKCS-v1.5 using SHA-256
RS384 = "RS384" // RSASSA-PKCS-v1.5 using SHA-384
RS512 = "RS512" // RSASSA-PKCS-v1.5 using SHA-512
ES256 = "ES256" // ECDSA using P-256 and SHA-256
ES384 = "ES384" // ECDSA using P-384 and SHA-384
ES512 = "ES512" // ECDSA using P-521 and SHA-512
PS256 = "PS256" // RSASSA-PSS using SHA256 and MGF1-SHA256
PS384 = "PS384" // RSASSA-PSS using SHA384 and MGF1-SHA384
PS512 = "PS512" // RSASSA-PSS using SHA512 and MGF1-SHA512
)
package oidc
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"sync"
"time"
"github.com/pquerna/cachecontrol"
"golang.org/x/net/context"
"golang.org/x/net/context/ctxhttp"
jose "gopkg.in/square/go-jose.v2"
)
// keysExpiryDelta is the allowed clock skew between a client and the OpenID Connect
// server.
//
// When keys expire, they are valid for this amount of time after.
//
// If the keys have not expired, and an ID Token claims it was signed by a key not in
// the cache, if and only if the keys expire in this amount of time, the keys will be
// updated.
const keysExpiryDelta = 30 * time.Second
func newRemoteKeySet(ctx context.Context, jwksURL string, now func() time.Time) *remoteKeySet {
if now == nil {
now = time.Now
}
return &remoteKeySet{jwksURL: jwksURL, ctx: ctx, now: now}
}
type remoteKeySet struct {
jwksURL string
ctx context.Context
now func() time.Time
// guard all other fields
mu sync.Mutex
// inflightCtx is the context of the current HTTP request to update the keys.
// Its Err() method returns any errors encountered during that attempt.
//
// If nil, there is no inflight request.
inflightCtx context.Context
// A set of cached keys and their expiry.
cachedKeys []jose.JSONWebKey
expiry time.Time
}
// errContext is a context with a customizable Err() return value.
type errContext struct {
context.Context
cf context.CancelFunc
err error
}
func newErrContext(parent context.Context) *errContext {
ctx, cancel := context.WithCancel(parent)
return &errContext{ctx, cancel, nil}
}
func (e errContext) Err() error {
return e.err
}
// cancel cancels the errContext causing listeners on Done() to return.
func (e errContext) cancel(err error) {
e.err = err
e.cf()
}
func (r *remoteKeySet) keysWithIDFromCache(keyIDs []string) ([]jose.JSONWebKey, bool) {
r.mu.Lock()
keys, expiry := r.cachedKeys, r.expiry
r.mu.Unlock()
// Have the keys expired?
if expiry.Add(keysExpiryDelta).Before(r.now()) {
return nil, false
}
var signingKeys []jose.JSONWebKey
for _, key := range keys {
if contains(keyIDs, key.KeyID) {
signingKeys = append(signingKeys, key)
}
}
if len(signingKeys) == 0 {
// Are the keys about to expire?
if r.now().Add(keysExpiryDelta).After(expiry) {
return nil, false
}
}
return signingKeys, true
}
func (r *remoteKeySet) keysWithID(ctx context.Context, keyIDs []string) ([]jose.JSONWebKey, error) {
keys, ok := r.keysWithIDFromCache(keyIDs)
if ok {
return keys, nil
}
var inflightCtx context.Context
func() {
r.mu.Lock()
defer r.mu.Unlock()
// If there's not a current inflight request, create one.
if r.inflightCtx == nil {
// Use the remoteKeySet's context instead of the requests context
// because a re-sync is unique to the keys set and will span multiple
// requests.
errCtx := newErrContext(r.ctx)
r.inflightCtx = errCtx
go func() {
// TODO(ericchiang): Upstream Kubernetes request that we recover every time
// we spawn a goroutine, because panics in a goroutine will bring down the
// entire program. There's no way to recover from another goroutine's panic.
//
// Most users actually want to let the panic propagate and bring down the
// program because it implies some unrecoverable state.
//
// Add a context key to allow the recover behavior.
//
// See: https://github.com/coreos/go-oidc/issues/89
// Sync keys and close inflightCtx when that's done.
errCtx.cancel(r.updateKeys(r.inflightCtx))
r.mu.Lock()
defer r.mu.Unlock()
r.inflightCtx = nil
}()
}
inflightCtx = r.inflightCtx
}()
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-inflightCtx.Done():
if err := inflightCtx.Err(); err != nil {
return nil, err
}
}
// Since we've just updated keys, we don't care about the cache miss.
keys, _ = r.keysWithIDFromCache(keyIDs)
return keys, nil
}
func (r *remoteKeySet) updateKeys(ctx context.Context) error {
req, err := http.NewRequest("GET", r.jwksURL, nil)
if err != nil {
return fmt.Errorf("oidc: can't create request: %v", err)
}
resp, err := ctxhttp.Do(ctx, clientFromContext(ctx), req)
if err != nil {
return fmt.Errorf("oidc: get keys failed %v", err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("oidc: read response body: %v", err)
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("oidc: get keys failed: %s %s", resp.Status, body)
}
var keySet jose.JSONWebKeySet
if err := json.Unmarshal(body, &keySet); err != nil {
return fmt.Errorf("oidc: failed to decode keys: %v %s", err, body)
}
// If the server doesn't provide cache control headers, assume the
// keys expire immediately.
expiry := r.now()
_, e, err := cachecontrol.CachableResponse(req, resp, cachecontrol.Options{})
if err == nil && e.After(expiry) {
expiry = e
}
r.mu.Lock()
defer r.mu.Unlock()
r.cachedKeys = keySet.Keys
r.expiry = expiry
return nil
}
package oidc
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"strings"
"time"
"golang.org/x/net/context"
"golang.org/x/oauth2"
jose "gopkg.in/square/go-jose.v2"
)
const (
// ScopeOpenID is the mandatory scope for all OpenID Connect OAuth2 requests.
ScopeOpenID = "openid"
// ScopeOfflineAccess is an optional scope defined by OpenID Connect for requesting
// OAuth2 refresh tokens.
//
// Support for this scope differs between OpenID Connect providers. For instance
// Google rejects it, favoring appending "access_type=offline" as part of the
// authorization request instead.
//
// See: https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
ScopeOfflineAccess = "offline_access"
)
// ClientContext returns a new Context that carries the provided HTTP client.
//
// This method sets the same context key used by the golang.org/x/oauth2 package,
// so the returned context works for that package too.
func ClientContext(ctx context.Context, client *http.Client) context.Context {
return context.WithValue(ctx, oauth2.HTTPClient, client)
}
func clientFromContext(ctx context.Context) *http.Client {
if client, ok := ctx.Value(oauth2.HTTPClient).(*http.Client); ok {
return client
}
return http.DefaultClient
}
// Provider represents an OpenID Connect server's configuration.
type Provider struct {
issuer string
authURL string
tokenURL string
userInfoURL string
// Raw claims returned by the server.
rawClaims []byte
remoteKeySet *remoteKeySet
}
type cachedKeys struct {
keys []jose.JSONWebKey
expiry time.Time
}
type providerJSON struct {
Issuer string `json:"issuer"`
AuthURL string `json:"authorization_endpoint"`
TokenURL string `json:"token_endpoint"`
JWKSURL string `json:"jwks_uri"`
UserInfoURL string `json:"userinfo_endpoint"`
}
// NewProvider uses the OpenID Connect disovery mechanism to construct a Provider.
func NewProvider(ctx context.Context, issuer string) (*Provider, error) {
wellKnown := strings.TrimSuffix(issuer, "/") + "/.well-known/openid-configuration"
resp, err := clientFromContext(ctx).Get(wellKnown)
if err != nil {
return nil, err
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%s: %s", resp.Status, body)
}
defer resp.Body.Close()
var p providerJSON
if err := json.Unmarshal(body, &p); err != nil {
return nil, fmt.Errorf("oidc: failed to decode provider discovery object: %v", err)
}
if p.Issuer != issuer {
return nil, fmt.Errorf("oidc: issuer did not match the issuer returned by provider, expected %q got %q", issuer, p.Issuer)
}
return &Provider{
issuer: p.Issuer,
authURL: p.AuthURL,
tokenURL: p.TokenURL,
userInfoURL: p.UserInfoURL,
rawClaims: body,
remoteKeySet: newRemoteKeySet(ctx, p.JWKSURL, time.Now),
}, nil
}
// Claims unmarshals raw fields returned by the server during discovery.
//
// var claims struct {
// ScopesSupported []string `json:"scopes_supported"`
// ClaimsSupported []string `json:"claims_supported"`
// }
//
// if err := provider.Claims(&claims); err != nil {
// // handle unmarshaling error
// }
//
// For a list of fields defined by the OpenID Connect spec see:
// https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
func (p *Provider) Claims(v interface{}) error {
if p.rawClaims == nil {
return errors.New("oidc: claims not set")
}
return json.Unmarshal(p.rawClaims, v)
}
// Endpoint returns the OAuth2 auth and token endpoints for the given provider.
func (p *Provider) Endpoint() oauth2.Endpoint {
return oauth2.Endpoint{AuthURL: p.authURL, TokenURL: p.tokenURL}
}
// UserInfo represents the OpenID Connect userinfo claims.
type UserInfo struct {
Subject string `json:"sub"`
Profile string `json:"profile"`
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
claims []byte
}
// Claims unmarshals the raw JSON object claims into the provided object.
func (u *UserInfo) Claims(v interface{}) error {
if u.claims == nil {
return errors.New("oidc: claims not set")
}
return json.Unmarshal(u.claims, v)
}
// UserInfo uses the token source to query the provider's user info endpoint.
func (p *Provider) UserInfo(ctx context.Context, tokenSource oauth2.TokenSource) (*UserInfo, error) {
if p.userInfoURL == "" {
return nil, errors.New("oidc: user info endpoint is not supported by this provider")
}
resp, err := clientFromContext(ctx).Get(p.userInfoURL)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%s: %s", resp.Status, body)
}
var userInfo UserInfo
if err := json.Unmarshal(body, &userInfo); err != nil {
return nil, fmt.Errorf("oidc: failed to decode userinfo: %v", err)
}
userInfo.claims = body
return &userInfo, nil
}
// IDToken is an OpenID Connect extension that provides a predictable representation
// of an authorization event.
//
// The ID Token only holds fields OpenID Connect requires. To access additional
// claims returned by the server, use the Claims method.
type IDToken struct {
// The URL of the server which issued this token. This will always be the same
// as the URL used for initial discovery.
Issuer string
// The client, or set of clients, that this token is issued for.
Audience []string
// A unique string which identifies the end user.
Subject string
IssuedAt time.Time
Expiry time.Time
Nonce string
// Raw payload of the id_token.
claims []byte
}
// Claims unmarshals the raw JSON payload of the ID Token into a provided struct.
//
// idToken, err := idTokenVerifier.Verify(rawIDToken)
// if err != nil {
// // handle error
// }
// var claims struct {
// Email string `json:"email"`
// EmailVerified bool `json:"email_verified"`
// }
// if err := idToken.Claims(&claims); err != nil {
// // handle error
// }
//
func (i *IDToken) Claims(v interface{}) error {
if i.claims == nil {
return errors.New("oidc: claims not set")
}
return json.Unmarshal(i.claims, v)
}
type idToken struct {
Issuer string `json:"iss"`
Subject string `json:"sub"`
Audience audience `json:"aud"`
Expiry jsonTime `json:"exp"`
IssuedAt jsonTime `json:"iat"`
Nonce string `json:"nonce"`
}
type audience []string
func (a *audience) UnmarshalJSON(b []byte) error {
var s string
if json.Unmarshal(b, &s) == nil {
*a = audience{s}
return nil
}
var auds []string
if err := json.Unmarshal(b, &auds); err != nil {
return err
}
*a = audience(auds)
return nil
}
func (a audience) MarshalJSON() ([]byte, error) {
if len(a) == 1 {
return json.Marshal(a[0])
}
return json.Marshal([]string(a))
}
type jsonTime time.Time
func (j *jsonTime) UnmarshalJSON(b []byte) error {
var n json.Number
if err := json.Unmarshal(b, &n); err != nil {
return err
}
var unix int64
if t, err := n.Int64(); err == nil {
unix = t
} else {
f, err := n.Float64()
if err != nil {
return err
}
unix = int64(f)
}
*j = jsonTime(time.Unix(unix, 0))
return nil
}
func (j jsonTime) MarshalJSON() ([]byte, error) {
return json.Marshal(time.Time(j).Unix())
}
package oidc
import (
"net/http"
"testing"
"golang.org/x/net/context"
)
func TestClientContext(t *testing.T) {
myClient := &http.Client{}
ctx := ClientContext(context.Background(), myClient)
gotClient := clientFromContext(ctx)
// Compare pointer values.
if gotClient != myClient {
t.Fatal("clientFromContext did not return the value set by ClientContext")
}
}
#!/bin/bash -e
#
# Run all tests (not including functional)
# ./test
# ./test -v
#
# Run tests for one package
# PKG=./unit ./test
# PKG=ssh ./test
#
# Invoke ./cover for HTML output
COVER=${COVER:-"-cover"}
RACE=${RACE:-"-race"}
source ./build
TESTABLE="http jose key oauth2 oidc"
FORMATTABLE="$TESTABLE"
# user has not provided PKG override
if [ -z "$PKG" ]; then
TEST=$TESTABLE
FMT=$FORMATTABLE
# user has provided PKG override
else
# strip out slashes and dots from PKG=./foo/
TEST=${PKG//\//}
TEST=${TEST//./}
# only run gofmt on packages provided by user
FMT="$TEST"
fi
# split TEST into an array and prepend repo path to each local package
split=(${TEST// / })
TEST=${split[@]/#/github.com/coreos/go-oidc/}
echo "Running tests..."
go test $RACE ${COVER} $@ ${TEST}
echo "Checking gofmt..."
fmtRes=$(gofmt -l $FMT)
if [ -n "${fmtRes}" ]; then
echo -e "gofmt checking failed:\n${fmtRes}"
exit 255
fi
if [[ -z "$TRAVIS_GO_VERSION" || "$TRAVIS_GO_VERSION" != "1.4.3" ]]; then
echo "Checking govet..."
vetRes=$(go vet $TEST)
if [ -n "${vetRes}" ]; then
echo -e "govet checking failed:\n${vetRes}"
exit 255
fi
else
echo "Skipping govet (Go 1.4)"
fi
echo "Success"
#!/bin/bash
# Filter out any files with a !golint build tag.
LINTABLE=$( go list -tags=golint -f '
{{- range $i, $file := .GoFiles -}}
{{ $file }} {{ end }}
{{ range $i, $file := .TestGoFiles -}}
{{ $file }} {{ end }}' github.com/coreos/go-oidc )
go test -v -i -race github.com/coreos/go-oidc
go test -v -race github.com/coreos/go-oidc
golint $LINTABLE
go vet github.com/coreos/go-oidc
package oidc
import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"golang.org/x/net/context"
"golang.org/x/oauth2"
jose "gopkg.in/square/go-jose.v2"
)
// IDTokenVerifier provides verification for ID Tokens.
type IDTokenVerifier struct {
keySet *remoteKeySet
config *verificationConfig
}
// verificationConfig is the unexported configuration for an IDTokenVerifier.
//
// Users interact with this struct using a VerificationOption.
type verificationConfig struct {
issuer string
// If provided, this value must be in the ID Token audiences.
audience string
// If not nil, check the expiry of the id token.
checkExpiry func() time.Time
// If specified, only these sets of algorithms may be used to sign the JWT.
requiredAlgs []string
// If not nil, don't verify nonce.
nonceSource NonceSource
}
// VerificationOption provides additional checks on ID Tokens.
type VerificationOption interface {
// Unexport this method so other packages can't implement this interface.
updateConfig(c *verificationConfig)
}
// Verifier returns an IDTokenVerifier that uses the provider's key set to verify JWTs.
//
// The returned IDTokenVerifier is tied to the Provider's context and its behavior is
// undefined once the Provider's context is canceled.
func (p *Provider) Verifier(options ...VerificationOption) *IDTokenVerifier {
config := &verificationConfig{issuer: p.issuer}
for _, option := range options {
option.updateConfig(config)
}
return newVerifier(p.remoteKeySet, config)
}
func newVerifier(keySet *remoteKeySet, config *verificationConfig) *IDTokenVerifier {
// As discussed in the godocs for VerifrySigningAlg, because almost all providers
// only support RS256, default to only allowing it.
if len(config.requiredAlgs) == 0 {
config.requiredAlgs = []string{RS256}
}
return &IDTokenVerifier{
keySet: keySet,
config: config,
}
}
func parseJWT(p string) ([]byte, error) {
parts := strings.Split(p, ".")
if len(parts) < 2 {
return nil, fmt.Errorf("oidc: malformed jwt, expected 3 parts got %d", len(parts))
}
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return nil, fmt.Errorf("oidc: malformed jwt payload: %v", err)
}
return payload, nil
}
func contains(sli []string, ele string) bool {
for _, s := range sli {
if s == ele {
return true
}
}
return false
}
// Verify parses a raw ID Token, verifies it's been signed by the provider, preforms
// any additional checks passed as VerifictionOptions, and returns the payload.
//
// See: https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
//
// oauth2Token, err := oauth2Config.Exchange(ctx, r.URL.Query().Get("code"))
// if err != nil {
// // handle error
// }
//
// // Extract the ID Token from oauth2 token.
// rawIDToken, ok := oauth2Token.Extra("id_token").(string)
// if !ok {
// // handle error
// }
//
// token, err := verifier.Verify(ctx, rawIDToken)
//
func (v *IDTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*IDToken, error) {
jws, err := jose.ParseSigned(rawIDToken)
if err != nil {
return nil, fmt.Errorf("oidc: mallformed jwt: %v", err)
}
// Throw out tokens with invalid claims before trying to verify the token. This lets
// us do cheap checks before possibly re-syncing keys.
payload, err := parseJWT(rawIDToken)
if err != nil {
return nil, fmt.Errorf("oidc: malformed jwt: %v", err)
}
var token idToken
if err := json.Unmarshal(payload, &token); err != nil {
return nil, fmt.Errorf("oidc: failed to unmarshal claims: %v", err)
}
t := &IDToken{
Issuer: token.Issuer,
Subject: token.Subject,
Audience: []string(token.Audience),
Expiry: time.Time(token.Expiry),
IssuedAt: time.Time(token.IssuedAt),
Nonce: token.Nonce,
claims: payload,
}
// Check issuer.
if t.Issuer != v.config.issuer {
return nil, fmt.Errorf("oidc: id token issued by a different provider, expected %q got %q", v.config.issuer, t.Issuer)
}
// If a client ID has been provided, make sure it's part of the audience.
if v.config.audience != "" {
if !contains(t.Audience, v.config.audience) {
return nil, fmt.Errorf("oidc: expected audience %q got %q", v.config.audience, t.Audience)
}
}
// If a set of required algorithms has been provided, ensure that the signatures use those.
var keyIDs, gotAlgs []string
for _, sig := range jws.Signatures {
if len(v.config.requiredAlgs) == 0 || contains(v.config.requiredAlgs, sig.Header.Algorithm) {
keyIDs = append(keyIDs, sig.Header.KeyID)
} else {
gotAlgs = append(gotAlgs, sig.Header.Algorithm)
}
}
if len(keyIDs) == 0 {
return nil, fmt.Errorf("oidc: no signatures use a require algorithm, expected %q got %q", v.config.requiredAlgs, gotAlgs)
}
// Get keys from the remote key set. This may trigger a re-sync.
keys, err := v.keySet.keysWithID(ctx, keyIDs)
if err != nil {
return nil, fmt.Errorf("oidc: get keys for id token: %v", err)
}
if len(keys) == 0 {
return nil, fmt.Errorf("oidc: no keys match signature ID(s) %q", keyIDs)
}
// Try to use a key to validate the signature.
var gotPayload []byte
for _, key := range keys {
if p, err := jws.Verify(&key); err == nil {
gotPayload = p
}
}
if len(gotPayload) == 0 {
return nil, fmt.Errorf("oidc: failed to verify id token")
}
// Ensure that the payload returned by the square actually matches the payload parsed earlier.
if !bytes.Equal(gotPayload, payload) {
return nil, errors.New("oidc: internal error, payload parsed did not match previous payload")
}
// Check the nonce after we've verified the token. We don't want to allow unverified
// payloads to trigger a nonce lookup.
if v.config.nonceSource != nil {
if err := v.config.nonceSource.ClaimNonce(t.Nonce); err != nil {
return nil, err
}
}
return t, nil
}
// VerifyAudience ensures that an ID Token was issued for the specific client.
//
// Note that a verified token may be valid for other clients, as OpenID Connect allows a token to have
// multiple audiences.
func VerifyAudience(clientID string) VerificationOption {
return clientVerifier{clientID}
}
type clientVerifier struct {
clientID string
}
func (v clientVerifier) updateConfig(c *verificationConfig) {
c.audience = v.clientID
}
// VerifyExpiry ensures that an ID Token has not expired.
func VerifyExpiry() VerificationOption {
return expiryVerifier{}
}
type expiryVerifier struct{}
func (v expiryVerifier) updateConfig(c *verificationConfig) {
c.checkExpiry = time.Now
}
// VerifySigningAlg enforces that an ID Token is signed by a specific signing algorithm.
//
// Because so many providers only support RS256, if this verifiction option isn't used,
// the IDTokenVerifier defaults to only allowing RS256.
func VerifySigningAlg(allowedAlgs ...string) VerificationOption {
return algVerifier{allowedAlgs}
}
type algVerifier struct {
algs []string
}
func (v algVerifier) updateConfig(c *verificationConfig) {
c.requiredAlgs = v.algs
}
// Nonce returns an auth code option which requires the ID Token created by the
// OpenID Connect provider to contain the specified nonce.
func Nonce(nonce string) oauth2.AuthCodeOption {
return oauth2.SetAuthURLParam("nonce", nonce)
}
// NonceSource represents a source which can verify a nonce is valid and has not
// been claimed before.
type NonceSource interface {
ClaimNonce(nonce string) error
}
// VerifyNonce ensures that the ID Token contains a nonce which can be claimed by the nonce source.
func VerifyNonce(source NonceSource) VerificationOption {
return nonceVerifier{source}
}
type nonceVerifier struct {
nonceSource NonceSource
}
func (n nonceVerifier) updateConfig(c *verificationConfig) {
c.nonceSource = n.nonceSource
}
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