- Introduce `LoginWithOptions` method to allow for customizable login flows, enabling overrides for scopes, token expiration, and refresh token issuance. - Add `LoginOptions` struct to encapsulate optional parameters for login, improving flexibility in user authentication. - Update token handling in `issueTokens` to accommodate new options, ensuring proper management of access and refresh tokens based on user preferences. - Integrate OTP service initialization into the OpenAPI server setup for enhanced authentication capabilities.
95 lines
2.6 KiB
Go
95 lines
2.6 KiB
Go
package otp
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/yaoapp/yao/openapi/oauth"
|
|
oauthtypes "github.com/yaoapp/yao/openapi/oauth/types"
|
|
"github.com/yaoapp/yao/openapi/user"
|
|
"github.com/yaoapp/yao/openapi/utils"
|
|
)
|
|
|
|
// Login verifies an OTP code, resolves the user identity, issues tokens,
|
|
// and returns the result. It does NOT set HTTP cookies.
|
|
func (s *Service) Login(code string, locale string) (*LoginResult, error) {
|
|
payload, err := s.Verify(code)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
userID, teamID, err := s.resolveIdentity(payload)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to resolve OTP identity: %w", err)
|
|
}
|
|
|
|
loginCtx := &oauthtypes.LoginContext{
|
|
Locale: locale,
|
|
AuthSource: "otp",
|
|
}
|
|
|
|
opts := &user.LoginOptions{
|
|
SkipRefreshToken: true,
|
|
TokenExpiresIn: payload.TokenExpiresIn,
|
|
}
|
|
if payload.Scope != "" {
|
|
opts.Scopes = strings.Fields(payload.Scope)
|
|
}
|
|
|
|
loginResp, err := user.LoginWithOptions(userID, teamID, loginCtx, opts)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("OTP login failed: %w", err)
|
|
}
|
|
|
|
return &LoginResult{
|
|
UserID: loginResp.UserID,
|
|
Subject: loginResp.Subject,
|
|
AccessToken: loginResp.AccessToken,
|
|
IDToken: loginResp.IDToken,
|
|
RefreshToken: loginResp.RefreshToken,
|
|
ExpiresIn: loginResp.ExpiresIn,
|
|
RefreshTokenExpiresIn: loginResp.RefreshTokenExpiresIn,
|
|
TokenType: loginResp.TokenType,
|
|
Scope: loginResp.Scope,
|
|
Redirect: payload.Redirect,
|
|
}, nil
|
|
}
|
|
|
|
// resolveIdentity determines the user_id and team_id from the OTP payload.
|
|
// When member_id is present, it resolves the user_id from the member record.
|
|
func (s *Service) resolveIdentity(payload *Payload) (userID string, teamID string, err error) {
|
|
teamID = payload.TeamID
|
|
|
|
if payload.UserID != "" {
|
|
return payload.UserID, teamID, nil
|
|
}
|
|
|
|
if payload.MemberID == "" {
|
|
return "", "", fmt.Errorf("payload has neither user_id nor member_id")
|
|
}
|
|
|
|
userProvider, err := oauth.OAuth.GetUserProvider()
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("failed to get user provider: %w", err)
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
member, err := userProvider.GetMemberByMemberID(ctx, payload.MemberID)
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("failed to resolve member %s: %w", payload.MemberID, err)
|
|
}
|
|
|
|
userID = utils.ToString(member["user_id"])
|
|
if userID == "" {
|
|
return "", "", fmt.Errorf("member %s has no associated user_id", payload.MemberID)
|
|
}
|
|
|
|
if teamID == "" {
|
|
teamID = utils.ToString(member["team_id"])
|
|
}
|
|
|
|
return userID, teamID, nil
|
|
}
|