Separate second-factor verification and delivery from auth (#40)
This commit is contained in:
1 parent
12a38cd654
commit
ecb63b61c6
33 files changed
+1772
-123
No files matched your search
@@ -9,20 +9,18 @@ import (
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/product/auth/flow"
|
||||
"git.sendico.io/tech/sxchnge/product/auth/ipguard"
|
||||
authmodel "git.sendico.io/tech/sxchnge/product/auth/model"
|
||||
sresponse "git.sendico.io/tech/sxchnge/product/auth/response"
|
||||
customer "git.sendico.io/tech/sxchnge/product/customers/model"
|
||||
session "git.sendico.io/tech/sxchnge/product/sessions/model"
|
||||
factor "git.sendico.io/tech/sxchnge/product/twofactor/model"
|
||||
verificationmodel "git.sendico.io/tech/sxchnge/product/verification/model"
|
||||
verification "git.sendico.io/tech/sxchnge/product/verification/store"
|
||||
merrors "git.sendico.io/tech/sxchnge/shared/errors"
|
||||
"git.sendico.io/tech/sxchnge/shared/http/response"
|
||||
"git.sendico.io/tech/sxchnge/shared/logging/mask"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -35,12 +33,15 @@ type signupRequest struct {
|
||||
Key string `json:"idempotencyKey"`
|
||||
}
|
||||
type challengeRequest struct {
|
||||
ChallengeID string `json:"challengeId"`
|
||||
PendingToken string `json:"pendingToken"`
|
||||
Code string `json:"code"`
|
||||
Key string `json:"idempotencyKey"`
|
||||
Session session.SessionIdentifier `json:"sessionIdentifier"`
|
||||
}
|
||||
type challengeResponse struct {
|
||||
ChallengeID string `json:"challengeId"`
|
||||
Channel factor.Channel `json:"channel"`
|
||||
UserID string `json:"userId"`
|
||||
PendingToken sresponse.TokenData `json:"pendingToken"`
|
||||
Target string `json:"target"`
|
||||
@@ -161,19 +162,28 @@ func (pr *PublicRouter) beginLogin(r *http.Request, a *customer.Account, session
|
||||
return pr.flowError(err)
|
||||
}
|
||||
token := sresponse.TokenData{Token: raw, Expiration: time.Unix(claims["exp"].(int64), 0)}
|
||||
return pr.queueLogin(r, a, token, key)
|
||||
return pr.queueLogin(r, a, token, key, "")
|
||||
}
|
||||
func randomKey() string { return rand.Text() }
|
||||
func (pr *PublicRouter) queueLogin(r *http.Request, a *customer.Account, token sresponse.TokenData, key string) http.HandlerFunc {
|
||||
expires, err := pr.flow.Issue(r.Context(), a, verificationmodel.PurposeLogin, challengeScope(token.Token), key, token.Expiration)
|
||||
func loginScope(raw string) factor.Scope {
|
||||
return factor.Scope{Purpose: verificationmodel.PurposeLogin, ContextRef: challengeScope(raw)}
|
||||
}
|
||||
func (pr *PublicRouter) queueLogin(r *http.Request, a *customer.Account, token sresponse.TokenData, key, id string) http.HandlerFunc {
|
||||
if pr.factors == nil {
|
||||
return pr.flowError(merrors.ServiceUnavailable("second-factor service unavailable"))
|
||||
}
|
||||
req := factor.Request{Subject: a.ID, Scope: loginScope(token.Token), Deadline: token.Expiration, IdempotencyKey: key, IP: requestIP(r)}
|
||||
var challenge factor.Challenge
|
||||
var err error
|
||||
if id == "" {
|
||||
challenge, err = pr.factors.Begin(r.Context(), req)
|
||||
} else {
|
||||
challenge, err = pr.factors.Resend(r.Context(), req, id)
|
||||
}
|
||||
if err != nil {
|
||||
return pr.flowError(err)
|
||||
}
|
||||
if token.Expiration.Before(expires) {
|
||||
expires = token.Expiration
|
||||
}
|
||||
pr.logger.Info("Login verification queued", zap.String("account_ref", a.ID.Hex()))
|
||||
return response.Accepted(pr.logger, challengeResponse{UserID: a.ID.Hex(), PendingToken: token, Target: mask.Email(a.Login), ExpiresAt: expires, ResendAt: time.Now().Add(flow.Cooldown)})
|
||||
return response.Accepted(pr.logger, challengeResponse{ChallengeID: challenge.ID, Channel: challenge.Channel, UserID: a.ID.Hex(), PendingToken: token, Target: challenge.Target, ExpiresAt: challenge.ExpiresAt, ResendAt: challenge.ResendAt})
|
||||
}
|
||||
func (pr *PublicRouter) pending(ctx context.Context, raw string) (*customer.Account, *authmodel.AccountToken, error) {
|
||||
if len(raw) > 8192 {
|
||||
@@ -206,11 +216,14 @@ func (pr *PublicRouter) resendLogin(r *http.Request) http.HandlerFunc {
|
||||
if err := decodeFlow(r, &req); err != nil {
|
||||
return pr.flowError(err)
|
||||
}
|
||||
if req.ChallengeID == "" {
|
||||
return pr.flowError(merrors.InvalidArgument("challenge ID required"))
|
||||
}
|
||||
a, t, err := pr.pending(r.Context(), req.PendingToken)
|
||||
if err != nil {
|
||||
return pr.flowError(err)
|
||||
}
|
||||
return pr.queueLogin(r, a, sresponse.TokenData{Token: req.PendingToken, Expiration: t.Expiration}, req.Key)
|
||||
return pr.queueLogin(r, a, sresponse.TokenData{Token: req.PendingToken, Expiration: t.Expiration}, req.Key, req.ChallengeID)
|
||||
}
|
||||
func (pr *PublicRouter) verifyLogin(r *http.Request) http.HandlerFunc {
|
||||
var req challengeRequest
|
||||
@@ -228,13 +241,19 @@ func (pr *PublicRouter) verifyLogin(r *http.Request) http.HandlerFunc {
|
||||
if h = pr.validateClientAccountBinding(client, a); h != nil {
|
||||
return h
|
||||
}
|
||||
code := strings.TrimSpace(req.Code)
|
||||
if len(code) != 6 {
|
||||
return pr.flowError(verification.ErorrTokenNotFound())
|
||||
if pr.factors == nil {
|
||||
return pr.flowError(merrors.ServiceUnavailable("second-factor service unavailable"))
|
||||
}
|
||||
if _, err = pr.flow.Tokens.Consume(r.Context(), a.ID, verificationmodel.PurposeLogin, challengeScope(req.PendingToken), code); err != nil {
|
||||
scope := loginScope(req.PendingToken)
|
||||
var result http.HandlerFunc
|
||||
err = pr.factors.Complete(r.Context(), a.ID, scope, req.ChallengeID, req.Code, func(tx context.Context) error {
|
||||
var err error
|
||||
result, err = pr.prepareLogin(tx, r, &t.SessionIdentifier, a)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return pr.flowError(err)
|
||||
}
|
||||
pr.logger.Info("Login verification completed", zap.String("account_ref", a.ID.Hex()))
|
||||
return pr.refreshAndRespondLogin(r.Context(), r, &t.SessionIdentifier, a)
|
||||
pr.logger.Info("Login completed after second-factor proof consumption", zap.String("account_ref", a.ID.Hex()))
|
||||
return result
|
||||
}
|
||||
Reference in new issue
Block a user