diff --git a/go.mod b/go.mod index b94e4a4b2..801365541 100644 --- a/go.mod +++ b/go.mod @@ -45,7 +45,7 @@ require ( github.com/containerd/plugin v1.0.0 // indirect github.com/containerd/ttrpc v1.2.7 // indirect github.com/containerd/typeurl/v2 v2.2.3 // indirect - github.com/coreos/go-oidc/v3 v3.17.0 // indirect + github.com/coreos/go-oidc/v3 v3.20.0 // indirect github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect github.com/cyphar/filepath-securejoin v0.6.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect diff --git a/go.sum b/go.sum index 062760e63..9ce245532 100644 --- a/go.sum +++ b/go.sum @@ -131,8 +131,8 @@ github.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRq github.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o= github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40= github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk= -github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc= -github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= +github.com/coreos/go-oidc/v3 v3.20.0 h1:EtE0WIBHk03N+DqGkY4+UONzzZHk7amKt6IyNd7OsZE= +github.com/coreos/go-oidc/v3 v3.20.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= diff --git a/vendor/github.com/coreos/go-oidc/v3/oidc/doc.go b/vendor/github.com/coreos/go-oidc/v3/oidc/doc.go new file mode 100644 index 000000000..f443bf154 --- /dev/null +++ b/vendor/github.com/coreos/go-oidc/v3/oidc/doc.go @@ -0,0 +1,65 @@ +// Package oidc implements OpenID Connect client logic for the golang.org/x/oauth2 package. +// +// Construct a [Provider] and [oauth2.Config] through the identity provider's +// discovery document with [NewProvider], and construct an ID Token verifier +// with [Provider.Verifier]: +// +// provider, err := oidc.NewProvider(ctx, "https://accounts.google.com") +// if err != nil { +// // handle error +// } +// +// // Configure an OpenID Connect aware OAuth2 client. +// oauth2Config := oauth2.Config{ +// ClientID: clientID, +// ClientSecret: clientSecret, +// RedirectURL: redirectURL, +// // Discovery returns the OAuth2 endpoints. +// Endpoint: provider.Endpoint(), +// // "openid" is a required scope for OpenID Connect flows. +// Scopes: []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail}, +// } +// +// idTokenVerifier := provider.Verifier(&oidc.Config{ClientID: clientID}) +// +// OAuth 2.0 redirects then opt into the OpenID Connect flow with [ScopeOpenID]: +// +// func handleRedirect(w http.ResponseWriter, r *http.Request) { +// http.Redirect(w, r, oauth2Config.AuthCodeURL(state), http.StatusFound) +// } +// +// When handling an OAuth 2.0 response, an [IDTokenVerifier] can be used to +// validate the "id_token" payload, containing well-known fields such as the +// user's email, name, and picture URL: +// +// func handleOAuth2Callback(w http.ResponseWriter, r *http.Request) { +// // Verify state and other OAuth 2.0 responses. +// +// // Perform standard token exchange. +// oauth2Token, err := oauth2Config.Exchange(r.Context(), r.URL.Query().Get("code")) +// if err != nil { +// // ... +// } +// // Extract the ID Token from OAuth2 token. +// rawIDToken, ok := oauth2Token.Extra("id_token").(string) +// if !ok { +// // ... +// } +// // Parse and verify ID Token payload. +// idToken, err := idTokenVerifier.Verify(r.Context(), rawIDToken) +// if err != nil { +// // ... +// } +// // Parse well-known claims from the token. +// // https://openid.net/specs/openid-connect-core-1_0.html#Claims +// var claims struct { +// Email string `json:"email"` +// EmailVerified bool `json:"email_verified"` +// Name string `json:"name"` +// Picture string `json:"picture"` +// } +// if err := idToken.Claims(&claims); err != nil { +// // ... +// } +// } +package oidc diff --git a/vendor/github.com/coreos/go-oidc/v3/oidc/jose.go b/vendor/github.com/coreos/go-oidc/v3/oidc/jose.go index f42d37d48..fbcc5406c 100644 --- a/vendor/github.com/coreos/go-oidc/v3/oidc/jose.go +++ b/vendor/github.com/coreos/go-oidc/v3/oidc/jose.go @@ -30,3 +30,11 @@ var allAlgs = []jose.SignatureAlgorithm{ jose.PS512, jose.EdDSA, } + +var supportedJOSEAlgs = map[string]bool{} + +func init() { + for _, alg := range allAlgs { + supportedJOSEAlgs[string(alg)] = true + } +} diff --git a/vendor/github.com/coreos/go-oidc/v3/oidc/jwks.go b/vendor/github.com/coreos/go-oidc/v3/oidc/jwks.go index c5e4d787c..eab30aabb 100644 --- a/vendor/github.com/coreos/go-oidc/v3/oidc/jwks.go +++ b/vendor/github.com/coreos/go-oidc/v3/oidc/jwks.go @@ -6,6 +6,7 @@ import ( "crypto/ecdsa" "crypto/ed25519" "crypto/rsa" + "encoding/json" "errors" "fmt" "io" @@ -17,8 +18,8 @@ import ( // StaticKeySet is a verifier that validates JWT against a static set of public keys. type StaticKeySet struct { - // PublicKeys used to verify the JWT. Supported types are *rsa.PublicKey and - // *ecdsa.PublicKey. + // PublicKeys used to verify the JWT. Supported types are *rsa.PublicKey, + // *ecdsa.PublicKey, and ed25519.PublicKey. PublicKeys []crypto.PublicKey } @@ -53,8 +54,10 @@ func (s *StaticKeySet) VerifySignature(ctx context.Context, jwt string) ([]byte, // 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 any -// keys change. Reuse a common remote key set instead of creating new ones as needed. +// The returned KeySet is a long lived verifier that caches keys in memory, +// re-fetching from the remote URL when it encounters a key ID it hasn't seen. +// Reuse a single remote key set rather than creating a new one for each +// verification. func NewRemoteKeySet(ctx context.Context, jwksURL string) *RemoteKeySet { return newRemoteKeySet(ctx, jwksURL) } @@ -123,7 +126,7 @@ func (i *inflight) result() ([]jose.JSONWebKey, error) { return i.keys, i.err } -// paresdJWTKey is a context key that allows common setups to avoid parsing the +// parsedJWTKey is a context key that allows common setups to avoid parsing the // JWT twice. It holds a *jose.JSONWebSignature value. var parsedJWTKey contextKey @@ -233,11 +236,61 @@ func (r *RemoteKeySet) keysFromRemote(ctx context.Context) ([]jose.JSONWebKey, e } } +// jwkJSON implements custom logic for unmarshaling JSON Web Key Sets. +type jwkJSON struct { + Keys []jose.JSONWebKey +} + +func (j *jwkJSON) UnmarshalJSON(data []byte) error { + var raw struct { + Keys []json.RawMessage `json:"keys"` + } + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + // For each key, attempt to determine if the algorithm is supported before + // unmarshaling the key. This allows us to ignore keys with unsupported + // algorithms, rather than failing to load the entire set. + for _, key := range raw.Keys { + var metadata struct { + Alg string `json:"alg"` + } + if err := json.Unmarshal(key, &metadata); err != nil { + return err + } + if metadata.Alg != "" { + // Algorithms are technically optional, but if the key advertises an + // algorithm we don't support, ignore it rather than failing to load + // the entire key set. + // + // https://datatracker.ietf.org/doc/html/rfc7517#section-4.4 + // + // This skip is currently implemented due to secp256k1, which some + // providers use for signing with "ES256K". While we could also + // ignore the curve, this seems like a more general check in case + // providers start throwing in post-quantum algorithims or something + // like that. + // + // https://github.com/coreos/go-oidc/issues/490 + if !supportedJOSEAlgs[metadata.Alg] { + continue + } + } + var jwk jose.JSONWebKey + if err := json.Unmarshal(key, &jwk); err != nil { + return err + } + j.Keys = append(j.Keys, jwk) + } + return nil +} + func (r *RemoteKeySet) updateKeys() ([]jose.JSONWebKey, error) { req, err := http.NewRequest("GET", r.jwksURL, nil) if err != nil { return nil, fmt.Errorf("oidc: can't create request: %v", err) } + req.Header.Set("Cache-Control", "no-cache") resp, err := doRequest(r.ctx, req) if err != nil { @@ -254,7 +307,7 @@ func (r *RemoteKeySet) updateKeys() ([]jose.JSONWebKey, error) { return nil, fmt.Errorf("oidc: get keys failed: %s %s", resp.Status, body) } - var keySet jose.JSONWebKeySet + var keySet jwkJSON err = unmarshalResp(resp, body, &keySet) if err != nil { return nil, fmt.Errorf("oidc: failed to decode keys: %v %s", err, body) diff --git a/vendor/github.com/coreos/go-oidc/v3/oidc/logout.go b/vendor/github.com/coreos/go-oidc/v3/oidc/logout.go new file mode 100644 index 000000000..41f077208 --- /dev/null +++ b/vendor/github.com/coreos/go-oidc/v3/oidc/logout.go @@ -0,0 +1,188 @@ +package oidc + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "slices" + "time" +) + +// LogoutToken represents a verified token from a Back-Channel Logout. Use +// [IDTokenVerifier.VerifyLogout] within a POST handler to receive and validate +// a token. +// +// See the ./example/logout at the top-level of this repo for a full example +// application. +type LogoutToken struct { + // The required token ID claim ("jti"). When processing this token for + // logout, applications should validate that no recent token with the same + // value has been processed. + TokenID string + // The unique identifier of the user that this logout token is for. This + // will match the subject value of the ID Token. + // + // If not set, SessionID will be. + Subject string + + // The "iss" claim. This is validated against the provider URL unless + // explicitly skipped through SkipIssuerCheck. + Issuer string + // The Client ID this logout token is for. Validated against the config + // unless SkipClientIDCheck is provided. + Audience []string + // When this token was issued. + IssuedAt time.Time + // When this token expires. Validated unless SkipExpiryCheck is provided. + Expiry time.Time + // Optional session ID claim ("sid"). + // + // The exact semantics of session IDs vary between identity providers. Use + // your provider's documentation to determine what this correlates to and + // how it should be handled. + SessionID string + + claims []byte +} + +// Claims unmarshals the raw JSON payload of the Logout Token into a provided +// struct. This can be used to access field not exposed by the LogoutToken +// fields. +// +// logoutToken, err := idTokenVerifier.VerifyLogout(ctx, rawLogoutToken) +// if err != nil{ +// // ... +// } +// var claims struct { +// TraceID string `json:"trace_id"` +// } +// if err := logoutToken.Claims(&claims); err != nil { +// // ... +// } +func (l *LogoutToken) Claims(v any) error { + if l.claims == nil { + return errors.New("oidc: claims not set") + } + return json.Unmarshal(l.claims, v) +} + +// https://openid.net/specs/openid-connect-backchannel-1_0.html#LogoutToken +type logoutTokenJSON struct { + Issuer string `json:"iss"` + Subject string `json:"sub"` + Audience audience `json:"aud"` + Expiry jsonTime `json:"exp"` + IssuedAt jsonTime `json:"iat"` + JTI string `json:"jti"` + Events struct { + Logout json.RawMessage `json:"http://schemas.openid.net/event/backchannel-logout"` + } + SessionID string `json:"sid"` + // Nonce is parsed as a raw message so its mere presence can be detected. + // The spec requires logout tokens to not contain a nonce claim, regardless + // of its value. + Nonce json.RawMessage `json:"nonce"` +} + +// VerifyLogout validates a back-channel logout token. Logout tokens are +// received by the relying party (this package) from the identity provider at a +// preconfigured "backchannel_logout_uri" through a POST. Then on certain +// events, such as RP-Initiated Logout, the identity provider will send a signed +// token indicating that sessions for a specific user should be terminated. +// +// To support back-channel logout within your app, register a POST endpoint and +// verify the token: +// +// oidcConfig := &oidc.Config{ +// ClientID: clientID, +// } +// verifier := provider.Verifier(oidcConfig) +// +// mux.HandleFunc("POST /logout", func(w http.ResponseWriter, r *http.Request) { +// rawLogoutToken := r.PostFormValue("logout_token") +// if rawLogoutToken == "" { +// // ... +// } +// logoutToken, err := verifier.VerifyLogout(r.Context(), rawLogoutToken) +// if err != nil { +// // ... +// } +// // Use fields in the logoutToken to determine what sessions to +// // terminate. +// +// }) +// +// Back-channel logout spec: https://openid.net/specs/openid-connect-backchannel-1_0.html +// +// RP-initiated logout spec: https://openid.net/specs/openid-connect-rpinitiated-1_0.html +func (v *IDTokenVerifier) VerifyLogout(ctx context.Context, rawLogoutToken string) (*LogoutToken, error) { + payload, _, err := v.verifyJWT(ctx, rawLogoutToken) + if err != nil { + return nil, err + } + var logoutToken logoutTokenJSON + if err := json.Unmarshal(payload, &logoutToken); err != nil { + return nil, fmt.Errorf("oidc: parsing logout token payload: %v", err) + } + + if len(logoutToken.Events.Logout) == 0 { + return nil, fmt.Errorf("oidc: logout token missing required http://schemas.openid.net/event/backchannel-logout event") + } + + // A logout token MUST NOT contain a nonce claim. This prevents an ID token + // from being replayed as a logout token. + if len(logoutToken.Nonce) != 0 { + return nil, fmt.Errorf("oidc: logout token must not contain a 'nonce' claim") + } + + t := &LogoutToken{ + Issuer: logoutToken.Issuer, + Subject: logoutToken.Subject, + Audience: logoutToken.Audience, + IssuedAt: time.Time(logoutToken.IssuedAt), + Expiry: time.Time(logoutToken.Expiry), + SessionID: logoutToken.SessionID, + TokenID: logoutToken.JTI, + claims: payload, + } + if t.TokenID == "" { + return nil, fmt.Errorf("oidc: logout token missing required claim 'jti'") + } + if t.Expiry.IsZero() { + return nil, fmt.Errorf("oidc: logout token must contain an 'exp' claim") + } + + // A logout token MUST contain a 'sub' claim, a 'sid' claim, or both. + if t.Subject == "" && t.SessionID == "" { + return nil, fmt.Errorf("oidc: logout token must contain a 'sub' claim, a 'sid' claim, or both") + } + + if !v.config.SkipIssuerCheck && t.Issuer != v.issuer { + return nil, fmt.Errorf("oidc: logout token issued by a different provider, expected %q, got %q", v.issuer, t.Issuer) + } + + if !v.config.SkipClientIDCheck { + if v.config.ClientID != "" { + if !slices.Contains(t.Audience, v.config.ClientID) { + return nil, fmt.Errorf("oidc: expected logout token 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, &TokenExpiredError{Expiry: t.Expiry} + } + } + return t, nil +} diff --git a/vendor/github.com/coreos/go-oidc/v3/oidc/oidc.go b/vendor/github.com/coreos/go-oidc/v3/oidc/oidc.go index 2659518cc..15c3a97f0 100644 --- a/vendor/github.com/coreos/go-oidc/v3/oidc/oidc.go +++ b/vendor/github.com/coreos/go-oidc/v3/oidc/oidc.go @@ -1,4 +1,3 @@ -// Package oidc implements OpenID Connect client logic for the golang.org/x/oauth2 package. package oidc import ( @@ -24,6 +23,26 @@ const ( // ScopeOpenID is the mandatory scope for all OpenID Connect OAuth2 requests. ScopeOpenID = "openid" + // ScopeProfile can be used to request information about the user's profile, + // such as "name", "picture", etc. + // + // The exact set of claims supported by identity providers differs widely, + // though "name" and "picture" are commonly returned. + // + // See: https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims + ScopeProfile = "profile" + + // ScopeEmail can be used to request the user's email address through the + // "email" and "email_verified" claims. + // + // What it means to verify an email isn't well defined. Clients can + // generally throw out emails when the "emvail_verified" claim is false, but + // should consult identity provider specific docs if attempting to ensure + // that the user controls the returned email address. + // + // See: https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims + ScopeEmail = "email" + // ScopeOfflineAccess is an optional scope defined by OpenID Connect for requesting // OAuth2 refresh tokens. // @@ -92,7 +111,24 @@ func doRequest(ctx context.Context, req *http.Request) (*http.Response, error) { return client.Do(req.WithContext(ctx)) } -// Provider represents an OpenID Connect server's configuration. +// Provider represents an OpenID Connect server's configuration, fetched from +// the discovery document. +// +// To access fields in the discovery document that aren't exposed directly +// through this package's API, use the [Provider.Claims] method. For example, to +// access the registration or end session endpoints: +// +// p, err := oidc.NewProvider(ctx, "https://issuer.example.com") +// if err != nil { +// // ... +// } +// var metadata struct { +// EndSessionEndpoint string `json:"end_session_endpoint"` +// RegistrationEndpoint string `json:"registration_endpoint"` +// } +// if err := p.Claims(&metadata); err != nil { +// // ... +// } type Provider struct { issuer string authURL string @@ -213,6 +249,8 @@ type ProviderConfig struct { // // The provided context is only used for [http.Client] configuration through // [ClientContext], not cancelation. +// +// For providers that implement discovery, use [NewProvider] instead. func (p *ProviderConfig) NewProvider(ctx context.Context) *Provider { return &Provider{ issuer: p.IssuerURL, @@ -226,12 +264,36 @@ func (p *ProviderConfig) NewProvider(ctx context.Context) *Provider { } } +// IssuerMismatchError is returned by [NewProvider] when the "iss" value +// reported by the upstream is different than the expected value. +// +// Issuer mismatches can occur due to trailing slashes ("https://example.com" +// vs. "https://example.com/") or represent significant misconfiguration for +// multi-tenant issuers. +// +// Issuers must match exactly as they are also used to validate ID Tokens. +// +// https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata +type IssuerMismatchError struct { + // The value provided to this package. The expected value. + Provided string + // The value advertised by the discovery document. + Discovered string +} + +func (e *IssuerMismatchError) Error() string { + return fmt.Sprintf("oidc: issuer URL provided to client (%q) did not match the issuer URL returned by provider (%q)", e.Provided, e.Discovered) +} + // NewProvider uses the OpenID Connect discovery mechanism to construct a Provider. // The issuer is the URL identifier for the service. For example: "https://accounts.google.com" // or "https://login.salesforce.com". // +// If the "iss" value returned in the discovery document doesn't match the value +// provided here, [IssuerMismatchError] is returned. +// // OpenID Connect providers that don't implement discovery or host the discovery -// document at a non-spec complaint path (such as requiring a URL parameter), +// document at a non-spec compliant path (such as requiring a URL parameter), // should use [ProviderConfig] instead. // // See: https://openid.net/specs/openid-connect-discovery-1_0.html @@ -267,7 +329,10 @@ func NewProvider(ctx context.Context, issuer string) (*Provider, error) { issuerURL = issuer } if p.Issuer != issuerURL && !skipIssuerValidation { - return nil, fmt.Errorf("oidc: issuer URL provided to client (%q) did not match the issuer URL returned by provider (%q)", issuer, p.Issuer) + return nil, &IssuerMismatchError{ + Provided: issuerURL, + Discovered: p.Issuer, + } } var algs []string for _, a := range p.Algorithms { @@ -301,7 +366,7 @@ func NewProvider(ctx context.Context, issuer string) (*Provider, 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 { +func (p *Provider) Claims(v any) error { if p.rawClaims == nil { return errors.New("oidc: claims not set") } @@ -340,7 +405,7 @@ type userInfoRaw struct { } // Claims unmarshals the raw JSON object claims into the provided object. -func (u *UserInfo) Claims(v interface{}) error { +func (u *UserInfo) Claims(v any) error { if u.claims == nil { return errors.New("oidc: claims not set") } @@ -348,6 +413,44 @@ func (u *UserInfo) Claims(v interface{}) error { } // UserInfo uses the token source to query the provider's user info endpoint. +// +// It's fewer round trips and better supported to validate the ID Token with +// [Provider.Verifier], rather than using the UserInfo endpoint. The ID Token +// contains all information [UserInfo] provides: +// +// p, err := oidc.NewProvider(ctx, "https://issuer.example.com") +// if err != nil { +// // ... +// } +// config := &oidc.Config{ +// ClientID: clientID, +// } +// v := p.Verifier(config) +// http.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) { +// oauth2Token, err := config.Exchange(ctx, r.URL.Query().Get("code")) +// if err != nil { +// // ... +// } +// rawIDToken, ok := oauth2Token.Extra("id_token").(string) +// if !ok { +// // ... +// } +// idToken, err := verifier.Verify(ctx, rawIDToken) +// if err != nil { +// // ... +// } +// // https://openid.net/specs/openid-connect-core-1_0.html#Claims +// var claims struct { +// Email string `json:"email"` +// EmailVerified bool `json:"email_verified"` +// Name string `json:"name"` +// Picture string `json:"picture"` +// } +// if err := idToken.Claims(&claims); err != nil { +// // ... +// } +// // Use claims... +// }) 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") @@ -425,7 +528,7 @@ type IDToken struct { // A unique string which identifies the end user. Subject string - // Expiry of the token. Ths package will not process tokens that have + // Expiry of the token. This package will not process tokens that have // expired unless that validation is explicitly turned off. Expiry time.Time // When the token was issued by the provider. @@ -433,7 +536,7 @@ type IDToken struct { // Initial nonce provided during the authentication redirect. // - // This package does NOT provided verification on the value of this field + // This package does NOT provide verification on the value of this field // and it's the user's responsibility to ensure it contains a valid value. Nonce string @@ -465,15 +568,15 @@ type IDToken struct { // if err := idToken.Claims(&claims); err != nil { // // handle error // } -func (i *IDToken) Claims(v interface{}) error { +func (i *IDToken) Claims(v any) error { if i.claims == nil { return errors.New("oidc: claims not set") } return json.Unmarshal(i.claims, v) } -// VerifyAccessToken verifies that the hash of the access token that corresponds to the iD token -// matches the hash in the id token. It returns an error if the hashes don't match. +// VerifyAccessToken verifies that the hash of the access token that corresponds to the ID token +// matches the hash in the ID token. It returns an error if the hashes don't match. // It is the caller's responsibility to ensure that the optional access token hash is present for the ID token // before calling this method. See https://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken func (i *IDToken) VerifyAccessToken(accessToken string) error { @@ -570,7 +673,7 @@ func (j *jsonTime) UnmarshalJSON(b []byte) error { return nil } -func unmarshalResp(r *http.Response, body []byte, v interface{}) error { +func unmarshalResp(r *http.Response, body []byte, v any) error { err := json.Unmarshal(body, &v) if err == nil { return nil diff --git a/vendor/github.com/coreos/go-oidc/v3/oidc/verify.go b/vendor/github.com/coreos/go-oidc/v3/oidc/verify.go index a8bf107d4..1aa2bd740 100644 --- a/vendor/github.com/coreos/go-oidc/v3/oidc/verify.go +++ b/vendor/github.com/coreos/go-oidc/v3/oidc/verify.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "net/http" + "slices" "time" jose "github.com/go-jose/go-jose/v4" @@ -17,9 +18,10 @@ const ( issuerGoogleAccountsNoScheme = "accounts.google.com" ) -// TokenExpiredError indicates that Verify failed because the token was expired. This -// error does NOT indicate that the token is not also invalid for other reasons. Other -// checks might have failed if the expiration check had not failed. +// TokenExpiredError indicates that [IDTokenVerifier.Verify] or +// [IDTokenVerifier.VerifyLogout] failed because the token was expired. This +// error does NOT indicate that the token is not also invalid for other reasons. +// Other checks might have failed if the expiration check had not failed. type TokenExpiredError struct { // Expiry is the time when the token expired. Expiry time.Time @@ -29,7 +31,7 @@ func (e *TokenExpiredError) Error() string { return fmt.Sprintf("oidc: token is expired (Token Expiry: %v)", e.Expiry) } -// KeySet is a set of publc JSON Web Keys that can be used to validate the signature +// KeySet is a set of public 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 { @@ -44,7 +46,7 @@ type KeySet interface { VerifySignature(ctx context.Context, jwt string) (payload []byte, err error) } -// IDTokenVerifier provides verification for ID Tokens. +// IDTokenVerifier provides verification for ID Tokens and Logout Tokens. type IDTokenVerifier struct { keySet KeySet config *Config @@ -54,8 +56,8 @@ type IDTokenVerifier struct { // 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. +// one directly. This method is intended to be used with providers that don't support +// metadata discovery, or to avoid 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: @@ -81,8 +83,8 @@ type Config struct { 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 + // If the IDTokenVerifier is created from a provider with [Provider.Verifier], this + // defaults to the set of algorithms the provider supports. Otherwise this value // defaults to RS256. SupportedSigningAlgs []string @@ -91,7 +93,7 @@ type Config struct { // If true, token expiry is not checked. SkipExpiryCheck bool - // SkipIssuerCheck is intended for specialized cases where the the caller wishes to + // SkipIssuerCheck is intended for specialized cases where the caller wishes to // defer issuer validation. When enabled, callers MUST independently verify the Token's // Issuer is a known good value. // @@ -116,8 +118,9 @@ type Config struct { } // VerifierContext returns an IDTokenVerifier that uses the provider's key set to -// verify JWTs. As opposed to Verifier, the context is used to configure requests -// to the upstream JWKs endpoint. The provided context's cancellation is ignored. +// verify JWTs. As opposed to [Provider.Verifier], the context is used to configure +// requests to the upstream JWKs endpoint. The provided context's cancellation is +// ignored. func (p *Provider) VerifierContext(ctx context.Context, config *Config) *IDTokenVerifier { return p.newVerifier(NewRemoteKeySet(ctx, p.jwksURL), config) } @@ -125,7 +128,7 @@ func (p *Provider) VerifierContext(ctx context.Context, config *Config) *IDToken // Verifier returns an IDTokenVerifier that uses the provider's key set to verify JWTs. // // The returned verifier uses a background context for all requests to the upstream -// JWKs endpoint. To control that context, use VerifierContext instead. +// JWKs endpoint. To control that context, use [Provider.VerifierContext] instead. func (p *Provider) Verifier(config *Config) *IDTokenVerifier { return p.newVerifier(p.remoteKeySet(), config) } @@ -141,15 +144,6 @@ func (p *Provider) newVerifier(keySet KeySet, config *Config) *IDTokenVerifier { return NewVerifier(p.issuer, keySet, config) } -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) @@ -186,7 +180,7 @@ func resolveDistributedClaim(ctx context.Context, verifier *IDTokenVerifier, src // Verify parses a raw ID Token, verifies it's been signed by the provider, performs // any additional checks depending on the Config, and returns the payload. // -// Verify does NOT do nonce validation, which is the callers responsibility. +// Verify does NOT do nonce validation, which is the caller's responsibility. // // See: https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation // @@ -203,48 +197,9 @@ func resolveDistributedClaim(ctx context.Context, verifier *IDTokenVerifier, src // // token, err := verifier.Verify(ctx, rawIDToken) func (v *IDTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*IDToken, error) { - var supportedSigAlgs []jose.SignatureAlgorithm - for _, alg := range v.config.SupportedSigningAlgs { - supportedSigAlgs = append(supportedSigAlgs, jose.SignatureAlgorithm(alg)) - } - if len(supportedSigAlgs) == 0 { - // If no algorithms were specified by both the config and discovery, default - // to the one mandatory algorithm "RS256". - supportedSigAlgs = []jose.SignatureAlgorithm{jose.RS256} - } - if v.config.InsecureSkipSignatureCheck { - // "none" is a required value to even parse a JWT with the "none" algorithm - // using go-jose. - supportedSigAlgs = append(supportedSigAlgs, "none") - } - - // Parse and verify the signature first. This at least forces the user to have - // a valid, signed ID token before we do any other processing. - jws, err := jose.ParseSigned(rawIDToken, supportedSigAlgs) + payload, sig, err := v.verifyJWT(ctx, rawIDToken) if err != nil { - return nil, fmt.Errorf("oidc: malformed jwt: %v", err) - } - 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] - - var payload []byte - if v.config.InsecureSkipSignatureCheck { - // Yolo mode. - payload = jws.UnsafePayloadWithoutVerification() - } else { - // The JWT is attached here for the happy path to avoid the verifier from - // having to parse the JWT twice. - ctx = context.WithValue(ctx, parsedJWTKey, jws) - payload, err = v.keySet.VerifySignature(ctx, rawIDToken) - if err != nil { - return nil, fmt.Errorf("failed to verify signature: %v", err) - } + return nil, err } var token idToken if err := json.Unmarshal(payload, &token); err != nil { @@ -295,7 +250,7 @@ func (v *IDTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*IDTok // 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) { + if !slices.Contains(t.Audience, v.config.ClientID) { return nil, fmt.Errorf("oidc: expected audience %q got %q", v.config.ClientID, t.Audience) } } else { @@ -336,3 +291,50 @@ func (v *IDTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*IDTok func Nonce(nonce string) oauth2.AuthCodeOption { return oauth2.SetAuthURLParam("nonce", nonce) } + +func (v *IDTokenVerifier) verifyJWT(ctx context.Context, rawIDToken string) ([]byte, jose.Signature, error) { + var supportedSigAlgs []jose.SignatureAlgorithm + for _, alg := range v.config.SupportedSigningAlgs { + supportedSigAlgs = append(supportedSigAlgs, jose.SignatureAlgorithm(alg)) + } + if len(supportedSigAlgs) == 0 { + // If no algorithms were specified by both the config and discovery, default + // to the one mandatory algorithm "RS256". + supportedSigAlgs = []jose.SignatureAlgorithm{jose.RS256} + } + if v.config.InsecureSkipSignatureCheck { + // "none" is a required value to even parse a JWT with the "none" algorithm + // using go-jose. + supportedSigAlgs = append(supportedSigAlgs, "none") + } + + // Parse and verify the signature first. This at least forces the user to have + // a valid, signed ID token before we do any other processing. + jws, err := jose.ParseSigned(rawIDToken, supportedSigAlgs) + if err != nil { + return nil, jose.Signature{}, fmt.Errorf("oidc: malformed jwt: %v", err) + } + switch len(jws.Signatures) { + case 0: + return nil, jose.Signature{}, fmt.Errorf("oidc: id token not signed") + case 1: + default: + return nil, jose.Signature{}, fmt.Errorf("oidc: multiple signatures on id token not supported") + } + sig := jws.Signatures[0] + + var payload []byte + if v.config.InsecureSkipSignatureCheck { + // Yolo mode. + payload = jws.UnsafePayloadWithoutVerification() + } else { + // The JWT is attached here for the happy path to avoid the verifier from + // having to parse the JWT twice. + ctx = context.WithValue(ctx, parsedJWTKey, jws) + payload, err = v.keySet.VerifySignature(ctx, rawIDToken) + if err != nil { + return nil, jose.Signature{}, fmt.Errorf("failed to verify signature: %v", err) + } + } + return payload, sig, nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index af4c5f21c..5c199f662 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -192,8 +192,8 @@ github.com/containerd/ttrpc # github.com/containerd/typeurl/v2 v2.2.3 ## explicit; go 1.21 github.com/containerd/typeurl/v2 -# github.com/coreos/go-oidc/v3 v3.17.0 -## explicit; go 1.24.0 +# github.com/coreos/go-oidc/v3 v3.20.0 +## explicit; go 1.25.0 github.com/coreos/go-oidc/v3/oidc # github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 ## explicit