feat: oidc (#1126)
* feat: oidc * feat: oidc remove comments * feat: oidc add shared type * feat: oidc add id_token sign alg * feat: oidc i18n & batch delete * feat: oidc i18n * feat: oidc import fix * feat: oidc saml list fix * feat: oidc i18n move to plugin * feat: oidc cr fix * feat: oidc cr fix * feat: oidc cr fix * feat: oidc fix nonce value * feat: oidc page extension fix * feat: oidc remove canceltoken Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
parent
78fe77e2cf
commit
1ac0032e5c
1
packages/app/client/src/plugins/oidc.ts
Normal file
1
packages/app/client/src/plugins/oidc.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from '@nocobase/plugin-oidc/client';
|
@ -18,18 +18,19 @@ import {
|
||||
RemoteRouteSwitchProvider,
|
||||
RouteSchemaComponent,
|
||||
RouteSwitch,
|
||||
useRoutes
|
||||
useRoutes,
|
||||
} from '../route-switch';
|
||||
import {
|
||||
AntdSchemaComponentProvider,
|
||||
DesignableSwitch,
|
||||
MenuItemInitializers,
|
||||
SchemaComponentProvider
|
||||
SchemaComponentProvider,
|
||||
} from '../schema-component';
|
||||
import { SchemaInitializerProvider } from '../schema-initializer';
|
||||
import { BlockTemplateDetails, BlockTemplatePage, SchemaTemplateShortcut } from '../schema-templates';
|
||||
import { SystemSettingsProvider, SystemSettingsShortcut } from '../system-settings';
|
||||
import { SigninPage, SignupPage } from '../user';
|
||||
import { SigninPageExtensionProvider } from '../user/SigninPageExtension';
|
||||
import { compose } from './compose';
|
||||
|
||||
export interface ApplicationOptions {
|
||||
@ -112,6 +113,7 @@ export class Application {
|
||||
});
|
||||
this.use(BlockSchemaComponentProvider);
|
||||
this.use(AntdSchemaComponentProvider);
|
||||
this.use(SigninPageExtensionProvider);
|
||||
this.use(ACLProvider);
|
||||
this.use(RemoteDocumentTitleProvider);
|
||||
this.use(PMProvider);
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { ISchema, useForm } from '@formily/react';
|
||||
import { Tabs } from 'antd';
|
||||
import React, { useCallback } from 'react';
|
||||
import { Space, Tabs } from 'antd';
|
||||
import React, { useCallback, useContext } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link, useHistory, useLocation } from 'react-router-dom';
|
||||
import { SchemaComponent, useAPIClient, useCurrentDocumentTitle, useSystemSettings } from '..';
|
||||
import { useSigninPageExtension } from './SigninPageExtension';
|
||||
import VerificationCode from './VerificationCode';
|
||||
|
||||
const passwordForm: ISchema = {
|
||||
@ -47,7 +49,7 @@ const passwordForm: ISchema = {
|
||||
},
|
||||
};
|
||||
|
||||
function useRedirect(next = '/admin') {
|
||||
export function useRedirect(next = '/admin') {
|
||||
const location = useLocation<any>();
|
||||
const history = useHistory();
|
||||
const redirect = location?.['query']?.redirect;
|
||||
@ -130,10 +132,16 @@ export const SigninPage = (props: SigninPageProps) => {
|
||||
const { t } = useTranslation();
|
||||
useCurrentDocumentTitle('Signin');
|
||||
const ctx = useSystemSettings();
|
||||
const signinExtension = useSigninPageExtension();
|
||||
const { allowSignUp, smsAuthEnabled } = ctx?.data?.data || {};
|
||||
const { schema, components, scope } = props;
|
||||
return (
|
||||
<div>
|
||||
<Space
|
||||
direction="vertical"
|
||||
className={css`
|
||||
display: flex;
|
||||
`}
|
||||
>
|
||||
{smsAuthEnabled ? (
|
||||
<Tabs defaultActiveKey="password">
|
||||
<Tabs.TabPane tab={t('Sign in via account')} key="password">
|
||||
@ -157,11 +165,12 @@ export const SigninPage = (props: SigninPageProps) => {
|
||||
schema={schema || passwordForm}
|
||||
/>
|
||||
)}
|
||||
<div>{signinExtension}</div>
|
||||
{allowSignUp && (
|
||||
<div>
|
||||
<Link to="/signup">{t('Create an account')}</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
24
packages/core/client/src/user/SigninPageExtension.tsx
Normal file
24
packages/core/client/src/user/SigninPageExtension.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import React, { createContext, FunctionComponent, useContext } from 'react';
|
||||
|
||||
export interface SigninPageExtensionContextValue {
|
||||
components: FunctionComponent[];
|
||||
}
|
||||
|
||||
export const SigninPageExtensionContext = createContext<SigninPageExtensionContextValue>({ components: [] });
|
||||
|
||||
export const useSigninPageExtension = () => {
|
||||
const { components } = useContext(SigninPageExtensionContext);
|
||||
return components.map((component, index) => React.createElement(component, { key: index }));
|
||||
};
|
||||
|
||||
export const SigninPageExtensionProvider = (props: { component: FunctionComponent; children: JSX.Element }) => {
|
||||
const { components } = useContext(SigninPageExtensionContext);
|
||||
|
||||
const list = props.component ? [...components, props.component] : components;
|
||||
|
||||
return (
|
||||
<SigninPageExtensionContext.Provider value={{ components: list }}>
|
||||
{props.children}
|
||||
</SigninPageExtensionContext.Provider>
|
||||
);
|
||||
};
|
@ -2,4 +2,4 @@ export * from './CurrentUser';
|
||||
export * from './CurrentUserProvider';
|
||||
export * from './SigninPage';
|
||||
export * from './SignupPage';
|
||||
|
||||
export * from './SigninPageExtension';
|
||||
|
@ -66,6 +66,7 @@ export class PluginManager {
|
||||
if (options?.method !== 'install' || options.reload) {
|
||||
await this.repository.load();
|
||||
}
|
||||
this.app.acl.allow('applicationPlugins', 'list');
|
||||
});
|
||||
this.addStaticMultiple(options.plugins);
|
||||
}
|
||||
@ -329,7 +330,9 @@ export class PluginManager {
|
||||
}
|
||||
|
||||
static getPluginPkgPrefix() {
|
||||
return (process.env.PLUGIN_PACKAGE_PREFIX || '@nocobase/plugin-,@nocobase/preset-,@nocobase/plugin-pro-').split(',');
|
||||
return (process.env.PLUGIN_PACKAGE_PREFIX || '@nocobase/plugin-,@nocobase/preset-,@nocobase/plugin-pro-').split(
|
||||
',',
|
||||
);
|
||||
}
|
||||
|
||||
static async findPackage(name: string) {
|
||||
|
4
packages/plugins/oidc/client.d.ts
vendored
Executable file
4
packages/plugins/oidc/client.d.ts
vendored
Executable file
@ -0,0 +1,4 @@
|
||||
// @ts-nocheck
|
||||
export * from './lib/client';
|
||||
export { default } from './lib/client';
|
||||
|
30
packages/plugins/oidc/client.js
Executable file
30
packages/plugins/oidc/client.js
Executable file
@ -0,0 +1,30 @@
|
||||
"use strict";
|
||||
|
||||
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
|
||||
|
||||
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
|
||||
|
||||
var _index = _interopRequireWildcard(require("./lib/client"));
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
var _exportNames = {};
|
||||
Object.defineProperty(exports, "default", {
|
||||
enumerable: true,
|
||||
get: function get() {
|
||||
return _index.default;
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(_index).forEach(function (key) {
|
||||
if (key === "default" || key === "__esModule") return;
|
||||
if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
|
||||
if (key in exports && exports[key] === _index[key]) return;
|
||||
Object.defineProperty(exports, key, {
|
||||
enumerable: true,
|
||||
get: function get() {
|
||||
return _index[key];
|
||||
}
|
||||
});
|
||||
});
|
12
packages/plugins/oidc/package.json
Normal file
12
packages/plugins/oidc/package.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-oidc",
|
||||
"version": "0.8.0-alpha.13",
|
||||
"main": "lib/server/index.js",
|
||||
"devDependencies": {
|
||||
"@nocobase/server": "0.8.0-alpha.13",
|
||||
"@nocobase/test": "0.8.0-alpha.13"
|
||||
},
|
||||
"dependencies": {
|
||||
"openid-client": "^5.3.0"
|
||||
}
|
||||
}
|
4
packages/plugins/oidc/server.d.ts
vendored
Executable file
4
packages/plugins/oidc/server.d.ts
vendored
Executable file
@ -0,0 +1,4 @@
|
||||
// @ts-nocheck
|
||||
export * from './lib/server';
|
||||
export { default } from './lib/server';
|
||||
|
30
packages/plugins/oidc/server.js
Executable file
30
packages/plugins/oidc/server.js
Executable file
@ -0,0 +1,30 @@
|
||||
"use strict";
|
||||
|
||||
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
|
||||
|
||||
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
|
||||
|
||||
var _index = _interopRequireWildcard(require("./lib/server"));
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
var _exportNames = {};
|
||||
Object.defineProperty(exports, "default", {
|
||||
enumerable: true,
|
||||
get: function get() {
|
||||
return _index.default;
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(_index).forEach(function (key) {
|
||||
if (key === "default" || key === "__esModule") return;
|
||||
if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
|
||||
if (key in exports && exports[key] === _index[key]) return;
|
||||
Object.defineProperty(exports, key, {
|
||||
enumerable: true,
|
||||
get: function get() {
|
||||
return _index[key];
|
||||
}
|
||||
});
|
||||
});
|
101
packages/plugins/oidc/src/client/OIDCList.tsx
Normal file
101
packages/plugins/oidc/src/client/OIDCList.tsx
Normal file
@ -0,0 +1,101 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { Button, Space } from 'antd';
|
||||
import { LoginOutlined } from '@ant-design/icons';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { css } from '@emotion/css';
|
||||
import { useAPIClient, useRedirect } from '@nocobase/client';
|
||||
|
||||
export interface OIDCProvider {
|
||||
clientId: string;
|
||||
title: string;
|
||||
authorizeUrl: string;
|
||||
}
|
||||
|
||||
export const OIDCList = () => {
|
||||
const [list, setList] = useState<OIDCProvider[]>([]);
|
||||
const [windowHandler, setWindowHandler] = useState<Window | undefined>();
|
||||
const api = useAPIClient();
|
||||
const redirect = useRedirect();
|
||||
|
||||
const getOidcList = async () => {
|
||||
const { data: pluginsRes } = await api.request({
|
||||
url: 'app:getPlugins',
|
||||
});
|
||||
if (!(pluginsRes.data as string[]).includes('oidc')) return;
|
||||
const { data: providersRes } = await api.request({
|
||||
url: 'oidcProviders:list',
|
||||
params: {
|
||||
filter: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
setList(providersRes.data);
|
||||
};
|
||||
|
||||
/**
|
||||
* 打开登录弹出框
|
||||
*/
|
||||
const handleOpen = useMemoizedFn(async (item: OIDCProvider) => {
|
||||
const response = await api.request({
|
||||
method: 'post',
|
||||
url: 'oidc:getAuthUrl',
|
||||
data: {
|
||||
clientId: item.clientId,
|
||||
},
|
||||
});
|
||||
|
||||
const authUrl = response?.data?.data;
|
||||
const { width, height } = screen;
|
||||
|
||||
const win = window.open(
|
||||
authUrl,
|
||||
'_blank',
|
||||
`width=800,height=600,left=${(width - 800) / 2},top=${
|
||||
(height - 600) / 2
|
||||
},toolbar=no,menubar=no,location=no,status=no`,
|
||||
);
|
||||
|
||||
setWindowHandler(win);
|
||||
});
|
||||
|
||||
/**
|
||||
* 从弹出窗口,发消息回来进行登录
|
||||
*/
|
||||
const handleOIDCLogin = useMemoizedFn(async (event: MessageEvent) => {
|
||||
await api.auth.signIn(event.data, 'oidc');
|
||||
windowHandler.close();
|
||||
setWindowHandler(undefined);
|
||||
redirect();
|
||||
});
|
||||
|
||||
/**
|
||||
* 监听弹出窗口的消息
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!windowHandler) return;
|
||||
window.addEventListener('message', handleOIDCLogin);
|
||||
return () => {
|
||||
window.removeEventListener('message', handleOIDCLogin);
|
||||
};
|
||||
}, [windowHandler]);
|
||||
|
||||
useEffect(() => {
|
||||
getOidcList();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Space
|
||||
direction="vertical"
|
||||
className={css`
|
||||
display: flex;
|
||||
`}
|
||||
>
|
||||
{list.map((item) => (
|
||||
<Button shape="round" block key={item.clientId} icon={<LoginOutlined />} onClick={() => handleOpen(item)}>
|
||||
OIDC: {item.title}
|
||||
</Button>
|
||||
))}
|
||||
</Space>
|
||||
);
|
||||
};
|
23
packages/plugins/oidc/src/client/OIDCPanel.tsx
Normal file
23
packages/plugins/oidc/src/client/OIDCPanel.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { SchemaComponent, useRecord } from '@nocobase/client';
|
||||
import { Card } from 'antd';
|
||||
import { oidcSchema } from './schemas/oidc';
|
||||
import { RedirectURLInput } from './RedirectURLInput';
|
||||
import { useOidcTranslation } from './locale';
|
||||
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
[uid()]: oidcSchema,
|
||||
},
|
||||
};
|
||||
|
||||
export const OIDCPanel = () => {
|
||||
const { t } = useOidcTranslation();
|
||||
return (
|
||||
<Card bordered={false}>
|
||||
<SchemaComponent components={{ RedirectURLInput }} schema={schema} scope={{ t }} />
|
||||
</Card>
|
||||
);
|
||||
};
|
27
packages/plugins/oidc/src/client/RedirectURLInput.tsx
Normal file
27
packages/plugins/oidc/src/client/RedirectURLInput.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import { FormLayout } from '@formily/antd';
|
||||
import { Field } from '@formily/core';
|
||||
import { observer, useField, useForm } from '@formily/react';
|
||||
import { useEffect } from 'react';
|
||||
import { Input, useRecord } from '@nocobase/client';
|
||||
|
||||
export const RedirectURLInput = observer(() => {
|
||||
const form = useForm();
|
||||
const field = useField<Field>();
|
||||
const record = useRecord();
|
||||
|
||||
const clientId = form.values.clientId ?? record.clientId;
|
||||
|
||||
useEffect(() => {
|
||||
const { protocol, host } = window.location;
|
||||
field.setValue(`${protocol}//${host}/api/oidc:redirect?clientId=${clientId}`);
|
||||
}, [clientId]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FormLayout layout={'vertical'}>
|
||||
<Input disabled value={field.value} />
|
||||
</FormLayout>
|
||||
</div>
|
||||
);
|
||||
});
|
38
packages/plugins/oidc/src/client/index.tsx
Normal file
38
packages/plugins/oidc/src/client/index.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import { PluginManagerContext, SettingsCenterProvider, SigninPageExtensionProvider } from '@nocobase/client';
|
||||
import React, { useContext } from 'react';
|
||||
import { useOidcTranslation } from './locale';
|
||||
import { OIDCList } from './OIDCList';
|
||||
import { OIDCPanel } from './OIDCPanel';
|
||||
|
||||
export default function (props) {
|
||||
const { t } = useOidcTranslation();
|
||||
const ctx = useContext(PluginManagerContext);
|
||||
return (
|
||||
<SigninPageExtensionProvider component={OIDCList}>
|
||||
<SettingsCenterProvider
|
||||
settings={{
|
||||
'oidc-manager': {
|
||||
title: t('OIDC manager'),
|
||||
icon: 'FileOutlined',
|
||||
tabs: {
|
||||
storages: {
|
||||
title: t('OIDC Providers'),
|
||||
component: OIDCPanel,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<PluginManagerContext.Provider
|
||||
value={{
|
||||
components: {
|
||||
...ctx?.components,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</PluginManagerContext.Provider>
|
||||
</SettingsCenterProvider>
|
||||
</SigninPageExtensionProvider>
|
||||
);
|
||||
}
|
20
packages/plugins/oidc/src/client/locale/en-US.ts
Normal file
20
packages/plugins/oidc/src/client/locale/en-US.ts
Normal file
@ -0,0 +1,20 @@
|
||||
export default {
|
||||
Enable: 'Enable',
|
||||
Issuer: 'Issuer',
|
||||
'OIDC manager': 'OIDC manager',
|
||||
'OIDC Providers': 'OIDC Providers',
|
||||
'Provider name': 'Name',
|
||||
'Client id': 'Client id',
|
||||
'Client secret': 'Client secret',
|
||||
'Openid configuration': 'Openid configuration',
|
||||
'Authorization endpoint': 'Authorization endpoint',
|
||||
'Access token endpoint': 'Access token endpoint',
|
||||
'JWKS endpoint': 'JWKS endpoint',
|
||||
'Userinfo endpoint': 'Userinfo endpoint',
|
||||
'Redirect url': 'Redirect url',
|
||||
'Logout endpoint': 'Logout endpoint',
|
||||
'Id token sign alg': 'Id token sign alg',
|
||||
'Add provider': 'Add provider',
|
||||
'Edit provider': 'Edit provider',
|
||||
'Delete provider': 'Delete provider',
|
||||
};
|
24
packages/plugins/oidc/src/client/locale/index.ts
Normal file
24
packages/plugins/oidc/src/client/locale/index.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { i18n } from '@nocobase/client';
|
||||
|
||||
import zhCN from './zh-CN';
|
||||
import enUS from './en-US';
|
||||
import jaJP from './ja-JP';
|
||||
import ruRU from './ru-RU';
|
||||
import trTR from './tr-TR';
|
||||
|
||||
export const NAMESPACE = 'workflow';
|
||||
|
||||
i18n.addResources('zh-CN', NAMESPACE, zhCN);
|
||||
i18n.addResources('en-US', NAMESPACE, enUS);
|
||||
i18n.addResources('ja-JP', NAMESPACE, jaJP);
|
||||
i18n.addResources('ru-RU', NAMESPACE, ruRU);
|
||||
i18n.addResources('tr-TR', NAMESPACE, trTR);
|
||||
|
||||
export function lang(key: string) {
|
||||
return i18n.t(key, { ns: NAMESPACE });
|
||||
}
|
||||
|
||||
export function useOidcTranslation() {
|
||||
return useTranslation(NAMESPACE);
|
||||
}
|
1
packages/plugins/oidc/src/client/locale/ja-JP.ts
Normal file
1
packages/plugins/oidc/src/client/locale/ja-JP.ts
Normal file
@ -0,0 +1 @@
|
||||
export default {};
|
1
packages/plugins/oidc/src/client/locale/ru-RU.ts
Normal file
1
packages/plugins/oidc/src/client/locale/ru-RU.ts
Normal file
@ -0,0 +1 @@
|
||||
export default {};
|
1
packages/plugins/oidc/src/client/locale/tr-TR.ts
Normal file
1
packages/plugins/oidc/src/client/locale/tr-TR.ts
Normal file
@ -0,0 +1 @@
|
||||
export default {};
|
23
packages/plugins/oidc/src/client/locale/zh-CN.ts
Normal file
23
packages/plugins/oidc/src/client/locale/zh-CN.ts
Normal file
@ -0,0 +1,23 @@
|
||||
export default {
|
||||
Issuer: 'Issuer',
|
||||
Enable: '启用',
|
||||
Actions: '操作',
|
||||
Delete: '删除',
|
||||
Edit: '编辑',
|
||||
'OIDC manager': 'OIDC 管理',
|
||||
'OIDC Providers': 'OIDC 身份提供者',
|
||||
'Provider name': '名称',
|
||||
'Client id': '客户端 id',
|
||||
'Client secret': '客户端 secret',
|
||||
'Openid configuration': '服务发现地址',
|
||||
'Authorization endpoint': '授权端点',
|
||||
'Access token endpoint': '令牌端点',
|
||||
'JWKS endpoint': 'JWKS 公钥端点',
|
||||
'Userinfo endpoint': '用户信息端点',
|
||||
'Redirect url': '重定向地址',
|
||||
'Logout endpoint': '登出端点',
|
||||
'Id token sign alg': 'Id token 签名算法',
|
||||
'Add provider': '添加身份提供者',
|
||||
'Edit provider': '编辑身份提供者',
|
||||
'Delete provider': '删除身份提供者',
|
||||
};
|
451
packages/plugins/oidc/src/client/schemas/oidc.ts
Normal file
451
packages/plugins/oidc/src/client/schemas/oidc.ts
Normal file
@ -0,0 +1,451 @@
|
||||
import { ISchema } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { useActionContext, useRequest } from '@nocobase/client';
|
||||
import { IDTOKEN_SIGN_ALG } from '../../server/shared/types';
|
||||
|
||||
const collection = {
|
||||
name: 'oidcProviders',
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
interface: 'input',
|
||||
uiSchema: {
|
||||
title: '{{t("Provider name")}}',
|
||||
type: 'string',
|
||||
'x-component': 'Input',
|
||||
required: true,
|
||||
} as ISchema,
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'clientId',
|
||||
interface: 'input',
|
||||
uiSchema: {
|
||||
title: '{{t("Client id")}}',
|
||||
type: 'string',
|
||||
'x-component': 'Input',
|
||||
required: true,
|
||||
} as ISchema,
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'clientSecret',
|
||||
interface: 'input',
|
||||
uiSchema: {
|
||||
title: '{{t("Client secret")}}',
|
||||
type: 'string',
|
||||
'x-component': 'Input',
|
||||
required: true,
|
||||
} as ISchema,
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'issuer',
|
||||
interface: 'input',
|
||||
uiSchema: {
|
||||
title: '{{t("Issuer")}}',
|
||||
type: 'string',
|
||||
'x-component': 'Input',
|
||||
required: true,
|
||||
} as ISchema,
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'openidConfiguration',
|
||||
interface: 'input',
|
||||
uiSchema: {
|
||||
title: '{{t("Openid configuration")}}',
|
||||
type: 'string',
|
||||
'x-component': 'Input',
|
||||
required: true,
|
||||
} as ISchema,
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'authorizeUrl',
|
||||
interface: 'input',
|
||||
uiSchema: {
|
||||
title: '{{t("Authorization endpoint")}}',
|
||||
type: 'string',
|
||||
'x-component': 'Input',
|
||||
required: true,
|
||||
} as ISchema,
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'tokenUrl',
|
||||
interface: 'input',
|
||||
uiSchema: {
|
||||
title: '{{t("Access token endpoint")}}',
|
||||
type: 'string',
|
||||
'x-component': 'Input',
|
||||
required: true,
|
||||
} as ISchema,
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'jwksUrl',
|
||||
interface: 'input',
|
||||
uiSchema: {
|
||||
title: '{{t("JWKS endpoint")}}',
|
||||
type: 'string',
|
||||
'x-component': 'Input',
|
||||
required: true,
|
||||
} as ISchema,
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'userinfoUrl',
|
||||
interface: 'input',
|
||||
uiSchema: {
|
||||
title: '{{t("Userinfo endpoint")}}',
|
||||
type: 'string',
|
||||
'x-component': 'Input',
|
||||
required: true,
|
||||
} as ISchema,
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'logoutUrl',
|
||||
interface: 'input',
|
||||
uiSchema: {
|
||||
title: '{{t("Logout endpoint")}}',
|
||||
type: 'string',
|
||||
'x-component': 'Input',
|
||||
required: true,
|
||||
} as ISchema,
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'idTokenSignAlg',
|
||||
interface: 'select',
|
||||
uiSchema: {
|
||||
title: '{{ t("Id token sign alg") }}',
|
||||
type: 'string',
|
||||
'x-component': 'Select',
|
||||
'x-component-props': {
|
||||
showSearch: false,
|
||||
filterSort: false,
|
||||
options: [
|
||||
{
|
||||
label: IDTOKEN_SIGN_ALG.RS256,
|
||||
value: IDTOKEN_SIGN_ALG.RS256,
|
||||
},
|
||||
{
|
||||
label: IDTOKEN_SIGN_ALG.HS256,
|
||||
value: IDTOKEN_SIGN_ALG.HS256,
|
||||
},
|
||||
],
|
||||
},
|
||||
required: true,
|
||||
} as ISchema,
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'redirectUrl',
|
||||
interface: 'input',
|
||||
uiSchema: {
|
||||
title: '{{t("Redirect url")}}',
|
||||
type: 'string',
|
||||
'x-component': 'RedirectURLInput',
|
||||
} as ISchema,
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
name: 'enabled',
|
||||
interface: 'boolean',
|
||||
uiSchema: {
|
||||
title: '{{t("Enable")}}',
|
||||
type: 'boolean',
|
||||
'x-component': 'Checkbox',
|
||||
} as ISchema,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const formProperties = {
|
||||
title: {
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
clientId: {
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
clientSecret: {
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
issuer: {
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
openidConfiguration: {
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
authorizeUrl: {
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
tokenUrl: {
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
jwksUrl: {
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
userinfoUrl: {
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
logoutUrl: {
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
idTokenSignAlg: {
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
redirectUrl: {
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
enabled: {
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
title: '',
|
||||
'x-content': '{{t("Enable")}}',
|
||||
},
|
||||
};
|
||||
|
||||
export const oidcSchema: ISchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
block1: {
|
||||
type: 'void',
|
||||
'x-decorator': 'ResourceActionProvider',
|
||||
'x-decorator-props': {
|
||||
collection,
|
||||
resourceName: 'oidcProviders',
|
||||
request: {
|
||||
resource: 'oidcProviders',
|
||||
action: 'list',
|
||||
params: {
|
||||
pageSize: 50,
|
||||
sort: ['id'],
|
||||
appends: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
'x-component': 'CollectionProvider',
|
||||
'x-component-props': {
|
||||
collection,
|
||||
},
|
||||
properties: {
|
||||
actions: {
|
||||
type: 'void',
|
||||
'x-component': 'ActionBar',
|
||||
'x-component-props': {
|
||||
style: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
},
|
||||
properties: {
|
||||
delete: {
|
||||
type: 'void',
|
||||
title: '{{ t("Delete") }}',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
useAction: '{{ cm.useBulkDestroyAction }}',
|
||||
confirm: {
|
||||
title: "{{t('Delete provider')}}",
|
||||
content: "{{t('Are you sure you want to delete it?')}}",
|
||||
},
|
||||
},
|
||||
},
|
||||
create: {
|
||||
type: 'void',
|
||||
title: '{{t("Add provider")}}',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
},
|
||||
properties: {
|
||||
drawer: {
|
||||
type: 'void',
|
||||
'x-component': 'Action.Drawer',
|
||||
'x-decorator': 'Form',
|
||||
'x-decorator-props': {
|
||||
useValues(options) {
|
||||
const ctx = useActionContext();
|
||||
// 初始化数据
|
||||
return useRequest(
|
||||
() =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
enable: true,
|
||||
idTokenSignAlg: IDTOKEN_SIGN_ALG.RS256,
|
||||
},
|
||||
}),
|
||||
{ ...options, refreshDeps: [ctx.visible] },
|
||||
);
|
||||
},
|
||||
},
|
||||
title: '{{t("Add provider")}}',
|
||||
properties: {
|
||||
...formProperties,
|
||||
footer: {
|
||||
type: 'void',
|
||||
'x-component': 'Action.Drawer.Footer',
|
||||
properties: {
|
||||
cancel: {
|
||||
title: '{{t("Cancel")}}',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
useAction: '{{ cm.useCancelAction }}',
|
||||
},
|
||||
},
|
||||
submit: {
|
||||
title: '{{t("Submit")}}',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
useAction: '{{ cm.useCreateAction }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
table: {
|
||||
type: 'void',
|
||||
'x-uid': 'input',
|
||||
'x-component': 'Table.Void',
|
||||
'x-component-props': {
|
||||
rowKey: 'id',
|
||||
rowSelection: {
|
||||
type: 'checkbox',
|
||||
},
|
||||
useDataSource: '{{ cm.useDataSourceFromRAC }}',
|
||||
},
|
||||
properties: {
|
||||
column1: {
|
||||
type: 'void',
|
||||
'x-decorator': 'Table.Column.Decorator',
|
||||
'x-component': 'Table.Column',
|
||||
properties: {
|
||||
title: {
|
||||
type: 'string',
|
||||
'x-component': 'CollectionField',
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
},
|
||||
},
|
||||
column2: {
|
||||
type: 'void',
|
||||
'x-decorator': 'Table.Column.Decorator',
|
||||
'x-component': 'Table.Column',
|
||||
properties: {
|
||||
redirectUrl: {
|
||||
type: 'string',
|
||||
'x-component': 'CollectionField',
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
},
|
||||
},
|
||||
column3: {
|
||||
type: 'void',
|
||||
'x-decorator': 'Table.Column.Decorator',
|
||||
'x-component': 'Table.Column',
|
||||
properties: {
|
||||
enabled: {
|
||||
type: 'boolean',
|
||||
'x-component': 'CollectionField',
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
},
|
||||
},
|
||||
column4: {
|
||||
type: 'void',
|
||||
title: '{{t("Actions")}}',
|
||||
'x-component': 'Table.Column',
|
||||
properties: {
|
||||
actions: {
|
||||
type: 'void',
|
||||
'x-component': 'Space',
|
||||
'x-component-props': {
|
||||
split: '|',
|
||||
},
|
||||
properties: {
|
||||
update: {
|
||||
type: 'void',
|
||||
title: '{{t("Edit")}}',
|
||||
'x-component': 'Action.Link',
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
},
|
||||
properties: {
|
||||
drawer: {
|
||||
type: 'void',
|
||||
'x-component': 'Action.Drawer',
|
||||
'x-decorator': 'Form',
|
||||
'x-decorator-props': {
|
||||
useValues: '{{ cm.useValuesFromRecord }}',
|
||||
},
|
||||
title: '{{t("Edit provider")}}',
|
||||
properties: {
|
||||
...formProperties,
|
||||
footer: {
|
||||
type: 'void',
|
||||
'x-component': 'Action.Drawer.Footer',
|
||||
properties: {
|
||||
cancel: {
|
||||
title: '{{t("Cancel")}}',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
useAction: '{{ cm.useCancelAction }}',
|
||||
},
|
||||
},
|
||||
submit: {
|
||||
title: '{{t("Submit")}}',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
useAction: '{{ cm.useUpdateAction }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
delete: {
|
||||
type: 'void',
|
||||
title: '{{ t("Delete") }}',
|
||||
'x-component': 'Action.Link',
|
||||
'x-component-props': {
|
||||
confirm: {
|
||||
title: "{{t('Delete role')}}",
|
||||
content: "{{t('Are you sure you want to delete it?')}}",
|
||||
},
|
||||
useAction: '{{cm.useDestroyAction}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
3
packages/plugins/oidc/src/index.ts
Normal file
3
packages/plugins/oidc/src/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { default } from './server';
|
||||
|
||||
export const namespace = require('../package.json').name;
|
24
packages/plugins/oidc/src/server/actions/getAuthUrl.ts
Normal file
24
packages/plugins/oidc/src/server/actions/getAuthUrl.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { Context } from '@nocobase/actions';
|
||||
import { createOIDCClient } from '../shared/createOIDCClient';
|
||||
import { OIDCProvider } from '../shared/types';
|
||||
|
||||
export const getAuthUrl = async (ctx: Context, next) => {
|
||||
const {
|
||||
params: { values },
|
||||
} = ctx.action;
|
||||
const providerRepo = ctx.db.getRepository('oidcProviders');
|
||||
const record = await providerRepo.findOne({
|
||||
filter: {
|
||||
clientId: values.clientId,
|
||||
},
|
||||
});
|
||||
const provider: OIDCProvider = record.toJSON();
|
||||
const client = await createOIDCClient(provider);
|
||||
|
||||
ctx.body = client.authorizationUrl({
|
||||
nonce: ctx.OIDC_NONCE,
|
||||
scope: 'openid profile',
|
||||
});
|
||||
|
||||
return next();
|
||||
};
|
27
packages/plugins/oidc/src/server/actions/redirect.ts
Normal file
27
packages/plugins/oidc/src/server/actions/redirect.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { Context } from '@nocobase/actions';
|
||||
|
||||
export const redirect = async (ctx: Context, next) => {
|
||||
const { params } = ctx.action;
|
||||
|
||||
const template = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title></title>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
window.opener.postMessage(${JSON.stringify(params)}, '*');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
ctx.body = template;
|
||||
ctx.withoutDataWrapping = true;
|
||||
|
||||
await next();
|
||||
};
|
52
packages/plugins/oidc/src/server/authenticators/oidc.ts
Normal file
52
packages/plugins/oidc/src/server/authenticators/oidc.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { Context } from '@nocobase/actions';
|
||||
import { createOIDCClient } from '../shared/createOIDCClient';
|
||||
import { OIDCProvider } from '../shared/types';
|
||||
|
||||
export const oidc = async (ctx: Context, next) => {
|
||||
const {
|
||||
params: { values },
|
||||
} = ctx.action;
|
||||
|
||||
const providerRepo = ctx.db.getRepository('oidcProviders');
|
||||
const record = await providerRepo.findOne({
|
||||
filter: {
|
||||
clientId: values.clientId,
|
||||
},
|
||||
});
|
||||
const provider: OIDCProvider = record.toJSON();
|
||||
const client = await createOIDCClient(provider);
|
||||
|
||||
const tokens = await client.callback(
|
||||
provider.redirectUrl,
|
||||
{
|
||||
code: values.code,
|
||||
},
|
||||
{
|
||||
nonce: ctx.OIDC_NONCE,
|
||||
},
|
||||
);
|
||||
|
||||
const userinfo = await client.userinfo(tokens.access_token);
|
||||
const usersRepo = ctx.db.getRepository('users');
|
||||
|
||||
const name = userinfo.preferred_username || userinfo.nickname || userinfo.name;
|
||||
|
||||
let user = await usersRepo.findOne({
|
||||
filter: {
|
||||
nickname: name,
|
||||
email: userinfo.email ?? null,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
user = await usersRepo.create({
|
||||
values: {
|
||||
nickname: name,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
ctx.state.currentUser = user;
|
||||
|
||||
return next();
|
||||
};
|
@ -0,0 +1,86 @@
|
||||
import { CollectionOptions } from '@nocobase/database';
|
||||
|
||||
export default {
|
||||
name: 'oidcProviders',
|
||||
title: '{{t("OIDC Providers")}}',
|
||||
fields: [
|
||||
{
|
||||
title: 'Provider 名称',
|
||||
comment: 'Provider 名称',
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
},
|
||||
{
|
||||
comment: '客户端id',
|
||||
type: 'string',
|
||||
name: 'clientId',
|
||||
unique: true,
|
||||
},
|
||||
{
|
||||
comment: '客户端密钥',
|
||||
type: 'string',
|
||||
name: 'clientSecret',
|
||||
},
|
||||
{
|
||||
title: 'issuer',
|
||||
comment: '用于标识 token 发放来源的字段',
|
||||
type: 'string',
|
||||
name: 'issuer',
|
||||
},
|
||||
{
|
||||
comment: '用于获取当前 Provider 支持的各端点信息和支持的模式、参数信息,可公开访问',
|
||||
type: 'string',
|
||||
name: 'openidConfiguration',
|
||||
},
|
||||
{
|
||||
comment: '单点登录的地址 ',
|
||||
type: 'string',
|
||||
name: 'authorizeUrl',
|
||||
},
|
||||
{
|
||||
comment: '获取取 token 的接口地址',
|
||||
type: 'string',
|
||||
name: 'tokenUrl',
|
||||
},
|
||||
{
|
||||
comment: '令牌吊销 token 的接口',
|
||||
type: 'string',
|
||||
name: 'revokeUrl',
|
||||
},
|
||||
{
|
||||
comment: '验证 id_token 的接口',
|
||||
type: 'string',
|
||||
name: 'jwksUrl',
|
||||
},
|
||||
{
|
||||
comment: '获取用户信息的接口',
|
||||
type: 'string',
|
||||
name: 'userinfoUrl',
|
||||
},
|
||||
{
|
||||
comment: '是否启用',
|
||||
type: 'boolean',
|
||||
name: 'enabled',
|
||||
},
|
||||
{
|
||||
comment: '重定向回调地址',
|
||||
type: 'string',
|
||||
name: 'redirectUrl',
|
||||
},
|
||||
{
|
||||
comment: '登出端点',
|
||||
type: 'string',
|
||||
name: 'logoutUrl',
|
||||
},
|
||||
{
|
||||
comment: 'id token 加密方式',
|
||||
type: 'string',
|
||||
name: 'idTokenSignAlg',
|
||||
},
|
||||
{
|
||||
comment: '获取用户信息的接口',
|
||||
type: 'json',
|
||||
name: 'attrsMap',
|
||||
},
|
||||
],
|
||||
} as CollectionOptions;
|
1
packages/plugins/oidc/src/server/index.ts
Normal file
1
packages/plugins/oidc/src/server/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './plugin';
|
62
packages/plugins/oidc/src/server/plugin.ts
Normal file
62
packages/plugins/oidc/src/server/plugin.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import UsersPlugin from '@nocobase/plugin-users';
|
||||
import { InstallOptions, Plugin } from '@nocobase/server';
|
||||
import { resolve } from 'path';
|
||||
import { generators } from 'openid-client';
|
||||
import { oidc } from './authenticators/oidc';
|
||||
import { getAuthUrl } from './actions/getAuthUrl';
|
||||
import { redirect } from './actions/redirect';
|
||||
|
||||
export class OidcPlugin extends Plugin {
|
||||
#OIDC_NONCE = null;
|
||||
|
||||
afterAdd() {}
|
||||
|
||||
beforeLoad() {}
|
||||
|
||||
async load() {
|
||||
// 导入 collection
|
||||
await this.db.import({
|
||||
directory: resolve(__dirname, 'collections'),
|
||||
});
|
||||
|
||||
// 获取 User 插件
|
||||
const userPlugin = this.app.getPlugin('users') as UsersPlugin;
|
||||
|
||||
// 注册 OIDC 验证器
|
||||
userPlugin.authenticators.register('oidc', oidc);
|
||||
|
||||
// 注册接口
|
||||
this.app.resource({
|
||||
name: 'oidc',
|
||||
actions: {
|
||||
getAuthUrl,
|
||||
redirect,
|
||||
},
|
||||
});
|
||||
|
||||
// 注册中间件,处理 nonce 值
|
||||
this.app.use(async (ctx, next) => {
|
||||
if (ctx.url.startsWith('/api/users:signin?authenticator=oidc')) {
|
||||
ctx.OIDC_NONCE = this.#OIDC_NONCE;
|
||||
}
|
||||
if (ctx.url.startsWith('/api/oidc:getAuthUrl')) {
|
||||
ctx.OIDC_NONCE = this.#OIDC_NONCE = generators.nonce();
|
||||
}
|
||||
await next();
|
||||
});
|
||||
|
||||
// 开放访问权限
|
||||
this.app.acl.allow('oidcProviders', '*');
|
||||
this.app.acl.allow('oidc', '*');
|
||||
}
|
||||
|
||||
async install(options?: InstallOptions) {}
|
||||
|
||||
async afterEnable() {}
|
||||
|
||||
async afterDisable() {}
|
||||
|
||||
async remove() {}
|
||||
}
|
||||
|
||||
export default OidcPlugin;
|
12
packages/plugins/oidc/src/server/shared/createOIDCClient.ts
Normal file
12
packages/plugins/oidc/src/server/shared/createOIDCClient.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Issuer } from 'openid-client';
|
||||
import { IDTOKEN_SIGN_ALG, OIDCProvider } from './types';
|
||||
|
||||
export const createOIDCClient = async (provider: OIDCProvider) => {
|
||||
const issuer = await Issuer.discover(provider.issuer);
|
||||
return new issuer.Client({
|
||||
client_id: provider.clientId,
|
||||
client_secret: provider.clientSecret,
|
||||
response_types: ['code'],
|
||||
id_token_signed_response_alg: provider.idTokenSignAlg,
|
||||
});
|
||||
};
|
22
packages/plugins/oidc/src/server/shared/types.ts
Normal file
22
packages/plugins/oidc/src/server/shared/types.ts
Normal file
@ -0,0 +1,22 @@
|
||||
export enum IDTOKEN_SIGN_ALG {
|
||||
HS256 = 'HS256',
|
||||
RS256 = 'RS256',
|
||||
}
|
||||
|
||||
export interface OIDCProvider {
|
||||
title?: string;
|
||||
clientId?: string;
|
||||
clientSecret?: string;
|
||||
issuer?: string;
|
||||
openidConfiguration?: string;
|
||||
authorizeUrl?: string;
|
||||
tokenUrl?: string;
|
||||
revodeUrl?: string;
|
||||
jwksUrl?: string;
|
||||
userinfoUrl?: string;
|
||||
enable?: string;
|
||||
redirectUrl?: string;
|
||||
logoutUrl?: string;
|
||||
idTokenSignAlg?: string;
|
||||
attrsMap?: Record<string, string>;
|
||||
}
|
@ -23571,4 +23571,4 @@ yocto-queue@^0.1.0:
|
||||
zwitch@^1.0.0:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.npmjs.org/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920"
|
||||
integrity sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==
|
||||
integrity sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==
|
Loading…
Reference in New Issue
Block a user