feat: add oidc support

This commit is contained in:
wzxjohn
2025-02-28 15:18:03 +08:00
parent ecb5b5630c
commit c433af284c
18 changed files with 582 additions and 54 deletions

View File

@@ -160,6 +160,14 @@ function App() {
</Suspense>
}
/>
<Route
path='/oauth/oidc'
element={
<Suspense fallback={<Loading></Loading>}>
<OAuth2Callback type='oidc'></OAuth2Callback>
</Suspense>
}
/>
<Route
path='/oauth/linuxdo'
element={

View File

@@ -9,7 +9,7 @@ import {
showSuccess,
updateAPI,
} from '../helpers';
import { onGitHubOAuthClicked, onLinuxDOOAuthClicked } from './utils';
import {onGitHubOAuthClicked, onOIDCClicked, onLinuxDOOAuthClicked} from './utils';
import Turnstile from 'react-turnstile';
import {
Button,
@@ -25,6 +25,7 @@ import Text from '@douyinfe/semi-ui/lib/es/typography/text';
import TelegramLoginButton from 'react-telegram-login';
import { IconGithubLogo, IconAlarm } from '@douyinfe/semi-icons';
import OIDCIcon from './OIDCIcon.js';
import WeChatIcon from './WeChatIcon';
import { setUserData } from '../helpers/data.js';
import LinuxDoIcon from './LinuxDoIcon.js';
@@ -229,6 +230,7 @@ const LoginForm = () => {
</Text>
</div>
{status.github_oauth ||
status.oidc ||
status.wechat_login ||
status.telegram_oauth ||
status.linuxdo_oauth ? (
@@ -254,6 +256,17 @@ const LoginForm = () => {
) : (
<></>
)}
{status.oidc ? (
<Button
type='primary'
icon={<OIDCIcon />}
onClick={() =>
onOIDCClicked(status.oidc_authorization_endpoint, status.oidc_client_id)
}
/>
) : (
<></>
)}
{status.linuxdo_oauth ? (
<Button
icon={<LinuxDoIcon />}

View File

@@ -0,0 +1,22 @@
import React from 'react';
import { Icon } from '@douyinfe/semi-ui';
const OIDCIcon = (props) => {
function CustomIcon() {
return (
<svg t="1723135116886" className="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
p-id="10969" width="1em" height="1em">
<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>
);
}
return <Icon svg={<CustomIcon />} />;
};
export default OIDCIcon;

View File

@@ -10,7 +10,7 @@ import {
} from '../helpers';
import Turnstile from 'react-turnstile';
import {UserContext} from '../context/User';
import {onGitHubOAuthClicked, onLinuxDOOAuthClicked} from './utils';
import {onGitHubOAuthClicked, onOIDCClicked, onLinuxDOOAuthClicked} from './utils';
import {
Avatar,
Banner,
@@ -640,6 +640,36 @@ const PersonalSetting = () => {
</div>
</div>
</div>
<div style={{marginTop: 10}}>
<Typography.Text strong>{t('OIDC')}</Typography.Text>
<div
style={{display: 'flex', justifyContent: 'space-between'}}
>
<div>
<Input
value={
userState.user && userState.user.oidc_id !== ''
? userState.user.oidc_id
: t('未绑定')
}
readonly={true}
></Input>
</div>
<div>
<Button
onClick={() => {
onOIDCClicked(status.oidc_authorization_endpoint, status.oidc_client_id);
}}
disabled={
(userState.user && userState.user.oidc_id !== '') ||
!status.oidc
}
>
{status.oidc ? t('绑定') : t('未启用')}
</Button>
</div>
</div>
</div>
<div style={{marginTop: 10}}>
<Typography.Text strong>{t('Telegram')}</Typography.Text>
<div

View File

@@ -6,7 +6,8 @@ import { Button, Card, Divider, Form, Icon, Layout, Modal } from '@douyinfe/semi
import Title from '@douyinfe/semi-ui/lib/es/typography/title';
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
import { IconGithubLogo } from '@douyinfe/semi-icons';
import { onGitHubOAuthClicked, onLinuxDOOAuthClicked } from './utils.js';
import {onGitHubOAuthClicked, onLinuxDOOAuthClicked, onOIDCClicked} from './utils.js';
import OIDCIcon from "./OIDCIcon.js";
import LinuxDoIcon from './LinuxDoIcon.js';
import WeChatIcon from './WeChatIcon.js';
import TelegramLoginButton from 'react-telegram-login/src';
@@ -262,6 +263,7 @@ const RegisterForm = () => {
</Text>
</div>
{status.github_oauth ||
status.oidc ||
status.wechat_login ||
status.telegram_oauth ||
status.linuxdo_oauth ? (
@@ -287,6 +289,17 @@ const RegisterForm = () => {
) : (
<></>
)}
{status.oidc ? (
<Button
type='primary'
icon={<OIDCIcon />}
onClick={() =>
onOIDCClicked(status.oidc_authorization_endpoint, status.oidc_client_id)
}
/>
) : (
<></>
)}
{status.linuxdo_oauth ? (
<Button
icon={<LinuxDoIcon />}

View File

@@ -20,6 +20,13 @@ const SystemSetting = () => {
GitHubOAuthEnabled: '',
GitHubClientId: '',
GitHubClientSecret: '',
OIDCEnabled: '',
OIDCClientId: '',
OIDCClientSecret: '',
OIDCWellKnown: '',
OIDCAuthorizationEndpoint: '',
OIDCTokenEndpoint: '',
OIDCUserInfoEndpoint: '',
Notice: '',
SMTPServer: '',
SMTPPort: '',
@@ -106,6 +113,7 @@ const SystemSetting = () => {
case 'PasswordRegisterEnabled':
case 'EmailVerificationEnabled':
case 'GitHubOAuthEnabled':
case 'OIDCEnabled':
case 'LinuxDOOAuthEnabled':
case 'WeChatAuthEnabled':
case 'TelegramOAuthEnabled':
@@ -159,6 +167,12 @@ const SystemSetting = () => {
name === 'PayAddress' ||
name === 'GitHubClientId' ||
name === 'GitHubClientSecret' ||
name === 'OIDCWellKnown' ||
name === 'OIDCClientId' ||
name === 'OIDCClientSecret' ||
name === 'OIDCAuthorizationEndpoint' ||
name === 'OIDCTokenEndpoint' ||
name === 'OIDCUserInfoEndpoint' ||
name === 'WeChatServerAddress' ||
name === 'WeChatServerToken' ||
name === 'WeChatAccountQRCodeImageURL' ||
@@ -286,6 +300,43 @@ const SystemSetting = () => {
}
};
const submitOIDCSettings = 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);
}
}
const submitTelegramSettings = async () => {
// await updateOption('TelegramOAuthEnabled', inputs.TelegramOAuthEnabled);
await updateOption('TelegramBotToken', inputs.TelegramBotToken);
@@ -370,7 +421,7 @@ const SystemSetting = () => {
</Header>
<Message info>
注意代理功能仅对图片请求和 Webhook 请求生效不会影响其他 API 请求如需配置 API 请求代理请参考
<a
<a
href='https://github.com/Calcium-Ion/new-api/blob/main/docs/channel/other_setting.md'
target='_blank'
rel='noreferrer'
@@ -518,6 +569,12 @@ const SystemSetting = () => {
name='GitHubOAuthEnabled'
onChange={handleInputChange}
/>
<Form.Checkbox
checked={inputs.OIDCEnabled === 'true'}
label='允许通过 OIDC 登录 & 注册'
name='OIDCEnabled'
onChange={handleInputChange}
/>
<Form.Checkbox
checked={inputs.LinuxDOOAuthEnabled === 'true'}
label='允许通过 LinuxDO 账户登录 & 注册'
@@ -864,6 +921,68 @@ const SystemSetting = () => {
<Form.Button onClick={submitLinuxDOOAuth}>
保存 LinuxDO OAuth 设置
</Form.Button>
<Divider />
<Header as='h3' inverted={isDark}>
配置 OIDC
<Header.Subheader>
用以支持通过 OIDC 登录例如 OktaAuth0 等兼容 OIDC 协议的 IdP
</Header.Subheader>
</Header>
<Message>
主页链接填 <code>{ inputs.ServerAddress }</code>
重定向 URL <code>{ `${ inputs.ServerAddress }/oauth/oidc` }</code>
</Message>
<Message>
若你的 OIDC Provider 支持 Discovery Endpoint你可以仅填写 OIDC Well-Known URL系统会自动获取 OIDC 配置
</Message>
<Form.Group widths={3}>
<Form.Input
label='Client ID'
name='OIDCClientId'
onChange={handleInputChange}
value={inputs.OIDCClientId}
placeholder='输入 OIDC 的 Client ID'
/>
<Form.Input
label='Client Secret'
name='OIDCClientSecret'
onChange={handleInputChange}
type='password'
value={inputs.OIDCClientSecret}
placeholder='敏感信息不会发送到前端显示'
/>
<Form.Input
label='Well-Known URL'
name='OIDCWellKnown'
onChange={handleInputChange}
value={inputs.OIDCWellKnown}
placeholder='请输入 OIDC 的 Well-Known URL'
/>
<Form.Input
label='Authorization Endpoint'
name='OIDCAuthorizationEndpoint'
onChange={handleInputChange}
value={inputs.OIDCAuthorizationEndpoint}
placeholder='输入 OIDC 的 Authorization Endpoint'
/>
<Form.Input
label='Token Endpoint'
name='OIDCTokenEndpoint'
onChange={handleInputChange}
value={inputs.OIDCTokenEndpoint}
placeholder='输入 OIDC 的 Token Endpoint'
/>
<Form.Input
label='Userinfo Endpoint'
name='OIDCUserInfoEndpoint'
onChange={handleInputChange}
value={inputs.OIDCUserInfoEndpoint}
placeholder='输入 OIDC 的 Userinfo Endpoint'
/>
</Form.Group>
<Form.Button onClick={submitOIDCSettings}>
保存 OIDC 设置
</Form.Button>
</Form>
</Grid.Column>
</Grid>

View File

@@ -16,6 +16,21 @@ export async function getOAuthState() {
}
}
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 async function onGitHubOAuthClicked(github_client_id) {
const state = await getOAuthState();
if (!state) return;

View File

@@ -151,6 +151,12 @@ const Home = () => {
? t('已启用')
: t('未启用')}
</p>
<p>
{t('OIDC 身份验证')}
{statusState?.status?.oidc === true
? t('已启用')
: t('未启用')}
</p>
<p>
{t('微信身份验证')}
{statusState?.status?.wechat_login === true

View File

@@ -26,6 +26,7 @@ const EditUser = (props) => {
display_name: '',
password: '',
github_id: '',
oidc_id: '',
wechat_id: '',
email: '',
quota: 0,
@@ -37,6 +38,7 @@ const EditUser = (props) => {
display_name,
password,
github_id,
oidc_id,
wechat_id,
telegram_id,
email,
@@ -232,6 +234,15 @@ const EditUser = (props) => {
placeholder={t('此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改')}
readonly
/>
<div style={{ marginTop: 20 }}>
<Typography.Text>{t('`已绑定的 OIDC 账户')}</Typography.Text>
</div>
<Input
name='oidc_id'
value={oidc_id}
placeholder={t('此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改')}
readonly
/>
<div style={{ marginTop: 20 }}>
<Typography.Text>{t('已绑定的微信账户')}</Typography.Text>
</div>