Skip to content

Commit

Permalink
feat: login with code
Browse files Browse the repository at this point in the history
  • Loading branch information
Benehiko committed Jul 13, 2023
1 parent 86ad5e1 commit 98b1e50
Show file tree
Hide file tree
Showing 15 changed files with 208 additions and 17 deletions.
4 changes: 4 additions & 0 deletions identity/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -67,6 +69,7 @@ const (
CredentialsTypeTOTP CredentialsType = "totp"
CredentialsTypeLookup CredentialsType = "lookup_secret"
CredentialsTypeWebAuthn CredentialsType = "webauthn"
CredentialsTypeCodeAuth CredentialsType = "code"
)

const (
Expand All @@ -84,6 +87,7 @@ func ParseCredentialsType(in string) (CredentialsType, bool) {
CredentialsTypeTOTP,
CredentialsTypeLookup,
CredentialsTypeWebAuthn,
CredentialsTypeCodeAuth,
CredentialsTypeRecoveryLink,
CredentialsTypeRecoveryCode,
} {
Expand Down
9 changes: 9 additions & 0 deletions identity/credentials_code.go
Original file line number Diff line number Diff line change
@@ -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"`
}
2 changes: 1 addition & 1 deletion selfservice/flow/recovery/flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
4 changes: 2 additions & 2 deletions selfservice/flow/recovery/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
2 changes: 1 addition & 1 deletion selfservice/flow/recovery/strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion selfservice/flow/verification/flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
4 changes: 2 additions & 2 deletions selfservice/flow/verification/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
2 changes: 1 addition & 1 deletion selfservice/flow/verification/strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions selfservice/strategy/code/.schema/login.schema.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
22 changes: 22 additions & 0 deletions selfservice/strategy/code/code_login.go
Original file line number Diff line number Diff line change
@@ -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"`
}
3 changes: 3 additions & 0 deletions selfservice/strategy/code/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
17 changes: 12 additions & 5 deletions selfservice/strategy/code/strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 {
Expand Down Expand Up @@ -65,6 +70,12 @@ type (
verification.StrategyProvider
verification.HookExecutorProvider

login.StrategyProvider
login.HookExecutorProvider
login.FlowPersistenceProvider

registration.StrategyProvider

RecoveryCodePersistenceProvider
VerificationCodePersistenceProvider
SenderProvider
Expand All @@ -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
}

Expand Down
119 changes: 119 additions & 0 deletions selfservice/strategy/code/strategy_login.go
Original file line number Diff line number Diff line change
@@ -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
}
6 changes: 3 additions & 3 deletions selfservice/strategy/code/strategy_recovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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().
Expand Down
2 changes: 1 addition & 1 deletion selfservice/strategy/code/strategy_verification.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit 98b1e50

Please sign in to comment.