From bcacf75c4f9ce8c94b94b784ab343e8daa4c5a4b Mon Sep 17 00:00:00 2001 From: wangsijie Date: Thu, 29 Sep 2022 15:16:16 +0800 Subject: [PATCH] feat(express,next,node): support fetchUserInfo --- packages/express-sample/src/app.ts | 17 +++++++-- packages/express/src/index.ts | 2 +- packages/express/src/types.ts | 1 + .../next-sample/pages/api/logto/user-info.ts | 3 ++ packages/next-sample/pages/index.tsx | 36 +++++++++++++++++-- packages/next/src/index.ts | 14 ++++++-- packages/next/src/types.ts | 1 + packages/node/src/index.test.ts | 15 +++++++- packages/node/src/index.ts | 7 +++- packages/node/src/types.ts | 3 +- 10 files changed, 87 insertions(+), 12 deletions(-) create mode 100644 packages/next-sample/pages/api/logto/user-info.ts diff --git a/packages/express-sample/src/app.ts b/packages/express-sample/src/app.ts index 0c1bdd31..ed1aa08a 100644 --- a/packages/express-sample/src/app.ts +++ b/packages/express-sample/src/app.ts @@ -24,7 +24,6 @@ const app = express(); app.use(cookieParser()); app.use(session({ secret: 'keyboard cat', cookie: { maxAge: 14 * 24 * 60 * 60 } })); app.use(handleAuthRoutes(config)); -app.use(withLogto(config)); app.get('/', (request, response) => { response.setHeader('content-type', 'text/html'); @@ -37,11 +36,23 @@ app.get('/', (request, response) => { ); }); -app.get('/user', (request, response) => { +app.get('/local-user-claims', withLogto(config), (request, response) => { response.json(request.user); }); -app.get('/protected', requireAuth, (request, response) => { +app.get( + '/remote-full-user', + withLogto({ + ...config, + // Fetch user info from remote, this may slowdown the response time, not recommended. + fetchUserInfo: true, + }), + (request, response) => { + response.json(request.user); + } +); + +app.get('/protected', withLogto(config), requireAuth, (request, response) => { response.end('protected resource'); }); diff --git a/packages/express/src/index.ts b/packages/express/src/index.ts index df23ee50..58457fd1 100644 --- a/packages/express/src/index.ts +++ b/packages/express/src/index.ts @@ -82,7 +82,7 @@ export const withLogto = (config: LogtoExpressConfig): Middleware => async (request: IncomingMessage, response: Response, next: NextFunction) => { const client = createNodeClient(request, response, config); - const user = await client.getContext(config.getAccessToken); + const user = await client.getContext(config.getAccessToken, config.fetchUserInfo); // eslint-disable-next-line @silverhand/fp/no-mutating-methods Object.defineProperty(request, 'user', { enumerable: true, get: () => user }); next(); diff --git a/packages/express/src/types.ts b/packages/express/src/types.ts index 021dff9d..0d5db07a 100644 --- a/packages/express/src/types.ts +++ b/packages/express/src/types.ts @@ -11,4 +11,5 @@ declare module 'http' { export type LogtoExpressConfig = LogtoConfig & { baseUrl: string; getAccessToken?: boolean; + fetchUserInfo?: boolean; }; diff --git a/packages/next-sample/pages/api/logto/user-info.ts b/packages/next-sample/pages/api/logto/user-info.ts new file mode 100644 index 00000000..e6a6fce8 --- /dev/null +++ b/packages/next-sample/pages/api/logto/user-info.ts @@ -0,0 +1,3 @@ +import { logtoClient } from '../../../libraries/logto'; + +export default logtoClient.handleUser({ fetchUserInfo: true }); diff --git a/packages/next-sample/pages/index.tsx b/packages/next-sample/pages/index.tsx index 958c3ed2..31e54811 100644 --- a/packages/next-sample/pages/index.tsx +++ b/packages/next-sample/pages/index.tsx @@ -4,17 +4,20 @@ import { useMemo } from 'react'; import useSWR from 'swr'; const Home = () => { + // Use server's id token claims const { data } = useSWR('/api/logto/user'); + // Remote full user info + const { data: dataWithUserInfo } = useSWR('/api/logto/user-info'); const { data: protectedResource } = useSWR<{ data: string }>('/api/protected-resource'); - const userInfo = useMemo(() => { + const claims = useMemo(() => { if (!data?.isAuthenticated || !data.claims) { return null; } return (
-

User info:

+

Claims:

@@ -35,6 +38,34 @@ const Home = () => { ); }, [data]); + const userInfo = useMemo(() => { + if (!dataWithUserInfo?.isAuthenticated || !dataWithUserInfo.userInfo) { + return null; + } + + return ( +
+

User info:

+
+ + + + + + + + {Object.entries(dataWithUserInfo.userInfo).map(([key, value]) => ( + + + + + ))} + +
NameValue
{key}{JSON.stringify(value)}
+
+ ); + }, [dataWithUserInfo]); + return (
@@ -51,6 +82,7 @@ const Home = () => { )} + {claims} {userInfo} {protectedResource && (
diff --git a/packages/next/src/index.ts b/packages/next/src/index.ts index 33d64a58..f6fbd287 100644 --- a/packages/next/src/index.ts +++ b/packages/next/src/index.ts @@ -84,7 +84,11 @@ export default class LogtoClient { withLogtoApiRoute = (handler: NextApiHandler, config: WithLogtoConfig = {}): NextApiHandler => withIronSessionApiRoute(async (request, response) => { - const user = await this.getLogtoUserFromRequest(request, config.getAccessToken); + const user = await this.getLogtoUserFromRequest( + request, + config.getAccessToken, + config.fetchUserInfo + ); // eslint-disable-next-line @silverhand/fp/no-mutating-methods Object.defineProperty(request, 'user', { enumerable: true, get: () => user }); @@ -128,9 +132,13 @@ export default class LogtoClient { }; } - private async getLogtoUserFromRequest(request: IncomingMessage, getAccessToken?: boolean) { + private async getLogtoUserFromRequest( + request: IncomingMessage, + getAccessToken?: boolean, + fetchUserInfo?: boolean + ) { const nodeClient = this.createNodeClient(request); - return nodeClient.getContext(getAccessToken); + return nodeClient.getContext(getAccessToken, fetchUserInfo); } } diff --git a/packages/next/src/types.ts b/packages/next/src/types.ts index 1e9e49f4..11e4dc98 100644 --- a/packages/next/src/types.ts +++ b/packages/next/src/types.ts @@ -28,4 +28,5 @@ export type LogtoNextConfig = LogtoConfig & { */ export type WithLogtoConfig = { getAccessToken?: boolean; + fetchUserInfo?: boolean; }; diff --git a/packages/node/src/index.test.ts b/packages/node/src/index.test.ts index 9c426f2d..c51dddd8 100644 --- a/packages/node/src/index.test.ts +++ b/packages/node/src/index.test.ts @@ -11,6 +11,7 @@ const storage = { }; const getAccessToken = jest.fn(async () => true); +const fetchUserInfo = jest.fn(async () => ({ name: 'name' })); const getIdTokenClaims = jest.fn(async () => ({ sub: 'sub' })); const isAuthenticated = jest.fn(async () => true); jest.mock('@logto/client', () => ({ @@ -19,6 +20,7 @@ jest.mock('@logto/client', () => ({ getAccessToken, getIdTokenClaims, isAuthenticated, + fetchUserInfo, })), createRequester: jest.fn(), })); @@ -33,6 +35,7 @@ describe('LogtoClient', () => { describe('getContext', () => { beforeEach(() => { getAccessToken.mockClear(); + fetchUserInfo.mockClear(); }); it('should set isAuthenticated to false when "getAccessToken" is enabled and is unable to getAccessToken', async () => { @@ -42,7 +45,16 @@ describe('LogtoClient', () => { expect(getAccessToken).toHaveBeenCalled(); }); - it('should return context and not call getAccessToken by default', async () => { + it('should fetch remote user info and return when "fetchUserInfo" is enabled', async () => { + const client = new LogtoClient({ endpoint, appId }, { navigate, storage }); + await expect(client.getContext(false, true)).resolves.toMatchObject({ + claims: { sub: 'sub' }, + userInfo: { name: 'name' }, + }); + expect(fetchUserInfo).toHaveBeenCalled(); + }); + + it('should return context and not call getAccessToken and fetchUserInfo by default', async () => { const client = new LogtoClient({ endpoint, appId }, { navigate, storage }); await expect(client.getContext()).resolves.toEqual({ isAuthenticated: true, @@ -50,6 +62,7 @@ describe('LogtoClient', () => { }); expect(getIdTokenClaims).toHaveBeenCalled(); expect(getAccessToken).not.toHaveBeenCalled(); + expect(fetchUserInfo).not.toHaveBeenCalled(); }); }); }); diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index dac4114f..f1ee5583 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -1,4 +1,5 @@ import BaseClient, { LogtoConfig, createRequester, ClientAdapter } from '@logto/client'; +import { conditional } from '@silverhand/essentials'; import fetch from 'node-fetch'; import { LogtoContext } from './types'; @@ -55,7 +56,8 @@ export default class LogtoClient extends BaseClient { }); } - getContext = async (getAccessToken = false): Promise => { + /* eslint-disable complexity */ + getContext = async (getAccessToken = false, fetchUserInfo = false): Promise => { const isAuthenticated = await this.isAuthenticated(); if (!isAuthenticated) { @@ -70,6 +72,7 @@ export default class LogtoClient extends BaseClient { return { isAuthenticated, claims, + userInfo: conditional(fetchUserInfo && (await this.fetchUserInfo())), }; } @@ -79,6 +82,7 @@ export default class LogtoClient extends BaseClient { return { isAuthenticated, claims: await this.getIdTokenClaims(), + userInfo: conditional(fetchUserInfo && (await this.fetchUserInfo())), accessToken, }; } catch { @@ -87,4 +91,5 @@ export default class LogtoClient extends BaseClient { }; } }; + /* eslint-enable complexity */ } diff --git a/packages/node/src/types.ts b/packages/node/src/types.ts index 47ca6429..4903c1e7 100644 --- a/packages/node/src/types.ts +++ b/packages/node/src/types.ts @@ -1,4 +1,4 @@ -import { IdTokenClaims } from '@logto/client'; +import { IdTokenClaims, UserInfoResponse } from '@logto/client'; declare module 'http' { // Honor module definition @@ -12,4 +12,5 @@ export type LogtoContext = { isAuthenticated: boolean; claims?: IdTokenClaims; accessToken?: string; + userInfo?: UserInfoResponse; };