From 98b1e50f799052a3418b119bc935c7f7d60eb6b8 Mon Sep 17 00:00:00 2001 From: Alano Terblanche <18033717+Benehiko@users.noreply.github.com> Date: Fri, 30 Jun 2023 18:29:10 +0200 Subject: [PATCH] feat: login with code --- identity/credentials.go | 4 + identity/credentials_code.go | 9 ++ selfservice/flow/recovery/flow.go | 2 +- selfservice/flow/recovery/handler.go | 4 +- selfservice/flow/recovery/strategy.go | 2 +- selfservice/flow/verification/flow.go | 2 +- selfservice/flow/verification/handler.go | 4 +- selfservice/flow/verification/strategy.go | 2 +- .../strategy/code/.schema/login.schema.json | 27 ++++ selfservice/strategy/code/code_login.go | 22 ++++ selfservice/strategy/code/schema.go | 3 + selfservice/strategy/code/strategy.go | 17 ++- selfservice/strategy/code/strategy_login.go | 119 ++++++++++++++++++ .../strategy/code/strategy_recovery.go | 6 +- .../strategy/code/strategy_verification.go | 2 +- 15 files changed, 208 insertions(+), 17 deletions(-) create mode 100644 identity/credentials_code.go create mode 100644 selfservice/strategy/code/.schema/login.schema.json create mode 100644 selfservice/strategy/code/code_login.go create mode 100644 selfservice/strategy/code/strategy_login.go diff --git a/identity/credentials.go b/identity/credentials.go index 74b6857ef124..954d761e52a7 100644 --- a/identity/credentials.go +++ b/identity/credentials.go @@ -55,6 +55,8 @@ func (c CredentialsType) ToUiNodeGroup() node.UiNodeGroup { return node.WebAuthnGroup case CredentialsTypeLookup: return node.LookupGroup + case CredentialsTypeCodeAuth: + return node.CodeGroup default: return node.DefaultGroup } @@ -67,6 +69,7 @@ const ( CredentialsTypeTOTP CredentialsType = "totp" CredentialsTypeLookup CredentialsType = "lookup_secret" CredentialsTypeWebAuthn CredentialsType = "webauthn" + CredentialsTypeCodeAuth CredentialsType = "code" ) const ( @@ -84,6 +87,7 @@ func ParseCredentialsType(in string) (CredentialsType, bool) { CredentialsTypeTOTP, CredentialsTypeLookup, CredentialsTypeWebAuthn, + CredentialsTypeCodeAuth, CredentialsTypeRecoveryLink, CredentialsTypeRecoveryCode, } { diff --git a/identity/credentials_code.go b/identity/credentials_code.go new file mode 100644 index 000000000000..b66d0964bbd9 --- /dev/null +++ b/identity/credentials_code.go @@ -0,0 +1,9 @@ +package identity + +// CredentialsOTP represents an OTP code +// +// swagger:model identityCredentialsOTP +type CredentialsOTP struct { + // CodeHMAC represents the HMACed value of the login/registration code + CodeHMAC string `json:"code_hmac"` +} diff --git a/selfservice/flow/recovery/flow.go b/selfservice/flow/recovery/flow.go index 42770783fb42..c6cfc3dfa4c9 100644 --- a/selfservice/flow/recovery/flow.go +++ b/selfservice/flow/recovery/flow.go @@ -133,7 +133,7 @@ func NewFlow(conf *config.Config, exp time.Duration, csrf string, r *http.Reques } if strategy != nil { - flow.Active = sqlxx.NullString(strategy.RecoveryNodeGroup()) + flow.Active = sqlxx.NullString(strategy.NodeGroup()) if err := strategy.PopulateRecoveryMethod(r, flow); err != nil { return nil, err } diff --git a/selfservice/flow/recovery/handler.go b/selfservice/flow/recovery/handler.go index 24aed7695bba..90a62aab829c 100644 --- a/selfservice/flow/recovery/handler.go +++ b/selfservice/flow/recovery/handler.go @@ -430,12 +430,12 @@ func (h *Handler) updateRecoveryFlow(w http.ResponseWriter, r *http.Request, ps } else if errors.Is(err, flow.ErrCompletedByStrategy) { return } else if err != nil { - h.d.RecoveryFlowErrorHandler().WriteFlowError(w, r, f, ss.RecoveryNodeGroup(), err) + h.d.RecoveryFlowErrorHandler().WriteFlowError(w, r, f, ss.NodeGroup(), err) return } found = true - g = ss.RecoveryNodeGroup() + g = ss.NodeGroup() break } diff --git a/selfservice/flow/recovery/strategy.go b/selfservice/flow/recovery/strategy.go index 28e28673f1d3..3594420167df 100644 --- a/selfservice/flow/recovery/strategy.go +++ b/selfservice/flow/recovery/strategy.go @@ -26,7 +26,7 @@ const ( type ( Strategy interface { RecoveryStrategyID() string - RecoveryNodeGroup() node.UiNodeGroup + NodeGroup() node.UiNodeGroup PopulateRecoveryMethod(*http.Request, *Flow) error Recover(w http.ResponseWriter, r *http.Request, f *Flow) (err error) } diff --git a/selfservice/flow/verification/flow.go b/selfservice/flow/verification/flow.go index c43ef74bb342..f5b94dd4bf95 100644 --- a/selfservice/flow/verification/flow.go +++ b/selfservice/flow/verification/flow.go @@ -132,7 +132,7 @@ func NewFlow(conf *config.Config, exp time.Duration, csrf string, r *http.Reques } if strategy != nil { - f.Active = sqlxx.NullString(strategy.VerificationNodeGroup()) + f.Active = sqlxx.NullString(strategy.NodeGroup()) if err := strategy.PopulateVerificationMethod(r, f); err != nil { return nil, err } diff --git a/selfservice/flow/verification/handler.go b/selfservice/flow/verification/handler.go index dd21a7db1135..7be9983e38d3 100644 --- a/selfservice/flow/verification/handler.go +++ b/selfservice/flow/verification/handler.go @@ -420,12 +420,12 @@ func (h *Handler) updateVerificationFlow(w http.ResponseWriter, r *http.Request, } else if errors.Is(err, flow.ErrCompletedByStrategy) { return } else if err != nil { - h.d.VerificationFlowErrorHandler().WriteFlowError(w, r, f, ss.VerificationNodeGroup(), err) + h.d.VerificationFlowErrorHandler().WriteFlowError(w, r, f, ss.NodeGroup(), err) return } found = true - g = ss.VerificationNodeGroup() + g = ss.NodeGroup() break } diff --git a/selfservice/flow/verification/strategy.go b/selfservice/flow/verification/strategy.go index 9cb4ff848920..3d270bfb8732 100644 --- a/selfservice/flow/verification/strategy.go +++ b/selfservice/flow/verification/strategy.go @@ -26,7 +26,7 @@ const ( type ( Strategy interface { VerificationStrategyID() string - VerificationNodeGroup() node.UiNodeGroup + NodeGroup() node.UiNodeGroup PopulateVerificationMethod(*http.Request, *Flow) error Verify(w http.ResponseWriter, r *http.Request, f *Flow) (err error) SendVerificationEmail(context.Context, *Flow, *identity.Identity, *identity.VerifiableAddress) error diff --git a/selfservice/strategy/code/.schema/login.schema.json b/selfservice/strategy/code/.schema/login.schema.json new file mode 100644 index 000000000000..6572ba9f9d89 --- /dev/null +++ b/selfservice/strategy/code/.schema/login.schema.json @@ -0,0 +1,27 @@ +{ + "$id": "https://schemas.ory.sh/kratos/selfservice/strategy/code/login.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "method": { + "type": "string", + "enum": [ + "code" + ] + }, + "code": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "flow": { + "type": "string", + "format": "uuid" + }, + "csrf_token": { + "type": "string" + } + } +} diff --git a/selfservice/strategy/code/code_login.go b/selfservice/strategy/code/code_login.go new file mode 100644 index 000000000000..9243f1015e26 --- /dev/null +++ b/selfservice/strategy/code/code_login.go @@ -0,0 +1,22 @@ +package code + +import ( + "database/sql" + + "github.com/gofrs/uuid" +) + +type LoginRegistrationCode struct { + // ID is the primary key + // + // required: true + // type: string + // format: uuid + ID uuid.UUID `json:"id" db:"id" faker:"-"` + + // CodeHMAC represents the HMACed value of the login/registration code + CodeHMAC string `json:"-" db:"code_hmac"` + + // UsedAt is the timestamp of when the code was used or null if it wasn't yet + UsedAt sql.NullTime `json:"-" db:"used_at"` +} diff --git a/selfservice/strategy/code/schema.go b/selfservice/strategy/code/schema.go index 69d5bd07393e..24674c9a476d 100644 --- a/selfservice/strategy/code/schema.go +++ b/selfservice/strategy/code/schema.go @@ -12,3 +12,6 @@ var recoveryMethodSchema []byte //go:embed .schema/verification.schema.json var verificationMethodSchema []byte + +//go:embed .schema/login.schema.json +var loginMethodSchema []byte diff --git a/selfservice/strategy/code/strategy.go b/selfservice/strategy/code/strategy.go index b35d8bda30ff..262cc40e8963 100644 --- a/selfservice/strategy/code/strategy.go +++ b/selfservice/strategy/code/strategy.go @@ -9,7 +9,9 @@ import ( "github.com/ory/kratos/identity" "github.com/ory/kratos/schema" "github.com/ory/kratos/selfservice/errorx" + "github.com/ory/kratos/selfservice/flow/login" "github.com/ory/kratos/selfservice/flow/recovery" + "github.com/ory/kratos/selfservice/flow/registration" "github.com/ory/kratos/selfservice/flow/settings" "github.com/ory/kratos/selfservice/flow/verification" "github.com/ory/kratos/session" @@ -28,6 +30,9 @@ var _ verification.Strategy = new(Strategy) var _ verification.AdminHandler = new(Strategy) var _ verification.PublicHandler = new(Strategy) +var _ login.Strategy = new(Strategy) +var _ registration.Strategy = new(Strategy) + type ( // FlowMethod contains the configuration for this selfservice strategy. FlowMethod struct { @@ -65,6 +70,12 @@ type ( verification.StrategyProvider verification.HookExecutorProvider + login.StrategyProvider + login.HookExecutorProvider + login.FlowPersistenceProvider + + registration.StrategyProvider + RecoveryCodePersistenceProvider VerificationCodePersistenceProvider SenderProvider @@ -82,11 +93,7 @@ func NewStrategy(deps strategyDependencies) *Strategy { return &Strategy{deps: deps, dx: decoderx.NewHTTP()} } -func (s *Strategy) RecoveryNodeGroup() node.UiNodeGroup { - return node.CodeGroup -} - -func (s *Strategy) VerificationNodeGroup() node.UiNodeGroup { +func (s *Strategy) NodeGroup() node.UiNodeGroup { return node.CodeGroup } diff --git a/selfservice/strategy/code/strategy_login.go b/selfservice/strategy/code/strategy_login.go new file mode 100644 index 000000000000..126fea60d8b2 --- /dev/null +++ b/selfservice/strategy/code/strategy_login.go @@ -0,0 +1,119 @@ +package code + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + + "github.com/gofrs/uuid" + "github.com/ory/herodot" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/session" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" + "github.com/ory/kratos/x" + "github.com/ory/x/decoderx" + "github.com/ory/x/stringsx" +) + +var _ login.Strategy = new(Strategy) + +type loginSubmitPayload struct { + Method string `json:"method"` + CSRFToken string `json:"csrf_token"` + Code string `json:"code"` + Identifier string `json:"identifier"` +} + +func (s *Strategy) RegisterLoginRoutes(*x.RouterPublic) { +} + +func (s *Strategy) ID() identity.CredentialsType { + return identity.CredentialsTypeCodeAuth +} + +func (s *Strategy) CompletedAuthenticationMethod(ctx context.Context) session.AuthenticationMethod { + return session.AuthenticationMethod{ + Method: identity.CredentialsTypeCodeAuth, + AAL: identity.AuthenticatorAssuranceLevel2, + } +} + +func (s *Strategy) HandleLoginError(w http.ResponseWriter, r *http.Request, flow *login.Flow, body *loginSubmitPayload, err error) error { + if flow != nil { + email := "" + if body != nil { + email = body.Identifier + } + + flow.UI.SetCSRF(s.deps.GenerateCSRFToken(r)) + flow.UI.GetNodes().Upsert( + node.NewInputField("identifier", email, node.CodeGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute). + WithMetaLabel(text.NewInfoNodeInputEmail()), + ) + } + + return err +} + +func (s *Strategy) PopulateLoginMethod(r *http.Request, requestedAAL identity.AuthenticatorAssuranceLevel, lf *login.Flow) error { + if lf.Type != flow.TypeBrowser { + return nil + } + + if requestedAAL == identity.AuthenticatorAssuranceLevel2 { + return nil + } + + lf.UI.SetCSRF(s.deps.GenerateCSRFToken(r)) + lf.UI.GetNodes().Upsert( + node.NewInputField("identifier", "", node.CodeGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute). + WithMetaLabel(text.NewInfoNodeInputEmail()), + ) + return nil +} + +func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, identityID uuid.UUID) (i *identity.Identity, err error) { + if err := login.CheckAAL(f, identity.AuthenticatorAssuranceLevel1); err != nil { + return nil, err + } + + if err := flow.MethodEnabledAndAllowedFromRequest(r, s.ID().String(), s.deps); err != nil { + return nil, err + } + + var p loginSubmitPayload + if err := s.dx.Decode(r, &p, + decoderx.HTTPDecoderSetValidatePayloads(true), + decoderx.MustHTTPRawJSONSchemaCompiler(loginMethodSchema), + decoderx.HTTPDecoderAllowedMethods("POST"), + decoderx.HTTPDecoderJSONFollowsFormFormat()); err != nil { + return nil, s.HandleLoginError(w, r, f, &p, err) + } + + if err := flow.EnsureCSRF(s.deps, r, f.Type, s.deps.Config().DisableAPIFlowEnforcement(r.Context()), s.deps.GenerateCSRFToken, p.CSRFToken); err != nil { + return nil, s.HandleLoginError(w, r, f, &p, err) + } + + i, c, err := s.deps.PrivilegedIdentityPool().FindByCredentialsIdentifier(r.Context(), s.ID(), p.Identifier) + + if err != nil { + return nil, s.HandleLoginError(w, r, f, &p, err) + } + + var o identity.CredentialsOTP + d := json.NewDecoder(bytes.NewBuffer(c.Config)) + if err := d.Decode(&o); err != nil { + return nil, herodot.ErrInternalServerError.WithReason("The password credentials could not be decoded properly").WithDebug(err.Error()).WithWrap(err) + } + + f.Active = identity.CredentialsTypeCodeAuth + if err = s.deps.LoginFlowPersister().UpdateLoginFlow(r.Context(), f); err != nil { + return nil, s.HandleLoginError(w, r, f, &p, err) + } + + return i, nil +} diff --git a/selfservice/strategy/code/strategy_recovery.go b/selfservice/strategy/code/strategy_recovery.go index 466b9d26c3de..8d4a5e45986a 100644 --- a/selfservice/strategy/code/strategy_recovery.go +++ b/selfservice/strategy/code/strategy_recovery.go @@ -281,7 +281,7 @@ func (s Strategy) isCodeFlow(f *recovery.Flow) bool { if err != nil { return false } - return value == s.RecoveryNodeGroup().String() + return value == s.NodeGroup().String() } func (s *Strategy) Recover(w http.ResponseWriter, r *http.Request, f *recovery.Flow) (err error) { @@ -539,13 +539,13 @@ func (s *Strategy) recoveryHandleFormSubmission(w http.ResponseWriter, r *http.R f.UI.SetCSRF(s.deps.GenerateCSRFToken(r)) - f.Active = sqlxx.NullString(s.RecoveryNodeGroup()) + f.Active = sqlxx.NullString(s.NodeGroup()) f.State = recovery.StateEmailSent f.UI.Messages.Set(text.NewRecoveryEmailWithCodeSent()) f.UI.Nodes.Append(node.NewInputField("code", nil, node.CodeGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute). WithMetaLabel(text.NewInfoNodeLabelRecoveryCode()), ) - f.UI.Nodes.Append(node.NewInputField("method", s.RecoveryNodeGroup(), node.CodeGroup, node.InputAttributeTypeHidden)) + f.UI.Nodes.Append(node.NewInputField("method", s.NodeGroup(), node.CodeGroup, node.InputAttributeTypeHidden)) f.UI. GetNodes(). diff --git a/selfservice/strategy/code/strategy_verification.go b/selfservice/strategy/code/strategy_verification.go index 9a0902a404bb..4de4bf9d16a5 100644 --- a/selfservice/strategy/code/strategy_verification.go +++ b/selfservice/strategy/code/strategy_verification.go @@ -48,7 +48,7 @@ func (s *Strategy) PopulateVerificationMethod(r *http.Request, f *verification.F ) // Required for the re-send code button nodes.Append( - node.NewInputField("method", s.VerificationNodeGroup(), node.CodeGroup, node.InputAttributeTypeHidden), + node.NewInputField("method", s.NodeGroup(), node.CodeGroup, node.InputAttributeTypeHidden), ) f.UI.Messages.Set(text.NewVerificationEmailWithCodeSent()) default: