Skip to content

Commit

Permalink
refactor(next): remove iron session (#545)
Browse files Browse the repository at this point in the history
  • Loading branch information
wangsijie authored Sep 11, 2023
1 parent f611e75 commit 648bdeb
Show file tree
Hide file tree
Showing 13 changed files with 481 additions and 482 deletions.
71 changes: 47 additions & 24 deletions packages/next/edge/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { RequestCookies, ResponseCookies } from '@edge-runtime/cookies';
import { type GetContextParameters, type InteractionMode } from '@logto/node';
import NodeClient from '@logto/node/edge';
import { getIronSession } from 'iron-session/edge';
import { type NextRequest } from 'next/server';

import BaseClient from '../src/client';
import { createSession } from '../src/session';
import type { LogtoNextConfig } from '../src/types.js';

export type { LogtoContext, InteractionMode } from '@logto/node';
Expand All @@ -20,15 +21,15 @@ export default class LogtoClient extends BaseClient {
redirectUri = `${this.config.baseUrl}/api/logto/sign-in-callback`,
interactionMode?: InteractionMode
) =>
async (request: NextRequest) => {
async (request: Request) => {
const { nodeClient, headers } = await this.createNodeClientFromEdgeRequest(request);
await nodeClient.signIn(redirectUri, interactionMode);
await this.storage?.save();

const response = new Response(null, {
headers,
status: 307,
});
const session = await getIronSession(request, response, this.ironSessionConfigs);

const nodeClient = this.createNodeClient(session);
await nodeClient.signIn(redirectUri, interactionMode);
await this.storage?.save();

if (this.navigateUrl) {
response.headers.append('Location', this.navigateUrl);
Expand All @@ -40,15 +41,15 @@ export default class LogtoClient extends BaseClient {
handleSignOut =
(redirectUri = this.config.baseUrl) =>
async (request: NextRequest) => {
const { nodeClient, headers } = await this.createNodeClientFromEdgeRequest(request);
await nodeClient.signOut(redirectUri);
await this.storage?.destroy();
await this.storage?.save();

const response = new Response(null, {
headers,
status: 307,
});
const session = await getIronSession(request, response, this.ironSessionConfigs);

const nodeClient = this.createNodeClient(session);
await nodeClient.signOut(redirectUri);
session.destroy();
await this.storage?.save();

if (this.navigateUrl) {
response.headers.append('Location', this.navigateUrl);
Expand All @@ -60,15 +61,7 @@ export default class LogtoClient extends BaseClient {
handleSignInCallback =
(redirectTo = this.config.baseUrl) =>
async (request: NextRequest) => {
const response = new Response(null, {
status: 307,
headers: {
Location: redirectTo,
},
});
const session = await getIronSession(request, response, this.ironSessionConfigs);

const nodeClient = this.createNodeClient(session);
const { nodeClient, headers } = await this.createNodeClientFromEdgeRequest(request);

if (request.url) {
// When app is running behind reverse proxy which is common for edge runtime,
Expand All @@ -82,6 +75,11 @@ export default class LogtoClient extends BaseClient {
await this.storage?.save();
}

const response = new Response(null, {
status: 307,
headers,
});
response.headers.append('Location', redirectTo);
return response;
};

Expand All @@ -96,9 +94,34 @@ export default class LogtoClient extends BaseClient {
};

getLogtoContext = async (request: NextRequest, config: GetContextParameters = {}) => {
const session = await getIronSession(request, new Response(), this.ironSessionConfigs);
const context = await this.getLogtoUserFromRequest(session, config);
const { nodeClient } = await this.createNodeClientFromEdgeRequest(request);
const context = await nodeClient.getContext();

return context;
};

private async createNodeClientFromEdgeRequest(request: Request) {
const cookieName = `logto:${this.config.appId}`;
const cookies = new RequestCookies(request.headers);
const headers = new Headers();
const responseCookies = new ResponseCookies(headers);

const nodeClient = super.createNodeClient(
await createSession(
{
secret: this.config.cookieSecret,
crypto,
},
cookies.get(cookieName)?.value ?? '',
(value) => {
responseCookies.set(cookieName, value, {
maxAge: 14 * 3600 * 24,
secure: this.config.cookieSecure,
});
}
)
);

return { nodeClient, headers };
}
}
4 changes: 2 additions & 2 deletions packages/next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@
"postpack": "node test.cjs"
},
"dependencies": {
"@logto/node": "workspace:^2.1.1",
"iron-session": "^6.3.1"
"@edge-runtime/cookies": "^3.4.1",
"@logto/node": "workspace:^2.1.1"
},
"devDependencies": {
"@silverhand/eslint-config": "^4.0.1",
Expand Down
24 changes: 2 additions & 22 deletions packages/next/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import type { GetContextParameters } from '@logto/node';
import { type IronSession } from 'iron-session';

import NextStorage from './storage';
import type { Adapters, LogtoNextConfig } from './types';
import type { Adapters, LogtoNextConfig, Session } from './types';

export default class LogtoNextBaseClient {
protected navigateUrl?: string;
Expand All @@ -12,7 +9,7 @@ export default class LogtoNextBaseClient {
protected readonly adapters: Adapters
) {}

protected createNodeClient(session: IronSession) {
protected createNodeClient(session: Session) {
this.storage = new NextStorage(session);

return new this.adapters.NodeClient(this.config, {
Expand All @@ -22,21 +19,4 @@ export default class LogtoNextBaseClient {
},
});
}

protected get ironSessionConfigs() {
return {
cookieName: `logto:${this.config.appId}`,
password: this.config.cookieSecret,
cookieOptions: {
secure: this.config.cookieSecure,
maxAge: 14 * 24 * 60 * 60,
},
};
}

protected async getLogtoUserFromRequest(session: IronSession, configs: GetContextParameters) {
const nodeClient = this.createNodeClient(session);

return nodeClient.getContext(configs);
}
}
1 change: 1 addition & 0 deletions packages/next/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ jest.mock('./storage', () =>
setItem,
getItem,
removeItem: jest.fn(),
destroy: jest.fn(),
save: () => {
save();
},
Expand Down
106 changes: 72 additions & 34 deletions packages/next/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import * as crypto from 'crypto';
import { type IncomingMessage, type ServerResponse } from 'http';

import NodeClient, { type GetContextParameters, type InteractionMode } from '@logto/node';
import { withIronSessionApiRoute, withIronSessionSsr } from 'iron-session/next';
import {
type GetServerSidePropsResult,
type GetServerSidePropsContext,
type NextApiHandler,
} from 'next';
import { type NextApiRequestCookies } from 'next/dist/server/api-utils/index.js';

import LogtoNextBaseClient from './client.js';
import { createSession } from './session.js';
import type { LogtoNextConfig } from './types.js';

export { ReservedScope, UserScope } from '@logto/node';
Expand All @@ -20,43 +24,46 @@ export default class LogtoClient extends LogtoNextBaseClient {
});
}

handleSignIn = (
redirectUri = `${this.config.baseUrl}/api/logto/sign-in-callback`,
interactionMode?: InteractionMode
): NextApiHandler =>
withIronSessionApiRoute(async (request, response) => {
const nodeClient = this.createNodeClient(request.session);
handleSignIn =
(
redirectUri = `${this.config.baseUrl}/api/logto/sign-in-callback`,
interactionMode?: InteractionMode
): NextApiHandler =>
async (request, response) => {
const nodeClient = await this.createNodeClientFromNextApi(request, response);
await nodeClient.signIn(redirectUri, interactionMode);
await this.storage?.save();

if (this.navigateUrl) {
response.redirect(this.navigateUrl);
}
}, this.ironSessionConfigs);
};

handleSignInCallback = (redirectTo = this.config.baseUrl): NextApiHandler =>
withIronSessionApiRoute(async (request, response) => {
const nodeClient = this.createNodeClient(request.session);
handleSignInCallback =
(redirectTo = this.config.baseUrl): NextApiHandler =>
async (request, response) => {
const nodeClient = await this.createNodeClientFromNextApi(request, response);

if (request.url) {
await nodeClient.handleSignInCallback(`${this.config.baseUrl}${request.url}`);
await this.storage?.save();
response.redirect(redirectTo);
}
}, this.ironSessionConfigs);
};

handleSignOut = (redirectUri = this.config.baseUrl): NextApiHandler =>
withIronSessionApiRoute(async (request, response) => {
const nodeClient = this.createNodeClient(request.session);
handleSignOut =
(redirectUri = this.config.baseUrl): NextApiHandler =>
async (request, response) => {
const nodeClient = await this.createNodeClientFromNextApi(request, response);
await nodeClient.signOut(redirectUri);

request.session.destroy();
await this.storage?.destroy();
await this.storage?.save();

if (this.navigateUrl) {
response.redirect(this.navigateUrl);
}
}, this.ironSessionConfigs);
};

handleUser = (configs?: GetContextParameters) =>
this.withLogtoApiRoute((request, response) => {
Expand Down Expand Up @@ -91,30 +98,61 @@ export default class LogtoClient extends LogtoNextBaseClient {
response.status(404).end();
};

withLogtoApiRoute = (
handler: NextApiHandler,
config: GetContextParameters = {}
): NextApiHandler =>
withIronSessionApiRoute(async (request, response) => {
const user = await this.getLogtoUserFromRequest(request.session, config);
withLogtoApiRoute =
(handler: NextApiHandler, config: GetContextParameters = {}): NextApiHandler =>
async (request, response) => {
const nodeClient = await this.createNodeClientFromNextApi(request, response);
const user = await nodeClient.getContext(config);

// eslint-disable-next-line @silverhand/fp/no-mutating-methods
Object.defineProperty(request, 'user', { enumerable: true, get: () => user });

return handler(request, response);
}, this.ironSessionConfigs);

withLogtoSsr = <P extends Record<string, unknown> = Record<string, unknown>>(
handler: (
context: GetServerSidePropsContext
) => GetServerSidePropsResult<P> | Promise<GetServerSidePropsResult<P>>,
configs: GetContextParameters = {}
) =>
withIronSessionSsr(async (context) => {
const user = await this.getLogtoUserFromRequest(context.req.session, configs);
};

withLogtoSsr =
<P extends Record<string, unknown> = Record<string, unknown>>(
handler: (
context: GetServerSidePropsContext
) => GetServerSidePropsResult<P> | Promise<GetServerSidePropsResult<P>>,
configs: GetContextParameters = {}
) =>
async (context: GetServerSidePropsContext) => {
const nodeClient = await this.createNodeClientFromNextApi(context.req, context.res);
const user = await nodeClient.getContext(configs);

// eslint-disable-next-line @silverhand/fp/no-mutating-methods
Object.defineProperty(context.req, 'user', { enumerable: true, get: () => user });

return handler(context);
}, this.ironSessionConfigs);
};

private async createNodeClientFromNextApi(
request: IncomingMessage & {
cookies: NextApiRequestCookies;
},
response: ServerResponse
): Promise<NodeClient> {
const cookieName = `logto:${this.config.appId}`;

return super.createNodeClient(
await createSession(
{
secret: this.config.cookieSecret,
crypto,
},
request.cookies[cookieName] ?? '',
(value) => {
const secure = this.config.cookieSecure;
const maxAge = 14 * 3600 * 24;
response.setHeader(
'Set-Cookie',
`${cookieName}=${value}; Path=/; Max-Age=${maxAge}; ${
secure ? 'Secure; SameSite=None' : ''
}`
);
}
)
);
}
}
29 changes: 29 additions & 0 deletions packages/next/src/session.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as crypto from 'crypto';

import { PersistKey } from '@logto/node';

import { unwrapSession, wrapSession } from './session';

const secret = 'secret';

describe('session', () => {
it('should be able to wrap', async () => {
const cookie = await wrapSession({ [PersistKey.IdToken]: 'idToken' }, secret, crypto);
expect(cookie).toContain('.');
});

it('should be able to unwrap', async () => {
const session = await unwrapSession(
'BShU2NGKg5762PWEOFu8lhzXKZMktgjH1RR4ifik4aGOOerM7w==.DFFnnlzSnjRbTl7I',
secret,
crypto
);
expect(session[PersistKey.IdToken]).toEqual('idToken');
});

it('should be able to wrap and unwrap', async () => {
const cookie = await wrapSession({ [PersistKey.IdToken]: 'idToken' }, secret, crypto);
const session = await unwrapSession(cookie, secret, crypto);
expect(session[PersistKey.IdToken]).toEqual('idToken');
});
});
Loading

0 comments on commit 648bdeb

Please sign in to comment.