123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336 |
- package oidc
- import (
- "bytes"
- "context"
- "encoding/base64"
- "encoding/json"
- "errors"
- "fmt"
- "io/ioutil"
- "net/http"
- "strings"
- "time"
- "golang.org/x/oauth2"
- jose "gopkg.in/square/go-jose.v2"
- )
- const (
- issuerGoogleAccounts = "https://accounts.google.com"
- issuerGoogleAccountsNoScheme = "accounts.google.com"
- )
- // 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
- 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
- // the ID of the client that initialized the login flow. It may occasionally differ if
- // the provider supports the authorizing party (azp) claim.
- //
- // If not provided, users must explicitly set SkipClientIDCheck.
- ClientID string
- // If specified, only this set of algorithms may be used to sign the JWT.
- //
- // If the IDTokenVerifier is created from a provider with (*Provider).Verifier, this
- // defaults to the set of algorithms the provider supports. Otherwise this values
- // defaults to RS256.
- SupportedSigningAlgs []string
- // If true, no ClientID check performed. Must be true if ClientID field is empty.
- SkipClientIDCheck bool
- // If true, token expiry is not checked.
- SkipExpiryCheck bool
- // SkipIssuerCheck is intended for specialized cases where the the caller wishes to
- // defer issuer validation. When enabled, callers MUST independently verify the Token's
- // Issuer is a known good value.
- //
- // Mismatched issuers often indicate client mis-configuration. If mismatches are
- // unexpected, evaluate if the provided issuer URL is incorrect instead of enabling
- // this option.
- SkipIssuerCheck bool
- // Time function to check Token expiry. Defaults to time.Now
- Now func() time.Time
- }
- // 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(config *Config) *IDTokenVerifier {
- if len(config.SupportedSigningAlgs) == 0 && len(p.algorithms) > 0 {
- // Make a copy so we don't modify the config values.
- cp := &Config{}
- *cp = *config
- cp.SupportedSigningAlgs = p.algorithms
- config = cp
- }
- return NewVerifier(p.issuer, p.remoteKeySet, 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
- }
- // Returns the Claims from the distributed JWT token
- func resolveDistributedClaim(ctx context.Context, verifier *IDTokenVerifier, src claimSource) ([]byte, error) {
- req, err := http.NewRequest("GET", src.Endpoint, nil)
- if err != nil {
- return nil, fmt.Errorf("malformed request: %v", err)
- }
- if src.AccessToken != "" {
- req.Header.Set("Authorization", "Bearer "+src.AccessToken)
- }
- resp, err := doRequest(ctx, req)
- if err != nil {
- return nil, fmt.Errorf("oidc: Request to endpoint failed: %v", err)
- }
- defer resp.Body.Close()
- body, err := ioutil.ReadAll(resp.Body)
- if err != nil {
- return nil, fmt.Errorf("unable to read response body: %v", err)
- }
- if resp.StatusCode != http.StatusOK {
- return nil, fmt.Errorf("oidc: request failed: %v", resp.StatusCode)
- }
- token, err := verifier.Verify(ctx, string(body))
- if err != nil {
- return nil, fmt.Errorf("malformed response body: %v", err)
- }
- return token.claims, nil
- }
- func parseClaim(raw []byte, name string, v interface{}) error {
- var parsed map[string]json.RawMessage
- if err := json.Unmarshal(raw, &parsed); err != nil {
- return err
- }
- val, ok := parsed[name]
- if !ok {
- return fmt.Errorf("claim doesn't exist: %s", name)
- }
- return json.Unmarshal([]byte(val), v)
- }
- // Verify parses a raw ID Token, verifies it's been signed by the provider, preforms
- // any additional checks depending on the Config, and returns the payload.
- //
- // Verify does NOT do nonce validation, which is the callers responsibility.
- //
- // 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: malformed 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)
- }
- distributedClaims := make(map[string]claimSource)
- //step through the token to map claim names to claim sources"
- for cn, src := range token.ClaimNames {
- if src == "" {
- return nil, fmt.Errorf("oidc: failed to obtain source from claim name")
- }
- s, ok := token.ClaimSources[src]
- if !ok {
- return nil, fmt.Errorf("oidc: source does not exist")
- }
- distributedClaims[cn] = s
- }
- 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,
- AccessTokenHash: token.AtHash,
- claims: payload,
- distributedClaims: distributedClaims,
- }
- // Check issuer.
- if !v.config.SkipIssuerCheck && t.Issuer != v.issuer {
- // Google sometimes returns "accounts.google.com" as the issuer claim instead of
- // the required "https://accounts.google.com". Detect this case and allow it only
- // for Google.
- //
- // We will not add hooks to let other providers go off spec like this.
- if !(v.issuer == issuerGoogleAccounts && t.Issuer == issuerGoogleAccountsNoScheme) {
- return nil, fmt.Errorf("oidc: id token issued by a different provider, expected %q got %q", v.issuer, t.Issuer)
- }
- }
- // If a client ID has been provided, make sure it's part of the audience. SkipClientIDCheck must be true if ClientID is empty.
- //
- // This check DOES NOT ensure that the ClientID is the party to which the ID Token was issued (i.e. Authorized party).
- if !v.config.SkipClientIDCheck {
- if v.config.ClientID != "" {
- if !contains(t.Audience, v.config.ClientID) {
- 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")
- }
- }
- // If a SkipExpiryCheck is false, make sure token is not expired.
- if !v.config.SkipExpiryCheck {
- now := time.Now
- if v.config.Now != nil {
- now = v.config.Now
- }
- nowTime := now()
- if t.Expiry.Before(nowTime) {
- return nil, fmt.Errorf("oidc: token is expired (Token Expiry: %v)", t.Expiry)
- }
- // If nbf claim is provided in token, ensure that it is indeed in the past.
- if token.NotBefore != nil {
- nbfTime := time.Time(*token.NotBefore)
- leeway := 1 * time.Minute
- if nowTime.Add(leeway).Before(nbfTime) {
- return nil, fmt.Errorf("oidc: current time %v before the nbf (not before) time: %v", nowTime, nbfTime)
- }
- }
- }
- switch len(jws.Signatures) {
- case 0:
- return nil, fmt.Errorf("oidc: id token not signed")
- case 1:
- default:
- return nil, fmt.Errorf("oidc: multiple signatures on id token not supported")
- }
- sig := jws.Signatures[0]
- supportedSigAlgs := v.config.SupportedSigningAlgs
- if len(supportedSigAlgs) == 0 {
- supportedSigAlgs = []string{RS256}
- }
- if !contains(supportedSigAlgs, sig.Header.Algorithm) {
- return nil, fmt.Errorf("oidc: id token signed with unsupported algorithm, expected %q got %q", supportedSigAlgs, sig.Header.Algorithm)
- }
- t.sigAlgorithm = sig.Header.Algorithm
- gotPayload, err := v.keySet.VerifySignature(ctx, rawIDToken)
- if err != nil {
- return nil, fmt.Errorf("failed to verify signature: %v", err)
- }
- // 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")
- }
- return t, nil
- }
- // 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)
- }
|