2026-05-26 16:55:37 +00:00
|
|
|
package member
|
|
|
|
|
|
|
|
|
|
import (
|
2026-05-27 09:28:13 +00:00
|
|
|
"context"
|
|
|
|
|
"strings"
|
|
|
|
|
|
2026-05-26 16:55:37 +00:00
|
|
|
memberenum "gateway/internal/model/member/domain/enum"
|
|
|
|
|
domusecase "gateway/internal/model/member/domain/usecase"
|
2026-05-27 09:28:13 +00:00
|
|
|
"gateway/internal/svc"
|
|
|
|
|
"gateway/internal/types"
|
2026-05-26 16:55:37 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func ensurePlatformNativePassword(member *domusecase.MemberDTO) error {
|
|
|
|
|
if member == nil {
|
|
|
|
|
return errb.ResNotFound("member", "")
|
|
|
|
|
}
|
|
|
|
|
switch member.Origin {
|
|
|
|
|
case memberenum.MemberOriginPlatformNative:
|
|
|
|
|
return nil
|
|
|
|
|
case memberenum.MemberOriginOIDC:
|
|
|
|
|
return errb.AuthForbidden("social login accounts cannot change password here")
|
|
|
|
|
case memberenum.MemberOriginLDAP:
|
|
|
|
|
return errb.AuthForbidden("ldap accounts cannot change password here")
|
|
|
|
|
case memberenum.MemberOriginSCIM:
|
|
|
|
|
return errb.AuthForbidden("scim provisioned accounts cannot change password here")
|
|
|
|
|
default:
|
|
|
|
|
return errb.AuthForbidden("account cannot change password here")
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-27 09:28:13 +00:00
|
|
|
|
|
|
|
|
// totpEnrolledForActor uses TOTP status (authoritative) with profile flag as fallback.
|
|
|
|
|
func totpEnrolledForActor(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
sc *svc.ServiceContext,
|
|
|
|
|
tenantID, uid string,
|
|
|
|
|
member *domusecase.MemberDTO,
|
|
|
|
|
) (bool, error) {
|
|
|
|
|
if sc.MemberTOTP != nil {
|
|
|
|
|
status, err := sc.MemberTOTP.Status(ctx, tenantID, uid)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false, err
|
|
|
|
|
}
|
|
|
|
|
if status != nil {
|
|
|
|
|
return status.Enrolled, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if member != nil {
|
|
|
|
|
return member.TOTPEnrolled, nil
|
|
|
|
|
}
|
|
|
|
|
return false, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ensurePasswordChangeStepUp requires TOTP enrollment and a consumed step-up token
|
|
|
|
|
// or inline TOTP code before password change.
|
|
|
|
|
func ensurePasswordChangeStepUp(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
sc *svc.ServiceContext,
|
|
|
|
|
tenantID, uid string,
|
|
|
|
|
member *domusecase.MemberDTO,
|
|
|
|
|
req *types.ChangePasswordReq,
|
|
|
|
|
) error {
|
|
|
|
|
if sc.MemberTOTP == nil {
|
|
|
|
|
return errb.SysNotImplemented("member TOTP not configured")
|
|
|
|
|
}
|
|
|
|
|
enrolled, err := totpEnrolledForActor(ctx, sc, tenantID, uid, member)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if !enrolled {
|
|
|
|
|
return errb.AuthForbidden("enroll totp before changing password")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
token := strings.TrimSpace(req.StepUpToken)
|
|
|
|
|
code := strings.TrimSpace(req.TOTPCode)
|
|
|
|
|
if token == "" && code == "" {
|
|
|
|
|
return errb.AuthForbidden("totp verification required before password change")
|
|
|
|
|
}
|
|
|
|
|
if token != "" && code != "" {
|
|
|
|
|
return errb.InputInvalidFormat("provide either step_up_token or totp_code, not both")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if code != "" {
|
|
|
|
|
return sc.MemberTOTP.VerifyCode(ctx, tenantID, uid, code)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := requireStepUp(sc); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return sc.MemberStepUp.Consume(ctx, &domusecase.ConsumeStepUpRequest{
|
|
|
|
|
TokenID: token,
|
|
|
|
|
TenantID: tenantID,
|
|
|
|
|
UID: uid,
|
|
|
|
|
Purpose: memberenum.StepUpPurposeChangePassword,
|
|
|
|
|
})
|
|
|
|
|
}
|