Merge pull request #848 from wzxjohn/feature/oidc
feat: add oidc support
This commit is contained in:
@@ -156,6 +156,14 @@ function App() {
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/oauth/oidc'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<OAuth2Callback type='oidc'></OAuth2Callback>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/oauth/linuxdo'
|
||||
element={
|
||||
|
||||
@@ -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_enabled ||
|
||||
status.wechat_login ||
|
||||
status.telegram_oauth ||
|
||||
status.linuxdo_oauth ? (
|
||||
@@ -254,6 +256,17 @@ const LoginForm = () => {
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{status.oidc_enabled ? (
|
||||
<Button
|
||||
type='primary'
|
||||
icon={<OIDCIcon />}
|
||||
onClick={() =>
|
||||
onOIDCClicked(status.oidc_authorization_endpoint, status.oidc_client_id)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{status.linuxdo_oauth ? (
|
||||
<Button
|
||||
icon={<LinuxDoIcon />}
|
||||
|
||||
22
web/src/components/OIDCIcon.js
Normal file
22
web/src/components/OIDCIcon.js
Normal 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;
|
||||
@@ -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_enabled
|
||||
}
|
||||
>
|
||||
{status.oidc_enabled ? t('绑定') : t('未启用')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{marginTop: 10}}>
|
||||
<Typography.Text strong>{t('Telegram')}</Typography.Text>
|
||||
<div
|
||||
|
||||
@@ -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_enabled ||
|
||||
status.wechat_login ||
|
||||
status.telegram_oauth ||
|
||||
status.linuxdo_oauth ? (
|
||||
@@ -287,6 +289,17 @@ const RegisterForm = () => {
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{status.oidc_enabled ? (
|
||||
<Button
|
||||
type='primary'
|
||||
icon={<OIDCIcon />}
|
||||
onClick={() =>
|
||||
onOIDCClicked(status.oidc_authorization_endpoint, status.oidc_client_id)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{status.linuxdo_oauth ? (
|
||||
<Button
|
||||
icon={<LinuxDoIcon />}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
Message,
|
||||
Modal,
|
||||
} from 'semantic-ui-react';
|
||||
import { API, removeTrailingSlash, showError, verifyJSON } from '../helpers';
|
||||
import { API, removeTrailingSlash, showError, showSuccess, verifyJSON } from '../helpers';
|
||||
|
||||
import { useTheme } from '../context/Theme';
|
||||
|
||||
@@ -20,6 +20,13 @@ const SystemSetting = () => {
|
||||
GitHubOAuthEnabled: '',
|
||||
GitHubClientId: '',
|
||||
GitHubClientSecret: '',
|
||||
'oidc.enabled': '',
|
||||
'oidc.client_id': '',
|
||||
'oidc.client_secret': '',
|
||||
'oidc.well_known': '',
|
||||
'oidc.authorization_endpoint': '',
|
||||
'oidc.token_endpoint': '',
|
||||
'oidc.user_info_endpoint': '',
|
||||
Notice: '',
|
||||
SMTPServer: '',
|
||||
SMTPPort: '',
|
||||
@@ -106,6 +113,7 @@ const SystemSetting = () => {
|
||||
case 'PasswordRegisterEnabled':
|
||||
case 'EmailVerificationEnabled':
|
||||
case 'GitHubOAuthEnabled':
|
||||
case 'oidc.enabled':
|
||||
case 'LinuxDOOAuthEnabled':
|
||||
case 'WeChatAuthEnabled':
|
||||
case 'TelegramOAuthEnabled':
|
||||
@@ -159,6 +167,12 @@ const SystemSetting = () => {
|
||||
name === 'PayAddress' ||
|
||||
name === 'GitHubClientId' ||
|
||||
name === 'GitHubClientSecret' ||
|
||||
name === 'oidc.well_known' ||
|
||||
name === 'oidc.client_id' ||
|
||||
name === 'oidc.client_secret' ||
|
||||
name === 'oidc.authorization_endpoint' ||
|
||||
name === 'oidc.token_endpoint' ||
|
||||
name === 'oidc.user_info_endpoint' ||
|
||||
name === 'WeChatServerAddress' ||
|
||||
name === 'WeChatServerToken' ||
|
||||
name === 'WeChatAccountQRCodeImageURL' ||
|
||||
@@ -286,6 +300,44 @@ const SystemSetting = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const submitOIDCSettings = async () => {
|
||||
if (inputs['oidc.well_known'] !== '') {
|
||||
if (!inputs['oidc.well_known'].startsWith('http://') && !inputs['oidc.well_known'].startsWith('https://')) {
|
||||
showError('Well-Known URL 必须以 http:// 或 https:// 开头');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await API.get(inputs['oidc.well_known']);
|
||||
inputs['oidc.authorization_endpoint'] = res.data['authorization_endpoint'];
|
||||
inputs['oidc.token_endpoint'] = res.data['token_endpoint'];
|
||||
inputs['oidc.user_info_endpoint'] = res.data['userinfo_endpoint'];
|
||||
showSuccess('获取 OIDC 配置成功!');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
showError("获取 OIDC 配置失败,请检查网络状况和 Well-Known URL 是否正确");
|
||||
}
|
||||
}
|
||||
|
||||
if (originInputs['oidc.well_known'] !== inputs['oidc.well_known']) {
|
||||
await updateOption('oidc.well_known', inputs['oidc.well_known']);
|
||||
}
|
||||
if (originInputs['oidc.client_id'] !== inputs['oidc.client_id']) {
|
||||
await updateOption('oidc.client_id', inputs['oidc.client_id']);
|
||||
}
|
||||
if (originInputs['oidc.client_secret'] !== inputs['oidc.client_secret'] && inputs['oidc.client_secret'] !== '') {
|
||||
await updateOption('oidc.client_secret', inputs['oidc.client_secret']);
|
||||
}
|
||||
if (originInputs['oidc.authorization_endpoint'] !== inputs['oidc.authorization_endpoint']) {
|
||||
await updateOption('oidc.authorization_endpoint', inputs['oidc.authorization_endpoint']);
|
||||
}
|
||||
if (originInputs['oidc.token_endpoint'] !== inputs['oidc.token_endpoint']) {
|
||||
await updateOption('oidc.token_endpoint', inputs['oidc.token_endpoint']);
|
||||
}
|
||||
if (originInputs['oidc.user_info_endpoint'] !== inputs['oidc.user_info_endpoint']) {
|
||||
await updateOption('oidc.user_info_endpoint', inputs['oidc.user_info_endpoint']);
|
||||
}
|
||||
}
|
||||
|
||||
const submitTelegramSettings = async () => {
|
||||
// await updateOption('TelegramOAuthEnabled', inputs.TelegramOAuthEnabled);
|
||||
await updateOption('TelegramBotToken', inputs.TelegramBotToken);
|
||||
@@ -370,7 +422,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 +570,12 @@ const SystemSetting = () => {
|
||||
name='GitHubOAuthEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Form.Checkbox
|
||||
checked={inputs['oidc.enabled'] === 'true'}
|
||||
label='允许通过 OIDC 登录 & 注册'
|
||||
name='oidc.enabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Form.Checkbox
|
||||
checked={inputs.LinuxDOOAuthEnabled === 'true'}
|
||||
label='允许通过 LinuxDO 账户登录 & 注册'
|
||||
@@ -864,6 +922,68 @@ const SystemSetting = () => {
|
||||
<Form.Button onClick={submitLinuxDOOAuth}>
|
||||
保存 LinuxDO OAuth 设置
|
||||
</Form.Button>
|
||||
<Divider />
|
||||
<Header as='h3' inverted={isDark}>
|
||||
配置 OIDC
|
||||
<Header.Subheader>
|
||||
用以支持通过 OIDC 登录,例如 Okta、Auth0 等兼容 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='oidc.client_id'
|
||||
onChange={handleInputChange}
|
||||
value={inputs['oidc.client_id']}
|
||||
placeholder='输入 OIDC 的 Client ID'
|
||||
/>
|
||||
<Form.Input
|
||||
label='Client Secret'
|
||||
name='oidc.client_secret'
|
||||
onChange={handleInputChange}
|
||||
type='password'
|
||||
value={inputs['oidc.client_secret']}
|
||||
placeholder='敏感信息不会发送到前端显示'
|
||||
/>
|
||||
<Form.Input
|
||||
label='Well-Known URL'
|
||||
name='oidc.well_known'
|
||||
onChange={handleInputChange}
|
||||
value={inputs['oidc.well_known']}
|
||||
placeholder='请输入 OIDC 的 Well-Known URL'
|
||||
/>
|
||||
<Form.Input
|
||||
label='Authorization Endpoint'
|
||||
name='oidc.authorization_endpoint'
|
||||
onChange={handleInputChange}
|
||||
value={inputs['oidc.authorization_endpoint']}
|
||||
placeholder='输入 OIDC 的 Authorization Endpoint'
|
||||
/>
|
||||
<Form.Input
|
||||
label='Token Endpoint'
|
||||
name='oidc.token_endpoint'
|
||||
onChange={handleInputChange}
|
||||
value={inputs['oidc.token_endpoint']}
|
||||
placeholder='输入 OIDC 的 Token Endpoint'
|
||||
/>
|
||||
<Form.Input
|
||||
label='Userinfo Endpoint'
|
||||
name='oidc.user_info_endpoint'
|
||||
onChange={handleInputChange}
|
||||
value={inputs['oidc.user_info_endpoint']}
|
||||
placeholder='输入 OIDC 的 Userinfo Endpoint'
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={submitOIDCSettings}>
|
||||
保存 OIDC 设置
|
||||
</Form.Button>
|
||||
</Form>
|
||||
</Grid.Column>
|
||||
</Grid>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user