template-monorepo/internal/logic/auth/login_helper.go

220 lines
6.4 KiB
Go

package auth
import (
"context"
"strings"
"time"
errs "gateway/internal/library/errors"
"gateway/internal/library/errors/code"
"gateway/internal/library/zitadel"
domauth "gateway/internal/model/auth/domain/usecase"
memberdom "gateway/internal/model/member/domain"
memberenum "gateway/internal/model/member/domain/enum"
dommember "gateway/internal/model/member/domain/usecase"
"gateway/internal/svc"
"gateway/internal/types"
)
func issueAuthToken(ctx context.Context, sc *svc.ServiceContext, tenantID, uid string) (*types.AuthTokenData, error) {
if sc.AuthToken == nil {
return nil, errb.SysNotImplemented("auth token not configured")
}
pair, err := sc.AuthToken.IssuePair(ctx, &domauth.IssuePairRequest{
TenantID: tenantID,
UID: uid,
})
if err != nil {
return nil, err
}
return &types.AuthTokenData{
AccessToken: pair.AccessToken,
RefreshToken: pair.RefreshToken,
ExpiresIn: pair.ExpiresIn,
UID: uid,
TokenType: pair.TokenType,
}, nil
}
func tokenDataFromRefresh(ctx context.Context, sc *svc.ServiceContext, refreshToken string) (*types.AuthTokenData, error) {
if sc.AuthToken == nil {
return nil, errb.SysNotImplemented("auth token not configured")
}
pair, err := sc.AuthToken.Refresh(ctx, refreshToken)
if err != nil {
return nil, err
}
claims, err := sc.AuthToken.ParseAccessToken(ctx, pair.AccessToken)
if err != nil {
return nil, err
}
return &types.AuthTokenData{
AccessToken: pair.AccessToken,
RefreshToken: pair.RefreshToken,
ExpiresIn: pair.ExpiresIn,
UID: claims.UID,
TokenType: pair.TokenType,
}, nil
}
func zitadelIdentityFromToken(ctx context.Context, client *zitadel.Client, tok *zitadel.TokenResult) (*zitadel.IDTokenClaims, error) {
if tok == nil {
return nil, errb.SvcThirdParty("empty token result")
}
if tok.Subject != "" {
return &zitadel.IDTokenClaims{
Sub: tok.Subject,
Email: tok.Email,
}, nil
}
if tok.IDToken != "" {
claims, err := zitadel.ParseIDTokenClaims(tok.IDToken)
if err != nil {
return nil, errb.SvcThirdParty("parse id_token failed").WithCause(err)
}
return claims, nil
}
claims, err := client.FetchUserInfo(ctx, tok.AccessToken)
if err != nil {
return nil, wrapZitadelErr(err)
}
return claims, nil
}
func memberForLogin(ctx context.Context, sc *svc.ServiceContext, tenantID, zitadelSub string) (*dommember.MemberDTO, error) {
if sc.MemberProfile == nil {
return nil, errb.SysNotImplemented("member profile not configured")
}
dto, err := sc.MemberProfile.GetByZitadelUserID(ctx, tenantID, zitadelSub)
if err != nil {
if e := errs.FromError(err); e != nil && e.Category() == code.ResNotFound {
return nil, errb.AuthUnauthorized("invalid credentials").WithCause(memberdom.ErrNotFound)
}
return nil, err
}
if err := ensureLoginEligible(dto.Status); err != nil {
return nil, err
}
return dto, nil
}
func ensureLoginEligible(status memberenum.MemberStatus) error {
switch status {
case memberenum.MemberStatusActive:
return nil
case memberenum.MemberStatusUnverified:
return errb.AuthForbidden("account is not verified")
case memberenum.MemberStatusSuspended:
return errb.AuthForbidden("account is suspended")
case memberenum.MemberStatusDeleted:
return errb.AuthUnauthorized("invalid credentials")
default:
return errb.AuthForbidden("account is not allowed to login")
}
}
func normalizeLoginEmail(email string) string {
return strings.TrimSpace(strings.ToLower(email))
}
func requireLoginDeps(sc *svc.ServiceContext) error {
if sc.Zitadel == nil {
return errb.SysNotImplemented("zitadel not configured")
}
if sc.MemberProfile == nil {
return errb.SysNotImplemented("member profile not configured")
}
return nil
}
func isMemberNotFound(err error) bool {
e := errs.FromError(err)
return e != nil && e.Category() == code.ResNotFound
}
func loginDataFromTokens(tokens *types.AuthTokenData) *types.LoginData {
if tokens == nil {
return nil
}
return &types.LoginData{
AccessToken: tokens.AccessToken,
RefreshToken: tokens.RefreshToken,
ExpiresIn: tokens.ExpiresIn,
UID: tokens.UID,
TokenType: tokens.TokenType,
}
}
func beginLoginMFA(ctx context.Context, sc *svc.ServiceContext, tenantID, tenantSlug, uid string) (*types.LoginData, error) {
if sc.AuthLoginMFAChallenge == nil {
return nil, errb.SysNotImplemented("login mfa challenge not configured")
}
if sc.MemberTOTP == nil {
return nil, errb.SysNotImplemented("member TOTP not configured")
}
ttl := time.Duration(sc.Config.Auth.Defaults().RegistrationSessionTTLSeconds) * time.Second
challenge, err := sc.AuthLoginMFAChallenge.Create(ctx, &domauth.CreateLoginMFAChallengeRequest{
TenantID: tenantID,
TenantSlug: tenantSlug,
UID: uid,
TTL: ttl,
})
if err != nil {
return nil, err
}
return &types.LoginData{
MFARequired: true,
MFAChallengeID: challenge.ChallengeID,
MFAExpiresIn: challenge.ExpiresIn,
}, nil
}
func confirmLoginMFA(ctx context.Context, sc *svc.ServiceContext, tenantSlug, challengeID, totpCode string) (*types.AuthTokenData, error) {
if sc.AuthLoginMFAChallenge == nil {
return nil, errb.SysNotImplemented("login mfa challenge not configured")
}
if sc.MemberTOTP == nil {
return nil, errb.SysNotImplemented("member TOTP not configured")
}
tenant, err := resolveTenant(ctx, sc, tenantSlug)
if err != nil {
return nil, err
}
challenge, err := sc.AuthLoginMFAChallenge.Get(ctx, challengeID)
if err != nil {
return nil, err
}
if challenge.TenantID != tenant.TenantID {
return nil, errb.AuthForbidden("login mfa challenge tenant mismatch")
}
if !strings.EqualFold(strings.TrimSpace(challenge.TenantSlug), strings.TrimSpace(tenantSlug)) {
return nil, errb.AuthForbidden("login mfa challenge tenant mismatch")
}
member, err := sc.MemberProfile.GetByUID(ctx, &dommember.GetMemberRequest{
TenantID: challenge.TenantID,
UID: challenge.UID,
})
if err != nil {
return nil, err
}
if err := ensureLoginEligible(member.Status); err != nil {
return nil, err
}
if !member.TOTPEnrolled {
return nil, errb.ResInvalidState("totp not enrolled").WithCause(memberdom.ErrTOTPNotEnrolled)
}
if err := sc.MemberTOTP.VerifyCode(ctx, challenge.TenantID, challenge.UID, strings.TrimSpace(totpCode)); err != nil {
return nil, err
}
if err := sc.AuthLoginMFAChallenge.Delete(ctx, challengeID); err != nil {
return nil, err
}
return issueAuthToken(ctx, sc, challenge.TenantID, challenge.UID)
}