mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 14:06:36 +08:00
63cd906cfc
- Merge all files inside openflare-server to the repository root directory. - Relocate agent, relay, and flared subprojects from internal/ to internal/apps/. - Combine docker-compose files and update build context paths to root. - Update GitHub workflows and Dockerfiles to refer to new directories and package names. - Rewrite Go package imports across all files. - Resolve database renew test race condition and clean up docs.
142 lines
3.8 KiB
Go
142 lines
3.8 KiB
Go
// Copyright 2026 Arctel.net
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package oauth
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/Rain-kl/Wavelet/internal/db"
|
|
"github.com/Rain-kl/Wavelet/internal/model"
|
|
"github.com/coreos/go-oidc/v3/oidc"
|
|
"golang.org/x/oauth2"
|
|
)
|
|
|
|
func uniqueUsername(ctx context.Context, base string) (string, error) {
|
|
base = strings.TrimSpace(base)
|
|
if base == "" {
|
|
base = "user"
|
|
}
|
|
|
|
var existingUsernames []string
|
|
if err := db.DB(ctx).Model(&model.User{}).
|
|
Where("username = ? OR username LIKE ?", base, base+"-%").
|
|
Pluck("username", &existingUsernames).Error; err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// 将现有的用户名放入 map 中,以便 O(1) 查找
|
|
exists := make(map[string]bool, len(existingUsernames))
|
|
for _, u := range existingUsernames {
|
|
exists[strings.ToLower(u)] = true
|
|
}
|
|
|
|
// 检查 base 是否被占用
|
|
if !exists[strings.ToLower(base)] {
|
|
return base, nil
|
|
}
|
|
|
|
// 顺序查找第一个可用的带后缀用户名
|
|
for i := 1; i <= 1000; i++ {
|
|
candidate := fmt.Sprintf("%s-%d", base, i)
|
|
if !exists[strings.ToLower(candidate)] {
|
|
return candidate, nil
|
|
}
|
|
}
|
|
|
|
return "", errors.New(errUsernameGenerateFailed)
|
|
}
|
|
|
|
func buildOAuthUserInfo(ctx context.Context, source *model.AuthSource, code string, nonce string, redirectURL string) (*model.OAuthUserInfo, error) {
|
|
authConfig, verifier, err := buildOAuthConfig(ctx, source, redirectURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
token, err := authConfig.Exchange(ctx, code)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
userInfo := &model.OAuthUserInfo{Active: true}
|
|
if verifier != nil {
|
|
if verifyErr := verifyIDToken(ctx, verifier, token, nonce, userInfo); verifyErr != nil {
|
|
return nil, verifyErr
|
|
}
|
|
}
|
|
|
|
if userInfo.Username == "" && userInfo.PreferredUsername != "" {
|
|
userInfo.Username = userInfo.PreferredUsername
|
|
}
|
|
if userInfo.Username == "" && userInfo.Email != "" {
|
|
userInfo.Username = strings.Split(userInfo.Email, "@")[0]
|
|
}
|
|
if userInfo.Username == "" && userInfo.Sub != "" {
|
|
userInfo.Username = userInfo.Sub
|
|
}
|
|
if userInfo.Name == "" {
|
|
userInfo.Name = userInfo.Username
|
|
}
|
|
|
|
return userInfo, nil
|
|
}
|
|
|
|
// verifyIDToken 验证 OIDC ID Token 并将 Claims 解析到 userInfo
|
|
func verifyIDToken(ctx context.Context, verifier *oidc.IDTokenVerifier, token *oauth2.Token, nonce string, userInfo *model.OAuthUserInfo) error {
|
|
rawIDToken, ok := token.Extra("id_token").(string)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
idToken, verifyErr := verifier.Verify(ctx, rawIDToken)
|
|
if verifyErr != nil {
|
|
return fmt.Errorf(errIDTokenVerifyFailedFormat, errIDTokenVerifyFailed, verifyErr)
|
|
}
|
|
if nonce != "" && idToken.Nonce != nonce {
|
|
return errors.New(errNonceMismatch)
|
|
}
|
|
if claimsErr := idToken.Claims(userInfo); claimsErr != nil {
|
|
return claimsErr
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func normalizeOAuthUserInfo(userInfo *model.OAuthUserInfo) error {
|
|
userInfo.Username = strings.TrimSpace(userInfo.Username)
|
|
userInfo.PreferredUsername = strings.TrimSpace(userInfo.PreferredUsername)
|
|
userInfo.Email = strings.TrimSpace(userInfo.Email)
|
|
userInfo.Name = strings.TrimSpace(userInfo.Name)
|
|
userInfo.AvatarURL = strings.TrimSpace(userInfo.AvatarURL)
|
|
|
|
if userInfo.Username == "" && userInfo.PreferredUsername != "" {
|
|
userInfo.Username = userInfo.PreferredUsername
|
|
}
|
|
if userInfo.Username == "" && userInfo.Email != "" {
|
|
userInfo.Username = strings.Split(userInfo.Email, "@")[0]
|
|
}
|
|
if userInfo.Username == "" && userInfo.Sub != "" {
|
|
userInfo.Username = userInfo.Sub
|
|
}
|
|
if userInfo.Username == "" {
|
|
return errors.New(errUsernameFromSourceFailed)
|
|
}
|
|
if userInfo.Name == "" {
|
|
userInfo.Name = userInfo.Username
|
|
}
|
|
if !userInfo.Active {
|
|
userInfo.Active = true
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func buildCallbackResult(user *model.User, status string) OAuthCallbackResult {
|
|
result := OAuthCallbackResult{Status: status}
|
|
if user != nil {
|
|
info := BuildBasicUserInfo(user, false)
|
|
result.User = &info
|
|
}
|
|
return result
|
|
}
|