From 194215e73848d3666b1f8d115f3726f23a10ddb2 Mon Sep 17 00:00:00 2001 From: Jonas Hungershausen Date: Mon, 4 Sep 2023 18:19:51 +0200 Subject: [PATCH 1/9] feat: support native social sign using apple/google sdk --- ...odel_update_login_flow_with_oidc_method.go | 37 ++++++++++ ...date_registration_flow_with_oidc_method.go | 37 ++++++++++ ...odel_update_login_flow_with_oidc_method.go | 37 ++++++++++ ...date_registration_flow_with_oidc_method.go | 37 ++++++++++ selfservice/flow/login/flow.go | 3 + selfservice/flow/login/hook.go | 5 +- selfservice/flow/registration/flow.go | 3 + selfservice/flow/registration/hook.go | 5 +- selfservice/hook/session_issuer.go | 4 +- .../strategy/oidc/.schema/link.schema.json | 4 + selfservice/strategy/oidc/provider.go | 4 + selfservice/strategy/oidc/provider_apple.go | 19 +++++ selfservice/strategy/oidc/strategy.go | 19 ++++- selfservice/strategy/oidc/strategy_login.go | 33 ++++++++- .../strategy/oidc/strategy_registration.go | 74 ++++++++++++++----- spec/api.json | 8 ++ spec/swagger.json | 8 ++ 17 files changed, 314 insertions(+), 23 deletions(-) diff --git a/internal/client-go/model_update_login_flow_with_oidc_method.go b/internal/client-go/model_update_login_flow_with_oidc_method.go index f11ccd730c61..78cc88ef2103 100644 --- a/internal/client-go/model_update_login_flow_with_oidc_method.go +++ b/internal/client-go/model_update_login_flow_with_oidc_method.go @@ -19,6 +19,8 @@ import ( type UpdateLoginFlowWithOidcMethod struct { // The CSRF Token CsrfToken *string `json:"csrf_token,omitempty"` + // An optional id token provided by an OIDC provider If submitted, it is verified using the OIDC provider's public key set and the claims are used to populate the OIDC credentials of the identity. If the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use the `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken. Supported providers are Apple + IdToken *string `json:"id_token,omitempty"` // Method to use This field must be set to `oidc` when using the oidc method. Method string `json:"method"` // The provider to register with @@ -80,6 +82,38 @@ func (o *UpdateLoginFlowWithOidcMethod) SetCsrfToken(v string) { o.CsrfToken = &v } +// GetIdToken returns the IdToken field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithOidcMethod) GetIdToken() string { + if o == nil || o.IdToken == nil { + var ret string + return ret + } + return *o.IdToken +} + +// GetIdTokenOk returns a tuple with the IdToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithOidcMethod) GetIdTokenOk() (*string, bool) { + if o == nil || o.IdToken == nil { + return nil, false + } + return o.IdToken, true +} + +// HasIdToken returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithOidcMethod) HasIdToken() bool { + if o != nil && o.IdToken != nil { + return true + } + + return false +} + +// SetIdToken gets a reference to the given string and assigns it to the IdToken field. +func (o *UpdateLoginFlowWithOidcMethod) SetIdToken(v string) { + o.IdToken = &v +} + // GetMethod returns the Method field value func (o *UpdateLoginFlowWithOidcMethod) GetMethod() string { if o == nil { @@ -197,6 +231,9 @@ func (o UpdateLoginFlowWithOidcMethod) MarshalJSON() ([]byte, error) { if o.CsrfToken != nil { toSerialize["csrf_token"] = o.CsrfToken } + if o.IdToken != nil { + toSerialize["id_token"] = o.IdToken + } if true { toSerialize["method"] = o.Method } diff --git a/internal/client-go/model_update_registration_flow_with_oidc_method.go b/internal/client-go/model_update_registration_flow_with_oidc_method.go index 8f7d7a190b88..35e2c11ccdc3 100644 --- a/internal/client-go/model_update_registration_flow_with_oidc_method.go +++ b/internal/client-go/model_update_registration_flow_with_oidc_method.go @@ -19,6 +19,8 @@ import ( type UpdateRegistrationFlowWithOidcMethod struct { // The CSRF Token CsrfToken *string `json:"csrf_token,omitempty"` + // An optional id token provided by an OIDC provider If submitted, it is verified using the OIDC provider's public key set and the claims are used to populate the OIDC credentials of the identity. If the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use the `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken. Supported providers are Apple + IdToken *string `json:"id_token,omitempty"` // Method to use This field must be set to `oidc` when using the oidc method. Method string `json:"method"` // The provider to register with @@ -82,6 +84,38 @@ func (o *UpdateRegistrationFlowWithOidcMethod) SetCsrfToken(v string) { o.CsrfToken = &v } +// GetIdToken returns the IdToken field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithOidcMethod) GetIdToken() string { + if o == nil || o.IdToken == nil { + var ret string + return ret + } + return *o.IdToken +} + +// GetIdTokenOk returns a tuple with the IdToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithOidcMethod) GetIdTokenOk() (*string, bool) { + if o == nil || o.IdToken == nil { + return nil, false + } + return o.IdToken, true +} + +// HasIdToken returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithOidcMethod) HasIdToken() bool { + if o != nil && o.IdToken != nil { + return true + } + + return false +} + +// SetIdToken gets a reference to the given string and assigns it to the IdToken field. +func (o *UpdateRegistrationFlowWithOidcMethod) SetIdToken(v string) { + o.IdToken = &v +} + // GetMethod returns the Method field value func (o *UpdateRegistrationFlowWithOidcMethod) GetMethod() string { if o == nil { @@ -231,6 +265,9 @@ func (o UpdateRegistrationFlowWithOidcMethod) MarshalJSON() ([]byte, error) { if o.CsrfToken != nil { toSerialize["csrf_token"] = o.CsrfToken } + if o.IdToken != nil { + toSerialize["id_token"] = o.IdToken + } if true { toSerialize["method"] = o.Method } diff --git a/internal/httpclient/model_update_login_flow_with_oidc_method.go b/internal/httpclient/model_update_login_flow_with_oidc_method.go index f11ccd730c61..78cc88ef2103 100644 --- a/internal/httpclient/model_update_login_flow_with_oidc_method.go +++ b/internal/httpclient/model_update_login_flow_with_oidc_method.go @@ -19,6 +19,8 @@ import ( type UpdateLoginFlowWithOidcMethod struct { // The CSRF Token CsrfToken *string `json:"csrf_token,omitempty"` + // An optional id token provided by an OIDC provider If submitted, it is verified using the OIDC provider's public key set and the claims are used to populate the OIDC credentials of the identity. If the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use the `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken. Supported providers are Apple + IdToken *string `json:"id_token,omitempty"` // Method to use This field must be set to `oidc` when using the oidc method. Method string `json:"method"` // The provider to register with @@ -80,6 +82,38 @@ func (o *UpdateLoginFlowWithOidcMethod) SetCsrfToken(v string) { o.CsrfToken = &v } +// GetIdToken returns the IdToken field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithOidcMethod) GetIdToken() string { + if o == nil || o.IdToken == nil { + var ret string + return ret + } + return *o.IdToken +} + +// GetIdTokenOk returns a tuple with the IdToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithOidcMethod) GetIdTokenOk() (*string, bool) { + if o == nil || o.IdToken == nil { + return nil, false + } + return o.IdToken, true +} + +// HasIdToken returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithOidcMethod) HasIdToken() bool { + if o != nil && o.IdToken != nil { + return true + } + + return false +} + +// SetIdToken gets a reference to the given string and assigns it to the IdToken field. +func (o *UpdateLoginFlowWithOidcMethod) SetIdToken(v string) { + o.IdToken = &v +} + // GetMethod returns the Method field value func (o *UpdateLoginFlowWithOidcMethod) GetMethod() string { if o == nil { @@ -197,6 +231,9 @@ func (o UpdateLoginFlowWithOidcMethod) MarshalJSON() ([]byte, error) { if o.CsrfToken != nil { toSerialize["csrf_token"] = o.CsrfToken } + if o.IdToken != nil { + toSerialize["id_token"] = o.IdToken + } if true { toSerialize["method"] = o.Method } diff --git a/internal/httpclient/model_update_registration_flow_with_oidc_method.go b/internal/httpclient/model_update_registration_flow_with_oidc_method.go index 8f7d7a190b88..35e2c11ccdc3 100644 --- a/internal/httpclient/model_update_registration_flow_with_oidc_method.go +++ b/internal/httpclient/model_update_registration_flow_with_oidc_method.go @@ -19,6 +19,8 @@ import ( type UpdateRegistrationFlowWithOidcMethod struct { // The CSRF Token CsrfToken *string `json:"csrf_token,omitempty"` + // An optional id token provided by an OIDC provider If submitted, it is verified using the OIDC provider's public key set and the claims are used to populate the OIDC credentials of the identity. If the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use the `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken. Supported providers are Apple + IdToken *string `json:"id_token,omitempty"` // Method to use This field must be set to `oidc` when using the oidc method. Method string `json:"method"` // The provider to register with @@ -82,6 +84,38 @@ func (o *UpdateRegistrationFlowWithOidcMethod) SetCsrfToken(v string) { o.CsrfToken = &v } +// GetIdToken returns the IdToken field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithOidcMethod) GetIdToken() string { + if o == nil || o.IdToken == nil { + var ret string + return ret + } + return *o.IdToken +} + +// GetIdTokenOk returns a tuple with the IdToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithOidcMethod) GetIdTokenOk() (*string, bool) { + if o == nil || o.IdToken == nil { + return nil, false + } + return o.IdToken, true +} + +// HasIdToken returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithOidcMethod) HasIdToken() bool { + if o != nil && o.IdToken != nil { + return true + } + + return false +} + +// SetIdToken gets a reference to the given string and assigns it to the IdToken field. +func (o *UpdateRegistrationFlowWithOidcMethod) SetIdToken(v string) { + o.IdToken = &v +} + // GetMethod returns the Method field value func (o *UpdateRegistrationFlowWithOidcMethod) GetMethod() string { if o == nil { @@ -231,6 +265,9 @@ func (o UpdateRegistrationFlowWithOidcMethod) MarshalJSON() ([]byte, error) { if o.CsrfToken != nil { toSerialize["csrf_token"] = o.CsrfToken } + if o.IdToken != nil { + toSerialize["id_token"] = o.IdToken + } if true { toSerialize["method"] = o.Method } diff --git a/selfservice/flow/login/flow.go b/selfservice/flow/login/flow.go index e46698290819..74d854b49f5c 100644 --- a/selfservice/flow/login/flow.go +++ b/selfservice/flow/login/flow.go @@ -133,6 +133,9 @@ type Flow struct { // // required: true State State `json:"state" faker:"-" db:"state"` + + // Only used internally + IDToken string `json:"-" db:"-"` } var _ flow.Flow = new(Flow) diff --git a/selfservice/flow/login/hook.go b/selfservice/flow/login/hook.go index 203a62fdf989..b8ce1d15d3b0 100644 --- a/selfservice/flow/login/hook.go +++ b/selfservice/flow/login/hook.go @@ -199,7 +199,10 @@ func (e *HookExecutor) PostLoginHook( Method: a.Active.String(), SSOProvider: provider, })) - if handled, err := e.d.SessionManager().MaybeRedirectAPICodeFlow(w, r, a, s.ID, g); err != nil { + if a.IDToken != "" { + // We don't want to redirect with the code, if the flow was submitted with an ID token. + // This is the case for Sign in with native Apple SDK or Google SDK. + } else if handled, err := e.d.SessionManager().MaybeRedirectAPICodeFlow(w, r, a, s.ID, g); err != nil { return errors.WithStack(err) } else if handled { return nil diff --git a/selfservice/flow/registration/flow.go b/selfservice/flow/registration/flow.go index 39843a9e5b5c..f033c0dcb130 100644 --- a/selfservice/flow/registration/flow.go +++ b/selfservice/flow/registration/flow.go @@ -116,6 +116,9 @@ type Flow struct { // and only on creating the flow. SessionTokenExchangeCode string `json:"session_token_exchange_code,omitempty" faker:"-" db:"-"` + // only used internally + IDToken string `json:"-" faker:"-" db:"-"` + // State represents the state of this request: // // - choose_method: ask the user to choose a method (e.g. registration with email) diff --git a/selfservice/flow/registration/hook.go b/selfservice/flow/registration/hook.go index 426d81101dd6..a10d2a4daa07 100644 --- a/selfservice/flow/registration/hook.go +++ b/selfservice/flow/registration/hook.go @@ -227,7 +227,10 @@ func (e *HookExecutor) PostRegistrationHook(w http.ResponseWriter, r *http.Reque Debug("Post registration execution hooks completed successfully.") if a.Type == flow.TypeAPI || x.IsJSONRequest(r) { - if handled, err := e.d.SessionManager().MaybeRedirectAPICodeFlow(w, r, a, s.ID, ct.ToUiNodeGroup()); err != nil { + if a.IDToken != "" { + // We don't want to redirect with the code, if the flow was submitted with an ID token. + // This is the case for Sign in with native Apple SDK or Google SDK. + } else if handled, err := e.d.SessionManager().MaybeRedirectAPICodeFlow(w, r, a, s.ID, ct.ToUiNodeGroup()); err != nil { return errors.WithStack(err) } else if handled { return nil diff --git a/selfservice/hook/session_issuer.go b/selfservice/hook/session_issuer.go index 999403d53ace..06e8f0e59693 100644 --- a/selfservice/hook/session_issuer.go +++ b/selfservice/hook/session_issuer.go @@ -53,7 +53,9 @@ func (e *SessionIssuer) ExecutePostRegistrationPostPersistHook(w http.ResponseWr func (e *SessionIssuer) executePostRegistrationPostPersistHook(w http.ResponseWriter, r *http.Request, a *registration.Flow, s *session.Session) error { if a.Type == flow.TypeAPI { - if s.AuthenticatedVia(identity.CredentialsTypeOIDC) { + // We don't want to redirect with the code, if the flow was submitted with an ID token. + // This is the case for Sign in with native Apple SDK or Google SDK. + if s.AuthenticatedVia(identity.CredentialsTypeOIDC) && a.IDToken == "" { if handled, err := e.r.SessionManager().MaybeRedirectAPICodeFlow(w, r, a, s.ID, node.OpenIDConnectGroup); err != nil { return errors.WithStack(err) } else if handled { diff --git a/selfservice/strategy/oidc/.schema/link.schema.json b/selfservice/strategy/oidc/.schema/link.schema.json index f813f76eef1a..54914543aa21 100644 --- a/selfservice/strategy/oidc/.schema/link.schema.json +++ b/selfservice/strategy/oidc/.schema/link.schema.json @@ -35,6 +35,10 @@ }, "additionalProperties": false } + }, + "id_token": { + "type": "string", + "description": "An optional id token provided by an OIDC provider" } } } diff --git a/selfservice/strategy/oidc/provider.go b/selfservice/strategy/oidc/provider.go index ddef8dc10901..6bd6a9d02cb1 100644 --- a/selfservice/strategy/oidc/provider.go +++ b/selfservice/strategy/oidc/provider.go @@ -27,6 +27,10 @@ type TokenExchanger interface { Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) } +type IDTokenVerifier interface { + Verify(ctx context.Context, rawIDToken string) (*Claims, error) +} + // ConvertibleBoolean is used as Apple casually sends the email_verified field as a string. type Claims struct { Issuer string `json:"iss,omitempty"` diff --git a/selfservice/strategy/oidc/provider_apple.go b/selfservice/strategy/oidc/provider_apple.go index 3df61dc99eb2..a4c5a601c160 100644 --- a/selfservice/strategy/oidc/provider_apple.go +++ b/selfservice/strategy/oidc/provider_apple.go @@ -12,6 +12,7 @@ import ( "net/url" "time" + "github.com/coreos/go-oidc" "github.com/golang-jwt/jwt/v4" "github.com/pkg/errors" @@ -146,3 +147,21 @@ func decodeQuery(query url.Values, claims *Claims) { } } } + +var _ IDTokenVerifier = new(ProviderApple) + +func (a *ProviderApple) Verify(ctx context.Context, rawIDToken string) (*Claims, error) { + keySet := oidc.NewRemoteKeySet(ctx, "https://appleid.apple.com/auth/keys") + verifier := oidc.NewVerifier("https://appleid.apple.com", keySet, &oidc.Config{ + ClientID: a.config.ClientID, + }) + token, err := verifier.Verify(ctx, rawIDToken) + if err != nil { + return nil, err + } + claims := &Claims{} + if err := token.Claims(claims); err != nil { + return nil, err + } + return claims, nil +} diff --git a/selfservice/strategy/oidc/strategy.go b/selfservice/strategy/oidc/strategy.go index 1b4f9ca56034..5095fa6e769e 100644 --- a/selfservice/strategy/oidc/strategy.go +++ b/selfservice/strategy/oidc/strategy.go @@ -432,7 +432,7 @@ func (s *Strategy) handleCallback(w http.ResponseWriter, r *http.Request, ps htt return case *registration.Flow: a.TransientPayload = cntnr.TransientPayload - if ff, err := s.processRegistration(w, r, a, token, claims, provider, cntnr); err != nil { + if ff, err := s.processRegistration(w, r, a, token, claims, provider, cntnr, ""); err != nil { if ff != nil { s.forwardError(w, r, ff, err) return @@ -589,3 +589,20 @@ func (s *Strategy) CompletedAuthenticationMethod(ctx context.Context) session.Au AAL: identity.AuthenticatorAssuranceLevel1, } } + +func (s *Strategy) processIDToken(w http.ResponseWriter, r *http.Request, provider Provider, idToken string) (*Claims, error) { + verifier, ok := provider.(IDTokenVerifier) + if !ok { + return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Provider does not support ID Token verification.")) // TODO: move to global var + } + claims, err := verifier.Verify(r.Context(), idToken) + if err != nil { + return nil, errors.WithStack(err) + } + + if err := claims.Validate(); err != nil { + return nil, errors.WithStack(err) + } + + return claims, nil +} diff --git a/selfservice/strategy/oidc/strategy_login.go b/selfservice/strategy/oidc/strategy_login.go index 24af92562f5c..4106b311bfcc 100644 --- a/selfservice/strategy/oidc/strategy_login.go +++ b/selfservice/strategy/oidc/strategy_login.go @@ -81,6 +81,18 @@ type UpdateLoginFlowWithOidcMethod struct { // // required: false UpstreamParameters json.RawMessage `json:"upstream_parameters"` + + // An optional id token provided by an OIDC provider + // + // If submitted, it is verified using the OIDC provider's public key set and the claims are used to populate + // the OIDC credentials of the identity. + // If the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use + // the `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken. + // + // Supported providers are + // - Apple + // required: false + IDToken string `json:"id_token,omitempty"` } func (s *Strategy) processLogin(w http.ResponseWriter, r *http.Request, loginFlow *login.Flow, token *oauth2.Token, claims *Claims, provider Provider, container *authCodeContainer) (*registration.Flow, error) { @@ -123,12 +135,13 @@ func (s *Strategy) processLogin(w http.ResponseWriter, r *http.Request, loginFlo return nil, s.handleError(w, r, loginFlow, provider.Config().ID, nil, err) } + registrationFlow.IDToken = loginFlow.IDToken registrationFlow.RequestURL, err = x.TakeOverReturnToParameter(loginFlow.RequestURL, registrationFlow.RequestURL) if err != nil { return nil, s.handleError(w, r, loginFlow, provider.Config().ID, nil, err) } - if _, err := s.processRegistration(w, r, registrationFlow, token, claims, provider, container); err != nil { + if _, err := s.processRegistration(w, r, registrationFlow, token, claims, provider, container, loginFlow.IDToken); err != nil { return registrationFlow, err } @@ -167,6 +180,8 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, return nil, s.handleError(w, r, f, "", nil, errors.WithStack(herodot.ErrBadRequest.WithDebug(err.Error()).WithReasonf("Unable to parse HTTP form request: %s", err.Error()))) } + f.IDToken = p.IDToken + pid := p.Provider // this can come from both url query and post body if pid == "" { return nil, errors.WithStack(flow.ErrStrategyNotResponsible) @@ -196,6 +211,22 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, } else if authenticated { return i, nil } + + if p.IDToken != "" { + claims, err := s.processIDToken(w, r, provider, p.IDToken) + if err != nil { + return nil, s.handleError(w, r, f, pid, nil, err) + } + _, err = s.processLogin(w, r, f, nil, claims, provider, &authCodeContainer{ + FlowID: f.ID.String(), + Traits: p.Traits, + }) + if err != nil { + return nil, s.handleError(w, r, f, pid, nil, err) + } + return nil, errors.WithStack(flow.ErrCompletedByStrategy) + } + state := generateState(f.ID.String()) if code, hasCode, _ := s.d.SessionTokenExchangePersister().CodeForFlow(r.Context(), f.ID); hasCode { state.setCode(code.InitCode) diff --git a/selfservice/strategy/oidc/strategy_registration.go b/selfservice/strategy/oidc/strategy_registration.go index debc45d770db..3b9196411bb8 100644 --- a/selfservice/strategy/oidc/strategy_registration.go +++ b/selfservice/strategy/oidc/strategy_registration.go @@ -98,6 +98,18 @@ type UpdateRegistrationFlowWithOidcMethod struct { // // required: false UpstreamParameters json.RawMessage `json:"upstream_parameters"` + + // An optional id token provided by an OIDC provider + // + // If submitted, it is verified using the OIDC provider's public key set and the claims are used to populate + // the OIDC credentials of the identity. + // If the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use + // the `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken. + // + // Supported providers are + // - Apple + // required: false + IDToken string `json:"id_token,omitempty"` } func (s *Strategy) newLinkDecoder(p interface{}, r *http.Request) error { @@ -136,6 +148,7 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat } f.TransientPayload = p.TransientPayload + f.IDToken = p.IDToken pid := p.Provider // this can come from both url query and post body if pid == "" { @@ -167,6 +180,22 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat return errors.WithStack(registration.ErrAlreadyLoggedIn) } + if p.IDToken != "" { + claims, err := s.processIDToken(w, r, provider, p.IDToken) + if err != nil { + return s.handleError(w, r, f, pid, nil, err) + } + _, err = s.processRegistration(w, r, f, nil, claims, provider, &authCodeContainer{ + FlowID: f.ID.String(), + Traits: p.Traits, + TransientPayload: f.TransientPayload, + }, p.IDToken) + if err != nil { + return s.handleError(w, r, f, pid, nil, err) + } + return errors.WithStack(flow.ErrCompletedByStrategy) + } + state := generateState(f.ID.String()) if code, hasCode, _ := s.d.SessionTokenExchangePersister().CodeForFlow(r.Context(), f.ID); hasCode { state.setCode(code.InitCode) @@ -226,7 +255,7 @@ func (s *Strategy) registrationToLogin(w http.ResponseWriter, r *http.Request, r return lf, nil } -func (s *Strategy) processRegistration(w http.ResponseWriter, r *http.Request, rf *registration.Flow, token *oauth2.Token, claims *Claims, provider Provider, container *authCodeContainer) (*login.Flow, error) { +func (s *Strategy) processRegistration(w http.ResponseWriter, r *http.Request, rf *registration.Flow, token *oauth2.Token, claims *Claims, provider Provider, container *authCodeContainer, idToken string) (*login.Flow, error) { if _, _, err := s.d.PrivilegedIdentityPool().FindByCredentialsIdentifier(r.Context(), identity.CredentialsTypeOIDC, identity.OIDCUniqueID(provider.Config().ID, claims.Subject)); err == nil { // If the identity already exists, we should perform the login flow instead. @@ -281,21 +310,26 @@ func (s *Strategy) processRegistration(w http.ResponseWriter, r *http.Request, r } } - var it string - if idToken, ok := token.Extra("id_token").(string); ok { - if it, err = s.d.Cipher(r.Context()).Encrypt(r.Context(), []byte(idToken)); err != nil { - return nil, s.handleError(w, r, rf, provider.Config().ID, i.Traits, err) + var it string = idToken + var ( + cat, crt string + ) + if token != nil { + if idToken, ok := token.Extra("id_token").(string); ok { + if it, err = s.d.Cipher(r.Context()).Encrypt(r.Context(), []byte(idToken)); err != nil { + return nil, s.handleError(w, r, rf, provider.Config().ID, i.Traits, err) + } } - } - cat, err := s.d.Cipher(r.Context()).Encrypt(r.Context(), []byte(token.AccessToken)) - if err != nil { - return nil, s.handleError(w, r, rf, provider.Config().ID, i.Traits, err) - } + cat, err = s.d.Cipher(r.Context()).Encrypt(r.Context(), []byte(token.AccessToken)) + if err != nil { + return nil, s.handleError(w, r, rf, provider.Config().ID, i.Traits, err) + } - crt, err := s.d.Cipher(r.Context()).Encrypt(r.Context(), []byte(token.RefreshToken)) - if err != nil { - return nil, s.handleError(w, r, rf, provider.Config().ID, i.Traits, err) + crt, err = s.d.Cipher(r.Context()).Encrypt(r.Context(), []byte(token.RefreshToken)) + if err != nil { + return nil, s.handleError(w, r, rf, provider.Config().ID, i.Traits, err) + } } creds, err := identity.NewCredentialsOIDC(it, cat, crt, provider.Config().ID, claims.Subject) @@ -362,12 +396,16 @@ func (s *Strategy) setTraits(w http.ResponseWriter, r *http.Request, a *registra return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("OpenID Connect Jsonnet mapper did not return an object for key identity.traits. Please check your Jsonnet code!")) } - traits, err := merge(container.Traits, json.RawMessage(jsonTraits.Raw)) - if err != nil { - return s.handleError(w, r, a, provider.Config().ID, nil, err) - } + if container != nil { + traits, err := merge(container.Traits, json.RawMessage(jsonTraits.Raw)) + if err != nil { + return s.handleError(w, r, a, provider.Config().ID, nil, err) + } - i.Traits = traits + i.Traits = traits + } else { + i.Traits = identity.Traits(json.RawMessage(jsonTraits.Raw)) + } s.d.Logger(). WithRequest(r). WithField("oidc_provider", provider.Config().ID). diff --git a/spec/api.json b/spec/api.json index 6481b5b22cbf..819cd26ae2c4 100644 --- a/spec/api.json +++ b/spec/api.json @@ -2488,6 +2488,10 @@ "description": "The CSRF Token", "type": "string" }, + "id_token": { + "description": "An optional id token provided by an OIDC provider\n\nIf submitted, it is verified using the OIDC provider's public key set and the claims are used to populate\nthe OIDC credentials of the identity.\nIf the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use\nthe `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken.\n\nSupported providers are\nApple", + "type": "string" + }, "method": { "description": "Method to use\n\nThis field must be set to `oidc` when using the oidc method.", "type": "string" @@ -2732,6 +2736,10 @@ "description": "The CSRF Token", "type": "string" }, + "id_token": { + "description": "An optional id token provided by an OIDC provider\n\nIf submitted, it is verified using the OIDC provider's public key set and the claims are used to populate\nthe OIDC credentials of the identity.\nIf the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use\nthe `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken.\n\nSupported providers are\nApple", + "type": "string" + }, "method": { "description": "Method to use\n\nThis field must be set to `oidc` when using the oidc method.", "type": "string" diff --git a/spec/swagger.json b/spec/swagger.json index d48afb33998b..772d8b30fec3 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -5343,6 +5343,10 @@ "description": "The CSRF Token", "type": "string" }, + "id_token": { + "description": "An optional id token provided by an OIDC provider\n\nIf submitted, it is verified using the OIDC provider's public key set and the claims are used to populate\nthe OIDC credentials of the identity.\nIf the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use\nthe `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken.\n\nSupported providers are\nApple", + "type": "string" + }, "method": { "description": "Method to use\n\nThis field must be set to `oidc` when using the oidc method.", "type": "string" @@ -5551,6 +5555,10 @@ "description": "The CSRF Token", "type": "string" }, + "id_token": { + "description": "An optional id token provided by an OIDC provider\n\nIf submitted, it is verified using the OIDC provider's public key set and the claims are used to populate\nthe OIDC credentials of the identity.\nIf the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use\nthe `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken.\n\nSupported providers are\nApple", + "type": "string" + }, "method": { "description": "Method to use\n\nThis field must be set to `oidc` when using the oidc method.", "type": "string" From 487d6445bdb389d0e361a1ba404d6b4821627bcb Mon Sep 17 00:00:00 2001 From: Jonas Hungershausen Date: Tue, 5 Sep 2023 19:20:09 +0200 Subject: [PATCH 2/9] chore: add tests --- selfservice/strategy/oidc/provider_apple.go | 2 +- selfservice/strategy/oidc/provider_auth0.go | 2 +- selfservice/strategy/oidc/provider_config.go | 79 +++----- .../strategy/oidc/provider_dingtalk.go | 2 +- selfservice/strategy/oidc/provider_discord.go | 2 +- .../strategy/oidc/provider_facebook.go | 2 +- .../strategy/oidc/provider_generic_oidc.go | 2 +- selfservice/strategy/oidc/provider_github.go | 2 +- .../strategy/oidc/provider_github_app.go | 2 +- selfservice/strategy/oidc/provider_gitlab.go | 2 +- selfservice/strategy/oidc/provider_google.go | 2 +- selfservice/strategy/oidc/provider_lark.go | 2 +- .../strategy/oidc/provider_linkedin.go | 2 +- .../strategy/oidc/provider_microsoft.go | 2 +- selfservice/strategy/oidc/provider_netid.go | 2 +- selfservice/strategy/oidc/provider_patreon.go | 2 +- selfservice/strategy/oidc/provider_slack.go | 2 +- selfservice/strategy/oidc/provider_spotify.go | 2 +- selfservice/strategy/oidc/provider_test.go | 35 ++++ selfservice/strategy/oidc/provider_vk.go | 2 +- selfservice/strategy/oidc/provider_yandex.go | 2 +- selfservice/strategy/oidc/strategy.go | 12 +- selfservice/strategy/oidc/strategy_test.go | 174 ++++++++++++++++-- 23 files changed, 255 insertions(+), 83 deletions(-) diff --git a/selfservice/strategy/oidc/provider_apple.go b/selfservice/strategy/oidc/provider_apple.go index a4c5a601c160..2822332c35e2 100644 --- a/selfservice/strategy/oidc/provider_apple.go +++ b/selfservice/strategy/oidc/provider_apple.go @@ -27,7 +27,7 @@ type ProviderApple struct { func NewProviderApple( config *Configuration, reg dependencies, -) *ProviderApple { +) Provider { config.IssuerURL = "https://appleid.apple.com" return &ProviderApple{ ProviderGenericOIDC: &ProviderGenericOIDC{ diff --git a/selfservice/strategy/oidc/provider_auth0.go b/selfservice/strategy/oidc/provider_auth0.go index fbb71687e959..3b9f7a3dff5f 100644 --- a/selfservice/strategy/oidc/provider_auth0.go +++ b/selfservice/strategy/oidc/provider_auth0.go @@ -33,7 +33,7 @@ type ProviderAuth0 struct { func NewProviderAuth0( config *Configuration, reg dependencies, -) *ProviderAuth0 { +) Provider { return &ProviderAuth0{ ProviderGenericOIDC: &ProviderGenericOIDC{ config: config, diff --git a/selfservice/strategy/oidc/provider_config.go b/selfservice/strategy/oidc/provider_config.go index 8be144dbb1c8..45fc0734ae91 100644 --- a/selfservice/strategy/oidc/provider_config.go +++ b/selfservice/strategy/oidc/provider_config.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/pkg/errors" + "golang.org/x/exp/maps" "github.com/ory/herodot" @@ -116,61 +117,41 @@ type ConfigurationCollection struct { Providers []Configuration `json:"providers"` } +// !!! WARNING !!! +// +// If you add a provider here, please also add a test to +// provider_private_net_test.go +var supportedProviders = map[string]func(config *Configuration, reg dependencies) Provider{ + "generic": NewProviderGenericOIDC, + "google": NewProviderGoogle, + "github": NewProviderGitHub, + "github-app": NewProviderGitHubApp, + "gitlab": NewProviderGitLab, + "microsoft": NewProviderMicrosoft, + "discord": NewProviderDiscord, + "slack": NewProviderSlack, + "facebook": NewProviderFacebook, + "auth0": NewProviderAuth0, + "vk": NewProviderVK, + "yandex": NewProviderYandex, + "apple": NewProviderApple, + "spotify": NewProviderSpotify, + "netid": NewProviderNetID, + "dingtalk": NewProviderDingTalk, + "linkedin": NewProviderLinkedIn, + "patreon": NewProviderPatreon, + "lark": NewProviderLark, +} + func (c ConfigurationCollection) Provider(id string, reg dependencies) (Provider, error) { for k := range c.Providers { p := c.Providers[k] if p.ID == id { - var providerNames []string - var addProviderName = func(pn string) string { - providerNames = append(providerNames, pn) - return pn + if f, ok := supportedProviders[p.Provider]; ok { + return f(&p, reg), nil } - // !!! WARNING !!! - // - // If you add a provider here, please also add a test to - // provider_private_net_test.go - switch p.Provider { - case addProviderName("generic"): - return NewProviderGenericOIDC(&p, reg), nil - case addProviderName("google"): - return NewProviderGoogle(&p, reg), nil - case addProviderName("github"): - return NewProviderGitHub(&p, reg), nil - case addProviderName("github-app"): - return NewProviderGitHubApp(&p, reg), nil - case addProviderName("gitlab"): - return NewProviderGitLab(&p, reg), nil - case addProviderName("microsoft"): - return NewProviderMicrosoft(&p, reg), nil - case addProviderName("discord"): - return NewProviderDiscord(&p, reg), nil - case addProviderName("slack"): - return NewProviderSlack(&p, reg), nil - case addProviderName("facebook"): - return NewProviderFacebook(&p, reg), nil - case addProviderName("auth0"): - return NewProviderAuth0(&p, reg), nil - case addProviderName("vk"): - return NewProviderVK(&p, reg), nil - case addProviderName("yandex"): - return NewProviderYandex(&p, reg), nil - case addProviderName("apple"): - return NewProviderApple(&p, reg), nil - case addProviderName("spotify"): - return NewProviderSpotify(&p, reg), nil - case addProviderName("netid"): - return NewProviderNetID(&p, reg), nil - case addProviderName("dingtalk"): - return NewProviderDingTalk(&p, reg), nil - case addProviderName("linkedin"): - return NewProviderLinkedIn(&p, reg), nil - case addProviderName("patreon"): - return NewProviderPatreon(&p, reg), nil - case addProviderName("lark"): - return NewProviderLark(&p, reg), nil - } - return nil, errors.Errorf("provider type %s is not supported, supported are: %v", p.Provider, providerNames) + return nil, errors.Errorf("provider type %s is not supported, supported are: %v", p.Provider, maps.Keys(supportedProviders)) } } return nil, errors.WithStack(herodot.ErrNotFound.WithReasonf(`OpenID Connect Provider "%s" is unknown or has not been configured`, id)) diff --git a/selfservice/strategy/oidc/provider_dingtalk.go b/selfservice/strategy/oidc/provider_dingtalk.go index 71ae65c870cc..7b4a835e5734 100644 --- a/selfservice/strategy/oidc/provider_dingtalk.go +++ b/selfservice/strategy/oidc/provider_dingtalk.go @@ -28,7 +28,7 @@ type ProviderDingTalk struct { func NewProviderDingTalk( config *Configuration, reg dependencies, -) *ProviderDingTalk { +) Provider { return &ProviderDingTalk{ config: config, reg: reg, diff --git a/selfservice/strategy/oidc/provider_discord.go b/selfservice/strategy/oidc/provider_discord.go index 9a91021d0995..e48ce351a6dc 100644 --- a/selfservice/strategy/oidc/provider_discord.go +++ b/selfservice/strategy/oidc/provider_discord.go @@ -27,7 +27,7 @@ type ProviderDiscord struct { func NewProviderDiscord( config *Configuration, reg dependencies, -) *ProviderDiscord { +) Provider { return &ProviderDiscord{ config: config, reg: reg, diff --git a/selfservice/strategy/oidc/provider_facebook.go b/selfservice/strategy/oidc/provider_facebook.go index 70e7df99b749..7cf89538f978 100644 --- a/selfservice/strategy/oidc/provider_facebook.go +++ b/selfservice/strategy/oidc/provider_facebook.go @@ -31,7 +31,7 @@ type ProviderFacebook struct { func NewProviderFacebook( config *Configuration, reg dependencies, -) *ProviderFacebook { +) Provider { config.IssuerURL = "https://www.facebook.com" return &ProviderFacebook{ ProviderGenericOIDC: &ProviderGenericOIDC{ diff --git a/selfservice/strategy/oidc/provider_generic_oidc.go b/selfservice/strategy/oidc/provider_generic_oidc.go index c09185575969..59d86f985994 100644 --- a/selfservice/strategy/oidc/provider_generic_oidc.go +++ b/selfservice/strategy/oidc/provider_generic_oidc.go @@ -27,7 +27,7 @@ type ProviderGenericOIDC struct { func NewProviderGenericOIDC( config *Configuration, reg dependencies, -) *ProviderGenericOIDC { +) Provider { return &ProviderGenericOIDC{ config: config, reg: reg, diff --git a/selfservice/strategy/oidc/provider_github.go b/selfservice/strategy/oidc/provider_github.go index d53a055fc247..8b8b97ee5f83 100644 --- a/selfservice/strategy/oidc/provider_github.go +++ b/selfservice/strategy/oidc/provider_github.go @@ -30,7 +30,7 @@ type ProviderGitHub struct { func NewProviderGitHub( config *Configuration, reg dependencies, -) *ProviderGitHub { +) Provider { return &ProviderGitHub{ config: config, reg: reg, diff --git a/selfservice/strategy/oidc/provider_github_app.go b/selfservice/strategy/oidc/provider_github_app.go index 86c19b3069c3..f48de9664547 100644 --- a/selfservice/strategy/oidc/provider_github_app.go +++ b/selfservice/strategy/oidc/provider_github_app.go @@ -27,7 +27,7 @@ type ProviderGitHubApp struct { func NewProviderGitHubApp( config *Configuration, reg dependencies, -) *ProviderGitHubApp { +) Provider { return &ProviderGitHubApp{ config: config, reg: reg, diff --git a/selfservice/strategy/oidc/provider_gitlab.go b/selfservice/strategy/oidc/provider_gitlab.go index a7dbee735a0f..fde2506e0a63 100644 --- a/selfservice/strategy/oidc/provider_gitlab.go +++ b/selfservice/strategy/oidc/provider_gitlab.go @@ -32,7 +32,7 @@ type ProviderGitLab struct { func NewProviderGitLab( config *Configuration, reg dependencies, -) *ProviderGitLab { +) Provider { return &ProviderGitLab{ ProviderGenericOIDC: &ProviderGenericOIDC{ config: config, diff --git a/selfservice/strategy/oidc/provider_google.go b/selfservice/strategy/oidc/provider_google.go index cb98696484ca..dc084ec81b6b 100644 --- a/selfservice/strategy/oidc/provider_google.go +++ b/selfservice/strategy/oidc/provider_google.go @@ -19,7 +19,7 @@ type ProviderGoogle struct { func NewProviderGoogle( config *Configuration, reg dependencies, -) *ProviderGoogle { +) Provider { config.IssuerURL = "https://accounts.google.com" return &ProviderGoogle{ ProviderGenericOIDC: &ProviderGenericOIDC{ diff --git a/selfservice/strategy/oidc/provider_lark.go b/selfservice/strategy/oidc/provider_lark.go index 5541a73335af..b239a207c095 100644 --- a/selfservice/strategy/oidc/provider_lark.go +++ b/selfservice/strategy/oidc/provider_lark.go @@ -32,7 +32,7 @@ var ( func NewProviderLark( config *Configuration, reg dependencies, -) *ProviderLark { +) Provider { return &ProviderLark{ &ProviderGenericOIDC{ config: config, diff --git a/selfservice/strategy/oidc/provider_linkedin.go b/selfservice/strategy/oidc/provider_linkedin.go index 8a85bb9c9ed5..a9dde8ce37e3 100644 --- a/selfservice/strategy/oidc/provider_linkedin.go +++ b/selfservice/strategy/oidc/provider_linkedin.go @@ -71,7 +71,7 @@ type ProviderLinkedIn struct { func NewProviderLinkedIn( config *Configuration, reg dependencies, -) *ProviderLinkedIn { +) Provider { return &ProviderLinkedIn{ config: config, reg: reg, diff --git a/selfservice/strategy/oidc/provider_microsoft.go b/selfservice/strategy/oidc/provider_microsoft.go index e5c4a8ec68ff..af664f7f8aca 100644 --- a/selfservice/strategy/oidc/provider_microsoft.go +++ b/selfservice/strategy/oidc/provider_microsoft.go @@ -30,7 +30,7 @@ type ProviderMicrosoft struct { func NewProviderMicrosoft( config *Configuration, reg dependencies, -) *ProviderMicrosoft { +) Provider { return &ProviderMicrosoft{ ProviderGenericOIDC: &ProviderGenericOIDC{ config: config, diff --git a/selfservice/strategy/oidc/provider_netid.go b/selfservice/strategy/oidc/provider_netid.go index 07e52953882a..a919c177728f 100644 --- a/selfservice/strategy/oidc/provider_netid.go +++ b/selfservice/strategy/oidc/provider_netid.go @@ -35,7 +35,7 @@ type ProviderNetID struct { func NewProviderNetID( config *Configuration, reg dependencies, -) *ProviderNetID { +) Provider { config.IssuerURL = fmt.Sprintf("%s://%s/", defaultBrokerScheme, defaultBrokerHost) if !stringslice.Has(config.Scope, gooidc.ScopeOpenID) { config.Scope = append(config.Scope, gooidc.ScopeOpenID) diff --git a/selfservice/strategy/oidc/provider_patreon.go b/selfservice/strategy/oidc/provider_patreon.go index 4dbb60b42d9c..dbd740ff88bf 100644 --- a/selfservice/strategy/oidc/provider_patreon.go +++ b/selfservice/strategy/oidc/provider_patreon.go @@ -40,7 +40,7 @@ type PatreonIdentityResponse struct { func NewProviderPatreon( config *Configuration, reg dependencies, -) *ProviderPatreon { +) Provider { return &ProviderPatreon{ config: config, reg: reg, diff --git a/selfservice/strategy/oidc/provider_slack.go b/selfservice/strategy/oidc/provider_slack.go index f23d543a40c5..951b7fab1874 100644 --- a/selfservice/strategy/oidc/provider_slack.go +++ b/selfservice/strategy/oidc/provider_slack.go @@ -27,7 +27,7 @@ type ProviderSlack struct { func NewProviderSlack( config *Configuration, reg dependencies, -) *ProviderSlack { +) Provider { return &ProviderSlack{ config: config, reg: reg, diff --git a/selfservice/strategy/oidc/provider_spotify.go b/selfservice/strategy/oidc/provider_spotify.go index a60efb552b83..3c0d95ea043d 100644 --- a/selfservice/strategy/oidc/provider_spotify.go +++ b/selfservice/strategy/oidc/provider_spotify.go @@ -30,7 +30,7 @@ type ProviderSpotify struct { func NewProviderSpotify( config *Configuration, reg dependencies, -) *ProviderSpotify { +) Provider { return &ProviderSpotify{ config: config, reg: reg, diff --git a/selfservice/strategy/oidc/provider_test.go b/selfservice/strategy/oidc/provider_test.go index 1b464bd2eec5..74fdb05031fc 100644 --- a/selfservice/strategy/oidc/provider_test.go +++ b/selfservice/strategy/oidc/provider_test.go @@ -4,6 +4,9 @@ package oidc import ( + "context" + "encoding/json" + "fmt" "testing" "github.com/stretchr/testify/require" @@ -17,3 +20,35 @@ func TestClaimsValidate(t *testing.T) { require.Error(t, (&Claims{Subject: "not-empty"}).Validate()) require.NoError(t, (&Claims{Issuer: "not-empty", Subject: "not-empty"}).Validate()) } + +type TestProvider struct { + *ProviderGenericOIDC +} + +func NewTestProvider(c *Configuration, reg dependencies) Provider { + return &TestProvider{ + ProviderGenericOIDC: NewProviderGenericOIDC(c, reg).(*ProviderGenericOIDC), + } +} + +func RegisterTestProvider(id string) func() { + supportedProviders[id] = func(c *Configuration, reg dependencies) Provider { + return NewTestProvider(c, reg) + } + return func() { + delete(supportedProviders, id) + } +} + +var _ IDTokenVerifier = new(TestProvider) + +func (t *TestProvider) Verify(ctx context.Context, token string) (*Claims, error) { + if token == "error" { + return nil, fmt.Errorf("stub error") + } + c := Claims{} + if err := json.Unmarshal([]byte(token), &c); err != nil { + return nil, err + } + return &c, nil +} diff --git a/selfservice/strategy/oidc/provider_vk.go b/selfservice/strategy/oidc/provider_vk.go index 602a8574f999..6d89170d77a1 100644 --- a/selfservice/strategy/oidc/provider_vk.go +++ b/selfservice/strategy/oidc/provider_vk.go @@ -27,7 +27,7 @@ type ProviderVK struct { func NewProviderVK( config *Configuration, reg dependencies, -) *ProviderVK { +) Provider { return &ProviderVK{ config: config, reg: reg, diff --git a/selfservice/strategy/oidc/provider_yandex.go b/selfservice/strategy/oidc/provider_yandex.go index 12a845185b91..4c93a13a196e 100644 --- a/selfservice/strategy/oidc/provider_yandex.go +++ b/selfservice/strategy/oidc/provider_yandex.go @@ -25,7 +25,7 @@ type ProviderYandex struct { func NewProviderYandex( config *Configuration, reg dependencies, -) *ProviderYandex { +) Provider { return &ProviderYandex{ config: config, reg: reg, diff --git a/selfservice/strategy/oidc/strategy.go b/selfservice/strategy/oidc/strategy.go index 5095fa6e769e..970ffdd993cf 100644 --- a/selfservice/strategy/oidc/strategy.go +++ b/selfservice/strategy/oidc/strategy.go @@ -590,18 +590,24 @@ func (s *Strategy) CompletedAuthenticationMethod(ctx context.Context) session.Au } } +var ( + ErrIDTokenVerificationFailed = herodot.ErrInternalServerError.WithReasonf("Could not verify ID token") + ErrUnsupportedProvider = herodot.ErrInternalServerError.WithReasonf("Provider does not support ID Token verification") + ErrClaimValidationFailed = herodot.ErrInternalServerError.WithReasonf("Could not verify token claims") +) + func (s *Strategy) processIDToken(w http.ResponseWriter, r *http.Request, provider Provider, idToken string) (*Claims, error) { verifier, ok := provider.(IDTokenVerifier) if !ok { - return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Provider does not support ID Token verification.")) // TODO: move to global var + return nil, errors.WithStack(ErrUnsupportedProvider) } claims, err := verifier.Verify(r.Context(), idToken) if err != nil { - return nil, errors.WithStack(err) + return nil, errors.WithStack(ErrIDTokenVerificationFailed.WithError(err.Error())) } if err := claims.Validate(); err != nil { - return nil, errors.WithStack(err) + return nil, errors.WithStack(ErrClaimValidationFailed.WithError(err.Error())) } return claims, nil diff --git a/selfservice/strategy/oidc/strategy_test.go b/selfservice/strategy/oidc/strategy_test.go index 0969caa38da5..10b0e666dc9a 100644 --- a/selfservice/strategy/oidc/strategy_test.go +++ b/selfservice/strategy/oidc/strategy_test.go @@ -197,19 +197,15 @@ func TestStrategy(t *testing.T) { return code } - var exchangeCodeForToken = func(t *testing.T, codes sessiontokenexchange.Codes) (codeResponse session.CodeExchangeResponse, err error) { + var exchangeCodeForToken = func(t *testing.T, codes sessiontokenexchange.Codes) (codeResponse session.CodeExchangeResponse) { + t.Helper() tokenURL := urlx.ParseOrPanic(ts.URL) tokenURL.Path = "/sessions/token-exchange" tokenURL.RawQuery = fmt.Sprintf("init_code=%s&return_to_code=%s", codes.InitCode, codes.ReturnToCode) res, err := ts.Client().Get(tokenURL.String()) - if err != nil { - return codeResponse, err - } - if res.StatusCode != 200 { - return codeResponse, fmt.Errorf("got status code %d", res.StatusCode) - } + require.NoError(t, err) + require.Equal(t, http.StatusOK, res.StatusCode, "expected status code 200 but got %d: %s", res.StatusCode, ioutilx.MustReadAll(res.Body)) require.NoError(t, json.NewDecoder(res.Body).Decode(&codeResponse)) - return } @@ -504,16 +500,14 @@ func TestStrategy(t *testing.T) { scope = []string{"openid"} var loginOrRegister = func(t *testing.T, id uuid.UUID, code string) { - _, err := exchangeCodeForToken(t, sessiontokenexchange.Codes{InitCode: code}) - require.Error(t, err) + exchangeCodeForToken(t, sessiontokenexchange.Codes{InitCode: code}) action := assertFormValues(t, id, "valid") returnToCode := makeAPICodeFlowRequest(t, "valid", action) - codeResponse, err := exchangeCodeForToken(t, sessiontokenexchange.Codes{ + codeResponse := exchangeCodeForToken(t, sessiontokenexchange.Codes{ InitCode: code, ReturnToCode: returnToCode, }) - require.NoError(t, err) assert.NotEmpty(t, codeResponse.Token) assert.Equal(t, subject, gjson.GetBytes(codeResponse.Session.Identity.Traits, "subject").String()) @@ -549,7 +543,163 @@ func TestStrategy(t *testing.T) { tc.then(t) }) } + }) + + t.Run("case=register using idToken", func(t *testing.T) { + viperSetProviderConfig( + t, + conf, + newOIDCProvider(t, ts, remotePublic, remoteAdmin, "valid"), + oidc.Configuration{ + Provider: "test-provider", + ID: "test-provider", + ClientID: invalid.ClientID, + ClientSecret: invalid.ClientSecret, + IssuerURL: remotePublic + "/", + Mapper: "file://./stub/oidc.facebook.jsonnet", + }, + ) + cleanup := oidc.RegisterTestProvider("test-provider") + t.Cleanup(cleanup) + cl := http.Client{} + + type testCase struct { + name string + idToken string + provider string + expect func(t *testing.T, res *http.Response, body []byte) + } + + var prep = func(tc *testCase) (string, string) { + provider := tc.provider + if provider == "" { + provider = "test-provider" + } + token := tc.idToken + if strings.Contains(tc.idToken, "%s") { + token = fmt.Sprintf(tc.idToken, testhelpers.RandomEmail()) + } + return provider, token + } + + for _, tc := range []testCase{ + { + name: "should fail if provider does not support id_token submission", + idToken: "error", + provider: "valid", + expect: func(t *testing.T, res *http.Response, body []byte) { + require.Equal(t, "Provider does not support ID Token verification", gjson.GetBytes(body, "error.reason").String(), "%s", body) + }, + }, + { + name: "should fail because id_token is invalid", + idToken: "error", + expect: func(t *testing.T, res *http.Response, body []byte) { + require.Equal(t, "Could not verify ID token", gjson.GetBytes(body, "error.reason").String(), "%s", body) + require.Equal(t, "stub error", gjson.GetBytes(body, "error.message").String(), "%s", body) + }, + }, + { + name: "should fail because claims are invalid", + idToken: "{}", + expect: func(t *testing.T, res *http.Response, body []byte) { + require.Equal(t, "Could not verify token claims", gjson.GetBytes(body, "error.reason").String(), "%s", body) + }, + }, + { + name: "should pass if claims are valid", + idToken: `{ + "iss": "https://appleid.apple.com", + "sub": "%s" + }`, + expect: func(t *testing.T, res *http.Response, body []byte) { + require.NotEmpty(t, gjson.GetBytes(body, "session_token").String(), "%s", body) + }, + }, + } { + tc := tc + t.Run(fmt.Sprintf("flow=registration/case=%s", tc.name), func(t *testing.T) { + f := newAPIRegistrationFlow(t, returnTS.URL, time.Minute) + provider, token := prep(&tc) + action := assertFormValues(t, f.ID, "test-provider") + res, err := cl.PostForm(action, url.Values{ + "id_token": {token}, + "provider": {provider}, + }) + require.NoError(t, err) + body := ioutilx.MustReadAll(res.Body) + tc.expect(t, res, body) + }) + + t.Run(fmt.Sprintf("flow=login/case=%s", tc.name), func(t *testing.T) { + provider, token := prep(&tc) + rf := newAPIRegistrationFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, rf.ID, "test-provider") + v := url.Values{ + "id_token": {token}, + "provider": {provider}, + } + res, err := cl.PostForm(action, v) + require.NoError(t, err) + + lf := newAPILoginFlow(t, returnTS.URL, time.Minute) + action = assertFormValues(t, lf.ID, "test-provider") + + res, err = cl.PostForm(action, v) + require.NoError(t, err) + body := ioutilx.MustReadAll(res.Body) + tc.expect(t, res, body) + }) + + t.Run(fmt.Sprintf("flow=login_without_registration/case=%s", tc.name), func(t *testing.T) { + provider, token := prep(&tc) + rf := newAPIRegistrationFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, rf.ID, "test-provider") + + v := url.Values{ + "id_token": {token}, + "provider": {provider}, + } + res, err := cl.PostForm(action, v) + require.NoError(t, err) + + lf := newAPIRegistrationFlow(t, returnTS.URL, time.Minute) + action = assertFormValues(t, lf.ID, "test-provider") + + res, err = cl.PostForm(action, v) + require.NoError(t, err) + body := ioutilx.MustReadAll(res.Body) + tc.expect(t, res, body) + }) + t.Run(fmt.Sprintf("flow=login_with_return_session_token_exchange_code/case=%s", tc.name), func(t *testing.T) { + provider, token := prep(&tc) + v := url.Values{ + "id_token": {token}, + "provider": {provider}, + } + lf := newAPILoginFlow(t, returnTS.URL+"?return_session_token_exchange_code=true&return_to=/app_code", time.Minute) + action := assertFormValues(t, lf.ID, "test-provider") + res, err := cl.PostForm(action, v) + require.NoError(t, err) + body := ioutilx.MustReadAll(res.Body) + tc.expect(t, res, body) + }) + + t.Run(fmt.Sprintf("flow=registration_with_return_session_token_exchange_code/case=%s", tc.name), func(t *testing.T) { + provider, token := prep(&tc) + v := url.Values{ + "id_token": {token}, + "provider": {provider}, + } + lf := newAPIRegistrationFlow(t, returnTS.URL+"?return_session_token_exchange_code=true&return_to=/app_code", time.Minute) + action := assertFormValues(t, lf.ID, "test-provider") + res, err := cl.PostForm(action, v) + require.NoError(t, err) + body := ioutilx.MustReadAll(res.Body) + tc.expect(t, res, body) + }) + } }) t.Run("case=login without registered account with return_to", func(t *testing.T) { From 68c6033b562169d1e74afb709e1418db836317a5 Mon Sep 17 00:00:00 2001 From: Jonas Hungershausen Date: Tue, 5 Sep 2023 19:41:29 +0200 Subject: [PATCH 3/9] chore: naming --- selfservice/strategy/oidc/strategy_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfservice/strategy/oidc/strategy_test.go b/selfservice/strategy/oidc/strategy_test.go index 10b0e666dc9a..e7f9dfd4f9cc 100644 --- a/selfservice/strategy/oidc/strategy_test.go +++ b/selfservice/strategy/oidc/strategy_test.go @@ -545,7 +545,7 @@ func TestStrategy(t *testing.T) { } }) - t.Run("case=register using idToken", func(t *testing.T) { + t.Run("case=submit id_token during registration or login", func(t *testing.T) { viperSetProviderConfig( t, conf, From 04174c0fd91f20b4dd781d579202fe9805bcd35e Mon Sep 17 00:00:00 2001 From: Jonas Hungershausen Date: Wed, 6 Sep 2023 17:29:14 +0200 Subject: [PATCH 4/9] chore: add nonce support --- go.mod | 3 + go.sum | 18 +++ ...odel_update_login_flow_with_oidc_method.go | 39 +++++- ...date_registration_flow_with_oidc_method.go | 39 +++++- ...odel_update_login_flow_with_oidc_method.go | 39 +++++- ...date_registration_flow_with_oidc_method.go | 39 +++++- selfservice/flow/login/flow.go | 3 + selfservice/flow/registration/flow.go | 8 +- .../strategy/oidc/.schema/link.schema.json | 4 + selfservice/strategy/oidc/provider.go | 3 + selfservice/strategy/oidc/provider_apple.go | 11 +- .../strategy/oidc/provider_apple_test.go | 62 +++++++++ selfservice/strategy/oidc/provider_test.go | 4 + selfservice/strategy/oidc/strategy.go | 34 +++-- selfservice/strategy/oidc/strategy_login.go | 12 +- .../strategy/oidc/strategy_registration.go | 11 +- selfservice/strategy/oidc/strategy_test.go | 120 +++++++++++++----- selfservice/strategy/oidc/stub/jwk.json | 14 ++ .../strategy/oidc/stub/jwks_public.json | 12 ++ spec/api.json | 12 +- spec/swagger.json | 12 +- 21 files changed, 440 insertions(+), 59 deletions(-) create mode 100644 selfservice/strategy/oidc/stub/jwk.json create mode 100644 selfservice/strategy/oidc/stub/jwks_public.json diff --git a/go.mod b/go.mod index b7a72b85f39b..1d8e3622d591 100644 --- a/go.mod +++ b/go.mod @@ -139,6 +139,7 @@ require ( github.com/fatih/structs v1.1.0 // indirect github.com/felixge/fgprof v0.9.3 // indirect github.com/felixge/httpsnoop v1.0.3 // indirect + github.com/form3tech-oss/jwt-go v3.2.5+incompatible // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/fxamacker/cbor/v2 v2.4.0 // indirect github.com/go-crypt/x v0.2.1 // indirect @@ -170,6 +171,7 @@ require ( github.com/goccy/go-yaml v1.9.6 // indirect github.com/gofrs/flock v0.8.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang/glog v1.1.0 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/btree v1.0.1 // indirect @@ -258,6 +260,7 @@ require ( github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect + github.com/rakutentech/jwk-go v1.1.3 // indirect github.com/rjeczalik/notify v0.0.0-20181126183243-629144ba06a1 // indirect github.com/rogpeppe/go-internal v1.9.0 // indirect github.com/seatgeek/logrus-gelf-formatter v0.0.0-20210414080842-5b05eb8ff761 // indirect diff --git a/go.sum b/go.sum index 5422e5e53752..559f030eaa0f 100644 --- a/go.sum +++ b/go.sum @@ -200,8 +200,11 @@ github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNu github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/form3tech-oss/jwt-go v3.2.5+incompatible h1:/l4kBbb4/vGSsdtB5nUe8L7B9mImVMaBPw9L/0TBHU8= +github.com/form3tech-oss/jwt-go v3.2.5+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88= @@ -357,6 +360,8 @@ github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRx github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/gddo v0.0.0-20190904175337-72a348e765d2 h1:xisWqjiKEff2B0KfFYGpCqc3M3zdTz+OHQHRc09FeYk= @@ -525,6 +530,7 @@ github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0= github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY= github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= @@ -784,6 +790,10 @@ github.com/ogier/pflag v0.0.1 h1:RW6JSWSu/RkSatfcLtogGfFgpim5p7ARQ10ECk5O750= github.com/ogier/pflag v0.0.1/go.mod h1:zkFki7tvTa0tafRvTBIZTvzYyAu6kQhPZFnshFFPE+g= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034= @@ -877,6 +887,8 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1 github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= +github.com/rakutentech/jwk-go v1.1.3 h1:PiLwepKyUaW+QFG3ki78DIO2+b4IVK3nMhlxM70zrQ4= +github.com/rakutentech/jwk-go v1.1.3/go.mod h1:LtzSv4/+Iti1nnNeVQiP6l5cI74GBStbhyXCYvgPZFk= github.com/rjeczalik/notify v0.0.0-20181126183243-629144ba06a1 h1:FLWDC+iIP9BWgYKvWKKtOUZux35LIQNAuIzp/63RQJU= github.com/rjeczalik/notify v0.0.0-20181126183243-629144ba06a1/go.mod h1:aErll2f0sUX9PXZnVNyeiObbmTlk5jnMoCa4QEjJeqM= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= @@ -1099,6 +1111,7 @@ golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= @@ -1156,6 +1169,7 @@ golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1240,6 +1254,7 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1263,6 +1278,7 @@ golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1547,6 +1563,7 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/mold.v2 v2.2.0/go.mod h1:XMyyRsGtakkDPbxXbrA5VODo6bUXyvoDjLd5l3T0XoA= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= @@ -1559,6 +1576,7 @@ gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473 h1:6D+BvnJ/j6e222UW gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473/go.mod h1:N1eN2tsCx0Ydtgjl4cqmbRCsY4/+z4cYDeqwZTk6zog= gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/validator.v2 v2.0.0-20180514200540-135c24b11c19/go.mod h1:o4V0GXN9/CAmCsvJ0oXYZvrZOe7syiDZSN1GWGZTGzc= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/client-go/model_update_login_flow_with_oidc_method.go b/internal/client-go/model_update_login_flow_with_oidc_method.go index 78cc88ef2103..f9a533b699cf 100644 --- a/internal/client-go/model_update_login_flow_with_oidc_method.go +++ b/internal/client-go/model_update_login_flow_with_oidc_method.go @@ -19,12 +19,14 @@ import ( type UpdateLoginFlowWithOidcMethod struct { // The CSRF Token CsrfToken *string `json:"csrf_token,omitempty"` - // An optional id token provided by an OIDC provider If submitted, it is verified using the OIDC provider's public key set and the claims are used to populate the OIDC credentials of the identity. If the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use the `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken. Supported providers are Apple + // IDToken is an optional id token provided by an OIDC provider If submitted, it is verified using the OIDC provider's public key set and the claims are used to populate the OIDC credentials of the identity. If the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use the `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken. Supported providers are Apple IdToken *string `json:"id_token,omitempty"` // Method to use This field must be set to `oidc` when using the oidc method. Method string `json:"method"` // The provider to register with Provider string `json:"provider"` + // RawIDTokenNonce is the nonce, used when generating the IDToken. If the provider supports nonce validation, the nonce will be validated against this value and required. + RawIdTokenNonce *string `json:"raw_id_token_nonce,omitempty"` // The identity traits. This is a placeholder for the registration flow. Traits map[string]interface{} `json:"traits,omitempty"` // UpstreamParameters are the parameters that are passed to the upstream identity provider. These parameters are optional and depend on what the upstream identity provider supports. Supported parameters are: `login_hint` (string): The `login_hint` parameter suppresses the account chooser and either pre-fills the email box on the sign-in form, or selects the proper session. `hd` (string): The `hd` parameter limits the login/registration process to a Google Organization, e.g. `mycollege.edu`. `prompt` (string): The `prompt` specifies whether the Authorization Server prompts the End-User for reauthentication and consent, e.g. `select_account`. @@ -162,6 +164,38 @@ func (o *UpdateLoginFlowWithOidcMethod) SetProvider(v string) { o.Provider = v } +// GetRawIdTokenNonce returns the RawIdTokenNonce field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithOidcMethod) GetRawIdTokenNonce() string { + if o == nil || o.RawIdTokenNonce == nil { + var ret string + return ret + } + return *o.RawIdTokenNonce +} + +// GetRawIdTokenNonceOk returns a tuple with the RawIdTokenNonce field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithOidcMethod) GetRawIdTokenNonceOk() (*string, bool) { + if o == nil || o.RawIdTokenNonce == nil { + return nil, false + } + return o.RawIdTokenNonce, true +} + +// HasRawIdTokenNonce returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithOidcMethod) HasRawIdTokenNonce() bool { + if o != nil && o.RawIdTokenNonce != nil { + return true + } + + return false +} + +// SetRawIdTokenNonce gets a reference to the given string and assigns it to the RawIdTokenNonce field. +func (o *UpdateLoginFlowWithOidcMethod) SetRawIdTokenNonce(v string) { + o.RawIdTokenNonce = &v +} + // GetTraits returns the Traits field value if set, zero value otherwise. func (o *UpdateLoginFlowWithOidcMethod) GetTraits() map[string]interface{} { if o == nil || o.Traits == nil { @@ -240,6 +274,9 @@ func (o UpdateLoginFlowWithOidcMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["provider"] = o.Provider } + if o.RawIdTokenNonce != nil { + toSerialize["raw_id_token_nonce"] = o.RawIdTokenNonce + } if o.Traits != nil { toSerialize["traits"] = o.Traits } diff --git a/internal/client-go/model_update_registration_flow_with_oidc_method.go b/internal/client-go/model_update_registration_flow_with_oidc_method.go index 35e2c11ccdc3..3ffbafe2c5a5 100644 --- a/internal/client-go/model_update_registration_flow_with_oidc_method.go +++ b/internal/client-go/model_update_registration_flow_with_oidc_method.go @@ -19,12 +19,14 @@ import ( type UpdateRegistrationFlowWithOidcMethod struct { // The CSRF Token CsrfToken *string `json:"csrf_token,omitempty"` - // An optional id token provided by an OIDC provider If submitted, it is verified using the OIDC provider's public key set and the claims are used to populate the OIDC credentials of the identity. If the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use the `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken. Supported providers are Apple + // IDToken is an optional id token provided by an OIDC provider If submitted, it is verified using the OIDC provider's public key set and the claims are used to populate the OIDC credentials of the identity. If the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use the `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken. Supported providers are Apple IdToken *string `json:"id_token,omitempty"` // Method to use This field must be set to `oidc` when using the oidc method. Method string `json:"method"` // The provider to register with Provider string `json:"provider"` + // RawIDTokenNonce is the nonce, used when generating the IDToken. If the provider supports nonce validation, the nonce will be validated against this value and required. + RawIdTokenNonce *string `json:"raw_id_token_nonce,omitempty"` // The identity traits Traits map[string]interface{} `json:"traits,omitempty"` // Transient data to pass along to any webhooks @@ -164,6 +166,38 @@ func (o *UpdateRegistrationFlowWithOidcMethod) SetProvider(v string) { o.Provider = v } +// GetRawIdTokenNonce returns the RawIdTokenNonce field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithOidcMethod) GetRawIdTokenNonce() string { + if o == nil || o.RawIdTokenNonce == nil { + var ret string + return ret + } + return *o.RawIdTokenNonce +} + +// GetRawIdTokenNonceOk returns a tuple with the RawIdTokenNonce field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithOidcMethod) GetRawIdTokenNonceOk() (*string, bool) { + if o == nil || o.RawIdTokenNonce == nil { + return nil, false + } + return o.RawIdTokenNonce, true +} + +// HasRawIdTokenNonce returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithOidcMethod) HasRawIdTokenNonce() bool { + if o != nil && o.RawIdTokenNonce != nil { + return true + } + + return false +} + +// SetRawIdTokenNonce gets a reference to the given string and assigns it to the RawIdTokenNonce field. +func (o *UpdateRegistrationFlowWithOidcMethod) SetRawIdTokenNonce(v string) { + o.RawIdTokenNonce = &v +} + // GetTraits returns the Traits field value if set, zero value otherwise. func (o *UpdateRegistrationFlowWithOidcMethod) GetTraits() map[string]interface{} { if o == nil || o.Traits == nil { @@ -274,6 +308,9 @@ func (o UpdateRegistrationFlowWithOidcMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["provider"] = o.Provider } + if o.RawIdTokenNonce != nil { + toSerialize["raw_id_token_nonce"] = o.RawIdTokenNonce + } if o.Traits != nil { toSerialize["traits"] = o.Traits } diff --git a/internal/httpclient/model_update_login_flow_with_oidc_method.go b/internal/httpclient/model_update_login_flow_with_oidc_method.go index 78cc88ef2103..f9a533b699cf 100644 --- a/internal/httpclient/model_update_login_flow_with_oidc_method.go +++ b/internal/httpclient/model_update_login_flow_with_oidc_method.go @@ -19,12 +19,14 @@ import ( type UpdateLoginFlowWithOidcMethod struct { // The CSRF Token CsrfToken *string `json:"csrf_token,omitempty"` - // An optional id token provided by an OIDC provider If submitted, it is verified using the OIDC provider's public key set and the claims are used to populate the OIDC credentials of the identity. If the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use the `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken. Supported providers are Apple + // IDToken is an optional id token provided by an OIDC provider If submitted, it is verified using the OIDC provider's public key set and the claims are used to populate the OIDC credentials of the identity. If the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use the `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken. Supported providers are Apple IdToken *string `json:"id_token,omitempty"` // Method to use This field must be set to `oidc` when using the oidc method. Method string `json:"method"` // The provider to register with Provider string `json:"provider"` + // RawIDTokenNonce is the nonce, used when generating the IDToken. If the provider supports nonce validation, the nonce will be validated against this value and required. + RawIdTokenNonce *string `json:"raw_id_token_nonce,omitempty"` // The identity traits. This is a placeholder for the registration flow. Traits map[string]interface{} `json:"traits,omitempty"` // UpstreamParameters are the parameters that are passed to the upstream identity provider. These parameters are optional and depend on what the upstream identity provider supports. Supported parameters are: `login_hint` (string): The `login_hint` parameter suppresses the account chooser and either pre-fills the email box on the sign-in form, or selects the proper session. `hd` (string): The `hd` parameter limits the login/registration process to a Google Organization, e.g. `mycollege.edu`. `prompt` (string): The `prompt` specifies whether the Authorization Server prompts the End-User for reauthentication and consent, e.g. `select_account`. @@ -162,6 +164,38 @@ func (o *UpdateLoginFlowWithOidcMethod) SetProvider(v string) { o.Provider = v } +// GetRawIdTokenNonce returns the RawIdTokenNonce field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithOidcMethod) GetRawIdTokenNonce() string { + if o == nil || o.RawIdTokenNonce == nil { + var ret string + return ret + } + return *o.RawIdTokenNonce +} + +// GetRawIdTokenNonceOk returns a tuple with the RawIdTokenNonce field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithOidcMethod) GetRawIdTokenNonceOk() (*string, bool) { + if o == nil || o.RawIdTokenNonce == nil { + return nil, false + } + return o.RawIdTokenNonce, true +} + +// HasRawIdTokenNonce returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithOidcMethod) HasRawIdTokenNonce() bool { + if o != nil && o.RawIdTokenNonce != nil { + return true + } + + return false +} + +// SetRawIdTokenNonce gets a reference to the given string and assigns it to the RawIdTokenNonce field. +func (o *UpdateLoginFlowWithOidcMethod) SetRawIdTokenNonce(v string) { + o.RawIdTokenNonce = &v +} + // GetTraits returns the Traits field value if set, zero value otherwise. func (o *UpdateLoginFlowWithOidcMethod) GetTraits() map[string]interface{} { if o == nil || o.Traits == nil { @@ -240,6 +274,9 @@ func (o UpdateLoginFlowWithOidcMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["provider"] = o.Provider } + if o.RawIdTokenNonce != nil { + toSerialize["raw_id_token_nonce"] = o.RawIdTokenNonce + } if o.Traits != nil { toSerialize["traits"] = o.Traits } diff --git a/internal/httpclient/model_update_registration_flow_with_oidc_method.go b/internal/httpclient/model_update_registration_flow_with_oidc_method.go index 35e2c11ccdc3..3ffbafe2c5a5 100644 --- a/internal/httpclient/model_update_registration_flow_with_oidc_method.go +++ b/internal/httpclient/model_update_registration_flow_with_oidc_method.go @@ -19,12 +19,14 @@ import ( type UpdateRegistrationFlowWithOidcMethod struct { // The CSRF Token CsrfToken *string `json:"csrf_token,omitempty"` - // An optional id token provided by an OIDC provider If submitted, it is verified using the OIDC provider's public key set and the claims are used to populate the OIDC credentials of the identity. If the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use the `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken. Supported providers are Apple + // IDToken is an optional id token provided by an OIDC provider If submitted, it is verified using the OIDC provider's public key set and the claims are used to populate the OIDC credentials of the identity. If the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use the `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken. Supported providers are Apple IdToken *string `json:"id_token,omitempty"` // Method to use This field must be set to `oidc` when using the oidc method. Method string `json:"method"` // The provider to register with Provider string `json:"provider"` + // RawIDTokenNonce is the nonce, used when generating the IDToken. If the provider supports nonce validation, the nonce will be validated against this value and required. + RawIdTokenNonce *string `json:"raw_id_token_nonce,omitempty"` // The identity traits Traits map[string]interface{} `json:"traits,omitempty"` // Transient data to pass along to any webhooks @@ -164,6 +166,38 @@ func (o *UpdateRegistrationFlowWithOidcMethod) SetProvider(v string) { o.Provider = v } +// GetRawIdTokenNonce returns the RawIdTokenNonce field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithOidcMethod) GetRawIdTokenNonce() string { + if o == nil || o.RawIdTokenNonce == nil { + var ret string + return ret + } + return *o.RawIdTokenNonce +} + +// GetRawIdTokenNonceOk returns a tuple with the RawIdTokenNonce field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithOidcMethod) GetRawIdTokenNonceOk() (*string, bool) { + if o == nil || o.RawIdTokenNonce == nil { + return nil, false + } + return o.RawIdTokenNonce, true +} + +// HasRawIdTokenNonce returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithOidcMethod) HasRawIdTokenNonce() bool { + if o != nil && o.RawIdTokenNonce != nil { + return true + } + + return false +} + +// SetRawIdTokenNonce gets a reference to the given string and assigns it to the RawIdTokenNonce field. +func (o *UpdateRegistrationFlowWithOidcMethod) SetRawIdTokenNonce(v string) { + o.RawIdTokenNonce = &v +} + // GetTraits returns the Traits field value if set, zero value otherwise. func (o *UpdateRegistrationFlowWithOidcMethod) GetTraits() map[string]interface{} { if o == nil || o.Traits == nil { @@ -274,6 +308,9 @@ func (o UpdateRegistrationFlowWithOidcMethod) MarshalJSON() ([]byte, error) { if true { toSerialize["provider"] = o.Provider } + if o.RawIdTokenNonce != nil { + toSerialize["raw_id_token_nonce"] = o.RawIdTokenNonce + } if o.Traits != nil { toSerialize["traits"] = o.Traits } diff --git a/selfservice/flow/login/flow.go b/selfservice/flow/login/flow.go index 74d854b49f5c..6e7426bd578c 100644 --- a/selfservice/flow/login/flow.go +++ b/selfservice/flow/login/flow.go @@ -136,6 +136,9 @@ type Flow struct { // Only used internally IDToken string `json:"-" db:"-"` + + // Only used internally + RawIDTokenNonce string `json:"-" db:"-"` } var _ flow.Flow = new(Flow) diff --git a/selfservice/flow/registration/flow.go b/selfservice/flow/registration/flow.go index f033c0dcb130..085cf353c8be 100644 --- a/selfservice/flow/registration/flow.go +++ b/selfservice/flow/registration/flow.go @@ -116,9 +116,6 @@ type Flow struct { // and only on creating the flow. SessionTokenExchangeCode string `json:"session_token_exchange_code,omitempty" faker:"-" db:"-"` - // only used internally - IDToken string `json:"-" faker:"-" db:"-"` - // State represents the state of this request: // // - choose_method: ask the user to choose a method (e.g. registration with email) @@ -126,6 +123,11 @@ type Flow struct { // - passed_challenge: the request was successful and the registration challenge was passed. // required: true State State `json:"state" faker:"-" db:"state"` + + // only used internally + IDToken string `json:"-" faker:"-" db:"-"` + // Only used internally + RawIDTokenNonce string `json:"-" db:"-"` } var _ flow.Flow = new(Flow) diff --git a/selfservice/strategy/oidc/.schema/link.schema.json b/selfservice/strategy/oidc/.schema/link.schema.json index 54914543aa21..46c442eb6471 100644 --- a/selfservice/strategy/oidc/.schema/link.schema.json +++ b/selfservice/strategy/oidc/.schema/link.schema.json @@ -39,6 +39,10 @@ "id_token": { "type": "string", "description": "An optional id token provided by an OIDC provider" + }, + "raw_id_token_nonce": { + "type": "string", + "description": "The nonce used when requesting the id_token from the provider. Required, if an id_token is given and the provider supports it." } } } diff --git a/selfservice/strategy/oidc/provider.go b/selfservice/strategy/oidc/provider.go index 6bd6a9d02cb1..ac4a24ed2745 100644 --- a/selfservice/strategy/oidc/provider.go +++ b/selfservice/strategy/oidc/provider.go @@ -29,6 +29,7 @@ type TokenExchanger interface { type IDTokenVerifier interface { Verify(ctx context.Context, rawIDToken string) (*Claims, error) + NonceSupported(*Claims) bool } // ConvertibleBoolean is used as Apple casually sends the email_verified field as a string. @@ -56,6 +57,8 @@ type Claims struct { UpdatedAt int64 `json:"updated_at,omitempty"` HD string `json:"hd,omitempty"` Team string `json:"team,omitempty"` + Nonce string `json:"nonce,omitempty"` + NonceSupported bool `json:"nonce_supported,omitempty"` RawClaims map[string]interface{} `json:"raw_claims,omitempty"` } diff --git a/selfservice/strategy/oidc/provider_apple.go b/selfservice/strategy/oidc/provider_apple.go index 2822332c35e2..a3f96e8b5e1c 100644 --- a/selfservice/strategy/oidc/provider_apple.go +++ b/selfservice/strategy/oidc/provider_apple.go @@ -14,6 +14,7 @@ import ( "github.com/coreos/go-oidc" "github.com/golang-jwt/jwt/v4" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "github.com/pkg/errors" @@ -22,6 +23,7 @@ import ( type ProviderApple struct { *ProviderGenericOIDC + jwksUrl string } func NewProviderApple( @@ -34,6 +36,7 @@ func NewProviderApple( config: config, reg: reg, }, + jwksUrl: "https://appleid.apple.com/auth/keys", } } @@ -151,11 +154,11 @@ func decodeQuery(query url.Values, claims *Claims) { var _ IDTokenVerifier = new(ProviderApple) func (a *ProviderApple) Verify(ctx context.Context, rawIDToken string) (*Claims, error) { - keySet := oidc.NewRemoteKeySet(ctx, "https://appleid.apple.com/auth/keys") + keySet := oidc.NewRemoteKeySet(ctx, a.jwksUrl) verifier := oidc.NewVerifier("https://appleid.apple.com", keySet, &oidc.Config{ ClientID: a.config.ClientID, }) - token, err := verifier.Verify(ctx, rawIDToken) + token, err := verifier.Verify(oidc.ClientContext(ctx, otelhttp.DefaultClient), rawIDToken) if err != nil { return nil, err } @@ -165,3 +168,7 @@ func (a *ProviderApple) Verify(ctx context.Context, rawIDToken string) (*Claims, } return claims, nil } + +func (a *ProviderApple) NonceSupported(c *Claims) bool { + return c.NonceSupported +} diff --git a/selfservice/strategy/oidc/provider_apple_test.go b/selfservice/strategy/oidc/provider_apple_test.go index 9aa4cd926ff9..8ff08f6d2e4b 100644 --- a/selfservice/strategy/oidc/provider_apple_test.go +++ b/selfservice/strategy/oidc/provider_apple_test.go @@ -4,11 +4,21 @@ package oidc import ( + "context" + "encoding/json" "fmt" + "net/http" + "net/http/httptest" "net/url" "testing" + "time" + _ "embed" + + "github.com/form3tech-oss/jwt-go" + "github.com/rakutentech/jwk-go/jwk" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestDecodeQuery(t *testing.T) { @@ -38,3 +48,55 @@ func TestDecodeQuery(t *testing.T) { } } + +//go:embed stub/jwk.json +var rawKey []byte + +//go:embed stub/jwks_public.json +var publicJWKS []byte + +type claims struct { + *jwt.StandardClaims + Email string `json:"email"` +} + +func createIdToken(t *testing.T) string { + key := &jwk.KeySpec{} + require.NoError(t, json.Unmarshal(rawKey, key)) + token := jwt.NewWithClaims(jwt.SigningMethodRS256, &claims{ + StandardClaims: &jwt.StandardClaims{ + Issuer: "https://appleid.apple.com", + Subject: "apple@ory.sh", + Audience: []string{"com.example.app"}, + ExpiresAt: time.Now().Add(24 * time.Hour).Unix(), + }, + Email: "apple@ory.sh", + }) + token.Header["kid"] = key.KeyID + s, err := token.SignedString(key.Key) + require.NoError(t, err) + return s +} + +func TestVerify(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + w.Write(publicJWKS) + })) + apple := ProviderApple{ + jwksUrl: ts.URL, + ProviderGenericOIDC: &ProviderGenericOIDC{ + config: &Configuration{ + ClientID: "com.example.app", + }, + }, + } + token := createIdToken(t) + + c, err := apple.Verify(context.Background(), token) + require.NoError(t, err) + assert.Equal(t, "apple@ory.sh", c.Email) + assert.Equal(t, "apple@ory.sh", c.Subject) + assert.Equal(t, "https://appleid.apple.com", c.Issuer) + +} diff --git a/selfservice/strategy/oidc/provider_test.go b/selfservice/strategy/oidc/provider_test.go index 74fdb05031fc..d5baba0971b5 100644 --- a/selfservice/strategy/oidc/provider_test.go +++ b/selfservice/strategy/oidc/provider_test.go @@ -52,3 +52,7 @@ func (t *TestProvider) Verify(ctx context.Context, token string) (*Claims, error } return &c, nil } + +func (t *TestProvider) NonceSupported(c *Claims) bool { + return true +} diff --git a/selfservice/strategy/oidc/strategy.go b/selfservice/strategy/oidc/strategy.go index 970ffdd993cf..0a0fed108d77 100644 --- a/selfservice/strategy/oidc/strategy.go +++ b/selfservice/strategy/oidc/strategy.go @@ -6,8 +6,10 @@ package oidc import ( "bytes" "context" + "crypto/sha256" "crypto/sha512" "encoding/base64" + "encoding/hex" "encoding/json" "fmt" "net/http" @@ -590,24 +592,36 @@ func (s *Strategy) CompletedAuthenticationMethod(ctx context.Context) session.Au } } -var ( - ErrIDTokenVerificationFailed = herodot.ErrInternalServerError.WithReasonf("Could not verify ID token") - ErrUnsupportedProvider = herodot.ErrInternalServerError.WithReasonf("Provider does not support ID Token verification") - ErrClaimValidationFailed = herodot.ErrInternalServerError.WithReasonf("Could not verify token claims") -) - -func (s *Strategy) processIDToken(w http.ResponseWriter, r *http.Request, provider Provider, idToken string) (*Claims, error) { +func (s *Strategy) processIDToken(w http.ResponseWriter, r *http.Request, provider Provider, idToken, idTokenNonce string) (*Claims, error) { verifier, ok := provider.(IDTokenVerifier) if !ok { - return nil, errors.WithStack(ErrUnsupportedProvider) + return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("The provider %s does not support id_token verification", provider.Config().Provider)) } claims, err := verifier.Verify(r.Context(), idToken) if err != nil { - return nil, errors.WithStack(ErrIDTokenVerificationFailed.WithError(err.Error())) + return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Could not verify id_token").WithError(err.Error())) } if err := claims.Validate(); err != nil { - return nil, errors.WithStack(ErrClaimValidationFailed.WithError(err.Error())) + return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("The id_token claims were invalid").WithError(err.Error())) + } + + // Not all providers support nonce, so we only check if the provider supports it. + if verifier.NonceSupported(claims) { + if idTokenNonce == "" { + return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("No nonce was provided but is required by the provider")) + } + + if claims.Nonce == "" { + return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("No nonce was included in the id_token but is required by the provider")) + } + sh := sha256.New() + sh.Write([]byte(idTokenNonce)) + hashedNonce := hex.EncodeToString(sh.Sum(nil)) + + if hashedNonce != claims.Nonce { + return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("The supplied nonce does not match the nonce from the id_token")) + } } return claims, nil diff --git a/selfservice/strategy/oidc/strategy_login.go b/selfservice/strategy/oidc/strategy_login.go index 4106b311bfcc..842e83954da6 100644 --- a/selfservice/strategy/oidc/strategy_login.go +++ b/selfservice/strategy/oidc/strategy_login.go @@ -82,7 +82,7 @@ type UpdateLoginFlowWithOidcMethod struct { // required: false UpstreamParameters json.RawMessage `json:"upstream_parameters"` - // An optional id token provided by an OIDC provider + // IDToken is an optional id token provided by an OIDC provider // // If submitted, it is verified using the OIDC provider's public key set and the claims are used to populate // the OIDC credentials of the identity. @@ -93,6 +93,12 @@ type UpdateLoginFlowWithOidcMethod struct { // - Apple // required: false IDToken string `json:"id_token,omitempty"` + + // RawIDTokenNonce is the nonce, used when generating the IDToken. + // If the provider supports nonce validation, the nonce will be validated against this value and required. + // + // required: false + RawIDTokenNonce string `json:"raw_id_token_nonce,omitempty"` } func (s *Strategy) processLogin(w http.ResponseWriter, r *http.Request, loginFlow *login.Flow, token *oauth2.Token, claims *Claims, provider Provider, container *authCodeContainer) (*registration.Flow, error) { @@ -136,6 +142,7 @@ func (s *Strategy) processLogin(w http.ResponseWriter, r *http.Request, loginFlo } registrationFlow.IDToken = loginFlow.IDToken + registrationFlow.RawIDTokenNonce = loginFlow.RawIDTokenNonce registrationFlow.RequestURL, err = x.TakeOverReturnToParameter(loginFlow.RequestURL, registrationFlow.RequestURL) if err != nil { return nil, s.handleError(w, r, loginFlow, provider.Config().ID, nil, err) @@ -181,6 +188,7 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, } f.IDToken = p.IDToken + f.RawIDTokenNonce = p.RawIDTokenNonce pid := p.Provider // this can come from both url query and post body if pid == "" { @@ -213,7 +221,7 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, } if p.IDToken != "" { - claims, err := s.processIDToken(w, r, provider, p.IDToken) + claims, err := s.processIDToken(w, r, provider, p.IDToken, p.RawIDTokenNonce) if err != nil { return nil, s.handleError(w, r, f, pid, nil, err) } diff --git a/selfservice/strategy/oidc/strategy_registration.go b/selfservice/strategy/oidc/strategy_registration.go index 3b9196411bb8..6e61cfc257a8 100644 --- a/selfservice/strategy/oidc/strategy_registration.go +++ b/selfservice/strategy/oidc/strategy_registration.go @@ -99,7 +99,7 @@ type UpdateRegistrationFlowWithOidcMethod struct { // required: false UpstreamParameters json.RawMessage `json:"upstream_parameters"` - // An optional id token provided by an OIDC provider + // IDToken is an optional id token provided by an OIDC provider // // If submitted, it is verified using the OIDC provider's public key set and the claims are used to populate // the OIDC credentials of the identity. @@ -110,6 +110,12 @@ type UpdateRegistrationFlowWithOidcMethod struct { // - Apple // required: false IDToken string `json:"id_token,omitempty"` + + // RawIDTokenNonce is the nonce, used when generating the IDToken. + // If the provider supports nonce validation, the nonce will be validated against this value and required. + // + // required: false + RawIDTokenNonce string `json:"raw_id_token_nonce,omitempty"` } func (s *Strategy) newLinkDecoder(p interface{}, r *http.Request) error { @@ -149,6 +155,7 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat f.TransientPayload = p.TransientPayload f.IDToken = p.IDToken + f.RawIDTokenNonce = p.RawIDTokenNonce pid := p.Provider // this can come from both url query and post body if pid == "" { @@ -181,7 +188,7 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat } if p.IDToken != "" { - claims, err := s.processIDToken(w, r, provider, p.IDToken) + claims, err := s.processIDToken(w, r, provider, p.IDToken, p.RawIDTokenNonce) if err != nil { return s.handleError(w, r, f, pid, nil, err) } diff --git a/selfservice/strategy/oidc/strategy_test.go b/selfservice/strategy/oidc/strategy_test.go index e7f9dfd4f9cc..0ca90f92955d 100644 --- a/selfservice/strategy/oidc/strategy_test.go +++ b/selfservice/strategy/oidc/strategy_test.go @@ -6,6 +6,8 @@ package oidc_test import ( "bytes" "context" + "crypto/sha256" + "encoding/hex" "encoding/json" "fmt" "io" @@ -20,6 +22,7 @@ import ( "github.com/ory/kratos/hydra" "github.com/ory/kratos/selfservice/sessiontokenexchange" "github.com/ory/kratos/session" + "github.com/ory/x/randx" "github.com/ory/x/snapshotx" "github.com/ory/kratos/text" @@ -567,19 +570,22 @@ func TestStrategy(t *testing.T) { name string idToken string provider string + v func(string, string, string) url.Values expect func(t *testing.T, res *http.Response, body []byte) } - var prep = func(tc *testCase) (string, string) { - provider := tc.provider + var prep = func(tc *testCase) (provider string, token string, nonce string) { + provider = tc.provider if provider == "" { provider = "test-provider" } - token := tc.idToken - if strings.Contains(tc.idToken, "%s") { - token = fmt.Sprintf(tc.idToken, testhelpers.RandomEmail()) - } - return provider, token + token = tc.idToken + token = strings.Replace(token, "{{sub}}", testhelpers.RandomEmail(), -1) + nonce = randx.MustString(16, randx.Alpha) + sh := sha256.New() + sh.Write([]byte(nonce)) + token = strings.Replace(token, "{{nonce}}", hex.EncodeToString(sh.Sum(nil)), -1) + return } for _, tc := range []testCase{ @@ -588,14 +594,14 @@ func TestStrategy(t *testing.T) { idToken: "error", provider: "valid", expect: func(t *testing.T, res *http.Response, body []byte) { - require.Equal(t, "Provider does not support ID Token verification", gjson.GetBytes(body, "error.reason").String(), "%s", body) + require.Equal(t, "The provider generic does not support id_token verification", gjson.GetBytes(body, "error.reason").String(), "%s", body) }, }, { name: "should fail because id_token is invalid", idToken: "error", expect: func(t *testing.T, res *http.Response, body []byte) { - require.Equal(t, "Could not verify ID token", gjson.GetBytes(body, "error.reason").String(), "%s", body) + require.Equal(t, "Could not verify id_token", gjson.GetBytes(body, "error.reason").String(), "%s", body) require.Equal(t, "stub error", gjson.GetBytes(body, "error.message").String(), "%s", body) }, }, @@ -603,14 +609,42 @@ func TestStrategy(t *testing.T) { name: "should fail because claims are invalid", idToken: "{}", expect: func(t *testing.T, res *http.Response, body []byte) { - require.Equal(t, "Could not verify token claims", gjson.GetBytes(body, "error.reason").String(), "%s", body) + require.Equal(t, "The id_token claims were invalid", gjson.GetBytes(body, "error.reason").String(), "%s", body) + }, + }, + { + name: "should fail if no nonce is included in the id_token", + idToken: `{ + "iss": "https://appleid.apple.com", + "sub": "{{sub}}" + }`, + expect: func(t *testing.T, res *http.Response, body []byte) { + require.Equal(t, "No nonce was included in the id_token but is required by the provider", gjson.GetBytes(body, "error.reason").String(), "%s", body) + }, + }, + { + name: "should fail if no nonce is supplied in request", + idToken: `{ + "iss": "https://appleid.apple.com", + "sub": "{{sub}}", + "nonce": "{{nonce}}" + }`, + v: func(provider, token, _ string) url.Values { + return url.Values{ + "id_token": {token}, + "provider": {provider}, + } + }, + expect: func(t *testing.T, res *http.Response, body []byte) { + require.Equal(t, "No nonce was provided but is required by the provider", gjson.GetBytes(body, "error.reason").String(), "%s", body) }, }, { name: "should pass if claims are valid", idToken: `{ "iss": "https://appleid.apple.com", - "sub": "%s" + "sub": "{{sub}}", + "nonce": "{{nonce}}" }`, expect: func(t *testing.T, res *http.Response, body []byte) { require.NotEmpty(t, gjson.GetBytes(body, "session_token").String(), "%s", body) @@ -620,24 +654,33 @@ func TestStrategy(t *testing.T) { tc := tc t.Run(fmt.Sprintf("flow=registration/case=%s", tc.name), func(t *testing.T) { f := newAPIRegistrationFlow(t, returnTS.URL, time.Minute) - provider, token := prep(&tc) + provider, token, nonce := prep(&tc) action := assertFormValues(t, f.ID, "test-provider") - res, err := cl.PostForm(action, url.Values{ - "id_token": {token}, - "provider": {provider}, - }) + v := url.Values{ + "id_token": {token}, + "provider": {provider}, + "raw_id_token_nonce": {nonce}, + } + if tc.v != nil { + v = tc.v(provider, token, nonce) + } + res, err := cl.PostForm(action, v) require.NoError(t, err) body := ioutilx.MustReadAll(res.Body) tc.expect(t, res, body) }) t.Run(fmt.Sprintf("flow=login/case=%s", tc.name), func(t *testing.T) { - provider, token := prep(&tc) + provider, token, nonce := prep(&tc) rf := newAPIRegistrationFlow(t, returnTS.URL, time.Minute) action := assertFormValues(t, rf.ID, "test-provider") v := url.Values{ - "id_token": {token}, - "provider": {provider}, + "id_token": {token}, + "provider": {provider}, + "raw_id_token_nonce": {nonce}, + } + if tc.v != nil { + v = tc.v(provider, token, nonce) } res, err := cl.PostForm(action, v) require.NoError(t, err) @@ -652,13 +695,17 @@ func TestStrategy(t *testing.T) { }) t.Run(fmt.Sprintf("flow=login_without_registration/case=%s", tc.name), func(t *testing.T) { - provider, token := prep(&tc) + provider, token, nonce := prep(&tc) rf := newAPIRegistrationFlow(t, returnTS.URL, time.Minute) action := assertFormValues(t, rf.ID, "test-provider") v := url.Values{ - "id_token": {token}, - "provider": {provider}, + "id_token": {token}, + "provider": {provider}, + "raw_id_token_nonce": {nonce}, + } + if tc.v != nil { + v = tc.v(provider, token, nonce) } res, err := cl.PostForm(action, v) require.NoError(t, err) @@ -673,13 +720,17 @@ func TestStrategy(t *testing.T) { }) t.Run(fmt.Sprintf("flow=login_with_return_session_token_exchange_code/case=%s", tc.name), func(t *testing.T) { - provider, token := prep(&tc) - v := url.Values{ - "id_token": {token}, - "provider": {provider}, - } + provider, token, nonce := prep(&tc) lf := newAPILoginFlow(t, returnTS.URL+"?return_session_token_exchange_code=true&return_to=/app_code", time.Minute) action := assertFormValues(t, lf.ID, "test-provider") + v := url.Values{ + "id_token": {token}, + "provider": {provider}, + "raw_id_token_nonce": {nonce}, + } + if tc.v != nil { + v = tc.v(provider, token, nonce) + } res, err := cl.PostForm(action, v) require.NoError(t, err) body := ioutilx.MustReadAll(res.Body) @@ -687,18 +738,23 @@ func TestStrategy(t *testing.T) { }) t.Run(fmt.Sprintf("flow=registration_with_return_session_token_exchange_code/case=%s", tc.name), func(t *testing.T) { - provider, token := prep(&tc) - v := url.Values{ - "id_token": {token}, - "provider": {provider}, - } + provider, token, nonce := prep(&tc) lf := newAPIRegistrationFlow(t, returnTS.URL+"?return_session_token_exchange_code=true&return_to=/app_code", time.Minute) action := assertFormValues(t, lf.ID, "test-provider") + v := url.Values{ + "id_token": {token}, + "provider": {provider}, + "raw_id_token_nonce": {nonce}, + } + if tc.v != nil { + v = tc.v(provider, token, nonce) + } res, err := cl.PostForm(action, v) require.NoError(t, err) body := ioutilx.MustReadAll(res.Body) tc.expect(t, res, body) }) + } }) diff --git a/selfservice/strategy/oidc/stub/jwk.json b/selfservice/strategy/oidc/stub/jwk.json new file mode 100644 index 000000000000..7922b733f7aa --- /dev/null +++ b/selfservice/strategy/oidc/stub/jwk.json @@ -0,0 +1,14 @@ +{ + "p": "8Bh18gHHaVBWHgHX2s9eAsTHSgFq1kSvAuhvWpipsfbrDMBYZ2nwPCB1g3hb-L6cIMzxTmsv8bxwC0l316I8f77NgjSB4mlZAHjXZp861Z9xAFQ_Kx9ZcRldGmbUQ0NOQaHxMCSh5C1hmr8X54BzTEuMTlOnjVQrKUAPoaxhRpU", + "kty": "RSA", + "q": "4id2MrhpalEruyQUIDeLts0rqqanYhny1PU_K7CibTmAFm88U2npceffxg0o6RLYccybx51VYvzPqp01uGL5-TWAJLHihpXbVt8pgidok21mN54IXScG_EOblrp6sPjzhn39dpiCzAgIZhgOQR-IHepWSYwEuKbD8mVnKkYD44c", + "d": "LsaTOZr2KEUMppL92JtlDatcVSXSDYBTqhG4Sr2Pn-Pbq-VVpHTIRtvHeAz3Kiyei8MYG7jhOLzoA2zQGq7DqNP3BGOAPjPgPqW7nQ33TFIAlS8Q-8iJnmZC3NNG4G0go5YJoNgu7aQeBmOqL_jngCwAxvF2KvmOEOpfZzG9Nt0XMBMnAWa5NskKrtnB7b8oxxjghPyLsFa_N9JfjYPUDh_zZWfgv866k2UEqLaWOUMDRhfFpXeEu_YL4i8xwGmX-mDAxESXEjRZQrK29_SldIhTAopzGoacmic9CJwntGENOt5hihlDzCpMUEKafiFzODCQVYZSl0GaYupopZoH2Q", + "e": "AQAB", + "use": "sig", + "kid": "RTSkc-Jk-iD1lx2TK9WtYDtBtWs0MSyCG0nQSR1mglU", + "qi": "ini3TIy3h4Lm85imQRXVjd5VyzA4_b7CkRhIAbj2Q-Pv2xoHJ6xhDgE6jRa1rher4DNrcAuBuOel5f_2U6EcVe6IoQU5-IvqS7hfo0JCWTjCjBKNTqpNFjUC2kE8kaWKaP_tfDESmW-ZO0iKAbuRCneSyNHgj2RQIcXFbq1bIRQ", + "dp": "nk7ClgtuPJZn8ektNm08g37UGIvOsfEfpD82DPpUCa_RU9sPb0B-0mZklYcqvVyQ_V-kTByIxE-HYSnUBy5FzcU1JAETEwJ7WMBU5qle1bQHgjwKWpiVFOmwZdQfaSpb0xLAQQomZJk3nh0Z2d7sJwY5QPwPojQ5MT24ENXkXfE", + "alg": "RS256", + "dq": "bbifi_QUkNRY1y6l5QuN6V6ZdO3t_5Z_Tfq-bz__Tea70iadqgqUjALnensgAhR2lp-iZLJcnu3xAuHLEm5SwSnHxgXX1VwXUopq5Q6hmgVVtl4hyLAKn5Fdhz9qDzp5TCMMOeG8c6jiCkZZhBb8PydWPdCE6eFe59dyufvGHzk", + "n": "1BqatHWJWPTN4mnkrhns2pSk4LRe7W0cyjs8vp7INp3PtgzQ0-KuUibnE0v0k-6DQcu-3hkP88fUOGlYm2z0x9y2urMoAepBwiWybs1v0xqaQx_eLxtkCiElF3i9zYTqmR6cdAOb_duKpTeuTZm326UdpmDoU3TwcGBcszNkNfcNjeq-3z2bvS3GXvJIR9A-6j17xAXVVifUwjPIcNs21ajJVhNFcM9cM90OG9mivbE645KkqMT-WQxFPEQ-DqjijmXnLed9QabcfXcZdkT96O7x_zxHTgF46FlVyJtswuLRUO-h-48ai3-6NmfnR0tOCoVYhHJxndcQ0L_-uqFXkw" +} diff --git a/selfservice/strategy/oidc/stub/jwks_public.json b/selfservice/strategy/oidc/stub/jwks_public.json new file mode 100644 index 000000000000..48ada384219d --- /dev/null +++ b/selfservice/strategy/oidc/stub/jwks_public.json @@ -0,0 +1,12 @@ +{ + "keys": [ + { + "kty": "RSA", + "e": "AQAB", + "use": "sig", + "kid": "RTSkc-Jk-iD1lx2TK9WtYDtBtWs0MSyCG0nQSR1mglU", + "alg": "RS256", + "n": "1BqatHWJWPTN4mnkrhns2pSk4LRe7W0cyjs8vp7INp3PtgzQ0-KuUibnE0v0k-6DQcu-3hkP88fUOGlYm2z0x9y2urMoAepBwiWybs1v0xqaQx_eLxtkCiElF3i9zYTqmR6cdAOb_duKpTeuTZm326UdpmDoU3TwcGBcszNkNfcNjeq-3z2bvS3GXvJIR9A-6j17xAXVVifUwjPIcNs21ajJVhNFcM9cM90OG9mivbE645KkqMT-WQxFPEQ-DqjijmXnLed9QabcfXcZdkT96O7x_zxHTgF46FlVyJtswuLRUO-h-48ai3-6NmfnR0tOCoVYhHJxndcQ0L_-uqFXkw" + } + ] +} diff --git a/spec/api.json b/spec/api.json index 819cd26ae2c4..b490a7b1e4e0 100644 --- a/spec/api.json +++ b/spec/api.json @@ -2489,7 +2489,7 @@ "type": "string" }, "id_token": { - "description": "An optional id token provided by an OIDC provider\n\nIf submitted, it is verified using the OIDC provider's public key set and the claims are used to populate\nthe OIDC credentials of the identity.\nIf the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use\nthe `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken.\n\nSupported providers are\nApple", + "description": "IDToken is an optional id token provided by an OIDC provider\n\nIf submitted, it is verified using the OIDC provider's public key set and the claims are used to populate\nthe OIDC credentials of the identity.\nIf the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use\nthe `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken.\n\nSupported providers are\nApple", "type": "string" }, "method": { @@ -2500,6 +2500,10 @@ "description": "The provider to register with", "type": "string" }, + "raw_id_token_nonce": { + "description": "RawIDTokenNonce is the nonce, used when generating the IDToken.\nIf the provider supports nonce validation, the nonce will be validated against this value and required.", + "type": "string" + }, "traits": { "description": "The identity traits. This is a placeholder for the registration flow.", "type": "object" @@ -2737,7 +2741,7 @@ "type": "string" }, "id_token": { - "description": "An optional id token provided by an OIDC provider\n\nIf submitted, it is verified using the OIDC provider's public key set and the claims are used to populate\nthe OIDC credentials of the identity.\nIf the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use\nthe `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken.\n\nSupported providers are\nApple", + "description": "IDToken is an optional id token provided by an OIDC provider\n\nIf submitted, it is verified using the OIDC provider's public key set and the claims are used to populate\nthe OIDC credentials of the identity.\nIf the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use\nthe `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken.\n\nSupported providers are\nApple", "type": "string" }, "method": { @@ -2748,6 +2752,10 @@ "description": "The provider to register with", "type": "string" }, + "raw_id_token_nonce": { + "description": "RawIDTokenNonce is the nonce, used when generating the IDToken.\nIf the provider supports nonce validation, the nonce will be validated against this value and required.", + "type": "string" + }, "traits": { "description": "The identity traits", "type": "object" diff --git a/spec/swagger.json b/spec/swagger.json index 772d8b30fec3..af07b0c5e701 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -5344,7 +5344,7 @@ "type": "string" }, "id_token": { - "description": "An optional id token provided by an OIDC provider\n\nIf submitted, it is verified using the OIDC provider's public key set and the claims are used to populate\nthe OIDC credentials of the identity.\nIf the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use\nthe `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken.\n\nSupported providers are\nApple", + "description": "IDToken is an optional id token provided by an OIDC provider\n\nIf submitted, it is verified using the OIDC provider's public key set and the claims are used to populate\nthe OIDC credentials of the identity.\nIf the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use\nthe `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken.\n\nSupported providers are\nApple", "type": "string" }, "method": { @@ -5355,6 +5355,10 @@ "description": "The provider to register with", "type": "string" }, + "raw_id_token_nonce": { + "description": "RawIDTokenNonce is the nonce, used when generating the IDToken.\nIf the provider supports nonce validation, the nonce will be validated against this value and required.", + "type": "string" + }, "traits": { "description": "The identity traits. This is a placeholder for the registration flow.", "type": "object" @@ -5556,7 +5560,7 @@ "type": "string" }, "id_token": { - "description": "An optional id token provided by an OIDC provider\n\nIf submitted, it is verified using the OIDC provider's public key set and the claims are used to populate\nthe OIDC credentials of the identity.\nIf the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use\nthe `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken.\n\nSupported providers are\nApple", + "description": "IDToken is an optional id token provided by an OIDC provider\n\nIf submitted, it is verified using the OIDC provider's public key set and the claims are used to populate\nthe OIDC credentials of the identity.\nIf the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use\nthe `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken.\n\nSupported providers are\nApple", "type": "string" }, "method": { @@ -5567,6 +5571,10 @@ "description": "The provider to register with", "type": "string" }, + "raw_id_token_nonce": { + "description": "RawIDTokenNonce is the nonce, used when generating the IDToken.\nIf the provider supports nonce validation, the nonce will be validated against this value and required.", + "type": "string" + }, "traits": { "description": "The identity traits", "type": "object" From 3f3b428ee3de5ffd54fcc097b34d7d41173706bd Mon Sep 17 00:00:00 2001 From: Jonas Hungershausen Date: Wed, 6 Sep 2023 19:55:29 +0200 Subject: [PATCH 5/9] chore: add more tests for apple provider --- .../strategy/oidc/provider_apple_test.go | 74 +++++++++++++++---- .../strategy/oidc/stub/jwks_public2.json | 12 +++ 2 files changed, 71 insertions(+), 15 deletions(-) create mode 100644 selfservice/strategy/oidc/stub/jwks_public2.json diff --git a/selfservice/strategy/oidc/provider_apple_test.go b/selfservice/strategy/oidc/provider_apple_test.go index 8ff08f6d2e4b..0c11249301a6 100644 --- a/selfservice/strategy/oidc/provider_apple_test.go +++ b/selfservice/strategy/oidc/provider_apple_test.go @@ -55,19 +55,24 @@ var rawKey []byte //go:embed stub/jwks_public.json var publicJWKS []byte +// Just a public key set, to be able to test what happens if an ID token was issued by a different private key. +// +//go:embed stub/jwks_public2.json +var publicJWKS2 []byte + type claims struct { *jwt.StandardClaims Email string `json:"email"` } -func createIdToken(t *testing.T) string { +func createIdToken(t *testing.T, aud string) string { key := &jwk.KeySpec{} require.NoError(t, json.Unmarshal(rawKey, key)) token := jwt.NewWithClaims(jwt.SigningMethodRS256, &claims{ StandardClaims: &jwt.StandardClaims{ Issuer: "https://appleid.apple.com", Subject: "apple@ory.sh", - Audience: []string{"com.example.app"}, + Audience: []string{aud}, ExpiresAt: time.Now().Add(24 * time.Hour).Unix(), }, Email: "apple@ory.sh", @@ -83,20 +88,59 @@ func TestVerify(t *testing.T) { w.WriteHeader(200) w.Write(publicJWKS) })) - apple := ProviderApple{ - jwksUrl: ts.URL, - ProviderGenericOIDC: &ProviderGenericOIDC{ - config: &Configuration{ - ClientID: "com.example.app", + + tsOtherJWKS := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + w.Write(publicJWKS2) + })) + t.Run("case=successful verification", func(t *testing.T) { + apple := ProviderApple{ + jwksUrl: ts.URL, + ProviderGenericOIDC: &ProviderGenericOIDC{ + config: &Configuration{ + ClientID: "com.example.app", + }, }, - }, - } - token := createIdToken(t) + } + token := createIdToken(t, "com.example.app") - c, err := apple.Verify(context.Background(), token) - require.NoError(t, err) - assert.Equal(t, "apple@ory.sh", c.Email) - assert.Equal(t, "apple@ory.sh", c.Subject) - assert.Equal(t, "https://appleid.apple.com", c.Issuer) + c, err := apple.Verify(context.Background(), token) + require.NoError(t, err) + assert.Equal(t, "apple@ory.sh", c.Email) + assert.Equal(t, "apple@ory.sh", c.Subject) + assert.Equal(t, "https://appleid.apple.com", c.Issuer) + }) + + t.Run("case=fails due to client_id mismatch", func(t *testing.T) { + apple := ProviderApple{ + jwksUrl: ts.URL, + ProviderGenericOIDC: &ProviderGenericOIDC{ + config: &Configuration{ + ClientID: "com.example.app", + }, + }, + } + token := createIdToken(t, "com.different-example.app") + + _, err := apple.Verify(context.Background(), token) + require.Error(t, err) + assert.Equal(t, `oidc: expected audience "com.example.app" got ["com.different-example.app"]`, err.Error()) + }) + + t.Run("case=fails due to jwks mismatch", func(t *testing.T) { + apple := ProviderApple{ + jwksUrl: tsOtherJWKS.URL, + ProviderGenericOIDC: &ProviderGenericOIDC{ + config: &Configuration{ + ClientID: "com.example.app", + }, + }, + } + token := createIdToken(t, "com.example.app") + + _, err := apple.Verify(context.Background(), token) + require.Error(t, err) + assert.Equal(t, "failed to verify signature: failed to verify id token signature", err.Error()) + }) } diff --git a/selfservice/strategy/oidc/stub/jwks_public2.json b/selfservice/strategy/oidc/stub/jwks_public2.json new file mode 100644 index 000000000000..6abd16936229 --- /dev/null +++ b/selfservice/strategy/oidc/stub/jwks_public2.json @@ -0,0 +1,12 @@ +{ + "keys": [ + { + "kty": "RSA", + "e": "AQAB", + "use": "sig", + "kid": "swo5qZbZECb_alwFmwoleMN7nFw6Us-TP6f-sKIPDF0", + "alg": "RS256", + "n": "kOB5UX-fhCEesMn7BBRKCwkV33blQrD4xZhRK3rQDySNGwf9Uoeemm6SsO5E3WBnYQHWyH4X4jlwVNmkqBHyijKs3v2DBhIZTXa0dU2qp6dGJXQObSHKN51RzPX6yE3DiuzhKcl0ORlvjZk2nzPDl3l9y_Fl6opjFsnnCdHUovqdTBi9HcdocF7E5QeFvQG0QHs8zDC1myjs73m1F18IlTF6peXgFhgsiPKrXrgugh8vItpzr4dfA8fK3ND-NBLloNaZbtCAGmW6vxJnRae2PVBzSf8GamMBCuCNOJBRaQMRkZnZvkAIMGc9WngaHFhGBnW3IC__B0e-_fCyAVr4KQ" + } + ] +} From c96c94b13243ee19643c21fa02028ab236fef21a Mon Sep 17 00:00:00 2001 From: Jonas Hungershausen Date: Thu, 7 Sep 2023 17:38:27 +0200 Subject: [PATCH 6/9] chore: fix test --- selfservice/strategy/oidc/strategy_test.go | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/selfservice/strategy/oidc/strategy_test.go b/selfservice/strategy/oidc/strategy_test.go index 0ca90f92955d..16d73ff71ba7 100644 --- a/selfservice/strategy/oidc/strategy_test.go +++ b/selfservice/strategy/oidc/strategy_test.go @@ -200,15 +200,19 @@ func TestStrategy(t *testing.T) { return code } - var exchangeCodeForToken = func(t *testing.T, codes sessiontokenexchange.Codes) (codeResponse session.CodeExchangeResponse) { - t.Helper() + var exchangeCodeForToken = func(t *testing.T, codes sessiontokenexchange.Codes) (codeResponse session.CodeExchangeResponse, err error) { tokenURL := urlx.ParseOrPanic(ts.URL) tokenURL.Path = "/sessions/token-exchange" tokenURL.RawQuery = fmt.Sprintf("init_code=%s&return_to_code=%s", codes.InitCode, codes.ReturnToCode) res, err := ts.Client().Get(tokenURL.String()) - require.NoError(t, err) - require.Equal(t, http.StatusOK, res.StatusCode, "expected status code 200 but got %d: %s", res.StatusCode, ioutilx.MustReadAll(res.Body)) + if err != nil { + return codeResponse, err + } + if res.StatusCode != 200 { + return codeResponse, fmt.Errorf("got status code %d", res.StatusCode) + } require.NoError(t, json.NewDecoder(res.Body).Decode(&codeResponse)) + return } @@ -503,14 +507,16 @@ func TestStrategy(t *testing.T) { scope = []string{"openid"} var loginOrRegister = func(t *testing.T, id uuid.UUID, code string) { - exchangeCodeForToken(t, sessiontokenexchange.Codes{InitCode: code}) + _, err := exchangeCodeForToken(t, sessiontokenexchange.Codes{InitCode: code}) + require.Error(t, err) action := assertFormValues(t, id, "valid") returnToCode := makeAPICodeFlowRequest(t, "valid", action) - codeResponse := exchangeCodeForToken(t, sessiontokenexchange.Codes{ + codeResponse, err := exchangeCodeForToken(t, sessiontokenexchange.Codes{ InitCode: code, ReturnToCode: returnToCode, }) + require.NoError(t, err) assert.NotEmpty(t, codeResponse.Token) assert.Equal(t, subject, gjson.GetBytes(codeResponse.Session.Identity.Traits, "subject").String()) From 4280ff4f33c03b86ffd694b08ac3792e9930bfa3 Mon Sep 17 00:00:00 2001 From: Jonas Hungershausen Date: Thu, 7 Sep 2023 19:07:30 +0200 Subject: [PATCH 7/9] fix: tests (once again) --- selfservice/strategy/oidc/strategy_helper_test.go | 13 +++++++++++-- selfservice/strategy/oidc/strategy_test.go | 4 ++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/selfservice/strategy/oidc/strategy_helper_test.go b/selfservice/strategy/oidc/strategy_helper_test.go index db3904d0221f..c708eb340022 100644 --- a/selfservice/strategy/oidc/strategy_helper_test.go +++ b/selfservice/strategy/oidc/strategy_helper_test.go @@ -324,8 +324,17 @@ func newOIDCProvider( func viperSetProviderConfig(t *testing.T, conf *config.Config, providers ...oidc.Configuration) { ctx := context.Background() - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeOIDC)+".config", &oidc.ConfigurationCollection{Providers: providers}) - conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeOIDC)+".enabled", true) + baseKey := fmt.Sprintf("%s.%s", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypeOIDC) + currentConfig := conf.GetProvider(ctx).Get(baseKey + ".config") + currentEnabled := conf.GetProvider(ctx).Get(baseKey + ".enabled") + + conf.MustSet(ctx, baseKey+".config", &oidc.ConfigurationCollection{Providers: providers}) + conf.MustSet(ctx, baseKey+".enabled", true) + + t.Cleanup(func() { + conf.MustSet(ctx, baseKey+".config", currentConfig) + conf.MustSet(ctx, baseKey+".enabled", currentEnabled) + }) } // AssertSystemError asserts an error ui response diff --git a/selfservice/strategy/oidc/strategy_test.go b/selfservice/strategy/oidc/strategy_test.go index 16d73ff71ba7..e33ee7823d71 100644 --- a/selfservice/strategy/oidc/strategy_test.go +++ b/selfservice/strategy/oidc/strategy_test.go @@ -568,8 +568,8 @@ func TestStrategy(t *testing.T) { Mapper: "file://./stub/oidc.facebook.jsonnet", }, ) - cleanup := oidc.RegisterTestProvider("test-provider") - t.Cleanup(cleanup) + t.Cleanup(oidc.RegisterTestProvider("test-provider")) + cl := http.Client{} type testCase struct { From afd91837424e3ce73aeb14f9e3b6c69713314bca Mon Sep 17 00:00:00 2001 From: Jonas Hungershausen Date: Fri, 8 Sep 2023 11:32:56 +0200 Subject: [PATCH 8/9] chore: cr --- ...odel_update_login_flow_with_oidc_method.go | 74 +++++++++---------- ...date_registration_flow_with_oidc_method.go | 74 +++++++++---------- ...odel_update_login_flow_with_oidc_method.go | 74 +++++++++---------- ...date_registration_flow_with_oidc_method.go | 74 +++++++++---------- .../strategy/oidc/.schema/link.schema.json | 2 +- .../strategy/oidc/provider_apple_test.go | 11 ++- selfservice/strategy/oidc/strategy.go | 33 ++++----- selfservice/strategy/oidc/strategy_login.go | 8 +- .../strategy/oidc/strategy_registration.go | 10 +-- selfservice/strategy/oidc/strategy_test.go | 47 +++++++----- spec/api.json | 16 ++-- spec/swagger.json | 16 ++-- 12 files changed, 222 insertions(+), 217 deletions(-) diff --git a/internal/client-go/model_update_login_flow_with_oidc_method.go b/internal/client-go/model_update_login_flow_with_oidc_method.go index f9a533b699cf..c196eb2121da 100644 --- a/internal/client-go/model_update_login_flow_with_oidc_method.go +++ b/internal/client-go/model_update_login_flow_with_oidc_method.go @@ -21,12 +21,12 @@ type UpdateLoginFlowWithOidcMethod struct { CsrfToken *string `json:"csrf_token,omitempty"` // IDToken is an optional id token provided by an OIDC provider If submitted, it is verified using the OIDC provider's public key set and the claims are used to populate the OIDC credentials of the identity. If the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use the `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken. Supported providers are Apple IdToken *string `json:"id_token,omitempty"` + // IDTokenNonce is the nonce, used when generating the IDToken. If the provider supports nonce validation, the nonce will be validated against this value and required. + IdTokenNonce *string `json:"id_token_nonce,omitempty"` // Method to use This field must be set to `oidc` when using the oidc method. Method string `json:"method"` // The provider to register with Provider string `json:"provider"` - // RawIDTokenNonce is the nonce, used when generating the IDToken. If the provider supports nonce validation, the nonce will be validated against this value and required. - RawIdTokenNonce *string `json:"raw_id_token_nonce,omitempty"` // The identity traits. This is a placeholder for the registration flow. Traits map[string]interface{} `json:"traits,omitempty"` // UpstreamParameters are the parameters that are passed to the upstream identity provider. These parameters are optional and depend on what the upstream identity provider supports. Supported parameters are: `login_hint` (string): The `login_hint` parameter suppresses the account chooser and either pre-fills the email box on the sign-in form, or selects the proper session. `hd` (string): The `hd` parameter limits the login/registration process to a Google Organization, e.g. `mycollege.edu`. `prompt` (string): The `prompt` specifies whether the Authorization Server prompts the End-User for reauthentication and consent, e.g. `select_account`. @@ -116,6 +116,38 @@ func (o *UpdateLoginFlowWithOidcMethod) SetIdToken(v string) { o.IdToken = &v } +// GetIdTokenNonce returns the IdTokenNonce field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithOidcMethod) GetIdTokenNonce() string { + if o == nil || o.IdTokenNonce == nil { + var ret string + return ret + } + return *o.IdTokenNonce +} + +// GetIdTokenNonceOk returns a tuple with the IdTokenNonce field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithOidcMethod) GetIdTokenNonceOk() (*string, bool) { + if o == nil || o.IdTokenNonce == nil { + return nil, false + } + return o.IdTokenNonce, true +} + +// HasIdTokenNonce returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithOidcMethod) HasIdTokenNonce() bool { + if o != nil && o.IdTokenNonce != nil { + return true + } + + return false +} + +// SetIdTokenNonce gets a reference to the given string and assigns it to the IdTokenNonce field. +func (o *UpdateLoginFlowWithOidcMethod) SetIdTokenNonce(v string) { + o.IdTokenNonce = &v +} + // GetMethod returns the Method field value func (o *UpdateLoginFlowWithOidcMethod) GetMethod() string { if o == nil { @@ -164,38 +196,6 @@ func (o *UpdateLoginFlowWithOidcMethod) SetProvider(v string) { o.Provider = v } -// GetRawIdTokenNonce returns the RawIdTokenNonce field value if set, zero value otherwise. -func (o *UpdateLoginFlowWithOidcMethod) GetRawIdTokenNonce() string { - if o == nil || o.RawIdTokenNonce == nil { - var ret string - return ret - } - return *o.RawIdTokenNonce -} - -// GetRawIdTokenNonceOk returns a tuple with the RawIdTokenNonce field value if set, nil otherwise -// and a boolean to check if the value has been set. -func (o *UpdateLoginFlowWithOidcMethod) GetRawIdTokenNonceOk() (*string, bool) { - if o == nil || o.RawIdTokenNonce == nil { - return nil, false - } - return o.RawIdTokenNonce, true -} - -// HasRawIdTokenNonce returns a boolean if a field has been set. -func (o *UpdateLoginFlowWithOidcMethod) HasRawIdTokenNonce() bool { - if o != nil && o.RawIdTokenNonce != nil { - return true - } - - return false -} - -// SetRawIdTokenNonce gets a reference to the given string and assigns it to the RawIdTokenNonce field. -func (o *UpdateLoginFlowWithOidcMethod) SetRawIdTokenNonce(v string) { - o.RawIdTokenNonce = &v -} - // GetTraits returns the Traits field value if set, zero value otherwise. func (o *UpdateLoginFlowWithOidcMethod) GetTraits() map[string]interface{} { if o == nil || o.Traits == nil { @@ -268,15 +268,15 @@ func (o UpdateLoginFlowWithOidcMethod) MarshalJSON() ([]byte, error) { if o.IdToken != nil { toSerialize["id_token"] = o.IdToken } + if o.IdTokenNonce != nil { + toSerialize["id_token_nonce"] = o.IdTokenNonce + } if true { toSerialize["method"] = o.Method } if true { toSerialize["provider"] = o.Provider } - if o.RawIdTokenNonce != nil { - toSerialize["raw_id_token_nonce"] = o.RawIdTokenNonce - } if o.Traits != nil { toSerialize["traits"] = o.Traits } diff --git a/internal/client-go/model_update_registration_flow_with_oidc_method.go b/internal/client-go/model_update_registration_flow_with_oidc_method.go index 3ffbafe2c5a5..509e978a0627 100644 --- a/internal/client-go/model_update_registration_flow_with_oidc_method.go +++ b/internal/client-go/model_update_registration_flow_with_oidc_method.go @@ -21,12 +21,12 @@ type UpdateRegistrationFlowWithOidcMethod struct { CsrfToken *string `json:"csrf_token,omitempty"` // IDToken is an optional id token provided by an OIDC provider If submitted, it is verified using the OIDC provider's public key set and the claims are used to populate the OIDC credentials of the identity. If the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use the `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken. Supported providers are Apple IdToken *string `json:"id_token,omitempty"` + // IDTokenNonce is the nonce, used when generating the IDToken. If the provider supports nonce validation, the nonce will be validated against this value and is required. + IdTokenNonce *string `json:"id_token_nonce,omitempty"` // Method to use This field must be set to `oidc` when using the oidc method. Method string `json:"method"` // The provider to register with Provider string `json:"provider"` - // RawIDTokenNonce is the nonce, used when generating the IDToken. If the provider supports nonce validation, the nonce will be validated against this value and required. - RawIdTokenNonce *string `json:"raw_id_token_nonce,omitempty"` // The identity traits Traits map[string]interface{} `json:"traits,omitempty"` // Transient data to pass along to any webhooks @@ -118,6 +118,38 @@ func (o *UpdateRegistrationFlowWithOidcMethod) SetIdToken(v string) { o.IdToken = &v } +// GetIdTokenNonce returns the IdTokenNonce field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithOidcMethod) GetIdTokenNonce() string { + if o == nil || o.IdTokenNonce == nil { + var ret string + return ret + } + return *o.IdTokenNonce +} + +// GetIdTokenNonceOk returns a tuple with the IdTokenNonce field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithOidcMethod) GetIdTokenNonceOk() (*string, bool) { + if o == nil || o.IdTokenNonce == nil { + return nil, false + } + return o.IdTokenNonce, true +} + +// HasIdTokenNonce returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithOidcMethod) HasIdTokenNonce() bool { + if o != nil && o.IdTokenNonce != nil { + return true + } + + return false +} + +// SetIdTokenNonce gets a reference to the given string and assigns it to the IdTokenNonce field. +func (o *UpdateRegistrationFlowWithOidcMethod) SetIdTokenNonce(v string) { + o.IdTokenNonce = &v +} + // GetMethod returns the Method field value func (o *UpdateRegistrationFlowWithOidcMethod) GetMethod() string { if o == nil { @@ -166,38 +198,6 @@ func (o *UpdateRegistrationFlowWithOidcMethod) SetProvider(v string) { o.Provider = v } -// GetRawIdTokenNonce returns the RawIdTokenNonce field value if set, zero value otherwise. -func (o *UpdateRegistrationFlowWithOidcMethod) GetRawIdTokenNonce() string { - if o == nil || o.RawIdTokenNonce == nil { - var ret string - return ret - } - return *o.RawIdTokenNonce -} - -// GetRawIdTokenNonceOk returns a tuple with the RawIdTokenNonce field value if set, nil otherwise -// and a boolean to check if the value has been set. -func (o *UpdateRegistrationFlowWithOidcMethod) GetRawIdTokenNonceOk() (*string, bool) { - if o == nil || o.RawIdTokenNonce == nil { - return nil, false - } - return o.RawIdTokenNonce, true -} - -// HasRawIdTokenNonce returns a boolean if a field has been set. -func (o *UpdateRegistrationFlowWithOidcMethod) HasRawIdTokenNonce() bool { - if o != nil && o.RawIdTokenNonce != nil { - return true - } - - return false -} - -// SetRawIdTokenNonce gets a reference to the given string and assigns it to the RawIdTokenNonce field. -func (o *UpdateRegistrationFlowWithOidcMethod) SetRawIdTokenNonce(v string) { - o.RawIdTokenNonce = &v -} - // GetTraits returns the Traits field value if set, zero value otherwise. func (o *UpdateRegistrationFlowWithOidcMethod) GetTraits() map[string]interface{} { if o == nil || o.Traits == nil { @@ -302,15 +302,15 @@ func (o UpdateRegistrationFlowWithOidcMethod) MarshalJSON() ([]byte, error) { if o.IdToken != nil { toSerialize["id_token"] = o.IdToken } + if o.IdTokenNonce != nil { + toSerialize["id_token_nonce"] = o.IdTokenNonce + } if true { toSerialize["method"] = o.Method } if true { toSerialize["provider"] = o.Provider } - if o.RawIdTokenNonce != nil { - toSerialize["raw_id_token_nonce"] = o.RawIdTokenNonce - } if o.Traits != nil { toSerialize["traits"] = o.Traits } diff --git a/internal/httpclient/model_update_login_flow_with_oidc_method.go b/internal/httpclient/model_update_login_flow_with_oidc_method.go index f9a533b699cf..c196eb2121da 100644 --- a/internal/httpclient/model_update_login_flow_with_oidc_method.go +++ b/internal/httpclient/model_update_login_flow_with_oidc_method.go @@ -21,12 +21,12 @@ type UpdateLoginFlowWithOidcMethod struct { CsrfToken *string `json:"csrf_token,omitempty"` // IDToken is an optional id token provided by an OIDC provider If submitted, it is verified using the OIDC provider's public key set and the claims are used to populate the OIDC credentials of the identity. If the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use the `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken. Supported providers are Apple IdToken *string `json:"id_token,omitempty"` + // IDTokenNonce is the nonce, used when generating the IDToken. If the provider supports nonce validation, the nonce will be validated against this value and required. + IdTokenNonce *string `json:"id_token_nonce,omitempty"` // Method to use This field must be set to `oidc` when using the oidc method. Method string `json:"method"` // The provider to register with Provider string `json:"provider"` - // RawIDTokenNonce is the nonce, used when generating the IDToken. If the provider supports nonce validation, the nonce will be validated against this value and required. - RawIdTokenNonce *string `json:"raw_id_token_nonce,omitempty"` // The identity traits. This is a placeholder for the registration flow. Traits map[string]interface{} `json:"traits,omitempty"` // UpstreamParameters are the parameters that are passed to the upstream identity provider. These parameters are optional and depend on what the upstream identity provider supports. Supported parameters are: `login_hint` (string): The `login_hint` parameter suppresses the account chooser and either pre-fills the email box on the sign-in form, or selects the proper session. `hd` (string): The `hd` parameter limits the login/registration process to a Google Organization, e.g. `mycollege.edu`. `prompt` (string): The `prompt` specifies whether the Authorization Server prompts the End-User for reauthentication and consent, e.g. `select_account`. @@ -116,6 +116,38 @@ func (o *UpdateLoginFlowWithOidcMethod) SetIdToken(v string) { o.IdToken = &v } +// GetIdTokenNonce returns the IdTokenNonce field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithOidcMethod) GetIdTokenNonce() string { + if o == nil || o.IdTokenNonce == nil { + var ret string + return ret + } + return *o.IdTokenNonce +} + +// GetIdTokenNonceOk returns a tuple with the IdTokenNonce field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithOidcMethod) GetIdTokenNonceOk() (*string, bool) { + if o == nil || o.IdTokenNonce == nil { + return nil, false + } + return o.IdTokenNonce, true +} + +// HasIdTokenNonce returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithOidcMethod) HasIdTokenNonce() bool { + if o != nil && o.IdTokenNonce != nil { + return true + } + + return false +} + +// SetIdTokenNonce gets a reference to the given string and assigns it to the IdTokenNonce field. +func (o *UpdateLoginFlowWithOidcMethod) SetIdTokenNonce(v string) { + o.IdTokenNonce = &v +} + // GetMethod returns the Method field value func (o *UpdateLoginFlowWithOidcMethod) GetMethod() string { if o == nil { @@ -164,38 +196,6 @@ func (o *UpdateLoginFlowWithOidcMethod) SetProvider(v string) { o.Provider = v } -// GetRawIdTokenNonce returns the RawIdTokenNonce field value if set, zero value otherwise. -func (o *UpdateLoginFlowWithOidcMethod) GetRawIdTokenNonce() string { - if o == nil || o.RawIdTokenNonce == nil { - var ret string - return ret - } - return *o.RawIdTokenNonce -} - -// GetRawIdTokenNonceOk returns a tuple with the RawIdTokenNonce field value if set, nil otherwise -// and a boolean to check if the value has been set. -func (o *UpdateLoginFlowWithOidcMethod) GetRawIdTokenNonceOk() (*string, bool) { - if o == nil || o.RawIdTokenNonce == nil { - return nil, false - } - return o.RawIdTokenNonce, true -} - -// HasRawIdTokenNonce returns a boolean if a field has been set. -func (o *UpdateLoginFlowWithOidcMethod) HasRawIdTokenNonce() bool { - if o != nil && o.RawIdTokenNonce != nil { - return true - } - - return false -} - -// SetRawIdTokenNonce gets a reference to the given string and assigns it to the RawIdTokenNonce field. -func (o *UpdateLoginFlowWithOidcMethod) SetRawIdTokenNonce(v string) { - o.RawIdTokenNonce = &v -} - // GetTraits returns the Traits field value if set, zero value otherwise. func (o *UpdateLoginFlowWithOidcMethod) GetTraits() map[string]interface{} { if o == nil || o.Traits == nil { @@ -268,15 +268,15 @@ func (o UpdateLoginFlowWithOidcMethod) MarshalJSON() ([]byte, error) { if o.IdToken != nil { toSerialize["id_token"] = o.IdToken } + if o.IdTokenNonce != nil { + toSerialize["id_token_nonce"] = o.IdTokenNonce + } if true { toSerialize["method"] = o.Method } if true { toSerialize["provider"] = o.Provider } - if o.RawIdTokenNonce != nil { - toSerialize["raw_id_token_nonce"] = o.RawIdTokenNonce - } if o.Traits != nil { toSerialize["traits"] = o.Traits } diff --git a/internal/httpclient/model_update_registration_flow_with_oidc_method.go b/internal/httpclient/model_update_registration_flow_with_oidc_method.go index 3ffbafe2c5a5..509e978a0627 100644 --- a/internal/httpclient/model_update_registration_flow_with_oidc_method.go +++ b/internal/httpclient/model_update_registration_flow_with_oidc_method.go @@ -21,12 +21,12 @@ type UpdateRegistrationFlowWithOidcMethod struct { CsrfToken *string `json:"csrf_token,omitempty"` // IDToken is an optional id token provided by an OIDC provider If submitted, it is verified using the OIDC provider's public key set and the claims are used to populate the OIDC credentials of the identity. If the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use the `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken. Supported providers are Apple IdToken *string `json:"id_token,omitempty"` + // IDTokenNonce is the nonce, used when generating the IDToken. If the provider supports nonce validation, the nonce will be validated against this value and is required. + IdTokenNonce *string `json:"id_token_nonce,omitempty"` // Method to use This field must be set to `oidc` when using the oidc method. Method string `json:"method"` // The provider to register with Provider string `json:"provider"` - // RawIDTokenNonce is the nonce, used when generating the IDToken. If the provider supports nonce validation, the nonce will be validated against this value and required. - RawIdTokenNonce *string `json:"raw_id_token_nonce,omitempty"` // The identity traits Traits map[string]interface{} `json:"traits,omitempty"` // Transient data to pass along to any webhooks @@ -118,6 +118,38 @@ func (o *UpdateRegistrationFlowWithOidcMethod) SetIdToken(v string) { o.IdToken = &v } +// GetIdTokenNonce returns the IdTokenNonce field value if set, zero value otherwise. +func (o *UpdateRegistrationFlowWithOidcMethod) GetIdTokenNonce() string { + if o == nil || o.IdTokenNonce == nil { + var ret string + return ret + } + return *o.IdTokenNonce +} + +// GetIdTokenNonceOk returns a tuple with the IdTokenNonce field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateRegistrationFlowWithOidcMethod) GetIdTokenNonceOk() (*string, bool) { + if o == nil || o.IdTokenNonce == nil { + return nil, false + } + return o.IdTokenNonce, true +} + +// HasIdTokenNonce returns a boolean if a field has been set. +func (o *UpdateRegistrationFlowWithOidcMethod) HasIdTokenNonce() bool { + if o != nil && o.IdTokenNonce != nil { + return true + } + + return false +} + +// SetIdTokenNonce gets a reference to the given string and assigns it to the IdTokenNonce field. +func (o *UpdateRegistrationFlowWithOidcMethod) SetIdTokenNonce(v string) { + o.IdTokenNonce = &v +} + // GetMethod returns the Method field value func (o *UpdateRegistrationFlowWithOidcMethod) GetMethod() string { if o == nil { @@ -166,38 +198,6 @@ func (o *UpdateRegistrationFlowWithOidcMethod) SetProvider(v string) { o.Provider = v } -// GetRawIdTokenNonce returns the RawIdTokenNonce field value if set, zero value otherwise. -func (o *UpdateRegistrationFlowWithOidcMethod) GetRawIdTokenNonce() string { - if o == nil || o.RawIdTokenNonce == nil { - var ret string - return ret - } - return *o.RawIdTokenNonce -} - -// GetRawIdTokenNonceOk returns a tuple with the RawIdTokenNonce field value if set, nil otherwise -// and a boolean to check if the value has been set. -func (o *UpdateRegistrationFlowWithOidcMethod) GetRawIdTokenNonceOk() (*string, bool) { - if o == nil || o.RawIdTokenNonce == nil { - return nil, false - } - return o.RawIdTokenNonce, true -} - -// HasRawIdTokenNonce returns a boolean if a field has been set. -func (o *UpdateRegistrationFlowWithOidcMethod) HasRawIdTokenNonce() bool { - if o != nil && o.RawIdTokenNonce != nil { - return true - } - - return false -} - -// SetRawIdTokenNonce gets a reference to the given string and assigns it to the RawIdTokenNonce field. -func (o *UpdateRegistrationFlowWithOidcMethod) SetRawIdTokenNonce(v string) { - o.RawIdTokenNonce = &v -} - // GetTraits returns the Traits field value if set, zero value otherwise. func (o *UpdateRegistrationFlowWithOidcMethod) GetTraits() map[string]interface{} { if o == nil || o.Traits == nil { @@ -302,15 +302,15 @@ func (o UpdateRegistrationFlowWithOidcMethod) MarshalJSON() ([]byte, error) { if o.IdToken != nil { toSerialize["id_token"] = o.IdToken } + if o.IdTokenNonce != nil { + toSerialize["id_token_nonce"] = o.IdTokenNonce + } if true { toSerialize["method"] = o.Method } if true { toSerialize["provider"] = o.Provider } - if o.RawIdTokenNonce != nil { - toSerialize["raw_id_token_nonce"] = o.RawIdTokenNonce - } if o.Traits != nil { toSerialize["traits"] = o.Traits } diff --git a/selfservice/strategy/oidc/.schema/link.schema.json b/selfservice/strategy/oidc/.schema/link.schema.json index 46c442eb6471..a52a243c6c6a 100644 --- a/selfservice/strategy/oidc/.schema/link.schema.json +++ b/selfservice/strategy/oidc/.schema/link.schema.json @@ -40,7 +40,7 @@ "type": "string", "description": "An optional id token provided by an OIDC provider" }, - "raw_id_token_nonce": { + "id_token_nonce": { "type": "string", "description": "The nonce used when requesting the id_token from the provider. Required, if an id_token is given and the provider supports it." } diff --git a/selfservice/strategy/oidc/provider_apple_test.go b/selfservice/strategy/oidc/provider_apple_test.go index 0c11249301a6..7690fec5d78a 100644 --- a/selfservice/strategy/oidc/provider_apple_test.go +++ b/selfservice/strategy/oidc/provider_apple_test.go @@ -15,7 +15,7 @@ import ( _ "embed" - "github.com/form3tech-oss/jwt-go" + "github.com/golang-jwt/jwt/v4" "github.com/rakutentech/jwk-go/jwk" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -61,7 +61,7 @@ var publicJWKS []byte var publicJWKS2 []byte type claims struct { - *jwt.StandardClaims + *jwt.RegisteredClaims Email string `json:"email"` } @@ -69,11 +69,11 @@ func createIdToken(t *testing.T, aud string) string { key := &jwk.KeySpec{} require.NoError(t, json.Unmarshal(rawKey, key)) token := jwt.NewWithClaims(jwt.SigningMethodRS256, &claims{ - StandardClaims: &jwt.StandardClaims{ + RegisteredClaims: &jwt.RegisteredClaims{ Issuer: "https://appleid.apple.com", Subject: "apple@ory.sh", - Audience: []string{aud}, - ExpiresAt: time.Now().Add(24 * time.Hour).Unix(), + Audience: jwt.ClaimStrings{aud}, + ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), }, Email: "apple@ory.sh", }) @@ -142,5 +142,4 @@ func TestVerify(t *testing.T) { require.Error(t, err) assert.Equal(t, "failed to verify signature: failed to verify id token signature", err.Error()) }) - } diff --git a/selfservice/strategy/oidc/strategy.go b/selfservice/strategy/oidc/strategy.go index 0a0fed108d77..a8625fa57f75 100644 --- a/selfservice/strategy/oidc/strategy.go +++ b/selfservice/strategy/oidc/strategy.go @@ -6,10 +6,8 @@ package oidc import ( "bytes" "context" - "crypto/sha256" "crypto/sha512" "encoding/base64" - "encoding/hex" "encoding/json" "fmt" "net/http" @@ -606,23 +604,24 @@ func (s *Strategy) processIDToken(w http.ResponseWriter, r *http.Request, provid return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("The id_token claims were invalid").WithError(err.Error())) } - // Not all providers support nonce, so we only check if the provider supports it. - if verifier.NonceSupported(claims) { - if idTokenNonce == "" { - return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("No nonce was provided but is required by the provider")) - } - - if claims.Nonce == "" { + // First check if the JWT contains the nonce claim. + if claims.Nonce == "" { + // If it doesn't, check if the provider supports nonces. + if verifier.NonceSupported(claims) { + // If the provider supports nonces, abort the flow! return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("No nonce was included in the id_token but is required by the provider")) } - sh := sha256.New() - sh.Write([]byte(idTokenNonce)) - hashedNonce := hex.EncodeToString(sh.Sum(nil)) - - if hashedNonce != claims.Nonce { - return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("The supplied nonce does not match the nonce from the id_token")) - } - } + // If the provider does not support nonces, we don't do validation and return the claim. + // This case only applies to Apple, as some of their devices do not support nonces. + // https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/authenticating_users_with_sign_in_with_apple + } else if idTokenNonce == "" { + // A nonce was present in the JWT token, but no nonce was submitted in the flow + return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("No nonce was provided but is required by the provider")) + } else if idTokenNonce != claims.Nonce { + // The nonce from the JWT token does not match the nonce from the flow. + return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("The supplied nonce does not match the nonce from the id_token")) + } + // Nonce checking was successful return claims, nil } diff --git a/selfservice/strategy/oidc/strategy_login.go b/selfservice/strategy/oidc/strategy_login.go index 842e83954da6..14d44702bb75 100644 --- a/selfservice/strategy/oidc/strategy_login.go +++ b/selfservice/strategy/oidc/strategy_login.go @@ -94,11 +94,11 @@ type UpdateLoginFlowWithOidcMethod struct { // required: false IDToken string `json:"id_token,omitempty"` - // RawIDTokenNonce is the nonce, used when generating the IDToken. + // IDTokenNonce is the nonce, used when generating the IDToken. // If the provider supports nonce validation, the nonce will be validated against this value and required. // // required: false - RawIDTokenNonce string `json:"raw_id_token_nonce,omitempty"` + IDTokenNonce string `json:"id_token_nonce,omitempty"` } func (s *Strategy) processLogin(w http.ResponseWriter, r *http.Request, loginFlow *login.Flow, token *oauth2.Token, claims *Claims, provider Provider, container *authCodeContainer) (*registration.Flow, error) { @@ -188,7 +188,7 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, } f.IDToken = p.IDToken - f.RawIDTokenNonce = p.RawIDTokenNonce + f.RawIDTokenNonce = p.IDTokenNonce pid := p.Provider // this can come from both url query and post body if pid == "" { @@ -221,7 +221,7 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, } if p.IDToken != "" { - claims, err := s.processIDToken(w, r, provider, p.IDToken, p.RawIDTokenNonce) + claims, err := s.processIDToken(w, r, provider, p.IDToken, p.IDTokenNonce) if err != nil { return nil, s.handleError(w, r, f, pid, nil, err) } diff --git a/selfservice/strategy/oidc/strategy_registration.go b/selfservice/strategy/oidc/strategy_registration.go index 6e61cfc257a8..15b42ac56c6f 100644 --- a/selfservice/strategy/oidc/strategy_registration.go +++ b/selfservice/strategy/oidc/strategy_registration.go @@ -111,11 +111,11 @@ type UpdateRegistrationFlowWithOidcMethod struct { // required: false IDToken string `json:"id_token,omitempty"` - // RawIDTokenNonce is the nonce, used when generating the IDToken. - // If the provider supports nonce validation, the nonce will be validated against this value and required. + // IDTokenNonce is the nonce, used when generating the IDToken. + // If the provider supports nonce validation, the nonce will be validated against this value and is required. // // required: false - RawIDTokenNonce string `json:"raw_id_token_nonce,omitempty"` + IDTokenNonce string `json:"id_token_nonce,omitempty"` } func (s *Strategy) newLinkDecoder(p interface{}, r *http.Request) error { @@ -155,7 +155,7 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat f.TransientPayload = p.TransientPayload f.IDToken = p.IDToken - f.RawIDTokenNonce = p.RawIDTokenNonce + f.RawIDTokenNonce = p.IDTokenNonce pid := p.Provider // this can come from both url query and post body if pid == "" { @@ -188,7 +188,7 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat } if p.IDToken != "" { - claims, err := s.processIDToken(w, r, provider, p.IDToken, p.RawIDTokenNonce) + claims, err := s.processIDToken(w, r, provider, p.IDToken, p.IDTokenNonce) if err != nil { return s.handleError(w, r, f, pid, nil, err) } diff --git a/selfservice/strategy/oidc/strategy_test.go b/selfservice/strategy/oidc/strategy_test.go index e33ee7823d71..674fa8576ec4 100644 --- a/selfservice/strategy/oidc/strategy_test.go +++ b/selfservice/strategy/oidc/strategy_test.go @@ -6,8 +6,6 @@ package oidc_test import ( "bytes" "context" - "crypto/sha256" - "encoding/hex" "encoding/json" "fmt" "io" @@ -588,9 +586,7 @@ func TestStrategy(t *testing.T) { token = tc.idToken token = strings.Replace(token, "{{sub}}", testhelpers.RandomEmail(), -1) nonce = randx.MustString(16, randx.Alpha) - sh := sha256.New() - sh.Write([]byte(nonce)) - token = strings.Replace(token, "{{nonce}}", hex.EncodeToString(sh.Sum(nil)), -1) + token = strings.Replace(token, "{{nonce}}", nonce, -1) return } @@ -656,6 +652,17 @@ func TestStrategy(t *testing.T) { require.NotEmpty(t, gjson.GetBytes(body, "session_token").String(), "%s", body) }, }, + { + name: "nonce mismatch", + idToken: `{ + "iss": "https://appleid.apple.com", + "sub": "{{sub}}", + "nonce": "random-nonce" + }`, + expect: func(t *testing.T, res *http.Response, body []byte) { + require.Equal(t, "The supplied nonce does not match the nonce from the id_token", gjson.GetBytes(body, "error.reason").String(), "%s", body) + }, + }, } { tc := tc t.Run(fmt.Sprintf("flow=registration/case=%s", tc.name), func(t *testing.T) { @@ -663,9 +670,9 @@ func TestStrategy(t *testing.T) { provider, token, nonce := prep(&tc) action := assertFormValues(t, f.ID, "test-provider") v := url.Values{ - "id_token": {token}, - "provider": {provider}, - "raw_id_token_nonce": {nonce}, + "id_token": {token}, + "provider": {provider}, + "id_token_nonce": {nonce}, } if tc.v != nil { v = tc.v(provider, token, nonce) @@ -681,9 +688,9 @@ func TestStrategy(t *testing.T) { rf := newAPIRegistrationFlow(t, returnTS.URL, time.Minute) action := assertFormValues(t, rf.ID, "test-provider") v := url.Values{ - "id_token": {token}, - "provider": {provider}, - "raw_id_token_nonce": {nonce}, + "id_token": {token}, + "provider": {provider}, + "id_token_nonce": {nonce}, } if tc.v != nil { v = tc.v(provider, token, nonce) @@ -706,9 +713,9 @@ func TestStrategy(t *testing.T) { action := assertFormValues(t, rf.ID, "test-provider") v := url.Values{ - "id_token": {token}, - "provider": {provider}, - "raw_id_token_nonce": {nonce}, + "id_token": {token}, + "provider": {provider}, + "id_token_nonce": {nonce}, } if tc.v != nil { v = tc.v(provider, token, nonce) @@ -730,9 +737,9 @@ func TestStrategy(t *testing.T) { lf := newAPILoginFlow(t, returnTS.URL+"?return_session_token_exchange_code=true&return_to=/app_code", time.Minute) action := assertFormValues(t, lf.ID, "test-provider") v := url.Values{ - "id_token": {token}, - "provider": {provider}, - "raw_id_token_nonce": {nonce}, + "id_token": {token}, + "provider": {provider}, + "id_token_nonce": {nonce}, } if tc.v != nil { v = tc.v(provider, token, nonce) @@ -748,9 +755,9 @@ func TestStrategy(t *testing.T) { lf := newAPIRegistrationFlow(t, returnTS.URL+"?return_session_token_exchange_code=true&return_to=/app_code", time.Minute) action := assertFormValues(t, lf.ID, "test-provider") v := url.Values{ - "id_token": {token}, - "provider": {provider}, - "raw_id_token_nonce": {nonce}, + "id_token": {token}, + "provider": {provider}, + "id_token_nonce": {nonce}, } if tc.v != nil { v = tc.v(provider, token, nonce) diff --git a/spec/api.json b/spec/api.json index b490a7b1e4e0..b45e62a7fd0c 100644 --- a/spec/api.json +++ b/spec/api.json @@ -2492,6 +2492,10 @@ "description": "IDToken is an optional id token provided by an OIDC provider\n\nIf submitted, it is verified using the OIDC provider's public key set and the claims are used to populate\nthe OIDC credentials of the identity.\nIf the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use\nthe `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken.\n\nSupported providers are\nApple", "type": "string" }, + "id_token_nonce": { + "description": "IDTokenNonce is the nonce, used when generating the IDToken.\nIf the provider supports nonce validation, the nonce will be validated against this value and required.", + "type": "string" + }, "method": { "description": "Method to use\n\nThis field must be set to `oidc` when using the oidc method.", "type": "string" @@ -2500,10 +2504,6 @@ "description": "The provider to register with", "type": "string" }, - "raw_id_token_nonce": { - "description": "RawIDTokenNonce is the nonce, used when generating the IDToken.\nIf the provider supports nonce validation, the nonce will be validated against this value and required.", - "type": "string" - }, "traits": { "description": "The identity traits. This is a placeholder for the registration flow.", "type": "object" @@ -2744,6 +2744,10 @@ "description": "IDToken is an optional id token provided by an OIDC provider\n\nIf submitted, it is verified using the OIDC provider's public key set and the claims are used to populate\nthe OIDC credentials of the identity.\nIf the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use\nthe `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken.\n\nSupported providers are\nApple", "type": "string" }, + "id_token_nonce": { + "description": "IDTokenNonce is the nonce, used when generating the IDToken.\nIf the provider supports nonce validation, the nonce will be validated against this value and is required.", + "type": "string" + }, "method": { "description": "Method to use\n\nThis field must be set to `oidc` when using the oidc method.", "type": "string" @@ -2752,10 +2756,6 @@ "description": "The provider to register with", "type": "string" }, - "raw_id_token_nonce": { - "description": "RawIDTokenNonce is the nonce, used when generating the IDToken.\nIf the provider supports nonce validation, the nonce will be validated against this value and required.", - "type": "string" - }, "traits": { "description": "The identity traits", "type": "object" diff --git a/spec/swagger.json b/spec/swagger.json index af07b0c5e701..517bc490fb0d 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -5347,6 +5347,10 @@ "description": "IDToken is an optional id token provided by an OIDC provider\n\nIf submitted, it is verified using the OIDC provider's public key set and the claims are used to populate\nthe OIDC credentials of the identity.\nIf the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use\nthe `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken.\n\nSupported providers are\nApple", "type": "string" }, + "id_token_nonce": { + "description": "IDTokenNonce is the nonce, used when generating the IDToken.\nIf the provider supports nonce validation, the nonce will be validated against this value and required.", + "type": "string" + }, "method": { "description": "Method to use\n\nThis field must be set to `oidc` when using the oidc method.", "type": "string" @@ -5355,10 +5359,6 @@ "description": "The provider to register with", "type": "string" }, - "raw_id_token_nonce": { - "description": "RawIDTokenNonce is the nonce, used when generating the IDToken.\nIf the provider supports nonce validation, the nonce will be validated against this value and required.", - "type": "string" - }, "traits": { "description": "The identity traits. This is a placeholder for the registration flow.", "type": "object" @@ -5563,6 +5563,10 @@ "description": "IDToken is an optional id token provided by an OIDC provider\n\nIf submitted, it is verified using the OIDC provider's public key set and the claims are used to populate\nthe OIDC credentials of the identity.\nIf the OIDC provider does not store additional claims (such as name, etc.) in the IDToken itself, you can use\nthe `traits` field to populate the identity's traits. Note, that Apple only includes the users email in the IDToken.\n\nSupported providers are\nApple", "type": "string" }, + "id_token_nonce": { + "description": "IDTokenNonce is the nonce, used when generating the IDToken.\nIf the provider supports nonce validation, the nonce will be validated against this value and is required.", + "type": "string" + }, "method": { "description": "Method to use\n\nThis field must be set to `oidc` when using the oidc method.", "type": "string" @@ -5571,10 +5575,6 @@ "description": "The provider to register with", "type": "string" }, - "raw_id_token_nonce": { - "description": "RawIDTokenNonce is the nonce, used when generating the IDToken.\nIf the provider supports nonce validation, the nonce will be validated against this value and required.", - "type": "string" - }, "traits": { "description": "The identity traits", "type": "object" From 17b81d0a6129a1c7878c4821f11faf7527f62d1e Mon Sep 17 00:00:00 2001 From: Jonas Hungershausen Date: Fri, 8 Sep 2023 12:09:29 +0200 Subject: [PATCH 9/9] chore: u --- selfservice/strategy/oidc/provider.go | 5 ++++- selfservice/strategy/oidc/provider_apple.go | 4 +++- selfservice/strategy/oidc/provider_test.go | 4 ---- selfservice/strategy/oidc/strategy.go | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/selfservice/strategy/oidc/provider.go b/selfservice/strategy/oidc/provider.go index ac4a24ed2745..ef486aeaf6f5 100644 --- a/selfservice/strategy/oidc/provider.go +++ b/selfservice/strategy/oidc/provider.go @@ -29,7 +29,10 @@ type TokenExchanger interface { type IDTokenVerifier interface { Verify(ctx context.Context, rawIDToken string) (*Claims, error) - NonceSupported(*Claims) bool +} + +type NonceValidationSkipper interface { + CanSkipNonce(*Claims) bool } // ConvertibleBoolean is used as Apple casually sends the email_verified field as a string. diff --git a/selfservice/strategy/oidc/provider_apple.go b/selfservice/strategy/oidc/provider_apple.go index a3f96e8b5e1c..686cad1c10ed 100644 --- a/selfservice/strategy/oidc/provider_apple.go +++ b/selfservice/strategy/oidc/provider_apple.go @@ -169,6 +169,8 @@ func (a *ProviderApple) Verify(ctx context.Context, rawIDToken string) (*Claims, return claims, nil } -func (a *ProviderApple) NonceSupported(c *Claims) bool { +var _ NonceValidationSkipper = new(ProviderApple) + +func (a *ProviderApple) CanSkipNonce(c *Claims) bool { return c.NonceSupported } diff --git a/selfservice/strategy/oidc/provider_test.go b/selfservice/strategy/oidc/provider_test.go index d5baba0971b5..74fdb05031fc 100644 --- a/selfservice/strategy/oidc/provider_test.go +++ b/selfservice/strategy/oidc/provider_test.go @@ -52,7 +52,3 @@ func (t *TestProvider) Verify(ctx context.Context, token string) (*Claims, error } return &c, nil } - -func (t *TestProvider) NonceSupported(c *Claims) bool { - return true -} diff --git a/selfservice/strategy/oidc/strategy.go b/selfservice/strategy/oidc/strategy.go index a8625fa57f75..d772bfd0f198 100644 --- a/selfservice/strategy/oidc/strategy.go +++ b/selfservice/strategy/oidc/strategy.go @@ -607,7 +607,7 @@ func (s *Strategy) processIDToken(w http.ResponseWriter, r *http.Request, provid // First check if the JWT contains the nonce claim. if claims.Nonce == "" { // If it doesn't, check if the provider supports nonces. - if verifier.NonceSupported(claims) { + if nonceSkipper, ok := verifier.(NonceValidationSkipper); !ok || !nonceSkipper.CanSkipNonce(claims) { // If the provider supports nonces, abort the flow! return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("No nonce was included in the id_token but is required by the provider")) }