| 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) |
| } |