feat: add oidc support (#1725)
* feat: add the ui for configuring the third-party standard OAuth2.0/OIDC. - update SystemSetting.js - add setup ui - add configuration * feat: add the ui for "allow the OAuth 2.0 to login" - update SystemSetting.js * feat: add OAuth 2.0 web ui and its process functions - update common.js - update AuthLogin.js - update config.js * fix: missing "Userinfo" endpoint configuration entry, used by OAuth clients to request user information from the IdP. - update config.js - update SystemSetting.js * feat: updated the icons for Lark and OIDC to match the style of the icons for WeChat, EMail, GitHub. - update lark.svg - new oidc.svg * refactor: Changing OAuth 2.0 to OIDC * feat: add OIDC login method * feat: Add support for OIDC login to the backend * fix: Change the AppId and AppSecret on the Web UI to the standard usage: ClientId, ClientSecret. * feat: Support quick configuration of OIDC through Well-Known Discovery Endpoint * feat: Standardize terminology, add well-known configuration - Change the AppId and AppSecret on the Server End to the standard usage: ClientId, ClientSecret. - add Well-Known configuration to store in database, no actual use in server end but store and display in web ui only
This commit is contained in:
parent
649ecbf29c
commit
99c8c77504
@ -35,6 +35,7 @@ var PasswordLoginEnabled = true
|
|||||||
var PasswordRegisterEnabled = true
|
var PasswordRegisterEnabled = true
|
||||||
var EmailVerificationEnabled = false
|
var EmailVerificationEnabled = false
|
||||||
var GitHubOAuthEnabled = false
|
var GitHubOAuthEnabled = false
|
||||||
|
var OidcEnabled = false
|
||||||
var WeChatAuthEnabled = false
|
var WeChatAuthEnabled = false
|
||||||
var TurnstileCheckEnabled = false
|
var TurnstileCheckEnabled = false
|
||||||
var RegisterEnabled = true
|
var RegisterEnabled = true
|
||||||
@ -70,6 +71,13 @@ var GitHubClientSecret = ""
|
|||||||
var LarkClientId = ""
|
var LarkClientId = ""
|
||||||
var LarkClientSecret = ""
|
var LarkClientSecret = ""
|
||||||
|
|
||||||
|
var OidcClientId = ""
|
||||||
|
var OidcClientSecret = ""
|
||||||
|
var OidcWellKnown = ""
|
||||||
|
var OidcAuthorizationEndpoint = ""
|
||||||
|
var OidcTokenEndpoint = ""
|
||||||
|
var OidcUserinfoEndpoint = ""
|
||||||
|
|
||||||
var WeChatServerAddress = ""
|
var WeChatServerAddress = ""
|
||||||
var WeChatServerToken = ""
|
var WeChatServerToken = ""
|
||||||
var WeChatAccountQRCodeImageURL = ""
|
var WeChatAccountQRCodeImageURL = ""
|
||||||
|
225
controller/auth/oidc.go
Normal file
225
controller/auth/oidc.go
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-contrib/sessions"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/songquanpeng/one-api/common/config"
|
||||||
|
"github.com/songquanpeng/one-api/common/logger"
|
||||||
|
"github.com/songquanpeng/one-api/controller"
|
||||||
|
"github.com/songquanpeng/one-api/model"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OidcResponse struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
IDToken string `json:"id_token"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
TokenType string `json:"token_type"`
|
||||||
|
ExpiresIn int `json:"expires_in"`
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OidcUser struct {
|
||||||
|
OpenID string `json:"sub"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
PreferredUsername string `json:"preferred_username"`
|
||||||
|
Picture string `json:"picture"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getOidcUserInfoByCode(code string) (*OidcUser, error) {
|
||||||
|
if code == "" {
|
||||||
|
return nil, errors.New("无效的参数")
|
||||||
|
}
|
||||||
|
values := map[string]string{
|
||||||
|
"client_id": config.OidcClientId,
|
||||||
|
"client_secret": config.OidcClientSecret,
|
||||||
|
"code": code,
|
||||||
|
"grant_type": "authorization_code",
|
||||||
|
"redirect_uri": fmt.Sprintf("%s/oauth/oidc", config.ServerAddress),
|
||||||
|
}
|
||||||
|
jsonData, err := json.Marshal(values)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest("POST", config.OidcTokenEndpoint, bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
client := http.Client{
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
}
|
||||||
|
res, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
logger.SysLog(err.Error())
|
||||||
|
return nil, errors.New("无法连接至 OIDC 服务器,请稍后重试!")
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
var oidcResponse OidcResponse
|
||||||
|
err = json.NewDecoder(res.Body).Decode(&oidcResponse)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req, err = http.NewRequest("GET", config.OidcUserinfoEndpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", "Bearer "+oidcResponse.AccessToken)
|
||||||
|
res2, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
logger.SysLog(err.Error())
|
||||||
|
return nil, errors.New("无法连接至 OIDC 服务器,请稍后重试!")
|
||||||
|
}
|
||||||
|
var oidcUser OidcUser
|
||||||
|
err = json.NewDecoder(res2.Body).Decode(&oidcUser)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &oidcUser, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func OidcAuth(c *gin.Context) {
|
||||||
|
session := sessions.Default(c)
|
||||||
|
state := c.Query("state")
|
||||||
|
if state == "" || session.Get("oauth_state") == nil || state != session.Get("oauth_state").(string) {
|
||||||
|
c.JSON(http.StatusForbidden, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "state is empty or not same",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
username := session.Get("username")
|
||||||
|
if username != nil {
|
||||||
|
OidcBind(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !config.OidcEnabled {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "管理员未开启通过 OIDC 登录以及注册",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
code := c.Query("code")
|
||||||
|
oidcUser, err := getOidcUserInfoByCode(code)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user := model.User{
|
||||||
|
OidcId: oidcUser.OpenID,
|
||||||
|
}
|
||||||
|
if model.IsOidcIdAlreadyTaken(user.OidcId) {
|
||||||
|
err := user.FillUserByOidcId()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if config.RegisterEnabled {
|
||||||
|
user.Email = oidcUser.Email
|
||||||
|
if oidcUser.PreferredUsername != "" {
|
||||||
|
user.Username = oidcUser.PreferredUsername
|
||||||
|
} else {
|
||||||
|
user.Username = "oidc_" + strconv.Itoa(model.GetMaxUserId()+1)
|
||||||
|
}
|
||||||
|
if oidcUser.Name != "" {
|
||||||
|
user.DisplayName = oidcUser.Name
|
||||||
|
} else {
|
||||||
|
user.DisplayName = "OIDC User"
|
||||||
|
}
|
||||||
|
err := user.Insert(0)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "管理员关闭了新用户注册",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Status != model.UserStatusEnabled {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": "用户已被封禁",
|
||||||
|
"success": false,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
controller.SetupLogin(&user, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func OidcBind(c *gin.Context) {
|
||||||
|
if !config.OidcEnabled {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "管理员未开启通过 OIDC 登录以及注册",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
code := c.Query("code")
|
||||||
|
oidcUser, err := getOidcUserInfoByCode(code)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user := model.User{
|
||||||
|
OidcId: oidcUser.OpenID,
|
||||||
|
}
|
||||||
|
if model.IsOidcIdAlreadyTaken(user.OidcId) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "该 OIDC 账户已被绑定",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
session := sessions.Default(c)
|
||||||
|
id := session.Get("id")
|
||||||
|
// id := c.GetInt("id") // critical bug!
|
||||||
|
user.Id = id.(int)
|
||||||
|
err = user.FillUserById()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user.OidcId = oidcUser.OpenID
|
||||||
|
err = user.Update(false)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "bind",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
@ -18,24 +18,30 @@ func GetStatus(c *gin.Context) {
|
|||||||
"success": true,
|
"success": true,
|
||||||
"message": "",
|
"message": "",
|
||||||
"data": gin.H{
|
"data": gin.H{
|
||||||
"version": common.Version,
|
"version": common.Version,
|
||||||
"start_time": common.StartTime,
|
"start_time": common.StartTime,
|
||||||
"email_verification": config.EmailVerificationEnabled,
|
"email_verification": config.EmailVerificationEnabled,
|
||||||
"github_oauth": config.GitHubOAuthEnabled,
|
"github_oauth": config.GitHubOAuthEnabled,
|
||||||
"github_client_id": config.GitHubClientId,
|
"github_client_id": config.GitHubClientId,
|
||||||
"lark_client_id": config.LarkClientId,
|
"lark_client_id": config.LarkClientId,
|
||||||
"system_name": config.SystemName,
|
"system_name": config.SystemName,
|
||||||
"logo": config.Logo,
|
"logo": config.Logo,
|
||||||
"footer_html": config.Footer,
|
"footer_html": config.Footer,
|
||||||
"wechat_qrcode": config.WeChatAccountQRCodeImageURL,
|
"wechat_qrcode": config.WeChatAccountQRCodeImageURL,
|
||||||
"wechat_login": config.WeChatAuthEnabled,
|
"wechat_login": config.WeChatAuthEnabled,
|
||||||
"server_address": config.ServerAddress,
|
"server_address": config.ServerAddress,
|
||||||
"turnstile_check": config.TurnstileCheckEnabled,
|
"turnstile_check": config.TurnstileCheckEnabled,
|
||||||
"turnstile_site_key": config.TurnstileSiteKey,
|
"turnstile_site_key": config.TurnstileSiteKey,
|
||||||
"top_up_link": config.TopUpLink,
|
"top_up_link": config.TopUpLink,
|
||||||
"chat_link": config.ChatLink,
|
"chat_link": config.ChatLink,
|
||||||
"quota_per_unit": config.QuotaPerUnit,
|
"quota_per_unit": config.QuotaPerUnit,
|
||||||
"display_in_currency": config.DisplayInCurrencyEnabled,
|
"display_in_currency": config.DisplayInCurrencyEnabled,
|
||||||
|
"oidc": config.OidcEnabled,
|
||||||
|
"oidc_client_id": config.OidcClientId,
|
||||||
|
"oidc_well_known": config.OidcWellKnown,
|
||||||
|
"oidc_authorization_endpoint": config.OidcAuthorizationEndpoint,
|
||||||
|
"oidc_token_endpoint": config.OidcTokenEndpoint,
|
||||||
|
"oidc_userinfo_endpoint": config.OidcUserinfoEndpoint,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
@ -28,6 +28,7 @@ func InitOptionMap() {
|
|||||||
config.OptionMap["PasswordRegisterEnabled"] = strconv.FormatBool(config.PasswordRegisterEnabled)
|
config.OptionMap["PasswordRegisterEnabled"] = strconv.FormatBool(config.PasswordRegisterEnabled)
|
||||||
config.OptionMap["EmailVerificationEnabled"] = strconv.FormatBool(config.EmailVerificationEnabled)
|
config.OptionMap["EmailVerificationEnabled"] = strconv.FormatBool(config.EmailVerificationEnabled)
|
||||||
config.OptionMap["GitHubOAuthEnabled"] = strconv.FormatBool(config.GitHubOAuthEnabled)
|
config.OptionMap["GitHubOAuthEnabled"] = strconv.FormatBool(config.GitHubOAuthEnabled)
|
||||||
|
config.OptionMap["OidcEnabled"] = strconv.FormatBool(config.OidcEnabled)
|
||||||
config.OptionMap["WeChatAuthEnabled"] = strconv.FormatBool(config.WeChatAuthEnabled)
|
config.OptionMap["WeChatAuthEnabled"] = strconv.FormatBool(config.WeChatAuthEnabled)
|
||||||
config.OptionMap["TurnstileCheckEnabled"] = strconv.FormatBool(config.TurnstileCheckEnabled)
|
config.OptionMap["TurnstileCheckEnabled"] = strconv.FormatBool(config.TurnstileCheckEnabled)
|
||||||
config.OptionMap["RegisterEnabled"] = strconv.FormatBool(config.RegisterEnabled)
|
config.OptionMap["RegisterEnabled"] = strconv.FormatBool(config.RegisterEnabled)
|
||||||
@ -130,6 +131,8 @@ func updateOptionMap(key string, value string) (err error) {
|
|||||||
config.EmailVerificationEnabled = boolValue
|
config.EmailVerificationEnabled = boolValue
|
||||||
case "GitHubOAuthEnabled":
|
case "GitHubOAuthEnabled":
|
||||||
config.GitHubOAuthEnabled = boolValue
|
config.GitHubOAuthEnabled = boolValue
|
||||||
|
case "OidcEnabled":
|
||||||
|
config.OidcEnabled = boolValue
|
||||||
case "WeChatAuthEnabled":
|
case "WeChatAuthEnabled":
|
||||||
config.WeChatAuthEnabled = boolValue
|
config.WeChatAuthEnabled = boolValue
|
||||||
case "TurnstileCheckEnabled":
|
case "TurnstileCheckEnabled":
|
||||||
@ -176,6 +179,18 @@ func updateOptionMap(key string, value string) (err error) {
|
|||||||
config.LarkClientId = value
|
config.LarkClientId = value
|
||||||
case "LarkClientSecret":
|
case "LarkClientSecret":
|
||||||
config.LarkClientSecret = value
|
config.LarkClientSecret = value
|
||||||
|
case "OidcClientId":
|
||||||
|
config.OidcClientId = value
|
||||||
|
case "OidcClientSecret":
|
||||||
|
config.OidcClientSecret = value
|
||||||
|
case "OidcWellKnown":
|
||||||
|
config.OidcWellKnown = value
|
||||||
|
case "OidcAuthorizationEndpoint":
|
||||||
|
config.OidcAuthorizationEndpoint = value
|
||||||
|
case "OidcTokenEndpoint":
|
||||||
|
config.OidcTokenEndpoint = value
|
||||||
|
case "OidcUserinfoEndpoint":
|
||||||
|
config.OidcUserinfoEndpoint = value
|
||||||
case "Footer":
|
case "Footer":
|
||||||
config.Footer = value
|
config.Footer = value
|
||||||
case "SystemName":
|
case "SystemName":
|
||||||
|
@ -39,6 +39,7 @@ type User struct {
|
|||||||
GitHubId string `json:"github_id" gorm:"column:github_id;index"`
|
GitHubId string `json:"github_id" gorm:"column:github_id;index"`
|
||||||
WeChatId string `json:"wechat_id" gorm:"column:wechat_id;index"`
|
WeChatId string `json:"wechat_id" gorm:"column:wechat_id;index"`
|
||||||
LarkId string `json:"lark_id" gorm:"column:lark_id;index"`
|
LarkId string `json:"lark_id" gorm:"column:lark_id;index"`
|
||||||
|
OidcId string `json:"oidc_id" gorm:"column:oidc_id;index"`
|
||||||
VerificationCode string `json:"verification_code" gorm:"-:all"` // this field is only for Email verification, don't save it to database!
|
VerificationCode string `json:"verification_code" gorm:"-:all"` // this field is only for Email verification, don't save it to database!
|
||||||
AccessToken string `json:"access_token" gorm:"type:char(32);column:access_token;uniqueIndex"` // this token is for system management
|
AccessToken string `json:"access_token" gorm:"type:char(32);column:access_token;uniqueIndex"` // this token is for system management
|
||||||
Quota int64 `json:"quota" gorm:"bigint;default:0"`
|
Quota int64 `json:"quota" gorm:"bigint;default:0"`
|
||||||
@ -245,6 +246,14 @@ func (user *User) FillUserByLarkId() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (user *User) FillUserByOidcId() error {
|
||||||
|
if user.OidcId == "" {
|
||||||
|
return errors.New("oidc id 为空!")
|
||||||
|
}
|
||||||
|
DB.Where(User{OidcId: user.OidcId}).First(user)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (user *User) FillUserByWeChatId() error {
|
func (user *User) FillUserByWeChatId() error {
|
||||||
if user.WeChatId == "" {
|
if user.WeChatId == "" {
|
||||||
return errors.New("WeChat id 为空!")
|
return errors.New("WeChat id 为空!")
|
||||||
@ -277,6 +286,10 @@ func IsLarkIdAlreadyTaken(githubId string) bool {
|
|||||||
return DB.Where("lark_id = ?", githubId).Find(&User{}).RowsAffected == 1
|
return DB.Where("lark_id = ?", githubId).Find(&User{}).RowsAffected == 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsOidcIdAlreadyTaken(oidcId string) bool {
|
||||||
|
return DB.Where("oidc_id = ?", oidcId).Find(&User{}).RowsAffected == 1
|
||||||
|
}
|
||||||
|
|
||||||
func IsUsernameAlreadyTaken(username string) bool {
|
func IsUsernameAlreadyTaken(username string) bool {
|
||||||
return DB.Where("username = ?", username).Find(&User{}).RowsAffected == 1
|
return DB.Where("username = ?", username).Find(&User{}).RowsAffected == 1
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail)
|
apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail)
|
||||||
apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), controller.ResetPassword)
|
apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), controller.ResetPassword)
|
||||||
apiRouter.GET("/oauth/github", middleware.CriticalRateLimit(), auth.GitHubOAuth)
|
apiRouter.GET("/oauth/github", middleware.CriticalRateLimit(), auth.GitHubOAuth)
|
||||||
|
apiRouter.GET("/oauth/oidc", middleware.CriticalRateLimit(), auth.OidcAuth)
|
||||||
apiRouter.GET("/oauth/lark", middleware.CriticalRateLimit(), auth.LarkOAuth)
|
apiRouter.GET("/oauth/lark", middleware.CriticalRateLimit(), auth.LarkOAuth)
|
||||||
apiRouter.GET("/oauth/state", middleware.CriticalRateLimit(), auth.GenerateOAuthCode)
|
apiRouter.GET("/oauth/state", middleware.CriticalRateLimit(), auth.GenerateOAuthCode)
|
||||||
apiRouter.GET("/oauth/wechat", middleware.CriticalRateLimit(), auth.WeChatAuth)
|
apiRouter.GET("/oauth/wechat", middleware.CriticalRateLimit(), auth.WeChatAuth)
|
||||||
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 4.3 KiB |
7
web/berry/src/assets/images/icons/oidc.svg
Normal file
7
web/berry/src/assets/images/icons/oidc.svg
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<svg t="1723135116886" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
p-id="10969" width="200" height="200">
|
||||||
|
<path d="M512 960C265 960 64 759 64 512S265 64 512 64s448 201 448 448-201 448-448 448z m0-882.6c-239.7 0-434.6 195-434.6 434.6s195 434.6 434.6 434.6 434.6-195 434.6-434.6S751.7 77.4 512 77.4z"
|
||||||
|
p-id="10970" fill="#2c2c2c" stroke="#2c2c2c" stroke-width="60"></path>
|
||||||
|
<path d="M197.7 512c0-78.3 31.6-98.8 87.2-98.8 56.2 0 87.2 20.5 87.2 98.8s-31 98.8-87.2 98.8c-55.7 0-87.2-20.5-87.2-98.8z m130.4 0c0-46.8-7.8-64.5-43.2-64.5-35.2 0-42.9 17.7-42.9 64.5 0 47.1 7.8 63.7 42.9 63.7 35.4 0 43.2-16.6 43.2-63.7zM409.7 415.9h42.1V608h-42.1V415.9zM653.9 512c0 74.2-37.1 96.1-93.6 96.1h-65.9V415.9h65.9c56.5 0 93.6 16.1 93.6 96.1z m-43.5 0c0-49.3-17.7-60.6-52.3-60.6h-21.6v120.7h21.6c35.4 0 52.3-13.3 52.3-60.1zM686.5 512c0-74.2 36.3-98.8 92.7-98.8 18.3 0 33.2 2.2 44.8 6.4v36.3c-11.9-4.2-26-6.6-42.1-6.6-34.6 0-49.8 15.5-49.8 62.6 0 50.1 15.2 62.6 49.3 62.6 15.8 0 30.2-2.2 44.8-7.5v36c-11.3 4.7-28.5 8-46.8 8-56.1-0.2-92.9-18.7-92.9-99z"
|
||||||
|
p-id="10971" fill="#2c2c2c" stroke="#2c2c2c" stroke-width="20"></path>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
@ -22,7 +22,12 @@ const config = {
|
|||||||
turnstile_site_key: '',
|
turnstile_site_key: '',
|
||||||
version: '',
|
version: '',
|
||||||
wechat_login: false,
|
wechat_login: false,
|
||||||
wechat_qrcode: ''
|
wechat_qrcode: '',
|
||||||
|
oidc: false,
|
||||||
|
oidc_client_id: '',
|
||||||
|
oidc_authorization_endpoint: '',
|
||||||
|
oidc_token_endpoint: '',
|
||||||
|
oidc_userinfo_endpoint: '',
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -70,6 +70,28 @@ const useLogin = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const oidcLogin = async (code, state) => {
|
||||||
|
try {
|
||||||
|
const res = await API.get(`/api/oauth/oidc?code=${code}&state=${state}`);
|
||||||
|
const { success, message, data } = res.data;
|
||||||
|
if (success) {
|
||||||
|
if (message === 'bind') {
|
||||||
|
showSuccess('绑定成功!');
|
||||||
|
navigate('/panel');
|
||||||
|
} else {
|
||||||
|
dispatch({ type: LOGIN, payload: data });
|
||||||
|
localStorage.setItem('user', JSON.stringify(data));
|
||||||
|
showSuccess('登录成功!');
|
||||||
|
navigate('/panel');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { success, message };
|
||||||
|
} catch (err) {
|
||||||
|
// 请求失败,设置错误信息
|
||||||
|
return { success: false, message: '' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const wechatLogin = async (code) => {
|
const wechatLogin = async (code) => {
|
||||||
try {
|
try {
|
||||||
const res = await API.get(`/api/oauth/wechat?code=${code}`);
|
const res = await API.get(`/api/oauth/wechat?code=${code}`);
|
||||||
@ -94,7 +116,7 @@ const useLogin = () => {
|
|||||||
navigate('/');
|
navigate('/');
|
||||||
};
|
};
|
||||||
|
|
||||||
return { login, logout, githubLogin, wechatLogin, larkLogin };
|
return { login, logout, githubLogin, wechatLogin, larkLogin,oidcLogin };
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useLogin;
|
export default useLogin;
|
||||||
|
@ -9,6 +9,7 @@ const AuthLogin = Loadable(lazy(() => import('views/Authentication/Auth/Login'))
|
|||||||
const AuthRegister = Loadable(lazy(() => import('views/Authentication/Auth/Register')));
|
const AuthRegister = Loadable(lazy(() => import('views/Authentication/Auth/Register')));
|
||||||
const GitHubOAuth = Loadable(lazy(() => import('views/Authentication/Auth/GitHubOAuth')));
|
const GitHubOAuth = Loadable(lazy(() => import('views/Authentication/Auth/GitHubOAuth')));
|
||||||
const LarkOAuth = Loadable(lazy(() => import('views/Authentication/Auth/LarkOAuth')));
|
const LarkOAuth = Loadable(lazy(() => import('views/Authentication/Auth/LarkOAuth')));
|
||||||
|
const OidcOAuth = Loadable(lazy(() => import('views/Authentication/Auth/OidcOAuth')));
|
||||||
const ForgetPassword = Loadable(lazy(() => import('views/Authentication/Auth/ForgetPassword')));
|
const ForgetPassword = Loadable(lazy(() => import('views/Authentication/Auth/ForgetPassword')));
|
||||||
const ResetPassword = Loadable(lazy(() => import('views/Authentication/Auth/ResetPassword')));
|
const ResetPassword = Loadable(lazy(() => import('views/Authentication/Auth/ResetPassword')));
|
||||||
const Home = Loadable(lazy(() => import('views/Home')));
|
const Home = Loadable(lazy(() => import('views/Home')));
|
||||||
@ -53,6 +54,10 @@ const OtherRoutes = {
|
|||||||
path: '/oauth/lark',
|
path: '/oauth/lark',
|
||||||
element: <LarkOAuth />
|
element: <LarkOAuth />
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'oauth/oidc',
|
||||||
|
element: <OidcOAuth />
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/404',
|
path: '/404',
|
||||||
element: <NotFoundView />
|
element: <NotFoundView />
|
||||||
|
@ -98,6 +98,21 @@ export async function onLarkOAuthClicked(lark_client_id) {
|
|||||||
window.open(`https://open.feishu.cn/open-apis/authen/v1/index?redirect_uri=${redirect_uri}&app_id=${lark_client_id}&state=${state}`);
|
window.open(`https://open.feishu.cn/open-apis/authen/v1/index?redirect_uri=${redirect_uri}&app_id=${lark_client_id}&state=${state}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function onOidcClicked(auth_url, client_id, openInNewTab = false) {
|
||||||
|
const state = await getOAuthState();
|
||||||
|
if (!state) return;
|
||||||
|
const redirect_uri = `${window.location.origin}/oauth/oidc`;
|
||||||
|
const response_type = "code";
|
||||||
|
const scope = "openid profile email";
|
||||||
|
const url = `${auth_url}?client_id=${client_id}&redirect_uri=${redirect_uri}&response_type=${response_type}&scope=${scope}&state=${state}`;
|
||||||
|
if (openInNewTab) {
|
||||||
|
window.open(url);
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
window.location.href = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function isAdmin() {
|
export function isAdmin() {
|
||||||
let user = localStorage.getItem('user');
|
let user = localStorage.getItem('user');
|
||||||
if (!user) return false;
|
if (!user) return false;
|
||||||
|
94
web/berry/src/views/Authentication/Auth/OidcOAuth.js
Normal file
94
web/berry/src/views/Authentication/Auth/OidcOAuth.js
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { showError } from 'utils/common';
|
||||||
|
import useLogin from 'hooks/useLogin';
|
||||||
|
|
||||||
|
// material-ui
|
||||||
|
import { useTheme } from '@mui/material/styles';
|
||||||
|
import { Grid, Stack, Typography, useMediaQuery, CircularProgress } from '@mui/material';
|
||||||
|
|
||||||
|
// project imports
|
||||||
|
import AuthWrapper from '../AuthWrapper';
|
||||||
|
import AuthCardWrapper from '../AuthCardWrapper';
|
||||||
|
import Logo from 'ui-component/Logo';
|
||||||
|
|
||||||
|
// assets
|
||||||
|
|
||||||
|
// ================================|| AUTH3 - LOGIN ||================================ //
|
||||||
|
|
||||||
|
const OidcOAuth = () => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const matchDownSM = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const [prompt, setPrompt] = useState('处理中...');
|
||||||
|
const { oidcLogin } = useLogin();
|
||||||
|
|
||||||
|
let navigate = useNavigate();
|
||||||
|
|
||||||
|
const sendCode = async (code, state, count) => {
|
||||||
|
const { success, message } = await oidcLogin(code, state);
|
||||||
|
if (!success) {
|
||||||
|
if (message) {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
if (count === 0) {
|
||||||
|
setPrompt(`操作失败,重定向至登录界面中...`);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
|
navigate('/login');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
count++;
|
||||||
|
setPrompt(`出现错误,第 ${count} 次重试中...`);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
|
await sendCode(code, state, count);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let code = searchParams.get('code');
|
||||||
|
let state = searchParams.get('state');
|
||||||
|
sendCode(code, state, 0).then();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthWrapper>
|
||||||
|
<Grid container direction="column" justifyContent="flex-end">
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Grid container justifyContent="center" alignItems="center" sx={{ minHeight: 'calc(100vh - 136px)' }}>
|
||||||
|
<Grid item sx={{ m: { xs: 1, sm: 3 }, mb: 0 }}>
|
||||||
|
<AuthCardWrapper>
|
||||||
|
<Grid container spacing={2} alignItems="center" justifyContent="center">
|
||||||
|
<Grid item sx={{ mb: 3 }}>
|
||||||
|
<Link to="#">
|
||||||
|
<Logo />
|
||||||
|
</Link>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Grid container direction={matchDownSM ? 'column-reverse' : 'row'} alignItems="center" justifyContent="center">
|
||||||
|
<Grid item>
|
||||||
|
<Stack alignItems="center" justifyContent="center" spacing={1}>
|
||||||
|
<Typography color={theme.palette.primary.main} gutterBottom variant={matchDownSM ? 'h3' : 'h2'}>
|
||||||
|
OIDC 登录
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} container direction="column" justifyContent="center" alignItems="center" style={{ height: '200px' }}>
|
||||||
|
<CircularProgress />
|
||||||
|
<Typography variant="h3" paddingTop={'20px'}>
|
||||||
|
{prompt}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</AuthCardWrapper>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</AuthWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OidcOAuth;
|
@ -36,7 +36,8 @@ import VisibilityOff from '@mui/icons-material/VisibilityOff';
|
|||||||
import Github from 'assets/images/icons/github.svg';
|
import Github from 'assets/images/icons/github.svg';
|
||||||
import Wechat from 'assets/images/icons/wechat.svg';
|
import Wechat from 'assets/images/icons/wechat.svg';
|
||||||
import Lark from 'assets/images/icons/lark.svg';
|
import Lark from 'assets/images/icons/lark.svg';
|
||||||
import { onGitHubOAuthClicked, onLarkOAuthClicked } from 'utils/common';
|
import OIDC from 'assets/images/icons/oidc.svg';
|
||||||
|
import { onGitHubOAuthClicked, onLarkOAuthClicked, onOidcClicked } from 'utils/common';
|
||||||
|
|
||||||
// ============================|| FIREBASE - LOGIN ||============================ //
|
// ============================|| FIREBASE - LOGIN ||============================ //
|
||||||
|
|
||||||
@ -50,7 +51,7 @@ const LoginForm = ({ ...others }) => {
|
|||||||
// const [checked, setChecked] = useState(true);
|
// const [checked, setChecked] = useState(true);
|
||||||
|
|
||||||
let tripartiteLogin = false;
|
let tripartiteLogin = false;
|
||||||
if (siteInfo.github_oauth || siteInfo.wechat_login || siteInfo.lark_client_id) {
|
if (siteInfo.github_oauth || siteInfo.wechat_login || siteInfo.lark_client_id || siteInfo.oidc) {
|
||||||
tripartiteLogin = true;
|
tripartiteLogin = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,6 +146,29 @@ const LoginForm = ({ ...others }) => {
|
|||||||
</AnimateButton>
|
</AnimateButton>
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
|
{siteInfo.oidc && (
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<AnimateButton>
|
||||||
|
<Button
|
||||||
|
disableElevation
|
||||||
|
fullWidth
|
||||||
|
onClick={() => onOidcClicked(siteInfo.oidc_authorization_endpoint,siteInfo.oidc_client_id)}
|
||||||
|
size="large"
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
color: 'grey.700',
|
||||||
|
backgroundColor: theme.palette.grey[50],
|
||||||
|
borderColor: theme.palette.grey[100]
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ mr: { xs: 1, sm: 2, width: 20 }, display: 'flex', alignItems: 'center' }}>
|
||||||
|
<img src={OIDC} alt="Lark" width={25} height={25} style={{ marginRight: matchDownSM ? 8 : 16 }} />
|
||||||
|
</Box>
|
||||||
|
使用 OIDC 登录
|
||||||
|
</Button>
|
||||||
|
</AnimateButton>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
|
@ -20,7 +20,7 @@ import SubCard from 'ui-component/cards/SubCard';
|
|||||||
import { IconBrandWechat, IconBrandGithub, IconMail } from '@tabler/icons-react';
|
import { IconBrandWechat, IconBrandGithub, IconMail } from '@tabler/icons-react';
|
||||||
import Label from 'ui-component/Label';
|
import Label from 'ui-component/Label';
|
||||||
import { API } from 'utils/api';
|
import { API } from 'utils/api';
|
||||||
import { showError, showSuccess } from 'utils/common';
|
import { onOidcClicked, showError, showSuccess } from 'utils/common';
|
||||||
import { onGitHubOAuthClicked, onLarkOAuthClicked, copy } from 'utils/common';
|
import { onGitHubOAuthClicked, onLarkOAuthClicked, copy } from 'utils/common';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import WechatModal from 'views/Authentication/AuthForms/WechatModal';
|
import WechatModal from 'views/Authentication/AuthForms/WechatModal';
|
||||||
@ -28,6 +28,7 @@ import { useSelector } from 'react-redux';
|
|||||||
import EmailModal from './component/EmailModal';
|
import EmailModal from './component/EmailModal';
|
||||||
import Turnstile from 'react-turnstile';
|
import Turnstile from 'react-turnstile';
|
||||||
import { ReactComponent as Lark } from 'assets/images/icons/lark.svg';
|
import { ReactComponent as Lark } from 'assets/images/icons/lark.svg';
|
||||||
|
import { ReactComponent as OIDC } from 'assets/images/icons/oidc.svg';
|
||||||
|
|
||||||
const validationSchema = Yup.object().shape({
|
const validationSchema = Yup.object().shape({
|
||||||
username: Yup.string().required('用户名 不能为空').min(3, '用户名 不能小于 3 个字符'),
|
username: Yup.string().required('用户名 不能为空').min(3, '用户名 不能小于 3 个字符'),
|
||||||
@ -123,6 +124,15 @@ export default function Profile() {
|
|||||||
loadUser().then();
|
loadUser().then();
|
||||||
}, [status]);
|
}, [status]);
|
||||||
|
|
||||||
|
function getOidcId(){
|
||||||
|
if (!inputs.oidc_id) return '';
|
||||||
|
let oidc_id = inputs.oidc_id;
|
||||||
|
if (inputs.oidc_id.length > 8) {
|
||||||
|
oidc_id = inputs.oidc_id.slice(0, 6) + '...' + inputs.oidc_id.slice(-6);
|
||||||
|
}
|
||||||
|
return oidc_id;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<UserCard>
|
<UserCard>
|
||||||
@ -141,6 +151,9 @@ export default function Profile() {
|
|||||||
<Label variant="ghost" color={inputs.lark_id ? 'primary' : 'default'}>
|
<Label variant="ghost" color={inputs.lark_id ? 'primary' : 'default'}>
|
||||||
<SvgIcon component={Lark} inheritViewBox="0 0 24 24" /> {inputs.lark_id || '未绑定'}
|
<SvgIcon component={Lark} inheritViewBox="0 0 24 24" /> {inputs.lark_id || '未绑定'}
|
||||||
</Label>
|
</Label>
|
||||||
|
<Label variant="ghost" color={inputs.oidc_id ? 'primary' : 'default'}>
|
||||||
|
<SvgIcon component={OIDC} inheritViewBox="0 0 24 24" /> {getOidcId() || '未绑定'}
|
||||||
|
</Label>
|
||||||
</Stack>
|
</Stack>
|
||||||
<SubCard title="个人信息">
|
<SubCard title="个人信息">
|
||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
@ -216,6 +229,13 @@ export default function Profile() {
|
|||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
|
{status.oidc && !inputs.oidc_id && (
|
||||||
|
<Grid xs={12} md={4}>
|
||||||
|
<Button variant="contained" onClick={() => onOidcClicked(status.oidc_authorization_endpoint,status.oidc_client_id,true)}>
|
||||||
|
绑定 OIDC 账号
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
<Grid xs={12} md={4}>
|
<Grid xs={12} md={4}>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
|
@ -33,6 +33,13 @@ const SystemSetting = () => {
|
|||||||
GitHubClientSecret: '',
|
GitHubClientSecret: '',
|
||||||
LarkClientId: '',
|
LarkClientId: '',
|
||||||
LarkClientSecret: '',
|
LarkClientSecret: '',
|
||||||
|
OidcEnabled: '',
|
||||||
|
OidcWellKnown: '',
|
||||||
|
OidcClientId: '',
|
||||||
|
OidcClientSecret: '',
|
||||||
|
OidcAuthorizationEndpoint: '',
|
||||||
|
OidcTokenEndpoint: '',
|
||||||
|
OidcUserinfoEndpoint: '',
|
||||||
Notice: '',
|
Notice: '',
|
||||||
SMTPServer: '',
|
SMTPServer: '',
|
||||||
SMTPPort: '',
|
SMTPPort: '',
|
||||||
@ -94,6 +101,7 @@ const SystemSetting = () => {
|
|||||||
case 'TurnstileCheckEnabled':
|
case 'TurnstileCheckEnabled':
|
||||||
case 'EmailDomainRestrictionEnabled':
|
case 'EmailDomainRestrictionEnabled':
|
||||||
case 'RegisterEnabled':
|
case 'RegisterEnabled':
|
||||||
|
case 'OidcEnabled':
|
||||||
value = inputs[key] === 'true' ? 'false' : 'true';
|
value = inputs[key] === 'true' ? 'false' : 'true';
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@ -142,8 +150,15 @@ const SystemSetting = () => {
|
|||||||
name === 'MessagePusherAddress' ||
|
name === 'MessagePusherAddress' ||
|
||||||
name === 'MessagePusherToken' ||
|
name === 'MessagePusherToken' ||
|
||||||
name === 'LarkClientId' ||
|
name === 'LarkClientId' ||
|
||||||
name === 'LarkClientSecret'
|
name === 'LarkClientSecret' ||
|
||||||
) {
|
name === 'OidcClientId' ||
|
||||||
|
name === 'OidcClientSecret' ||
|
||||||
|
name === 'OidcWellKnown' ||
|
||||||
|
name === 'OidcAuthorizationEndpoint' ||
|
||||||
|
name === 'OidcTokenEndpoint' ||
|
||||||
|
name === 'OidcUserinfoEndpoint'
|
||||||
|
)
|
||||||
|
{
|
||||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||||
} else {
|
} else {
|
||||||
await updateOption(name, value);
|
await updateOption(name, value);
|
||||||
@ -225,6 +240,43 @@ const SystemSetting = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const submitOidc = async () => {
|
||||||
|
if (inputs.OidcWellKnown !== '') {
|
||||||
|
if (!inputs.OidcWellKnown.startsWith('http://') && !inputs.OidcWellKnown.startsWith('https://')) {
|
||||||
|
showError('Well-Known URL 必须以 http:// 或 https:// 开头');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await API.get(inputs.OidcWellKnown);
|
||||||
|
inputs.OidcAuthorizationEndpoint = res.data['authorization_endpoint'];
|
||||||
|
inputs.OidcTokenEndpoint = res.data['token_endpoint'];
|
||||||
|
inputs.OidcUserinfoEndpoint = res.data['userinfo_endpoint'];
|
||||||
|
showSuccess('获取 OIDC 配置成功!');
|
||||||
|
} catch (err) {
|
||||||
|
showError("获取 OIDC 配置失败,请检查网络状况和 Well-Known URL 是否正确");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (originInputs['OidcWellKnown'] !== inputs.OidcWellKnown) {
|
||||||
|
await updateOption('OidcWellKnown', inputs.OidcWellKnown);
|
||||||
|
}
|
||||||
|
if (originInputs['OidcClientId'] !== inputs.OidcClientId) {
|
||||||
|
await updateOption('OidcClientId', inputs.OidcClientId);
|
||||||
|
}
|
||||||
|
if (originInputs['OidcClientSecret'] !== inputs.OidcClientSecret && inputs.OidcClientSecret !== '') {
|
||||||
|
await updateOption('OidcClientSecret', inputs.OidcClientSecret);
|
||||||
|
}
|
||||||
|
if (originInputs['OidcAuthorizationEndpoint'] !== inputs.OidcAuthorizationEndpoint) {
|
||||||
|
await updateOption('OidcAuthorizationEndpoint', inputs.OidcAuthorizationEndpoint);
|
||||||
|
}
|
||||||
|
if (originInputs['OidcTokenEndpoint'] !== inputs.OidcTokenEndpoint) {
|
||||||
|
await updateOption('OidcTokenEndpoint', inputs.OidcTokenEndpoint);
|
||||||
|
}
|
||||||
|
if (originInputs['OidcUserinfoEndpoint'] !== inputs.OidcUserinfoEndpoint) {
|
||||||
|
await updateOption('OidcUserinfoEndpoint', inputs.OidcUserinfoEndpoint);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack spacing={2}>
|
<Stack spacing={2}>
|
||||||
@ -291,6 +343,12 @@ const SystemSetting = () => {
|
|||||||
control={<Checkbox checked={inputs.GitHubOAuthEnabled === 'true'} onChange={handleInputChange} name="GitHubOAuthEnabled" />}
|
control={<Checkbox checked={inputs.GitHubOAuthEnabled === 'true'} onChange={handleInputChange} name="GitHubOAuthEnabled" />}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
<Grid xs={12} md={3}>
|
||||||
|
<FormControlLabel
|
||||||
|
label="允许通过 OIDC 登录 & 注册"
|
||||||
|
control={<Checkbox checked={inputs.OidcEnabled === 'true'} onChange={handleInputChange} name="OidcEnabled" />}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
<Grid xs={12} md={3}>
|
<Grid xs={12} md={3}>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
label="允许通过微信登录 & 注册"
|
label="允许通过微信登录 & 注册"
|
||||||
@ -616,6 +674,117 @@ const SystemSetting = () => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</SubCard>
|
</SubCard>
|
||||||
|
|
||||||
|
<SubCard
|
||||||
|
title="配置 OIDC"
|
||||||
|
subTitle={
|
||||||
|
<span>
|
||||||
|
用以支持通过 OIDC 登录,例如 Okta、Auth0 等兼容 OIDC 协议的 IdP
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Grid container spacing={ { xs: 3, sm: 2, md: 4 } }>
|
||||||
|
<Grid xs={ 12 } md={ 12 }>
|
||||||
|
<Alert severity="info" sx={ { wordWrap: 'break-word' } }>
|
||||||
|
主页链接填 <code>{ inputs.ServerAddress }</code>
|
||||||
|
,重定向 URL 填 <code>{ `${ inputs.ServerAddress }/oauth/oidc` }</code>
|
||||||
|
</Alert> <br />
|
||||||
|
<Alert severity="info" sx={ { wordWrap: 'break-word' } }>
|
||||||
|
若你的 OIDC Provider 支持 Discovery Endpoint,你可以仅填写 OIDC Well-Known URL,系统会自动获取 OIDC 配置
|
||||||
|
</Alert>
|
||||||
|
</Grid>
|
||||||
|
<Grid xs={ 12 } md={ 6 }>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel htmlFor="OidcClientId">Client ID</InputLabel>
|
||||||
|
<OutlinedInput
|
||||||
|
id="OidcClientId"
|
||||||
|
name="OidcClientId"
|
||||||
|
value={ inputs.OidcClientId || '' }
|
||||||
|
onChange={ handleInputChange }
|
||||||
|
label="Client ID"
|
||||||
|
placeholder="输入 OIDC 的 Client ID"
|
||||||
|
disabled={ loading }
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</Grid>
|
||||||
|
<Grid xs={ 12 } md={ 6 }>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel htmlFor="OidcClientSecret">Client Secret</InputLabel>
|
||||||
|
<OutlinedInput
|
||||||
|
id="OidcClientSecret"
|
||||||
|
name="OidcClientSecret"
|
||||||
|
value={ inputs.OidcClientSecret || '' }
|
||||||
|
onChange={ handleInputChange }
|
||||||
|
label="Client Secret"
|
||||||
|
placeholder="敏感信息不会发送到前端显示"
|
||||||
|
disabled={ loading }
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</Grid>
|
||||||
|
<Grid xs={ 12 } md={ 6 }>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel htmlFor="OidcWellKnown">Well-Known URL</InputLabel>
|
||||||
|
<OutlinedInput
|
||||||
|
id="OidcWellKnown"
|
||||||
|
name="OidcWellKnown"
|
||||||
|
value={ inputs.OidcWellKnown || '' }
|
||||||
|
onChange={ handleInputChange }
|
||||||
|
label="Well-Known URL"
|
||||||
|
placeholder="请输入 OIDC 的 Well-Known URL"
|
||||||
|
disabled={ loading }
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</Grid>
|
||||||
|
<Grid xs={ 12 } md={ 6 }>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel htmlFor="OidcAuthorizationEndpoint">Authorization Endpoint</InputLabel>
|
||||||
|
<OutlinedInput
|
||||||
|
id="OidcAuthorizationEndpoint"
|
||||||
|
name="OidcAuthorizationEndpoint"
|
||||||
|
value={ inputs.OidcAuthorizationEndpoint || '' }
|
||||||
|
onChange={ handleInputChange }
|
||||||
|
label="Authorization Endpoint"
|
||||||
|
placeholder="输入 OIDC 的 Authorization Endpoint"
|
||||||
|
disabled={ loading }
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</Grid>
|
||||||
|
<Grid xs={ 12 } md={ 6 }>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel htmlFor="OidcTokenEndpoint">Token Endpoint</InputLabel>
|
||||||
|
<OutlinedInput
|
||||||
|
id="OidcTokenEndpoint"
|
||||||
|
name="OidcTokenEndpoint"
|
||||||
|
value={ inputs.OidcTokenEndpoint || '' }
|
||||||
|
onChange={ handleInputChange }
|
||||||
|
label="Token Endpoint"
|
||||||
|
placeholder="输入 OIDC 的 Token Endpoint"
|
||||||
|
disabled={ loading }
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</Grid>
|
||||||
|
<Grid xs={ 12 } md={ 6 }>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel htmlFor="OidcUserinfoEndpoint">Userinfo Endpoint</InputLabel>
|
||||||
|
<OutlinedInput
|
||||||
|
id="OidcUserinfoEndpoint"
|
||||||
|
name="OidcUserinfoEndpoint"
|
||||||
|
value={ inputs.OidcUserinfoEndpoint || '' }
|
||||||
|
onChange={ handleInputChange }
|
||||||
|
label="Userinfo Endpoint"
|
||||||
|
placeholder="输入 OIDC 的 Userinfo Endpoint"
|
||||||
|
disabled={ loading }
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</Grid>
|
||||||
|
<Grid xs={ 12 }>
|
||||||
|
<Button variant="contained" onClick={ submitOidc }>
|
||||||
|
保存 OIDC 设置
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</SubCard>
|
||||||
|
|
||||||
<SubCard
|
<SubCard
|
||||||
title="配置 Message Pusher"
|
title="配置 Message Pusher"
|
||||||
subTitle={
|
subTitle={
|
||||||
|
Loading…
Reference in New Issue
Block a user