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