mirror of
https://github.com/songquanpeng/one-api.git
synced 2025-11-12 19:33:41 +08:00
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:
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 Wechat from 'assets/images/icons/wechat.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 ||============================ //
|
||||
|
||||
@@ -50,7 +51,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.oidc) {
|
||||
tripartiteLogin = true;
|
||||
}
|
||||
|
||||
@@ -145,6 +146,29 @@ const LoginForm = ({ ...others }) => {
|
||||
</AnimateButton>
|
||||
</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}>
|
||||
<Box
|
||||
sx={{
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<UserCard>
|
||||
@@ -141,6 +151,9 @@ export default function Profile() {
|
||||
<Label variant="ghost" color={inputs.lark_id ? 'primary' : 'default'}>
|
||||
<SvgIcon component={Lark} inheritViewBox="0 0 24 24" /> {inputs.lark_id || '未绑定'}
|
||||
</Label>
|
||||
<Label variant="ghost" color={inputs.oidc_id ? 'primary' : 'default'}>
|
||||
<SvgIcon component={OIDC} inheritViewBox="0 0 24 24" /> {getOidcId() || '未绑定'}
|
||||
</Label>
|
||||
</Stack>
|
||||
<SubCard title="个人信息">
|
||||
<Grid container spacing={2}>
|
||||
@@ -216,6 +229,13 @@ export default function Profile() {
|
||||
</Button>
|
||||
</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}>
|
||||
<Button
|
||||
variant="contained"
|
||||
|
||||
@@ -33,6 +33,13 @@ const SystemSetting = () => {
|
||||
GitHubClientSecret: '',
|
||||
LarkClientId: '',
|
||||
LarkClientSecret: '',
|
||||
OidcEnabled: '',
|
||||
OidcWellKnown: '',
|
||||
OidcClientId: '',
|
||||
OidcClientSecret: '',
|
||||
OidcAuthorizationEndpoint: '',
|
||||
OidcTokenEndpoint: '',
|
||||
OidcUserinfoEndpoint: '',
|
||||
Notice: '',
|
||||
SMTPServer: '',
|
||||
SMTPPort: '',
|
||||
@@ -94,6 +101,7 @@ const SystemSetting = () => {
|
||||
case 'TurnstileCheckEnabled':
|
||||
case 'EmailDomainRestrictionEnabled':
|
||||
case 'RegisterEnabled':
|
||||
case 'OidcEnabled':
|
||||
value = inputs[key] === 'true' ? 'false' : 'true';
|
||||
break;
|
||||
default:
|
||||
@@ -142,8 +150,15 @@ const SystemSetting = () => {
|
||||
name === 'MessagePusherAddress' ||
|
||||
name === 'MessagePusherToken' ||
|
||||
name === 'LarkClientId' ||
|
||||
name === 'LarkClientSecret'
|
||||
) {
|
||||
name === 'LarkClientSecret' ||
|
||||
name === 'OidcClientId' ||
|
||||
name === 'OidcClientSecret' ||
|
||||
name === 'OidcWellKnown' ||
|
||||
name === 'OidcAuthorizationEndpoint' ||
|
||||
name === 'OidcTokenEndpoint' ||
|
||||
name === 'OidcUserinfoEndpoint'
|
||||
)
|
||||
{
|
||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||
} else {
|
||||
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 (
|
||||
<>
|
||||
<Stack spacing={2}>
|
||||
@@ -291,6 +343,12 @@ const SystemSetting = () => {
|
||||
control={<Checkbox checked={inputs.GitHubOAuthEnabled === 'true'} onChange={handleInputChange} name="GitHubOAuthEnabled" />}
|
||||
/>
|
||||
</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}>
|
||||
<FormControlLabel
|
||||
label="允许通过微信登录 & 注册"
|
||||
@@ -616,6 +674,117 @@ const SystemSetting = () => {
|
||||
</Grid>
|
||||
</Grid>
|
||||
</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
|
||||
title="配置 Message Pusher"
|
||||
subTitle={
|
||||
|
||||
Reference in New Issue
Block a user