diff --git a/packages/express-sample/src/app.ts b/packages/express-sample/src/app.ts index 0c1bdd315..455b85831 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('/user', withLogto(config), (request, response) => { response.json(request.user); }); -app.get('/protected', requireAuth, (request, response) => { +app.get( + '/user-fetched', + 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 df23ee50c..58457fd1c 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 021dff9dd..0d5db07a4 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 000000000..e6a6fce85 --- /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 958c3ed2a..fcb7dc453 100644 --- a/packages/next-sample/pages/index.tsx +++ b/packages/next-sample/pages/index.tsx @@ -5,16 +5,17 @@ import useSWR from 'swr'; const Home = () => { const { data } = useSWR('/api/logto/user'); + 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 +36,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 +80,7 @@ const Home = () => { )} + {claims} {userInfo} {protectedResource && (
diff --git a/packages/next/src/index.ts b/packages/next/src/index.ts index 33d64a580..f6fbd2872 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 1e9e49f4a..11e4dc987 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 9c426f2d3..c51dddd8e 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 dac4114fa..051c08a18 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -55,7 +55,7 @@ export default class LogtoClient extends BaseClient { }); } - getContext = async (getAccessToken = false): Promise => { + getContext = async (getAccessToken = false, fetchUserInfo = false): Promise => { const isAuthenticated = await this.isAuthenticated(); if (!isAuthenticated) { @@ -70,6 +70,7 @@ export default class LogtoClient extends BaseClient { return { isAuthenticated, claims, + userInfo: await this._fetchUserInfo(fetchUserInfo), }; } @@ -79,6 +80,7 @@ export default class LogtoClient extends BaseClient { return { isAuthenticated, claims: await this.getIdTokenClaims(), + userInfo: await this._fetchUserInfo(fetchUserInfo), accessToken, }; } catch { @@ -87,4 +89,12 @@ export default class LogtoClient extends BaseClient { }; } }; + + private readonly _fetchUserInfo = async (fetch: boolean) => { + if (!fetch) { + return; + } + + return this.fetchUserInfo(); + }; } diff --git a/packages/node/src/types.ts b/packages/node/src/types.ts index 47ca64294..4903c1e72 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; };