Merge pull request #1080 from trheyi/main
Implement ID token signing and user fingerprint management in OAuth
This commit is contained in:
commit
d55ecb06d7
5 changed files with 565 additions and 28 deletions
|
|
@ -457,6 +457,125 @@ func (s *Service) VerifyToken(token string) (*types.TokenClaims, error) {
|
||||||
return s.verifyOpaqueToken(token)
|
return s.verifyOpaqueToken(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SignIDToken signs an ID token with specific parameters and stores it
|
||||||
|
func (s *Service) SignIDToken(clientID, scope string, expiresIn int, userdata *types.OIDCUserInfo) (string, error) {
|
||||||
|
if s.signingCerts == nil || s.signingCerts.SigningKey == nil {
|
||||||
|
return "", fmt.Errorf("signing certificates not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if userdata and subject are provided
|
||||||
|
if userdata == nil || userdata.Sub == "" {
|
||||||
|
return "", fmt.Errorf("userdata or userdata.Sub is required for ID token")
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenSubject := userdata.Sub
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
// Create OIDC ID Token claims
|
||||||
|
idTokenClaims := &types.OIDCIDToken{
|
||||||
|
// Required ID Token claims
|
||||||
|
Iss: s.config.IssuerURL,
|
||||||
|
Sub: tokenSubject,
|
||||||
|
Aud: clientID,
|
||||||
|
Exp: now.Add(time.Duration(expiresIn) * time.Second).Unix(),
|
||||||
|
Iat: now.Unix(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a map for custom claims that includes both standard and user info
|
||||||
|
claims := jwt.MapClaims{
|
||||||
|
// Standard OIDC ID Token claims
|
||||||
|
"iss": idTokenClaims.Iss,
|
||||||
|
"sub": idTokenClaims.Sub,
|
||||||
|
"aud": idTokenClaims.Aud,
|
||||||
|
"exp": idTokenClaims.Exp,
|
||||||
|
"iat": idTokenClaims.Iat,
|
||||||
|
"jti": generateJTI(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add user information from userdata
|
||||||
|
// Add standard OIDC user claims if they exist
|
||||||
|
if userdata.Name != "" {
|
||||||
|
claims["name"] = userdata.Name
|
||||||
|
}
|
||||||
|
if userdata.GivenName != "" {
|
||||||
|
claims["given_name"] = userdata.GivenName
|
||||||
|
}
|
||||||
|
if userdata.FamilyName != "" {
|
||||||
|
claims["family_name"] = userdata.FamilyName
|
||||||
|
}
|
||||||
|
if userdata.MiddleName != "" {
|
||||||
|
claims["middle_name"] = userdata.MiddleName
|
||||||
|
}
|
||||||
|
if userdata.Nickname != "" {
|
||||||
|
claims["nickname"] = userdata.Nickname
|
||||||
|
}
|
||||||
|
if userdata.PreferredUsername != "" {
|
||||||
|
claims["preferred_username"] = userdata.PreferredUsername
|
||||||
|
}
|
||||||
|
if userdata.Profile != "" {
|
||||||
|
claims["profile"] = userdata.Profile
|
||||||
|
}
|
||||||
|
if userdata.Picture != "" {
|
||||||
|
claims["picture"] = userdata.Picture
|
||||||
|
}
|
||||||
|
if userdata.Website != "" {
|
||||||
|
claims["website"] = userdata.Website
|
||||||
|
}
|
||||||
|
if userdata.Email != "" {
|
||||||
|
claims["email"] = userdata.Email
|
||||||
|
}
|
||||||
|
if userdata.EmailVerified != nil {
|
||||||
|
claims["email_verified"] = *userdata.EmailVerified
|
||||||
|
}
|
||||||
|
if userdata.Gender != "" {
|
||||||
|
claims["gender"] = userdata.Gender
|
||||||
|
}
|
||||||
|
if userdata.Birthdate != "" {
|
||||||
|
claims["birthdate"] = userdata.Birthdate
|
||||||
|
}
|
||||||
|
if userdata.Zoneinfo != "" {
|
||||||
|
claims["zoneinfo"] = userdata.Zoneinfo
|
||||||
|
}
|
||||||
|
if userdata.Locale != "" {
|
||||||
|
claims["locale"] = userdata.Locale
|
||||||
|
}
|
||||||
|
if userdata.PhoneNumber != "" {
|
||||||
|
claims["phone_number"] = userdata.PhoneNumber
|
||||||
|
}
|
||||||
|
if userdata.PhoneNumberVerified != nil {
|
||||||
|
claims["phone_number_verified"] = *userdata.PhoneNumberVerified
|
||||||
|
}
|
||||||
|
if userdata.Address != nil {
|
||||||
|
claims["address"] = userdata.Address
|
||||||
|
}
|
||||||
|
if userdata.UpdatedAt != nil {
|
||||||
|
claims["updated_at"] = *userdata.UpdatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add scope if provided (useful for determining which claims to include)
|
||||||
|
if scope != "" {
|
||||||
|
claims["scope"] = scope
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create token with claims
|
||||||
|
token := jwt.NewWithClaims(getSigningMethod(s.config.Token.AccessTokenSigningAlg), claims)
|
||||||
|
|
||||||
|
// Set key ID in header
|
||||||
|
token.Header["kid"] = s.GetKeyID()
|
||||||
|
|
||||||
|
// Set token type in header
|
||||||
|
token.Header["typ"] = "JWT"
|
||||||
|
|
||||||
|
// Sign token with private key
|
||||||
|
signedToken, err := token.SignedString(s.signingCerts.SigningKey)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to sign ID token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return signedToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
// signJWTToken signs a JWT token using the configured signing algorithm
|
// signJWTToken signs a JWT token using the configured signing algorithm
|
||||||
func (s *Service) signJWTToken(tokenType, clientID, scope, subject string, expiresIn int) (string, error) {
|
func (s *Service) signJWTToken(tokenType, clientID, scope, subject string, expiresIn int) (string, error) {
|
||||||
if s.signingCerts == nil || s.signingCerts.SigningKey == nil {
|
if s.signingCerts == nil || s.signingCerts.SigningKey == nil {
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
gonanoid "github.com/matoous/go-nanoid/v2"
|
||||||
"github.com/yaoapp/yao/openapi/oauth/types"
|
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
)
|
)
|
||||||
|
|
@ -242,7 +243,79 @@ func (s *Service) ValidateTokenBinding(ctx context.Context, token string, bindin
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Public Token helper methods for internal use
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// MakeAccessToken generates a new access token with specific parameters and stores it
|
||||||
|
func (s *Service) MakeAccessToken(clientID, scope, subject string, expiresIn int) (string, error) {
|
||||||
|
return s.generateAccessTokenWithScope(clientID, scope, subject, expiresIn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeRefreshToken generates a new refresh token with specific parameters and stores it
|
||||||
|
func (s *Service) MakeRefreshToken(clientID, scope, subject string) (string, error) {
|
||||||
|
return s.generateRefreshToken(clientID, scope, subject)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subject converts a userID to a subject using NanoID fingerprint
|
||||||
|
func (s *Service) Subject(clientID, userID string) (string, error) {
|
||||||
|
// Check if mapping already exists for this clientID+userID
|
||||||
|
mappingKey := s.userMappingKey(clientID, userID)
|
||||||
|
if existingNanoID, exists := s.store.Get(mappingKey); exists {
|
||||||
|
if nanoIDStr, ok := existingNanoID.(string); ok {
|
||||||
|
return nanoIDStr, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
maxRetries := 5
|
||||||
|
for i := 0; i < maxRetries; i++ {
|
||||||
|
// Generate 12-character NanoID
|
||||||
|
nanoID, err := generateNanoID(12)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to generate NanoID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this NanoID already exists for this client
|
||||||
|
key := s.userFingerprintKey(clientID, nanoID)
|
||||||
|
_, exists := s.store.Get(key)
|
||||||
|
if !exists {
|
||||||
|
// Store both mappings
|
||||||
|
// 1. clientID:nanoID -> userID
|
||||||
|
if err := s.store.Set(key, userID, 0); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to store user fingerprint: %w", err)
|
||||||
|
}
|
||||||
|
// 2. clientID:userID -> nanoID (for checking existing mapping)
|
||||||
|
if err := s.store.Set(mappingKey, nanoID, 0); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to store user mapping: %w", err)
|
||||||
|
}
|
||||||
|
return nanoID, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("failed to generate unique NanoID after %d retries", maxRetries)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserID converts a subject to a userID using fingerprint lookup
|
||||||
|
func (s *Service) UserID(clientID, subject string) (string, error) {
|
||||||
|
key := s.userFingerprintKey(clientID, subject)
|
||||||
|
userID, exists := s.store.Get(key)
|
||||||
|
if !exists {
|
||||||
|
return "", fmt.Errorf("fingerprint not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
userIDStr, ok := userID.(string)
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("invalid userID format")
|
||||||
|
}
|
||||||
|
|
||||||
|
return userIDStr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeAuthorizationCode generates a new authorization code with specific parameters and stores it
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
// Helper methods
|
// Helper methods
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
// validateAudience validates if an audience is valid
|
// validateAudience validates if an audience is valid
|
||||||
func (s *Service) validateAudience(audience string) error {
|
func (s *Service) validateAudience(audience string) error {
|
||||||
|
|
@ -520,6 +593,16 @@ func (s *Service) accessTokenKey(accessToken string) string {
|
||||||
return fmt.Sprintf("%soauth:access_token:%s", s.prefix, accessToken)
|
return fmt.Sprintf("%soauth:access_token:%s", s.prefix, accessToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// userFingerprintKey generates a key for user fingerprint storage
|
||||||
|
func (s *Service) userFingerprintKey(clientID, nanoID string) string {
|
||||||
|
return fmt.Sprintf("%soauth:user_fingerprint:%s:%s", s.prefix, clientID, nanoID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// userMappingKey generates a key for reverse user mapping (clientID+userID -> nanoID)
|
||||||
|
func (s *Service) userMappingKey(clientID, userID string) string {
|
||||||
|
return fmt.Sprintf("%soauth:user_mapping:%s:%s", s.prefix, clientID, userID)
|
||||||
|
}
|
||||||
|
|
||||||
// generateExchangedToken generates a new token for token exchange
|
// generateExchangedToken generates a new token for token exchange
|
||||||
func (s *Service) generateExchangedToken(subjectToken string, audience string) (string, error) {
|
func (s *Service) generateExchangedToken(subjectToken string, audience string) (string, error) {
|
||||||
// Extract token prefix for tracking purposes
|
// Extract token prefix for tracking purposes
|
||||||
|
|
@ -556,3 +639,21 @@ func (s *Service) generateToken(tokenType string, clientID string) (string, erro
|
||||||
|
|
||||||
return fmt.Sprintf("%s_%s_%s_%s", tokenType, clientID, timestamp, randomPart), nil
|
return fmt.Sprintf("%s_%s_%s_%s", tokenType, clientID, timestamp, randomPart), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// User Fingerprint Methods
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// generateNanoID generates a Nano ID using the library
|
||||||
|
func generateNanoID(length int) (string, error) {
|
||||||
|
// URL-safe alphabet (no ambiguous characters like 0/O, 1/l/I)
|
||||||
|
const alphabet = "23456789ABCDEFGHJKMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz"
|
||||||
|
return gonanoid.Generate(alphabet, length)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteUserFingerprint removes a fingerprint mapping
|
||||||
|
func (s *Service) DeleteUserFingerprint(clientID, nanoID string) error {
|
||||||
|
key := s.userFingerprintKey(clientID, nanoID)
|
||||||
|
s.store.Del(key)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,304 @@
|
||||||
package types
|
package types
|
||||||
|
|
||||||
// Map converts the OIDCUserInfo to a map[string]interface{}
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Map converts the OIDCUserInfo to a map[string]interface{}, excluding empty values
|
||||||
func (user OIDCUserInfo) Map() map[string]interface{} {
|
func (user OIDCUserInfo) Map() map[string]interface{} {
|
||||||
return map[string]interface{}{
|
result := make(map[string]interface{})
|
||||||
"sub": user.Sub,
|
|
||||||
"name": user.Name,
|
// Only add non-empty string fields
|
||||||
"given_name": user.GivenName,
|
if user.Sub != "" {
|
||||||
"family_name": user.FamilyName,
|
result["sub"] = user.Sub
|
||||||
"middle_name": user.MiddleName,
|
}
|
||||||
"nickname": user.Nickname,
|
if user.Name != "" {
|
||||||
"preferred_username": user.PreferredUsername,
|
result["name"] = user.Name
|
||||||
"profile": user.Profile,
|
}
|
||||||
"picture": user.Picture,
|
if user.GivenName != "" {
|
||||||
"website": user.Website,
|
result["given_name"] = user.GivenName
|
||||||
"email": user.Email,
|
}
|
||||||
"email_verified": user.EmailVerified,
|
if user.FamilyName != "" {
|
||||||
"gender": user.Gender,
|
result["family_name"] = user.FamilyName
|
||||||
|
}
|
||||||
|
if user.MiddleName != "" {
|
||||||
|
result["middle_name"] = user.MiddleName
|
||||||
|
}
|
||||||
|
if user.Nickname != "" {
|
||||||
|
result["nickname"] = user.Nickname
|
||||||
|
}
|
||||||
|
if user.PreferredUsername != "" {
|
||||||
|
result["preferred_username"] = user.PreferredUsername
|
||||||
|
}
|
||||||
|
if user.Profile != "" {
|
||||||
|
result["profile"] = user.Profile
|
||||||
|
}
|
||||||
|
if user.Picture != "" {
|
||||||
|
result["picture"] = user.Picture
|
||||||
|
}
|
||||||
|
if user.Website != "" {
|
||||||
|
result["website"] = user.Website
|
||||||
|
}
|
||||||
|
if user.Email != "" {
|
||||||
|
result["email"] = user.Email
|
||||||
|
}
|
||||||
|
if user.Gender != "" {
|
||||||
|
result["gender"] = user.Gender
|
||||||
|
}
|
||||||
|
if user.Birthdate != "" {
|
||||||
|
result["birthdate"] = user.Birthdate
|
||||||
|
}
|
||||||
|
if user.Zoneinfo != "" {
|
||||||
|
result["zoneinfo"] = user.Zoneinfo
|
||||||
|
}
|
||||||
|
if user.Locale != "" {
|
||||||
|
result["locale"] = user.Locale
|
||||||
|
}
|
||||||
|
if user.PhoneNumber != "" {
|
||||||
|
result["phone_number"] = user.PhoneNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only add non-nil boolean pointer fields
|
||||||
|
if user.EmailVerified != nil {
|
||||||
|
result["email_verified"] = user.EmailVerified
|
||||||
|
}
|
||||||
|
if user.PhoneNumberVerified != nil {
|
||||||
|
result["phone_number_verified"] = user.PhoneNumberVerified
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert and add UpdatedAt if not nil
|
||||||
|
if converted := unixToMySQL(user.UpdatedAt); converted != nil {
|
||||||
|
result["updated_at"] = converted
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add address if present and has content
|
||||||
|
if user.Address != nil {
|
||||||
|
addressMap := make(map[string]interface{})
|
||||||
|
if user.Address.Formatted != "" {
|
||||||
|
addressMap["formatted"] = user.Address.Formatted
|
||||||
|
}
|
||||||
|
if user.Address.StreetAddress != "" {
|
||||||
|
addressMap["street_address"] = user.Address.StreetAddress
|
||||||
|
}
|
||||||
|
if user.Address.Locality != "" {
|
||||||
|
addressMap["locality"] = user.Address.Locality
|
||||||
|
}
|
||||||
|
if user.Address.Region != "" {
|
||||||
|
addressMap["region"] = user.Address.Region
|
||||||
|
}
|
||||||
|
if user.Address.PostalCode != "" {
|
||||||
|
addressMap["postal_code"] = user.Address.PostalCode
|
||||||
|
}
|
||||||
|
if user.Address.Country != "" {
|
||||||
|
addressMap["country"] = user.Address.Country
|
||||||
|
}
|
||||||
|
if len(addressMap) > 0 {
|
||||||
|
result["address"] = addressMap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include raw data if available
|
||||||
|
// if user.Raw != nil {
|
||||||
|
// // Merge raw data, but let structured fields take precedence
|
||||||
|
// for k, v := range user.Raw {
|
||||||
|
// if _, exists := result[k]; !exists && v != nil && v != "" {
|
||||||
|
// result[k] = v
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeOIDCUserInfo creates a new OIDCUserInfo from a map[string]interface{}
|
||||||
|
func MakeOIDCUserInfo(user map[string]interface{}) *OIDCUserInfo {
|
||||||
|
userInfo := &OIDCUserInfo{
|
||||||
|
Raw: user, // Store original response
|
||||||
|
}
|
||||||
|
|
||||||
|
// String fields with safe type assertion
|
||||||
|
if sub, ok := user["sub"].(string); ok {
|
||||||
|
userInfo.Sub = sub
|
||||||
|
}
|
||||||
|
if name, ok := user["name"].(string); ok {
|
||||||
|
userInfo.Name = name
|
||||||
|
}
|
||||||
|
if givenName, ok := user["given_name"].(string); ok {
|
||||||
|
userInfo.GivenName = givenName
|
||||||
|
}
|
||||||
|
if familyName, ok := user["family_name"].(string); ok {
|
||||||
|
userInfo.FamilyName = familyName
|
||||||
|
}
|
||||||
|
if middleName, ok := user["middle_name"].(string); ok {
|
||||||
|
userInfo.MiddleName = middleName
|
||||||
|
}
|
||||||
|
if nickname, ok := user["nickname"].(string); ok {
|
||||||
|
userInfo.Nickname = nickname
|
||||||
|
}
|
||||||
|
if preferredUsername, ok := user["preferred_username"].(string); ok {
|
||||||
|
userInfo.PreferredUsername = preferredUsername
|
||||||
|
}
|
||||||
|
if profile, ok := user["profile"].(string); ok {
|
||||||
|
userInfo.Profile = profile
|
||||||
|
}
|
||||||
|
if picture, ok := user["picture"].(string); ok {
|
||||||
|
userInfo.Picture = picture
|
||||||
|
}
|
||||||
|
if website, ok := user["website"].(string); ok {
|
||||||
|
userInfo.Website = website
|
||||||
|
}
|
||||||
|
if email, ok := user["email"].(string); ok {
|
||||||
|
userInfo.Email = email
|
||||||
|
}
|
||||||
|
if gender, ok := user["gender"].(string); ok {
|
||||||
|
userInfo.Gender = gender
|
||||||
|
}
|
||||||
|
if birthdate, ok := user["birthdate"].(string); ok {
|
||||||
|
userInfo.Birthdate = birthdate
|
||||||
|
}
|
||||||
|
if zoneinfo, ok := user["zoneinfo"].(string); ok {
|
||||||
|
userInfo.Zoneinfo = zoneinfo
|
||||||
|
}
|
||||||
|
if locale, ok := user["locale"].(string); ok {
|
||||||
|
userInfo.Locale = locale
|
||||||
|
}
|
||||||
|
if phoneNumber, ok := user["phone_number"].(string); ok {
|
||||||
|
userInfo.PhoneNumber = phoneNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
// Boolean pointer fields
|
||||||
|
if emailVerified, ok := user["email_verified"].(bool); ok {
|
||||||
|
userInfo.EmailVerified = &emailVerified
|
||||||
|
}
|
||||||
|
if phoneVerified, ok := user["phone_number_verified"].(bool); ok {
|
||||||
|
userInfo.PhoneNumberVerified = &phoneVerified
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updated_at field
|
||||||
|
if updatedAt, ok := user["updated_at"]; ok {
|
||||||
|
if converted := toUnixTimestamp(updatedAt); converted != nil {
|
||||||
|
if unixTime, ok := converted.(int64); ok {
|
||||||
|
userInfo.UpdatedAt = &unixTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Address field (nested object)
|
||||||
|
if addressData, ok := user["address"].(map[string]interface{}); ok {
|
||||||
|
address := &OIDCAddress{}
|
||||||
|
if formatted, ok := addressData["formatted"].(string); ok {
|
||||||
|
address.Formatted = formatted
|
||||||
|
}
|
||||||
|
if streetAddress, ok := addressData["street_address"].(string); ok {
|
||||||
|
address.StreetAddress = streetAddress
|
||||||
|
}
|
||||||
|
if locality, ok := addressData["locality"].(string); ok {
|
||||||
|
address.Locality = locality
|
||||||
|
}
|
||||||
|
if region, ok := addressData["region"].(string); ok {
|
||||||
|
address.Region = region
|
||||||
|
}
|
||||||
|
if postalCode, ok := addressData["postal_code"].(string); ok {
|
||||||
|
address.PostalCode = postalCode
|
||||||
|
}
|
||||||
|
if country, ok := addressData["country"].(string); ok {
|
||||||
|
address.Country = country
|
||||||
|
}
|
||||||
|
userInfo.Address = address
|
||||||
|
}
|
||||||
|
|
||||||
|
return userInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// unixToMySQL converts interface{} to MySQL DATETIME string
|
||||||
|
func unixToMySQL(val interface{}) interface{} {
|
||||||
|
if val == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var unixTime int64
|
||||||
|
switch v := val.(type) {
|
||||||
|
case int64:
|
||||||
|
unixTime = v
|
||||||
|
case *int64:
|
||||||
|
if v == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
unixTime = *v
|
||||||
|
case int:
|
||||||
|
unixTime = int64(v)
|
||||||
|
case float64:
|
||||||
|
unixTime = int64(v)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.Unix(unixTime, 0).UTC().Format("2006-01-02 15:04:05")
|
||||||
|
}
|
||||||
|
|
||||||
|
// mysqlToUnix converts interface{} to Unix timestamp
|
||||||
|
func mysqlToUnix(val interface{}) interface{} {
|
||||||
|
if val == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var dateTime string
|
||||||
|
switch v := val.(type) {
|
||||||
|
case string:
|
||||||
|
dateTime = v
|
||||||
|
case *string:
|
||||||
|
if v == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
dateTime = *v
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if dateTime == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try MySQL DATETIME format
|
||||||
|
if t, err := time.Parse("2006-01-02 15:04:05", dateTime); err == nil {
|
||||||
|
return t.Unix()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try ISO format as fallback
|
||||||
|
if t, err := time.Parse("2006-01-02T15:04:05Z", dateTime); err == nil {
|
||||||
|
return t.Unix()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// toUnixTimestamp converts any interface{} to Unix timestamp
|
||||||
|
func toUnixTimestamp(val interface{}) interface{} {
|
||||||
|
if val == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v := val.(type) {
|
||||||
|
case int64:
|
||||||
|
return v
|
||||||
|
case *int64:
|
||||||
|
if v == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return *v
|
||||||
|
case int:
|
||||||
|
return int64(v)
|
||||||
|
case float64:
|
||||||
|
return int64(v)
|
||||||
|
case string:
|
||||||
|
// Handle MySQL DATETIME or ISO format
|
||||||
|
return mysqlToUnix(v)
|
||||||
|
case *string:
|
||||||
|
if v == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return mysqlToUnix(*v)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package signin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/yaoapp/kun/log"
|
"github.com/yaoapp/kun/log"
|
||||||
"github.com/yaoapp/yao/openapi/oauth"
|
"github.com/yaoapp/yao/openapi/oauth"
|
||||||
|
|
@ -93,13 +94,46 @@ func LoginByUserID(userid string, ip string) (*LoginResponse, error) {
|
||||||
log.Warn("Failed to update last login: %s", err.Error())
|
log.Warn("Failed to update last login: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var scopes []string
|
||||||
|
if v, ok := user["scopes"].([]string); ok {
|
||||||
|
scopes = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Config form app.yao config ()
|
||||||
|
clientID := "1234567890"
|
||||||
|
oidcExpiresIn := 3600
|
||||||
|
accessTokenExpiresIn := 3600
|
||||||
|
|
||||||
|
subject, err := oauth.OAuth.Subject(clientID, userid)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("Failed to store user fingerprint: %s", err.Error())
|
||||||
|
}
|
||||||
|
oidcUserInfo := oauthtypes.MakeOIDCUserInfo(user)
|
||||||
|
oidcUserInfo.Sub = subject
|
||||||
|
|
||||||
|
// OIDC Token
|
||||||
|
oidcToken, err := oauth.OAuth.SignIDToken(clientID, strings.Join(scopes, " "), oidcExpiresIn, oidcUserInfo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Access Token
|
||||||
|
accessToken, err := oauth.OAuth.MakeAccessToken(clientID, strings.Join(scopes, " "), subject, accessTokenExpiresIn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh Token
|
||||||
|
refreshToken, err := oauth.OAuth.MakeRefreshToken(clientID, strings.Join(scopes, " "), subject)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return &LoginResponse{
|
return &LoginResponse{
|
||||||
AccessToken: "mock_access_token",
|
AccessToken: accessToken,
|
||||||
IDToken: "mock_id_token",
|
IDToken: oidcToken,
|
||||||
RefreshToken: "mock_refresh_token",
|
RefreshToken: refreshToken,
|
||||||
ExpiresIn: 3600,
|
ExpiresIn: accessTokenExpiresIn,
|
||||||
TokenType: "Bearer",
|
TokenType: "Bearer",
|
||||||
Scope: "openid profile email",
|
Scope: strings.Join(scopes, " "),
|
||||||
User: user,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -153,13 +153,12 @@ type OIDCAddress = oauthtypes.OIDCAddress
|
||||||
|
|
||||||
// LoginResponse represents the response for login
|
// LoginResponse represents the response for login
|
||||||
type LoginResponse struct {
|
type LoginResponse struct {
|
||||||
AccessToken string `json:"access_token"`
|
AccessToken string `json:"access_token"`
|
||||||
IDToken string `json:"id_token,omitempty"`
|
IDToken string `json:"id_token,omitempty"`
|
||||||
RefreshToken string `json:"refresh_token,omitempty"`
|
RefreshToken string `json:"refresh_token,omitempty"`
|
||||||
ExpiresIn int `json:"expires_in,omitempty"`
|
ExpiresIn int `json:"expires_in,omitempty"`
|
||||||
TokenType string `json:"token_type,omitempty"`
|
TokenType string `json:"token_type,omitempty"`
|
||||||
Scope string `json:"scope,omitempty"`
|
Scope string `json:"scope,omitempty"`
|
||||||
User map[string]interface{} `json:"user,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Built-in preset mapping types
|
// Built-in preset mapping types
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue