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,
 | 
					  RemoteRouteSwitchProvider,
 | 
				
			||||||
  RouteSchemaComponent,
 | 
					  RouteSchemaComponent,
 | 
				
			||||||
  RouteSwitch,
 | 
					  RouteSwitch,
 | 
				
			||||||
  useRoutes
 | 
					  useRoutes,
 | 
				
			||||||
} from '../route-switch';
 | 
					} from '../route-switch';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  AntdSchemaComponentProvider,
 | 
					  AntdSchemaComponentProvider,
 | 
				
			||||||
  DesignableSwitch,
 | 
					  DesignableSwitch,
 | 
				
			||||||
  MenuItemInitializers,
 | 
					  MenuItemInitializers,
 | 
				
			||||||
  SchemaComponentProvider
 | 
					  SchemaComponentProvider,
 | 
				
			||||||
} from '../schema-component';
 | 
					} from '../schema-component';
 | 
				
			||||||
import { SchemaInitializerProvider } from '../schema-initializer';
 | 
					import { SchemaInitializerProvider } from '../schema-initializer';
 | 
				
			||||||
import { BlockTemplateDetails, BlockTemplatePage, SchemaTemplateShortcut } from '../schema-templates';
 | 
					import { BlockTemplateDetails, BlockTemplatePage, SchemaTemplateShortcut } from '../schema-templates';
 | 
				
			||||||
import { SystemSettingsProvider, SystemSettingsShortcut } from '../system-settings';
 | 
					import { SystemSettingsProvider, SystemSettingsShortcut } from '../system-settings';
 | 
				
			||||||
import { SigninPage, SignupPage } from '../user';
 | 
					import { SigninPage, SignupPage } from '../user';
 | 
				
			||||||
 | 
					import { SigninPageExtensionProvider } from '../user/SigninPageExtension';
 | 
				
			||||||
import { compose } from './compose';
 | 
					import { compose } from './compose';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface ApplicationOptions {
 | 
					export interface ApplicationOptions {
 | 
				
			||||||
@ -112,6 +113,7 @@ export class Application {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
    this.use(BlockSchemaComponentProvider);
 | 
					    this.use(BlockSchemaComponentProvider);
 | 
				
			||||||
    this.use(AntdSchemaComponentProvider);
 | 
					    this.use(AntdSchemaComponentProvider);
 | 
				
			||||||
 | 
					    this.use(SigninPageExtensionProvider);
 | 
				
			||||||
    this.use(ACLProvider);
 | 
					    this.use(ACLProvider);
 | 
				
			||||||
    this.use(RemoteDocumentTitleProvider);
 | 
					    this.use(RemoteDocumentTitleProvider);
 | 
				
			||||||
    this.use(PMProvider);
 | 
					    this.use(PMProvider);
 | 
				
			||||||
 | 
				
			|||||||
@ -1,9 +1,11 @@
 | 
				
			|||||||
import { ISchema, useForm } from '@formily/react';
 | 
					import { ISchema, useForm } from '@formily/react';
 | 
				
			||||||
import { Tabs } from 'antd';
 | 
					import { Space, Tabs } from 'antd';
 | 
				
			||||||
import React, { useCallback } from 'react';
 | 
					import React, { useCallback, useContext } from 'react';
 | 
				
			||||||
 | 
					import { css } from '@emotion/css';
 | 
				
			||||||
import { useTranslation } from 'react-i18next';
 | 
					import { useTranslation } from 'react-i18next';
 | 
				
			||||||
import { Link, useHistory, useLocation } from 'react-router-dom';
 | 
					import { Link, useHistory, useLocation } from 'react-router-dom';
 | 
				
			||||||
import { SchemaComponent, useAPIClient, useCurrentDocumentTitle, useSystemSettings } from '..';
 | 
					import { SchemaComponent, useAPIClient, useCurrentDocumentTitle, useSystemSettings } from '..';
 | 
				
			||||||
 | 
					import { useSigninPageExtension } from './SigninPageExtension';
 | 
				
			||||||
import VerificationCode from './VerificationCode';
 | 
					import VerificationCode from './VerificationCode';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const passwordForm: ISchema = {
 | 
					const passwordForm: ISchema = {
 | 
				
			||||||
@ -47,7 +49,7 @@ const passwordForm: ISchema = {
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function useRedirect(next = '/admin') {
 | 
					export function useRedirect(next = '/admin') {
 | 
				
			||||||
  const location = useLocation<any>();
 | 
					  const location = useLocation<any>();
 | 
				
			||||||
  const history = useHistory();
 | 
					  const history = useHistory();
 | 
				
			||||||
  const redirect = location?.['query']?.redirect;
 | 
					  const redirect = location?.['query']?.redirect;
 | 
				
			||||||
@ -130,10 +132,16 @@ export const SigninPage = (props: SigninPageProps) => {
 | 
				
			|||||||
  const { t } = useTranslation();
 | 
					  const { t } = useTranslation();
 | 
				
			||||||
  useCurrentDocumentTitle('Signin');
 | 
					  useCurrentDocumentTitle('Signin');
 | 
				
			||||||
  const ctx = useSystemSettings();
 | 
					  const ctx = useSystemSettings();
 | 
				
			||||||
 | 
					  const signinExtension = useSigninPageExtension();
 | 
				
			||||||
  const { allowSignUp, smsAuthEnabled } = ctx?.data?.data || {};
 | 
					  const { allowSignUp, smsAuthEnabled } = ctx?.data?.data || {};
 | 
				
			||||||
  const { schema, components, scope } = props;
 | 
					  const { schema, components, scope } = props;
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div>
 | 
					    <Space
 | 
				
			||||||
 | 
					      direction="vertical"
 | 
				
			||||||
 | 
					      className={css`
 | 
				
			||||||
 | 
					        display: flex;
 | 
				
			||||||
 | 
					      `}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
      {smsAuthEnabled ? (
 | 
					      {smsAuthEnabled ? (
 | 
				
			||||||
        <Tabs defaultActiveKey="password">
 | 
					        <Tabs defaultActiveKey="password">
 | 
				
			||||||
          <Tabs.TabPane tab={t('Sign in via account')} key="password">
 | 
					          <Tabs.TabPane tab={t('Sign in via account')} key="password">
 | 
				
			||||||
@ -157,11 +165,12 @@ export const SigninPage = (props: SigninPageProps) => {
 | 
				
			|||||||
          schema={schema || passwordForm}
 | 
					          schema={schema || passwordForm}
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
      )}
 | 
					      )}
 | 
				
			||||||
 | 
					      <div>{signinExtension}</div>
 | 
				
			||||||
      {allowSignUp && (
 | 
					      {allowSignUp && (
 | 
				
			||||||
        <div>
 | 
					        <div>
 | 
				
			||||||
          <Link to="/signup">{t('Create an account')}</Link>
 | 
					          <Link to="/signup">{t('Create an account')}</Link>
 | 
				
			||||||
        </div>
 | 
					        </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 './CurrentUserProvider';
 | 
				
			||||||
export * from './SigninPage';
 | 
					export * from './SigninPage';
 | 
				
			||||||
export * from './SignupPage';
 | 
					export * from './SignupPage';
 | 
				
			||||||
 | 
					export * from './SigninPageExtension';
 | 
				
			||||||
 | 
				
			|||||||
@ -66,6 +66,7 @@ export class PluginManager {
 | 
				
			|||||||
      if (options?.method !== 'install' || options.reload) {
 | 
					      if (options?.method !== 'install' || options.reload) {
 | 
				
			||||||
        await this.repository.load();
 | 
					        await this.repository.load();
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      this.app.acl.allow('applicationPlugins', 'list');
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    this.addStaticMultiple(options.plugins);
 | 
					    this.addStaticMultiple(options.plugins);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -329,7 +330,9 @@ export class PluginManager {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static getPluginPkgPrefix() {
 | 
					  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) {
 | 
					  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:
 | 
					zwitch@^1.0.0:
 | 
				
			||||||
  version "1.0.5"
 | 
					  version "1.0.5"
 | 
				
			||||||
  resolved "https://registry.npmjs.org/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920"
 | 
					  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