From 1106bcabf2ac2e482f6eb3d33c7f623536c6f6da Mon Sep 17 00:00:00 2001 From: OnEvent Date: Thu, 8 Aug 2024 16:51:01 +0800 Subject: [PATCH 01/11] feat: add the ui for configuring the third-party standard OAuth2.0/OIDC. - update SystemSetting.js - add setup ui - add configuration --- .../views/Setting/component/SystemSetting.js | 116 +++++++++++++++++- 1 file changed, 114 insertions(+), 2 deletions(-) diff --git a/web/berry/src/views/Setting/component/SystemSetting.js b/web/berry/src/views/Setting/component/SystemSetting.js index 6f82fb26..417f4fc9 100644 --- a/web/berry/src/views/Setting/component/SystemSetting.js +++ b/web/berry/src/views/Setting/component/SystemSetting.js @@ -33,6 +33,11 @@ const SystemSetting = () => { GitHubClientSecret: '', LarkClientId: '', LarkClientSecret: '', + OAuth2Enabled: '', + OAuth2AppId: '', + OAuth2AppSecret: '', + OAuth2AuthorizationEndpoint: '', + OAuth2TokenEndpoint: '', Notice: '', SMTPServer: '', SMTPPort: '', @@ -142,8 +147,13 @@ const SystemSetting = () => { name === 'MessagePusherAddress' || name === 'MessagePusherToken' || name === 'LarkClientId' || - name === 'LarkClientSecret' - ) { + name === 'LarkClientSecret' || + name === 'OAuth2AppId' || + name === 'OAuth2AppSecret' || + name === 'OAuth2AuthorizationEndpoint' || + name === 'OAuth2TokenEndpoint' + ) + { setInputs((inputs) => ({ ...inputs, [name]: value })); } else { await updateOption(name, value); @@ -225,6 +235,28 @@ const SystemSetting = () => { } }; + const submitOAuth2 = async () => { + const OAuth2Config = { + OAuth2AppId: inputs.OAuth2AppId, + OAuth2AppSecret: inputs.OAuth2AppSecret, + OAuth2AuthorizationEndpoint: inputs.OAuth2AuthorizationEndpoint, + OAuth2TokenEndpoint: inputs.OAuth2TokenEndpoint + }; + console.log(OAuth2Config); + if (originInputs['OAuth2AppId'] !== inputs.OAuth2AppId) { + await updateOption('OAuth2AppId', inputs.OAuth2AppId); + } + if (originInputs['OAuth2AppSecret'] !== inputs.OAuth2AppSecret && inputs.OAuth2AppSecret !== '') { + await updateOption('OAuth2AppSecret', inputs.OAuth2AppSecret); + } + if (originInputs['OAuth2AuthorizationEndpoint'] !== inputs.OAuth2AuthorizationEndpoint) { + await updateOption('OAuth2AuthorizationEndpoint', inputs.OAuth2AuthorizationEndpoint); + } + if (originInputs['OAuth2TokenEndpoint'] !== inputs.OAuth2TokenEndpoint) { + await updateOption('OAuth2TokenEndpoint', inputs.OAuth2TokenEndpoint); + } + }; + return ( <> @@ -616,6 +648,86 @@ const SystemSetting = () => { + + + 用以支持通过第三方 OAuth2 登录,例如 Okta、Auth0 或自建的兼容 OAuth2.0 协议的 IdP 等 + + } + > + + + + 主页链接填 { inputs.ServerAddress } + ,重定向 URL 填 { `${ inputs.ServerAddress }/oauth/oidc` } + + + + + App ID + + + + + + App Secret + + + + + + 授权地址 + + + + + + 认证地址 + + + + + + + + + Date: Thu, 8 Aug 2024 18:20:13 +0800 Subject: [PATCH 02/11] feat: add the ui for "allow the OAuth 2.0 to login" - update SystemSetting.js --- web/berry/src/views/Setting/component/SystemSetting.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/web/berry/src/views/Setting/component/SystemSetting.js b/web/berry/src/views/Setting/component/SystemSetting.js index 417f4fc9..f868006b 100644 --- a/web/berry/src/views/Setting/component/SystemSetting.js +++ b/web/berry/src/views/Setting/component/SystemSetting.js @@ -99,6 +99,7 @@ const SystemSetting = () => { case 'TurnstileCheckEnabled': case 'EmailDomainRestrictionEnabled': case 'RegisterEnabled': + case 'OAuth2Enabled': value = inputs[key] === 'true' ? 'false' : 'true'; break; default: @@ -323,6 +324,12 @@ const SystemSetting = () => { control={} /> + + } + /> + Date: Thu, 8 Aug 2024 18:22:29 +0800 Subject: [PATCH 03/11] feat: add OAuth 2.0 web ui and its process functions - update common.js - update AuthLogin.js - update config.js --- web/berry/src/config.js | 6 ++++- web/berry/src/utils/common.js | 15 +++++++++++ .../Authentication/AuthForms/AuthLogin.js | 27 +++++++++++++++++-- 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/web/berry/src/config.js b/web/berry/src/config.js index eeeda99a..b5eaa831 100644 --- a/web/berry/src/config.js +++ b/web/berry/src/config.js @@ -22,7 +22,11 @@ const config = { turnstile_site_key: '', version: '', wechat_login: false, - wechat_qrcode: '' + wechat_qrcode: '', + oauth2: false, + oauth2_app_id: '', + oauth2_authorization_endpoint: '', + oauth2_token_endpoint: '', } }; diff --git a/web/berry/src/utils/common.js b/web/berry/src/utils/common.js index d74d032e..a471c2cf 100644 --- a/web/berry/src/utils/common.js +++ b/web/berry/src/utils/common.js @@ -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}`); } +export async function onOAuth2Clicked(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 = "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() { let user = localStorage.getItem('user'); if (!user) return false; diff --git a/web/berry/src/views/Authentication/AuthForms/AuthLogin.js b/web/berry/src/views/Authentication/AuthForms/AuthLogin.js index bc7a35c0..582fdfbe 100644 --- a/web/berry/src/views/Authentication/AuthForms/AuthLogin.js +++ b/web/berry/src/views/Authentication/AuthForms/AuthLogin.js @@ -36,7 +36,7 @@ import VisibilityOff from '@mui/icons-material/VisibilityOff'; import Github from 'assets/images/icons/github.svg'; import Wechat from 'assets/images/icons/wechat.svg'; import Lark from 'assets/images/icons/lark.svg'; -import { onGitHubOAuthClicked, onLarkOAuthClicked } from 'utils/common'; +import { onGitHubOAuthClicked, onLarkOAuthClicked, onOAuth2Clicked } from 'utils/common'; // ============================|| FIREBASE - LOGIN ||============================ // @@ -50,7 +50,7 @@ const LoginForm = ({ ...others }) => { // const [checked, setChecked] = useState(true); 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.oauth2) { tripartiteLogin = true; } @@ -145,6 +145,29 @@ const LoginForm = ({ ...others }) => { )} + {siteInfo.oauth2 && ( + + + + + + )} Date: Thu, 8 Aug 2024 19:10:14 +0800 Subject: [PATCH 04/11] fix: missing "Userinfo" endpoint configuration entry, used by OAuth clients to request user information from the IdP. - update config.js - update SystemSetting.js --- web/berry/src/config.js | 1 + .../views/Setting/component/SystemSetting.js | 24 +++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/web/berry/src/config.js b/web/berry/src/config.js index b5eaa831..f38d16fb 100644 --- a/web/berry/src/config.js +++ b/web/berry/src/config.js @@ -27,6 +27,7 @@ const config = { oauth2_app_id: '', oauth2_authorization_endpoint: '', oauth2_token_endpoint: '', + oauth2_userinfo_endpoint: '', } }; diff --git a/web/berry/src/views/Setting/component/SystemSetting.js b/web/berry/src/views/Setting/component/SystemSetting.js index f868006b..c77ae55f 100644 --- a/web/berry/src/views/Setting/component/SystemSetting.js +++ b/web/berry/src/views/Setting/component/SystemSetting.js @@ -38,6 +38,7 @@ const SystemSetting = () => { OAuth2AppSecret: '', OAuth2AuthorizationEndpoint: '', OAuth2TokenEndpoint: '', + OAuth2UserinfoEndpoint: '', Notice: '', SMTPServer: '', SMTPPort: '', @@ -152,7 +153,8 @@ const SystemSetting = () => { name === 'OAuth2AppId' || name === 'OAuth2AppSecret' || name === 'OAuth2AuthorizationEndpoint' || - name === 'OAuth2TokenEndpoint' + name === 'OAuth2TokenEndpoint' || + name === 'OAuth2UserinfoEndpoint' ) { setInputs((inputs) => ({ ...inputs, [name]: value })); @@ -241,7 +243,8 @@ const SystemSetting = () => { OAuth2AppId: inputs.OAuth2AppId, OAuth2AppSecret: inputs.OAuth2AppSecret, OAuth2AuthorizationEndpoint: inputs.OAuth2AuthorizationEndpoint, - OAuth2TokenEndpoint: inputs.OAuth2TokenEndpoint + OAuth2TokenEndpoint: inputs.OAuth2TokenEndpoint, + OAuth2UserinfoEndpoint: inputs.OAuth2UserinfoEndpoint }; console.log(OAuth2Config); if (originInputs['OAuth2AppId'] !== inputs.OAuth2AppId) { @@ -256,6 +259,9 @@ const SystemSetting = () => { if (originInputs['OAuth2TokenEndpoint'] !== inputs.OAuth2TokenEndpoint) { await updateOption('OAuth2TokenEndpoint', inputs.OAuth2TokenEndpoint); } + if (originInputs['OAuth2UserinfoEndpoint'] !== inputs.OAuth2UserinfoEndpoint) { + await updateOption('OAuth2UserinfoEndpoint', inputs.OAuth2UserinfoEndpoint); + } }; return ( @@ -727,6 +733,20 @@ const SystemSetting = () => { /> + + + 用户地址 + + + diff --git a/web/berry/src/views/Profile/index.js b/web/berry/src/views/Profile/index.js index 4705d8af..721ead7b 100644 --- a/web/berry/src/views/Profile/index.js +++ b/web/berry/src/views/Profile/index.js @@ -20,7 +20,7 @@ import SubCard from 'ui-component/cards/SubCard'; import { IconBrandWechat, IconBrandGithub, IconMail } from '@tabler/icons-react'; import Label from 'ui-component/Label'; 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 * as Yup from 'yup'; import WechatModal from 'views/Authentication/AuthForms/WechatModal'; @@ -28,6 +28,7 @@ import { useSelector } from 'react-redux'; import EmailModal from './component/EmailModal'; import Turnstile from 'react-turnstile'; 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({ username: Yup.string().required('用户名 不能为空').min(3, '用户名 不能小于 3 个字符'), @@ -123,6 +124,15 @@ export default function Profile() { loadUser().then(); }, [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 ( <> @@ -141,6 +151,9 @@ export default function Profile() { + @@ -216,6 +229,13 @@ export default function Profile() { )} + {status.oidc && !inputs.oidc_id && ( + + + + )} From 05ee77eb357369a4d3172d1ac48a3297f8a28a05 Mon Sep 17 00:00:00 2001 From: OnEvent Date: Fri, 9 Aug 2024 16:44:52 +0800 Subject: [PATCH 07/11] feat: add OIDC login method --- web/berry/src/hooks/useLogin.js | 24 +++++++++++++++++++++++- web/berry/src/routes/OtherRoutes.js | 5 +++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/web/berry/src/hooks/useLogin.js b/web/berry/src/hooks/useLogin.js index 39d8b407..6d89727d 100644 --- a/web/berry/src/hooks/useLogin.js +++ b/web/berry/src/hooks/useLogin.js @@ -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) => { try { const res = await API.get(`/api/oauth/wechat?code=${code}`); @@ -94,7 +116,7 @@ const useLogin = () => { navigate('/'); }; - return { login, logout, githubLogin, wechatLogin, larkLogin }; + return { login, logout, githubLogin, wechatLogin, larkLogin,oidcLogin }; }; export default useLogin; diff --git a/web/berry/src/routes/OtherRoutes.js b/web/berry/src/routes/OtherRoutes.js index 58c0b660..a4bdb5d3 100644 --- a/web/berry/src/routes/OtherRoutes.js +++ b/web/berry/src/routes/OtherRoutes.js @@ -9,6 +9,7 @@ const AuthLogin = Loadable(lazy(() => import('views/Authentication/Auth/Login')) const AuthRegister = Loadable(lazy(() => import('views/Authentication/Auth/Register'))); const GitHubOAuth = Loadable(lazy(() => import('views/Authentication/Auth/GitHubOAuth'))); 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 ResetPassword = Loadable(lazy(() => import('views/Authentication/Auth/ResetPassword'))); const Home = Loadable(lazy(() => import('views/Home'))); @@ -53,6 +54,10 @@ const OtherRoutes = { path: '/oauth/lark', element: }, + { + path: 'oauth/oidc', + element: + }, { path: '/404', element: From 8a283fff3bc045a9b447c443aa8d91893273579e Mon Sep 17 00:00:00 2001 From: OnEvent Date: Fri, 9 Aug 2024 16:46:34 +0800 Subject: [PATCH 08/11] feat: Add support for OIDC login to the backend --- common/config/config.go | 7 ++ controller/auth/oidc.go | 225 ++++++++++++++++++++++++++++++++++++++++ controller/misc.go | 41 ++++---- model/option.go | 13 +++ model/user.go | 13 +++ router/api.go | 1 + 6 files changed, 282 insertions(+), 18 deletions(-) create mode 100644 controller/auth/oidc.go diff --git a/common/config/config.go b/common/config/config.go index 11da0b96..f9e4a540 100644 --- a/common/config/config.go +++ b/common/config/config.go @@ -35,6 +35,7 @@ var PasswordLoginEnabled = true var PasswordRegisterEnabled = true var EmailVerificationEnabled = false var GitHubOAuthEnabled = false +var OidcEnabled = false var WeChatAuthEnabled = false var TurnstileCheckEnabled = false var RegisterEnabled = true @@ -70,6 +71,12 @@ var GitHubClientSecret = "" var LarkClientId = "" var LarkClientSecret = "" +var OidcAppId = "" +var OidcAppSecret = "" +var OidcAuthorizationEndpoint = "" +var OidcTokenEndpoint = "" +var OidcUserinfoEndpoint = "" + var WeChatServerAddress = "" var WeChatServerToken = "" var WeChatAccountQRCodeImageURL = "" diff --git a/controller/auth/oidc.go b/controller/auth/oidc.go new file mode 100644 index 00000000..02865c60 --- /dev/null +++ b/controller/auth/oidc.go @@ -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.OidcAppId, + "client_secret": config.OidcAppSecret, + "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 +} diff --git a/controller/misc.go b/controller/misc.go index 2928b8fb..0aef52c0 100644 --- a/controller/misc.go +++ b/controller/misc.go @@ -18,24 +18,29 @@ func GetStatus(c *gin.Context) { "success": true, "message": "", "data": gin.H{ - "version": common.Version, - "start_time": common.StartTime, - "email_verification": config.EmailVerificationEnabled, - "github_oauth": config.GitHubOAuthEnabled, - "github_client_id": config.GitHubClientId, - "lark_client_id": config.LarkClientId, - "system_name": config.SystemName, - "logo": config.Logo, - "footer_html": config.Footer, - "wechat_qrcode": config.WeChatAccountQRCodeImageURL, - "wechat_login": config.WeChatAuthEnabled, - "server_address": config.ServerAddress, - "turnstile_check": config.TurnstileCheckEnabled, - "turnstile_site_key": config.TurnstileSiteKey, - "top_up_link": config.TopUpLink, - "chat_link": config.ChatLink, - "quota_per_unit": config.QuotaPerUnit, - "display_in_currency": config.DisplayInCurrencyEnabled, + "version": common.Version, + "start_time": common.StartTime, + "email_verification": config.EmailVerificationEnabled, + "github_oauth": config.GitHubOAuthEnabled, + "github_client_id": config.GitHubClientId, + "lark_client_id": config.LarkClientId, + "system_name": config.SystemName, + "logo": config.Logo, + "footer_html": config.Footer, + "wechat_qrcode": config.WeChatAccountQRCodeImageURL, + "wechat_login": config.WeChatAuthEnabled, + "server_address": config.ServerAddress, + "turnstile_check": config.TurnstileCheckEnabled, + "turnstile_site_key": config.TurnstileSiteKey, + "top_up_link": config.TopUpLink, + "chat_link": config.ChatLink, + "quota_per_unit": config.QuotaPerUnit, + "display_in_currency": config.DisplayInCurrencyEnabled, + "oidc": config.OidcEnabled, + "oidc_app_id": config.OidcAppId, + "oidc_authorization_endpoint": config.OidcAuthorizationEndpoint, + "oidc_token_endpoint": config.OidcTokenEndpoint, + "oidc_userinfo_endpoint": config.OidcUserinfoEndpoint, }, }) return diff --git a/model/option.go b/model/option.go index bed8d4c3..fa9f9c98 100644 --- a/model/option.go +++ b/model/option.go @@ -28,6 +28,7 @@ func InitOptionMap() { config.OptionMap["PasswordRegisterEnabled"] = strconv.FormatBool(config.PasswordRegisterEnabled) config.OptionMap["EmailVerificationEnabled"] = strconv.FormatBool(config.EmailVerificationEnabled) config.OptionMap["GitHubOAuthEnabled"] = strconv.FormatBool(config.GitHubOAuthEnabled) + config.OptionMap["OidcEnabled"] = strconv.FormatBool(config.OidcEnabled) config.OptionMap["WeChatAuthEnabled"] = strconv.FormatBool(config.WeChatAuthEnabled) config.OptionMap["TurnstileCheckEnabled"] = strconv.FormatBool(config.TurnstileCheckEnabled) config.OptionMap["RegisterEnabled"] = strconv.FormatBool(config.RegisterEnabled) @@ -130,6 +131,8 @@ func updateOptionMap(key string, value string) (err error) { config.EmailVerificationEnabled = boolValue case "GitHubOAuthEnabled": config.GitHubOAuthEnabled = boolValue + case "OidcEnabled": + config.OidcEnabled = boolValue case "WeChatAuthEnabled": config.WeChatAuthEnabled = boolValue case "TurnstileCheckEnabled": @@ -176,6 +179,16 @@ func updateOptionMap(key string, value string) (err error) { config.LarkClientId = value case "LarkClientSecret": config.LarkClientSecret = value + case "OidcAppId": + config.OidcAppId = value + case "OidcAppSecret": + config.OidcAppSecret = value + case "OidcAuthorizationEndpoint": + config.OidcAuthorizationEndpoint = value + case "OidcTokenEndpoint": + config.OidcTokenEndpoint = value + case "OidcUserinfoEndpoint": + config.OidcUserinfoEndpoint = value case "Footer": config.Footer = value case "SystemName": diff --git a/model/user.go b/model/user.go index 924d72f9..a964a0d7 100644 --- a/model/user.go +++ b/model/user.go @@ -39,6 +39,7 @@ type User struct { GitHubId string `json:"github_id" gorm:"column:github_id;index"` WeChatId string `json:"wechat_id" gorm:"column:wechat_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! 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"` @@ -245,6 +246,14 @@ func (user *User) FillUserByLarkId() error { 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 { if user.WeChatId == "" { return errors.New("WeChat id 为空!") @@ -277,6 +286,10 @@ func IsLarkIdAlreadyTaken(githubId string) bool { 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 { return DB.Where("username = ?", username).Find(&User{}).RowsAffected == 1 } diff --git a/router/api.go b/router/api.go index d2ada4eb..6d00c6ea 100644 --- a/router/api.go +++ b/router/api.go @@ -23,6 +23,7 @@ func SetApiRouter(router *gin.Engine) { apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail) apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), controller.ResetPassword) 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/state", middleware.CriticalRateLimit(), auth.GenerateOAuthCode) apiRouter.GET("/oauth/wechat", middleware.CriticalRateLimit(), auth.WeChatAuth) From f8144fe534269fbe83f55cfdffacabdbe85ef273 Mon Sep 17 00:00:00 2001 From: OnEvent Date: Tue, 13 Aug 2024 13:57:46 +0800 Subject: [PATCH 09/11] fix: Change the AppId and AppSecret on the Web UI to the standard usage: ClientId, ClientSecret. --- web/berry/src/config.js | 2 +- .../Authentication/AuthForms/AuthLogin.js | 2 +- web/berry/src/views/Profile/index.js | 2 +- .../views/Setting/component/SystemSetting.js | 58 +++++++++---------- 4 files changed, 32 insertions(+), 32 deletions(-) diff --git a/web/berry/src/config.js b/web/berry/src/config.js index 14e6ffcb..8c1faf9b 100644 --- a/web/berry/src/config.js +++ b/web/berry/src/config.js @@ -24,7 +24,7 @@ const config = { wechat_login: false, wechat_qrcode: '', oidc: false, - oidc_app_id: '', + oidc_client_id: '', oidc_authorization_endpoint: '', oidc_token_endpoint: '', oidc_userinfo_endpoint: '', diff --git a/web/berry/src/views/Authentication/AuthForms/AuthLogin.js b/web/berry/src/views/Authentication/AuthForms/AuthLogin.js index 7e4abc1c..7efd0362 100644 --- a/web/berry/src/views/Authentication/AuthForms/AuthLogin.js +++ b/web/berry/src/views/Authentication/AuthForms/AuthLogin.js @@ -152,7 +152,7 @@ const LoginForm = ({ ...others }) => { diff --git a/web/berry/src/views/Setting/component/SystemSetting.js b/web/berry/src/views/Setting/component/SystemSetting.js index 0815d40b..186eec5f 100644 --- a/web/berry/src/views/Setting/component/SystemSetting.js +++ b/web/berry/src/views/Setting/component/SystemSetting.js @@ -34,8 +34,8 @@ const SystemSetting = () => { LarkClientId: '', LarkClientSecret: '', OidcEnabled: '', - OidcAppId: '', - OidcAppSecret: '', + OidcClientId: '', + OidcClientSecret: '', OidcAuthorizationEndpoint: '', OidcTokenEndpoint: '', OidcUserinfoEndpoint: '', @@ -240,18 +240,18 @@ const SystemSetting = () => { const submitOidc = async () => { const OidcConfig = { - OidcAppId: inputs.OidcAppId, - OidcAppSecret: inputs.OidcAppSecret, + OidcClientId: inputs.OidcClientId, + OidcClientSecret: inputs.OidcClientSecret, OidcAuthorizationEndpoint: inputs.OidcAuthorizationEndpoint, OidcTokenEndpoint: inputs.OidcTokenEndpoint, OidcUserinfoEndpoint: inputs.OidcUserinfoEndpoint }; console.log(OidcConfig); - if (originInputs['OidcAppId'] !== inputs.OidcAppId) { - await updateOption('OidcAppId', inputs.OidcAppId); + if (originInputs['OidcClientId'] !== inputs.OidcClientId) { + await updateOption('OidcClientId', inputs.OidcClientId); } - if (originInputs['OidcAppSecret'] !== inputs.OidcAppSecret && inputs.OidcAppSecret !== '') { - await updateOption('OidcAppSecret', inputs.OidcAppSecret); + if (originInputs['OidcClientSecret'] !== inputs.OidcClientSecret && inputs.OidcClientSecret !== '') { + await updateOption('OidcClientSecret', inputs.OidcClientSecret); } if (originInputs['OidcAuthorizationEndpoint'] !== inputs.OidcAuthorizationEndpoint) { await updateOption('OidcAuthorizationEndpoint', inputs.OidcAuthorizationEndpoint); @@ -332,7 +332,7 @@ const SystemSetting = () => { } /> @@ -679,27 +679,27 @@ const SystemSetting = () => { - App ID + Client ID - App Secret + Client Secret @@ -707,49 +707,49 @@ const SystemSetting = () => { - 授权地址 + Authorization Endpoint - 认证地址 + Token Endpoint - 用户地址 + Userinfo Endpoint From e66b73faf500b75225fc64e1a4721f353ce9eb7a Mon Sep 17 00:00:00 2001 From: OnEvent Date: Tue, 13 Aug 2024 14:35:06 +0800 Subject: [PATCH 10/11] feat: Support quick configuration of OIDC through Well-Known Discovery Endpoint --- .../views/Setting/component/SystemSetting.js | 52 +++++++++++++++---- 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/web/berry/src/views/Setting/component/SystemSetting.js b/web/berry/src/views/Setting/component/SystemSetting.js index 186eec5f..84e4f667 100644 --- a/web/berry/src/views/Setting/component/SystemSetting.js +++ b/web/berry/src/views/Setting/component/SystemSetting.js @@ -34,6 +34,7 @@ const SystemSetting = () => { LarkClientId: '', LarkClientSecret: '', OidcEnabled: '', + OidcWellKnown: '', OidcClientId: '', OidcClientSecret: '', OidcAuthorizationEndpoint: '', @@ -150,8 +151,9 @@ const SystemSetting = () => { name === 'MessagePusherToken' || name === 'LarkClientId' || name === 'LarkClientSecret' || - name === 'OidcAppId' || - name === 'OidcAppSecret' || + name === 'OidcClientId' || + name === 'OidcClientSecret' || + name === 'OidcWellKnown' || name === 'OidcAuthorizationEndpoint' || name === 'OidcTokenEndpoint' || name === 'OidcUserinfoEndpoint' @@ -239,14 +241,25 @@ const SystemSetting = () => { }; const submitOidc = async () => { - const OidcConfig = { - OidcClientId: inputs.OidcClientId, - OidcClientSecret: inputs.OidcClientSecret, - OidcAuthorizationEndpoint: inputs.OidcAuthorizationEndpoint, - OidcTokenEndpoint: inputs.OidcTokenEndpoint, - OidcUserinfoEndpoint: inputs.OidcUserinfoEndpoint - }; - console.log(OidcConfig); + 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); } @@ -675,6 +688,9 @@ const SystemSetting = () => { 主页链接填 { inputs.ServerAddress } ,重定向 URL 填 { `${ inputs.ServerAddress }/oauth/oidc` } +
+ + 若你的 OIDC Provider 支持 Discovery Endpoint,你可以仅填写 OIDC Well-Known URL,系统会自动获取 OIDC 配置 @@ -705,6 +721,20 @@ const SystemSetting = () => { /> + + + Well-Known URL + + + Authorization Endpoint @@ -741,7 +771,7 @@ const SystemSetting = () => { name="OidcUserinfoEndpoint" value={ inputs.OidcUserinfoEndpoint || '' } onChange={ handleInputChange } - label="认证地址" + label="Userinfo Endpoint" placeholder="输入 OIDC 的 Userinfo Endpoint" disabled={ loading } /> From af8be721c505a969eef809eed892fe272d17df92 Mon Sep 17 00:00:00 2001 From: OnEvent Date: Tue, 13 Aug 2024 15:26:05 +0800 Subject: [PATCH 11/11] 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 --- common/config/config.go | 5 +++-- controller/auth/oidc.go | 4 ++-- controller/misc.go | 3 ++- model/option.go | 10 ++++++---- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/common/config/config.go b/common/config/config.go index f9e4a540..43f56862 100644 --- a/common/config/config.go +++ b/common/config/config.go @@ -71,8 +71,9 @@ var GitHubClientSecret = "" var LarkClientId = "" var LarkClientSecret = "" -var OidcAppId = "" -var OidcAppSecret = "" +var OidcClientId = "" +var OidcClientSecret = "" +var OidcWellKnown = "" var OidcAuthorizationEndpoint = "" var OidcTokenEndpoint = "" var OidcUserinfoEndpoint = "" diff --git a/controller/auth/oidc.go b/controller/auth/oidc.go index 02865c60..7b4ad4b9 100644 --- a/controller/auth/oidc.go +++ b/controller/auth/oidc.go @@ -38,8 +38,8 @@ func getOidcUserInfoByCode(code string) (*OidcUser, error) { return nil, errors.New("无效的参数") } values := map[string]string{ - "client_id": config.OidcAppId, - "client_secret": config.OidcAppSecret, + "client_id": config.OidcClientId, + "client_secret": config.OidcClientSecret, "code": code, "grant_type": "authorization_code", "redirect_uri": fmt.Sprintf("%s/oauth/oidc", config.ServerAddress), diff --git a/controller/misc.go b/controller/misc.go index 0aef52c0..ae900870 100644 --- a/controller/misc.go +++ b/controller/misc.go @@ -37,7 +37,8 @@ func GetStatus(c *gin.Context) { "quota_per_unit": config.QuotaPerUnit, "display_in_currency": config.DisplayInCurrencyEnabled, "oidc": config.OidcEnabled, - "oidc_app_id": config.OidcAppId, + "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, diff --git a/model/option.go b/model/option.go index fa9f9c98..8fd30aee 100644 --- a/model/option.go +++ b/model/option.go @@ -179,10 +179,12 @@ func updateOptionMap(key string, value string) (err error) { config.LarkClientId = value case "LarkClientSecret": config.LarkClientSecret = value - case "OidcAppId": - config.OidcAppId = value - case "OidcAppSecret": - config.OidcAppSecret = value + case "OidcClientId": + config.OidcClientId = value + case "OidcClientSecret": + config.OidcClientSecret = value + case "OidcWellKnown": + config.OidcWellKnown = value case "OidcAuthorizationEndpoint": config.OidcAuthorizationEndpoint = value case "OidcTokenEndpoint":