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) }