Skip to content

Commit

Permalink
feat(express): with logto (#378)
Browse files Browse the repository at this point in the history
  • Loading branch information
wangsijie authored Aug 8, 2022
1 parent 5fbfc1b commit 9626b76
Show file tree
Hide file tree
Showing 11 changed files with 126 additions and 80 deletions.
17 changes: 15 additions & 2 deletions packages/express/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const getIdTokenClaims = jest.fn(() => ({
sub: 'user_id',
}));
const signOut = jest.fn();
const getAccessToken = jest.fn(async () => true);
const getContext = jest.fn(async () => ({ isAuthenticated: true }));

jest.mock('./storage', () =>
jest.fn(() => ({
Expand All @@ -41,7 +41,7 @@ jest.mock('@logto/node', () =>
signIn();
},
handleSignInCallback,
getAccessToken,
getContext,
getIdTokenClaims,
signOut: () => {
navigate(configs.baseUrl);
Expand Down Expand Up @@ -100,4 +100,17 @@ describe('Express', () => {
expect(signOut).toHaveBeenCalled();
});
});

describe('withLogto', () => {
it('should assign `user` to `request`', async () => {
const client = new LogtoClient(configs);
await testMiddleware({
middleware: client.withLogto(),
test: async ({ request }) => {
expect(request.user).toBeDefined();
},
});
expect(getContext).toHaveBeenCalled();
});
});
});
14 changes: 13 additions & 1 deletion packages/express/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { Request, Response, NextFunction, Router } from 'express';

import { LogtoExpressError } from './errors';
import ExpressStorage from './storage';
import { LogtoExpressConfig } from './types';
import { LogtoExpressConfig, WithLogtoConfig } from './types';

export type { LogtoContext } from '@logto/node';

export type Middleware = (
request: Request,
Expand Down Expand Up @@ -66,6 +68,16 @@ export default class LogtoClient {
return router;
};

withLogto =
(config: WithLogtoConfig = {}): Middleware =>
async (request: IncomingMessage, response: Response, next: NextFunction) => {
const client = this.createNodeClient(request, response);
const user = await client.getContext(config.getAccessToken);
// eslint-disable-next-line @silverhand/fp/no-mutating-methods
Object.defineProperty(request, 'user', { enumerable: true, get: () => user });
next();
};

private createNodeClient(request: IncomingMessage, response: Response) {
this.checkSession(request);
const storage = new ExpressStorage(request);
Expand Down
4 changes: 4 additions & 0 deletions packages/express/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,7 @@ declare module 'http' {
export type LogtoExpressConfig = LogtoConfig & {
baseUrl: string;
};

export type WithLogtoConfig = {
getAccessToken?: boolean;
};
4 changes: 2 additions & 2 deletions packages/next-sample/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { LogtoUser } from '@logto/next';
import { LogtoContext } from '@logto/next';
import Link from 'next/link';
import { useMemo } from 'react';
import useSWR from 'swr';

const Home = () => {
const { data } = useSWR<LogtoUser>('/api/logto/user');
const { data } = useSWR<LogtoContext>('/api/logto/user');
const { data: protectedResource } = useSWR<{ data: string }>('/api/protected-resource');

const userInfo = useMemo(() => {
Expand Down
4 changes: 2 additions & 2 deletions packages/next-sample/pages/profile-ssr.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { LogtoUser } from '@logto/next';
import { LogtoContext } from '@logto/next';
import { useMemo } from 'react';

import { logtoClient } from '../libraries/logto';

type Props = {
user: LogtoUser;
user: LogtoContext;
};

const ProfileSsr = ({ user }: Props) => {
Expand Down
28 changes: 4 additions & 24 deletions packages/next/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const getIdTokenClaims = jest.fn(() => ({
sub: 'user_id',
}));
const signOut = jest.fn();
const getAccessToken = jest.fn(async () => true);
const getContext = jest.fn(async () => true);

const mockResponse = (_: unknown, response: NextApiResponse) => {
response.status(200).end();
Expand Down Expand Up @@ -53,7 +53,7 @@ jest.mock('@logto/node', () =>
signIn();
},
handleSignInCallback,
getAccessToken,
getContext,
getIdTokenClaims,
signOut: () => {
navigate(configs.baseUrl);
Expand Down Expand Up @@ -107,26 +107,7 @@ describe('Next', () => {
});

describe('withLogtoApiRoute', () => {
it('should set isAuthenticated to false when "getAccessToken" is enabled and is unable to getAccessToken', async () => {
getAccessToken.mockRejectedValueOnce(new Error('Unauthorized'));
const client = new LogtoClient(configs);
await testApiHandler({
handler: client.withLogtoApiRoute(
(request, response) => {
expect(request.user).toBeDefined();
response.json(request.user);
},
{ getAccessToken: true }
),
test: async ({ fetch }) => {
const response = await fetch({ method: 'GET', redirect: 'manual' });
await expect(response.json()).resolves.toEqual({ isAuthenticated: false });
},
});
expect(getAccessToken).toHaveBeenCalled();
});

it('should assign `user` to `request` and not call getAccessToken by default', async () => {
it('should assign `user` to `request`', async () => {
const client = new LogtoClient(configs);
await testApiHandler({
handler: client.withLogtoApiRoute((request, response) => {
Expand All @@ -137,8 +118,7 @@ describe('Next', () => {
await fetch({ method: 'GET', redirect: 'manual' });
},
});
expect(getIdTokenClaims).toHaveBeenCalled();
expect(getAccessToken).not.toHaveBeenCalled();
expect(getContext).toHaveBeenCalled();
});
});

Expand Down
39 changes: 3 additions & 36 deletions packages/next/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { withIronSessionApiRoute, withIronSessionSsr } from 'iron-session/next';
import { GetServerSidePropsContext, GetServerSidePropsResult, NextApiHandler } from 'next';

import NextStorage from './storage';
import { LogtoNextConfig, LogtoUser, WithLogtoConfig } from './types';
import { LogtoNextConfig, WithLogtoConfig } from './types';

export type { LogtoUser } from './types';
export type { LogtoContext } from '@logto/node';

export default class LogtoClient {
private navigateUrl?: string;
Expand Down Expand Up @@ -134,40 +134,7 @@ export default class LogtoClient {

private async getLogtoUserFromRequest(request: IncomingMessage, getAccessToken?: boolean) {
const nodeClient = this.createNodeClient(request);
const { isAuthenticated } = nodeClient;

if (!isAuthenticated) {
const user: LogtoUser = {
isAuthenticated,
};

return user;
}

if (!getAccessToken) {
const user: LogtoUser = {
isAuthenticated,
claims: nodeClient.getIdTokenClaims(),
};

return user;
}

try {
const accessToken = await nodeClient.getAccessToken();
await this.storage?.save();

const user: LogtoUser = {
isAuthenticated,
claims: nodeClient.getIdTokenClaims(),
accessToken,
};

return user;
} catch {
return {
isAuthenticated: false,
};
}
return nodeClient.getContext(getAccessToken);
}
}
14 changes: 1 addition & 13 deletions packages/next/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IdTokenClaims, LogtoConfig } from '@logto/node';
import { LogtoConfig } from '@logto/node';
import { IronSession } from 'iron-session';
import { NextApiRequest } from 'next';

Expand All @@ -13,24 +13,12 @@ declare module 'iron-session' {
}
}

declare module 'http' {
interface IncomingMessage {
user: LogtoUser;
}
}

export type LogtoNextConfig = LogtoConfig & {
cookieSecret: string;
cookieSecure: boolean;
baseUrl: string;
};

export type LogtoUser = {
isAuthenticated: boolean;
claims?: IdTokenClaims;
accessToken?: string;
};

/**
* @getAccessToken: if set to true, will try to get an access token and attach to req.user,
* if unable to grant an access token, will set req.user.isAuthenticated to false,
Expand Down
35 changes: 35 additions & 0 deletions packages/node/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,45 @@ const storage = {
removeItem: jest.fn(),
};

const getAccessToken = jest.fn(async () => true);
const getIdTokenClaims = jest.fn(() => ({ sub: 'sub' }));
jest.mock('@logto/client', () => ({
__esModule: true,
default: jest.fn(() => ({
getAccessToken,
getIdTokenClaims,
isAuthenticated: true,
})),
createRequester: jest.fn(),
}));

describe('LogtoClient', () => {
describe('constructor', () => {
it('constructor should not throw', () => {
expect(() => new LogtoClient({ endpoint, appId }, { navigate, storage })).not.toThrow();
});
});

describe('getContext', () => {
beforeEach(() => {
getAccessToken.mockClear();
});

it('should set isAuthenticated to false when "getAccessToken" is enabled and is unable to getAccessToken', async () => {
getAccessToken.mockRejectedValueOnce(new Error('Unauthorized'));
const client = new LogtoClient({ endpoint, appId }, { navigate, storage });
await expect(client.getContext(true)).resolves.toEqual({ isAuthenticated: false });
expect(getAccessToken).toHaveBeenCalled();
});

it('should return context and not call getAccessToken by default', async () => {
const client = new LogtoClient({ endpoint, appId }, { navigate, storage });
await expect(client.getContext()).resolves.toEqual({
isAuthenticated: true,
claims: { sub: 'sub' },
});
expect(getIdTokenClaims).toHaveBeenCalled();
expect(getAccessToken).not.toHaveBeenCalled();
});
});
});
34 changes: 34 additions & 0 deletions packages/node/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import BaseClient, { LogtoConfig, createRequester, ClientAdapter } from '@logto/client';
import fetch from 'node-fetch';

import { LogtoContext } from './types';
import { generateCodeChallenge, generateCodeVerifier, generateState } from './utils/generators';

export type { LogtoContext } from './types';

export type {
IdTokenClaims,
LogtoErrorCode,
Expand Down Expand Up @@ -41,4 +44,35 @@ export default class LogtoClient extends BaseClient {
generateState,
});
}

getContext = async (getAccessToken = false): Promise<LogtoContext> => {
const { isAuthenticated } = this;

if (!isAuthenticated) {
return {
isAuthenticated,
};
}

if (!getAccessToken) {
return {
isAuthenticated,
claims: this.getIdTokenClaims(),
};
}

try {
const accessToken = await this.getAccessToken();

return {
isAuthenticated,
claims: this.getIdTokenClaims(),
accessToken,
};
} catch {
return {
isAuthenticated: false,
};
}
};
}
13 changes: 13 additions & 0 deletions packages/node/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { IdTokenClaims } from '@logto/client';

declare module 'http' {
interface IncomingMessage {
user: LogtoContext;
}
}

export type LogtoContext = {
isAuthenticated: boolean;
claims?: IdTokenClaims;
accessToken?: string;
};

0 comments on commit 9626b76

Please sign in to comment.