feat(plugin-cas): support cas authenticator (#2580)
* feat(plugin-cas): support cas login method * feat: add cas plugin in preset * chore: update version * feat: full support cas * chore: update package.json * feat: update docs and fix namespace * fix: locale --------- Co-authored-by: xilesun <2013xile@gmail.com>
This commit is contained in:
parent
e7d60389b9
commit
24179c4469
12
packages/plugins/cas/README.md
Normal file
12
packages/plugins/cas/README.md
Normal file
@ -0,0 +1,12 @@
|
||||
# @nocobase/plugin-cas
|
||||
|
||||
提供 CAS 统一登入方式
|
||||
|
||||
## 使用方法
|
||||
|
||||
> 首先你要开启此插件
|
||||
|
||||
1. 在 Authentication 中添加一个 Auth Type 为 CAS 类型的登录方式
|
||||
<img src="https://github.com/nocobase/nocobase/assets/29533304/a9dd6965-afce-4ff3-85d8-ff0c1baa7298">
|
||||
2. 然后你将在登录页看到一个名为 CAS Login 的按钮
|
||||
3. 完成
|
12
packages/plugins/cas/README.zh-CN.md
Normal file
12
packages/plugins/cas/README.zh-CN.md
Normal file
@ -0,0 +1,12 @@
|
||||
# @nocobase/plugin-cas
|
||||
|
||||
提供 CAS 统一登入方式
|
||||
|
||||
## 使用方法
|
||||
|
||||
> 首先你要开启此插件
|
||||
|
||||
1. 在 Authentication 中添加一个 Auth Type 为 CAS 类型的登录方式
|
||||
<img src="https://github.com/nocobase/nocobase/assets/29533304/a9dd6965-afce-4ff3-85d8-ff0c1baa7298">
|
||||
2. 然后你将在登录页看到一个名为 CAS Login 的按钮
|
||||
3. 完成
|
2
packages/plugins/cas/client.d.ts
vendored
Executable file
2
packages/plugins/cas/client.d.ts
vendored
Executable file
@ -0,0 +1,2 @@
|
||||
export * from './dist/client';
|
||||
export { default } from './dist/client';
|
1
packages/plugins/cas/client.js
Executable file
1
packages/plugins/cas/client.js
Executable file
@ -0,0 +1 @@
|
||||
module.exports = require('./dist/client/index.js');
|
22
packages/plugins/cas/package.json
Normal file
22
packages/plugins/cas/package.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-cas",
|
||||
"displayName": "SSO login - CAS",
|
||||
"displayName.zh-CN": "SSO 登录 - CAS",
|
||||
"description": "Unified identity authentication.",
|
||||
"description.zh-CN": "提供CAS登录功能",
|
||||
"version": "0.13.0-alpha.4",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "./dist/server/index.js",
|
||||
"devDependencies": {
|
||||
"@nocobase/actions": "0.13.0-alpha.4",
|
||||
"@nocobase/client": "0.13.0-alpha.4",
|
||||
"@nocobase/database": "0.13.0-alpha.4",
|
||||
"@nocobase/server": "0.13.0-alpha.4",
|
||||
"@nocobase/test": "0.13.0-alpha.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"antd": "5.x",
|
||||
"@ant-design/icons": "^5.1.4",
|
||||
"react-router-dom": "^6.11.2"
|
||||
}
|
||||
}
|
2
packages/plugins/cas/server.d.ts
vendored
Executable file
2
packages/plugins/cas/server.d.ts
vendored
Executable file
@ -0,0 +1,2 @@
|
||||
export * from './dist/server';
|
||||
export { default } from './dist/server';
|
1
packages/plugins/cas/server.js
Executable file
1
packages/plugins/cas/server.js
Executable file
@ -0,0 +1 @@
|
||||
module.exports = require('./dist/server/index.js');
|
42
packages/plugins/cas/src/client/Options.tsx
Normal file
42
packages/plugins/cas/src/client/Options.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import { SchemaComponent } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { Space } from 'antd';
|
||||
import { useAuthTranslation, generateNTemplate } from './locale';
|
||||
|
||||
export const Options = () => {
|
||||
const { t } = useAuthTranslation();
|
||||
return (
|
||||
<SchemaComponent
|
||||
scope={{ t }}
|
||||
components={{ Space }}
|
||||
schema={{
|
||||
type: 'object',
|
||||
properties: {
|
||||
autoSignup: {
|
||||
'x-decorator': 'FormItem',
|
||||
type: 'boolean',
|
||||
title: '{{t("Sign up automatically when the user does not exist")}}',
|
||||
'x-component': 'Checkbox',
|
||||
},
|
||||
casUrl: {
|
||||
title: '{{t("CAS URL")}}',
|
||||
'x-component': 'Input',
|
||||
'x-decorator': 'FormItem',
|
||||
required: true,
|
||||
},
|
||||
serviceDomain: {
|
||||
title: '{{t("Service domain")}}',
|
||||
'x-component': 'Input',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-decorator-props': {
|
||||
tooltip: generateNTemplate(
|
||||
'The domain is usually the address of your server, in local development, you can use the address of your local machine, such as: http://localhost:13000',
|
||||
),
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
47
packages/plugins/cas/src/client/SigninPage.tsx
Normal file
47
packages/plugins/cas/src/client/SigninPage.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import { Authenticator, useAPIClient, useRedirect, useCurrentUserContext } from '@nocobase/client';
|
||||
import React, { useEffect } from 'react';
|
||||
import { LoginOutlined } from '@ant-design/icons';
|
||||
import { Button, Space, App } from 'antd';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
export const SigninPage = (props: { authenticator: Authenticator }) => {
|
||||
const { message } = App.useApp();
|
||||
const api = useAPIClient();
|
||||
const navigate = useNavigate();
|
||||
const redirect = useRedirect();
|
||||
const location = useLocation();
|
||||
const { refreshAsync } = useCurrentUserContext();
|
||||
|
||||
const authenticator = props.authenticator;
|
||||
|
||||
const login = async () => {
|
||||
window.location.replace(`/api/cas:login?authenticator=${authenticator.name}`);
|
||||
redirect();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const usp = new URLSearchParams(location.search);
|
||||
if (usp.get('authenticator') === authenticator.name) {
|
||||
api.auth
|
||||
.signIn({}, authenticator.name)
|
||||
.then(async () => {
|
||||
await refreshAsync();
|
||||
redirect();
|
||||
})
|
||||
.catch((error) => {
|
||||
navigate({
|
||||
pathname: location.pathname,
|
||||
});
|
||||
message.error(error.message);
|
||||
});
|
||||
}
|
||||
}, [location.search, authenticator.name]);
|
||||
|
||||
return (
|
||||
<Space direction="vertical" style={{ display: 'flex' }}>
|
||||
<Button shape="round" block icon={<LoginOutlined />} onClick={login}>
|
||||
{authenticator.title}
|
||||
</Button>
|
||||
</Space>
|
||||
);
|
||||
};
|
25
packages/plugins/cas/src/client/index.tsx
Normal file
25
packages/plugins/cas/src/client/index.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import { OptionsComponentProvider, SigninPageExtensionProvider, SignupPageProvider } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { Plugin } from '@nocobase/client';
|
||||
|
||||
import { SigninPage } from './SigninPage';
|
||||
import { Options } from './Options';
|
||||
import { authType } from '../constants';
|
||||
|
||||
export function CASProvider(props) {
|
||||
return (
|
||||
<OptionsComponentProvider authType={authType} component={Options}>
|
||||
<SigninPageExtensionProvider authType={authType} component={SigninPage}>
|
||||
{props.children}
|
||||
</SigninPageExtensionProvider>
|
||||
</OptionsComponentProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export class SamlPlugin extends Plugin {
|
||||
async load() {
|
||||
this.app.use(CASProvider);
|
||||
}
|
||||
}
|
||||
|
||||
export default SamlPlugin;
|
11
packages/plugins/cas/src/client/locale/index.ts
Normal file
11
packages/plugins/cas/src/client/locale/index.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const NAMESPACE = 'cas';
|
||||
|
||||
export function useAuthTranslation() {
|
||||
return useTranslation(NAMESPACE);
|
||||
}
|
||||
|
||||
export function generateNTemplate(key: string) {
|
||||
return `{{t('${key}', { ns: '${NAMESPACE}', nsMode: 'fallback' })}}`;
|
||||
}
|
4
packages/plugins/cas/src/constants.ts
Normal file
4
packages/plugins/cas/src/constants.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export const authType = 'CAS';
|
||||
export const COOKIE_KEY_TICKET = '_cas_ticket_';
|
||||
export const COOKIE_KEY_AUTHENTICATOR = '_cas_authenticator_';
|
||||
export const namespace = 'cas';
|
1
packages/plugins/cas/src/index.ts
Normal file
1
packages/plugins/cas/src/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './server';
|
9
packages/plugins/cas/src/locale/zh-CN.ts
Normal file
9
packages/plugins/cas/src/locale/zh-CN.ts
Normal file
@ -0,0 +1,9 @@
|
||||
const locale = {
|
||||
'Sign in': '统一身份登录',
|
||||
'User will be registered automatically if not exists.': '用户不存在时将自动注册。',
|
||||
'Sign up automatically when the user does not exist': '用户不存在时自动注册',
|
||||
'Service domain': '服务域名',
|
||||
'The domain is usually the address of your server, in local development, you can use the address of your local machine, such as: http://localhost:13000': '域名通常是你的服务器地址,在本地开发时,可以使用你本机的地址,例如:http://localhost:13000',
|
||||
};
|
||||
|
||||
export default locale;
|
14
packages/plugins/cas/src/server/actions/login.ts
Normal file
14
packages/plugins/cas/src/server/actions/login.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { Context, Next } from '@nocobase/actions';
|
||||
import { CASAuth } from '../auth';
|
||||
import { COOKIE_KEY_AUTHENTICATOR } from '../../constants';
|
||||
|
||||
export const login = async (ctx: Context, next: Next) => {
|
||||
const { authenticator } = ctx.action.params;
|
||||
ctx.cookies.set(COOKIE_KEY_AUTHENTICATOR, authenticator, {
|
||||
httpOnly: true,
|
||||
});
|
||||
const auth = (await ctx.app.authManager.get(authenticator, ctx)) as CASAuth;
|
||||
const { casUrl, serviceUrl } = auth.getOptions();
|
||||
ctx.redirect(`${casUrl}/login?service=${serviceUrl}`);
|
||||
next();
|
||||
};
|
11
packages/plugins/cas/src/server/actions/service.ts
Normal file
11
packages/plugins/cas/src/server/actions/service.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Context, Next } from '@nocobase/actions';
|
||||
import { COOKIE_KEY_AUTHENTICATOR, COOKIE_KEY_TICKET } from '../../constants';
|
||||
|
||||
export const service = async (ctx: Context, next: Next) => {
|
||||
const { params } = ctx.action;
|
||||
ctx.cookies.set(COOKIE_KEY_TICKET, params.ticket, {
|
||||
httpOnly: true,
|
||||
});
|
||||
ctx.redirect(`/signin?authenticator=${ctx.cookies.get(COOKIE_KEY_AUTHENTICATOR)}`);
|
||||
return next();
|
||||
};
|
75
packages/plugins/cas/src/server/auth.ts
Normal file
75
packages/plugins/cas/src/server/auth.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import { AuthConfig, BaseAuth } from '@nocobase/auth';
|
||||
import { Model } from '@nocobase/database';
|
||||
import { AuthModel } from '@nocobase/plugin-auth';
|
||||
import axios from 'axios';
|
||||
import { COOKIE_KEY_AUTHENTICATOR, COOKIE_KEY_TICKET } from '../constants';
|
||||
|
||||
export class CASAuth extends BaseAuth {
|
||||
constructor(config: AuthConfig) {
|
||||
const { ctx } = config;
|
||||
super({
|
||||
...config,
|
||||
userCollection: ctx.db.getCollection('users'),
|
||||
});
|
||||
}
|
||||
|
||||
async signOut() {
|
||||
const ctx = this.ctx;
|
||||
ctx.cookies.set(COOKIE_KEY_TICKET, '');
|
||||
ctx.cookies.set(COOKIE_KEY_AUTHENTICATOR, '');
|
||||
await super.signOut();
|
||||
}
|
||||
|
||||
getOptions() {
|
||||
const opts = this.options || {};
|
||||
return {
|
||||
...opts,
|
||||
serviceUrl: `${opts.serviceDomain}/api/cas:service`,
|
||||
} as {
|
||||
casUrl?: string;
|
||||
serviceUrl?: string;
|
||||
autoSignup?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
serviceValidate(ticket) {
|
||||
const { casUrl, serviceUrl } = this.getOptions();
|
||||
const url = `${casUrl}/serviceValidate?ticket=${ticket}&service=${serviceUrl}`;
|
||||
return axios.get(url).catch((err) => {
|
||||
throw new Error('CSA serviceValidate error: ' + err.message);
|
||||
});
|
||||
}
|
||||
|
||||
async validate() {
|
||||
const ctx = this.ctx;
|
||||
let user: Model;
|
||||
const { autoSignup } = this.getOptions();
|
||||
const ticket = ctx.cookies.get(COOKIE_KEY_TICKET);
|
||||
const res = ticket ? await this.serviceValidate(ticket) : null;
|
||||
const pattern = /<(?:cas|sso):user>(.*?)<\/(?:cas|sso):user>/;
|
||||
const nickname = res?.data.match(pattern)?.[1];
|
||||
if (nickname) {
|
||||
const userRepo = this.userCollection.repository;
|
||||
user = await userRepo.findOne({
|
||||
filter: { nickname },
|
||||
});
|
||||
if (user) {
|
||||
await this.authenticator.addUser(user, {
|
||||
through: {
|
||||
uuid: nickname,
|
||||
},
|
||||
});
|
||||
return user;
|
||||
}
|
||||
}
|
||||
// New data
|
||||
const authenticator = this.authenticator as AuthModel;
|
||||
if (autoSignup) {
|
||||
user = await authenticator.findOrCreateUser(nickname, {
|
||||
nickname: nickname,
|
||||
});
|
||||
return user;
|
||||
}
|
||||
return user;
|
||||
}
|
||||
}
|
1
packages/plugins/cas/src/server/index.ts
Normal file
1
packages/plugins/cas/src/server/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './plugin';
|
38
packages/plugins/cas/src/server/plugin.ts
Normal file
38
packages/plugins/cas/src/server/plugin.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { InstallOptions, Plugin } from '@nocobase/server';
|
||||
import { service } from './actions/service';
|
||||
import { authType } from '../constants';
|
||||
import { CASAuth } from './auth';
|
||||
import { login } from './actions/login';
|
||||
|
||||
export class CasPlugin extends Plugin {
|
||||
afterAdd() {}
|
||||
|
||||
beforeLoad() {}
|
||||
|
||||
async load() {
|
||||
this.app.authManager.registerTypes(authType, {
|
||||
auth: CASAuth,
|
||||
});
|
||||
|
||||
this.app.resource({
|
||||
name: 'cas',
|
||||
actions: {
|
||||
service,
|
||||
login,
|
||||
},
|
||||
});
|
||||
|
||||
this.app.acl.allow('cas', '*', 'public');
|
||||
}
|
||||
|
||||
async install(options?: InstallOptions) {}
|
||||
|
||||
async afterEnable() {}
|
||||
|
||||
async afterDisable() {}
|
||||
|
||||
async remove() {}
|
||||
}
|
||||
|
||||
export default CasPlugin;
|
||||
//
|
@ -32,6 +32,7 @@
|
||||
"@nocobase/plugin-multi-app-share-collection": "0.13.0-alpha.4",
|
||||
"@nocobase/plugin-oidc": "0.13.0-alpha.4",
|
||||
"@nocobase/plugin-saml": "0.13.0-alpha.4",
|
||||
"@nocobase/plugin-cas": "0.13.0-alpha.4",
|
||||
"@nocobase/plugin-sample-hello": "0.13.0-alpha.4",
|
||||
"@nocobase/plugin-sequence-field": "0.13.0-alpha.4",
|
||||
"@nocobase/plugin-sms-auth": "0.13.0-alpha.4",
|
||||
|
@ -35,6 +35,7 @@ export class PresetNocoBase extends Plugin {
|
||||
'multi-app-share-collection',
|
||||
'oidc',
|
||||
'saml',
|
||||
'cas',
|
||||
'map',
|
||||
'snapshot-field',
|
||||
'graph-collection-manager',
|
||||
|
Loading…
Reference in New Issue
Block a user