refactor(auth): OIDC, SAML auth switch popup to redirectction (#2737)
* fix(auth): sso switch popup to rediect (fix T-2024) * refactor: auth process optimization * fix: test * chore: add error handler
This commit is contained in:
parent
53e49a2e9e
commit
beb4793051
@ -1,76 +1,57 @@
|
||||
import { LoginOutlined } from '@ant-design/icons';
|
||||
import { Authenticator, css, useAPIClient, useRedirect } from '@nocobase/client';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { Button, Space } from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Authenticator, css, useAPIClient, useCurrentUserContext, useRedirect } from '@nocobase/client';
|
||||
import { Button, Space, message } from 'antd';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useOidcTranslation } from './locale';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
export interface OIDCProvider {
|
||||
clientId: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const OIDCButton = (props: { authenticator: Authenticator }) => {
|
||||
export const OIDCButton = ({ authenticator }: { authenticator: Authenticator }) => {
|
||||
const { t } = useOidcTranslation();
|
||||
const [windowHandler, setWindowHandler] = useState<Window | undefined>();
|
||||
const api = useAPIClient();
|
||||
const redirect = useRedirect();
|
||||
const location = useLocation();
|
||||
const { refreshAsync: refresh } = useCurrentUserContext();
|
||||
|
||||
/**
|
||||
* 打开登录弹出框
|
||||
*/
|
||||
const handleOpen = async (name: string) => {
|
||||
const login = async () => {
|
||||
const response = await api.request({
|
||||
method: 'post',
|
||||
url: 'oidc:getAuthUrl',
|
||||
headers: {
|
||||
'X-Authenticator': name,
|
||||
'X-Authenticator': authenticator.name,
|
||||
},
|
||||
});
|
||||
|
||||
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);
|
||||
window.location.replace(authUrl);
|
||||
};
|
||||
|
||||
/**
|
||||
* 从弹出窗口,发消息回来进行登录
|
||||
*/
|
||||
const handleOIDCLogin = useMemoizedFn(async (event: MessageEvent) => {
|
||||
const { state } = event.data;
|
||||
const search = new URLSearchParams(state);
|
||||
const authenticator = search.get('name');
|
||||
try {
|
||||
await api.auth.signIn(event.data, authenticator);
|
||||
redirect();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(location.search);
|
||||
const token = params.get('token');
|
||||
const name = params.get('authenticator');
|
||||
const error = params.get('error');
|
||||
if (name !== authenticator.name) {
|
||||
return;
|
||||
}
|
||||
if (error) {
|
||||
message.error(t(error));
|
||||
return;
|
||||
}
|
||||
if (token) {
|
||||
api.auth.setToken(token);
|
||||
api.auth.setAuthenticator(name);
|
||||
refresh()
|
||||
.then(() => redirect())
|
||||
.catch((err) => console.log(err));
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 监听弹出窗口的消息
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!windowHandler) return;
|
||||
|
||||
const channel = new BroadcastChannel('nocobase-oidc-response');
|
||||
channel.onmessage = handleOIDCLogin;
|
||||
return () => {
|
||||
channel.close();
|
||||
};
|
||||
}, [windowHandler, handleOIDCLogin]);
|
||||
|
||||
const authenticator = props.authenticator;
|
||||
return (
|
||||
<Space
|
||||
direction="vertical"
|
||||
@ -78,7 +59,7 @@ export const OIDCButton = (props: { authenticator: Authenticator }) => {
|
||||
display: flex;
|
||||
`}
|
||||
>
|
||||
<Button shape="round" block icon={<LoginOutlined />} onClick={() => handleOpen(authenticator.name)}>
|
||||
<Button shape="round" block icon={<LoginOutlined />} onClick={login}>
|
||||
{t(authenticator.title)}
|
||||
</Button>
|
||||
</Space>
|
||||
|
@ -72,12 +72,7 @@ describe('oidc', () => {
|
||||
const res = await agent
|
||||
.set('X-Authenticator', 'oidc-auth')
|
||||
.set('Cookie', ['nocobase_oidc=token'])
|
||||
.resource('auth')
|
||||
.signIn()
|
||||
.send({
|
||||
code: '',
|
||||
state: 'token=token&name=oidc-auth',
|
||||
});
|
||||
.get('/auth:signIn?state=token%3Dtoken&name=oidc-auth');
|
||||
|
||||
expect(res.body.data.user).toBeDefined();
|
||||
expect(res.body.data.user.nickname).toBe('user1');
|
||||
|
@ -1,29 +1,18 @@
|
||||
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>
|
||||
const channel = new BroadcastChannel('nocobase-oidc-response');
|
||||
channel.postMessage(${JSON.stringify(params)})
|
||||
window.close();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
ctx.body = template;
|
||||
ctx.withoutDataWrapping = true;
|
||||
import { Context, Next } from '@nocobase/actions';
|
||||
import { OIDCAuth } from '../oidc-auth';
|
||||
|
||||
export const redirect = async (ctx: Context, next: Next) => {
|
||||
const {
|
||||
params: { state },
|
||||
} = ctx.action;
|
||||
const search = new URLSearchParams(state);
|
||||
const authenticator = search.get('name');
|
||||
const auth = (await ctx.app.authManager.get(authenticator, ctx)) as OIDCAuth;
|
||||
try {
|
||||
const { token } = await auth.signIn();
|
||||
ctx.redirect(`/signin?authenticator=${authenticator}&token=${token}`);
|
||||
} catch (error) {
|
||||
ctx.redirect(`/signin?authenticator=${authenticator}&error=${error.message}`);
|
||||
}
|
||||
await next();
|
||||
};
|
||||
|
@ -49,9 +49,7 @@ export class OIDCAuth extends BaseAuth {
|
||||
|
||||
async validate() {
|
||||
const ctx = this.ctx;
|
||||
const {
|
||||
params: { values },
|
||||
} = ctx.action;
|
||||
const { params: values } = ctx.action;
|
||||
const token = ctx.cookies.get(cookieName);
|
||||
const search = new URLSearchParams(values.state);
|
||||
if (search.get('token') !== token) {
|
||||
|
@ -1,69 +1,52 @@
|
||||
import { LoginOutlined } from '@ant-design/icons';
|
||||
import { Authenticator, css, useAPIClient, useRedirect } from '@nocobase/client';
|
||||
import { Button, Space } from 'antd';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { Authenticator, css, useAPIClient, useCurrentUserContext, useRedirect } from '@nocobase/client';
|
||||
import { Button, Space, message } from 'antd';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useSamlTranslation } from './locale';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
export const SAMLButton = (props: { authenticator: Authenticator }) => {
|
||||
export const SAMLButton = ({ authenticator }: { authenticator: Authenticator }) => {
|
||||
const { t } = useSamlTranslation();
|
||||
const [windowHandler, setWindowHandler] = useState<Window | undefined>();
|
||||
const api = useAPIClient();
|
||||
const redirect = useRedirect();
|
||||
const location = useLocation();
|
||||
const { refreshAsync: refresh } = useCurrentUserContext();
|
||||
|
||||
/**
|
||||
* 打开登录弹出框
|
||||
*/
|
||||
const handleOpen = async (name: string) => {
|
||||
const login = async () => {
|
||||
const response = await api.request({
|
||||
method: 'post',
|
||||
url: 'saml:getAuthUrl',
|
||||
headers: {
|
||||
'X-Authenticator': name,
|
||||
'X-Authenticator': authenticator.name,
|
||||
},
|
||||
});
|
||||
|
||||
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);
|
||||
window.location.replace(authUrl);
|
||||
};
|
||||
|
||||
const handleSAMLLogin = useCallback(
|
||||
async (event: MessageEvent) => {
|
||||
try {
|
||||
await api.auth.signIn(event.data, event.data?.authenticator);
|
||||
redirect();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
windowHandler.close();
|
||||
setWindowHandler(undefined);
|
||||
}
|
||||
},
|
||||
[api, redirect, windowHandler],
|
||||
);
|
||||
|
||||
/**
|
||||
* 监听弹出窗口的消息
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!windowHandler) return;
|
||||
const params = new URLSearchParams(location.search);
|
||||
const token = params.get('token');
|
||||
const name = params.get('authenticator');
|
||||
const error = params.get('error');
|
||||
if (name !== authenticator.name) {
|
||||
return;
|
||||
}
|
||||
if (error) {
|
||||
message.error(error);
|
||||
return;
|
||||
}
|
||||
if (token) {
|
||||
api.auth.setToken(token);
|
||||
api.auth.setAuthenticator(name);
|
||||
refresh()
|
||||
.then(() => redirect())
|
||||
.catch((err) => console.log(err));
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('message', handleSAMLLogin);
|
||||
return () => {
|
||||
window.removeEventListener('message', handleSAMLLogin);
|
||||
};
|
||||
}, [windowHandler, handleSAMLLogin]);
|
||||
|
||||
const authenticator = props.authenticator;
|
||||
return (
|
||||
<Space
|
||||
direction="vertical"
|
||||
@ -71,7 +54,7 @@ export const SAMLButton = (props: { authenticator: Authenticator }) => {
|
||||
display: flex;
|
||||
`}
|
||||
>
|
||||
<Button shape="round" block icon={<LoginOutlined />} onClick={() => handleOpen(authenticator.name)}>
|
||||
<Button shape="round" block icon={<LoginOutlined />} onClick={login}>
|
||||
{t(authenticator.title)}
|
||||
</Button>
|
||||
</Space>
|
||||
|
@ -61,15 +61,9 @@ describe('saml', () => {
|
||||
loggedOut: false,
|
||||
});
|
||||
|
||||
const res = await agent
|
||||
.set('X-Authenticator', 'saml-auth')
|
||||
.resource('auth')
|
||||
.signIn()
|
||||
.send({
|
||||
samlResponse: {
|
||||
SAMLResponse: '',
|
||||
},
|
||||
});
|
||||
const res = await agent.set('X-Authenticator', 'saml-auth').resource('auth').signIn().send({
|
||||
samlResponse: {},
|
||||
});
|
||||
|
||||
expect(res.body.data.user).toBeDefined();
|
||||
expect(res.body.data.user.nickname).toBe('Test Nocobase');
|
||||
|
@ -1,30 +1,14 @@
|
||||
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({
|
||||
authenticator: params.authenticator,
|
||||
samlResponse: params.values,
|
||||
})}, '*');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
ctx.body = template;
|
||||
ctx.withoutDataWrapping = true;
|
||||
import { Context, Next } from '@nocobase/actions';
|
||||
import { SAMLAuth } from '../saml-auth';
|
||||
|
||||
export const redirect = async (ctx: Context, next: Next) => {
|
||||
const { authenticator } = ctx.action.params || {};
|
||||
const auth = (await ctx.app.authManager.get(authenticator, ctx)) as SAMLAuth;
|
||||
try {
|
||||
const { token } = await auth.signIn();
|
||||
ctx.redirect(`/signin?authenticator=${authenticator}&token=${token}`);
|
||||
} catch (error) {
|
||||
ctx.redirect(`/signin?authenticator=${authenticator}&error=${error.message}`);
|
||||
}
|
||||
await next();
|
||||
};
|
||||
|
@ -35,9 +35,7 @@ export class SAMLAuth extends BaseAuth {
|
||||
async validate() {
|
||||
const ctx = this.ctx;
|
||||
const {
|
||||
params: {
|
||||
values: { samlResponse },
|
||||
},
|
||||
params: { values: samlResponse },
|
||||
} = ctx.action;
|
||||
const saml = new SAML(this.getOptions());
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user