diff --git a/packages/plugins/@nocobase/plugin-oidc/src/client/OIDCButton.tsx b/packages/plugins/@nocobase/plugin-oidc/src/client/OIDCButton.tsx index e16097827..a73a07646 100644 --- a/packages/plugins/@nocobase/plugin-oidc/src/client/OIDCButton.tsx +++ b/packages/plugins/@nocobase/plugin-oidc/src/client/OIDCButton.tsx @@ -1,57 +1,76 @@ import { LoginOutlined } from '@ant-design/icons'; -import { Authenticator, css, useAPIClient, useCurrentUserContext, useRedirect } from '@nocobase/client'; -import { Button, Space, message } from 'antd'; -import React, { useEffect } from 'react'; +import { Authenticator, css, useAPIClient, useRedirect } from '@nocobase/client'; +import { useMemoizedFn } from 'ahooks'; +import { Button, Space } from 'antd'; +import React, { useEffect, useState } from 'react'; import { useOidcTranslation } from './locale'; -import { useLocation } from 'react-router-dom'; export interface OIDCProvider { clientId: string; title: string; } -export const OIDCButton = ({ authenticator }: { authenticator: Authenticator }) => { +export const OIDCButton = (props: { authenticator: Authenticator }) => { const { t } = useOidcTranslation(); + const [windowHandler, setWindowHandler] = useState(); const api = useAPIClient(); const redirect = useRedirect(); - const location = useLocation(); - const { refreshAsync: refresh } = useCurrentUserContext(); - const login = async () => { + /** + * 打开登录弹出框 + */ + const handleOpen = async (name: string) => { const response = await api.request({ method: 'post', url: 'oidc:getAuthUrl', headers: { - 'X-Authenticator': authenticator.name, + 'X-Authenticator': name, }, }); 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 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; + /** + * 从弹出窗口,发消息回来进行登录 + */ + 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(() => { + if (!windowHandler) return; + + const channel = new BroadcastChannel('nocobase-oidc-response'); + channel.onmessage = handleOIDCLogin; + return () => { + channel.close(); + }; + }, [windowHandler, handleOIDCLogin]); + + const authenticator = props.authenticator; return ( - diff --git a/packages/plugins/@nocobase/plugin-oidc/src/server/__tests__/oidc.test.ts b/packages/plugins/@nocobase/plugin-oidc/src/server/__tests__/oidc.test.ts index 00c483024..673ed22bf 100644 --- a/packages/plugins/@nocobase/plugin-oidc/src/server/__tests__/oidc.test.ts +++ b/packages/plugins/@nocobase/plugin-oidc/src/server/__tests__/oidc.test.ts @@ -72,7 +72,12 @@ describe('oidc', () => { const res = await agent .set('X-Authenticator', 'oidc-auth') .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.nickname).toBe('user1'); diff --git a/packages/plugins/@nocobase/plugin-oidc/src/server/actions/redirect.ts b/packages/plugins/@nocobase/plugin-oidc/src/server/actions/redirect.ts index 74667a880..3f962f18c 100644 --- a/packages/plugins/@nocobase/plugin-oidc/src/server/actions/redirect.ts +++ b/packages/plugins/@nocobase/plugin-oidc/src/server/actions/redirect.ts @@ -1,18 +1,29 @@ -import { Context, Next } from '@nocobase/actions'; -import { OIDCAuth } from '../oidc-auth'; +import { Context } from '@nocobase/actions'; + +export const redirect = async (ctx: Context, next) => { + const { params } = ctx.action; + + const template = ` + + + + + + + + + + + + + `; + + 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(); }; diff --git a/packages/plugins/@nocobase/plugin-oidc/src/server/oidc-auth.ts b/packages/plugins/@nocobase/plugin-oidc/src/server/oidc-auth.ts index cbbd1c359..009102f13 100644 --- a/packages/plugins/@nocobase/plugin-oidc/src/server/oidc-auth.ts +++ b/packages/plugins/@nocobase/plugin-oidc/src/server/oidc-auth.ts @@ -49,7 +49,9 @@ 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) { diff --git a/packages/plugins/@nocobase/plugin-saml/src/client/SAMLButton.tsx b/packages/plugins/@nocobase/plugin-saml/src/client/SAMLButton.tsx index 9b102ef05..63dca36c0 100644 --- a/packages/plugins/@nocobase/plugin-saml/src/client/SAMLButton.tsx +++ b/packages/plugins/@nocobase/plugin-saml/src/client/SAMLButton.tsx @@ -1,52 +1,69 @@ import { LoginOutlined } from '@ant-design/icons'; -import { Authenticator, css, useAPIClient, useCurrentUserContext, useRedirect } from '@nocobase/client'; -import { Button, Space, message } from 'antd'; -import React, { useEffect } from 'react'; +import { Authenticator, css, useAPIClient, useRedirect } from '@nocobase/client'; +import { Button, Space } from 'antd'; +import React, { useCallback, useEffect, useState } from 'react'; 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 [windowHandler, setWindowHandler] = useState(); const api = useAPIClient(); const redirect = useRedirect(); - const location = useLocation(); - const { refreshAsync: refresh } = useCurrentUserContext(); - const login = async () => { + /** + * 打开登录弹出框 + */ + const handleOpen = async (name: string) => { const response = await api.request({ method: 'post', url: 'saml:getAuthUrl', headers: { - 'X-Authenticator': authenticator.name, + 'X-Authenticator': name, }, }); 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 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; - } - }); + 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; + + window.addEventListener('message', handleSAMLLogin); + return () => { + window.removeEventListener('message', handleSAMLLogin); + }; + }, [windowHandler, handleSAMLLogin]); + + const authenticator = props.authenticator; return ( - diff --git a/packages/plugins/@nocobase/plugin-saml/src/server/__tests__/saml.test.ts b/packages/plugins/@nocobase/plugin-saml/src/server/__tests__/saml.test.ts index 2e614cf3d..fd9de80bc 100644 --- a/packages/plugins/@nocobase/plugin-saml/src/server/__tests__/saml.test.ts +++ b/packages/plugins/@nocobase/plugin-saml/src/server/__tests__/saml.test.ts @@ -61,9 +61,15 @@ describe('saml', () => { loggedOut: false, }); - const res = await agent.set('X-Authenticator', 'saml-auth').resource('auth').signIn().send({ - samlResponse: {}, - }); + const res = await agent + .set('X-Authenticator', 'saml-auth') + .resource('auth') + .signIn() + .send({ + samlResponse: { + SAMLResponse: '', + }, + }); expect(res.body.data.user).toBeDefined(); expect(res.body.data.user.nickname).toBe('Test Nocobase'); diff --git a/packages/plugins/@nocobase/plugin-saml/src/server/actions/redirect.ts b/packages/plugins/@nocobase/plugin-saml/src/server/actions/redirect.ts index e2e88c74b..0e8c3eed3 100644 --- a/packages/plugins/@nocobase/plugin-saml/src/server/actions/redirect.ts +++ b/packages/plugins/@nocobase/plugin-saml/src/server/actions/redirect.ts @@ -1,14 +1,30 @@ -import { Context, Next } from '@nocobase/actions'; -import { SAMLAuth } from '../saml-auth'; +import { Context } from '@nocobase/actions'; + +export const redirect = async (ctx: Context, next) => { + const { params } = ctx.action; + + const template = ` + + + + + + + + + + + + + `; + + 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(); }; diff --git a/packages/plugins/@nocobase/plugin-saml/src/server/saml-auth.ts b/packages/plugins/@nocobase/plugin-saml/src/server/saml-auth.ts index 58f2ad02b..39367799a 100644 --- a/packages/plugins/@nocobase/plugin-saml/src/server/saml-auth.ts +++ b/packages/plugins/@nocobase/plugin-saml/src/server/saml-auth.ts @@ -35,7 +35,9 @@ 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());