Skip to content

Commit

Permalink
Merge pull request #909 from auth0/mw-for-api-routes
Browse files Browse the repository at this point in the history
  • Loading branch information
FPiety0521 authored and FPiety0521 committed Nov 16, 2022
2 parents 18b34a0 + 5ae5dd4 commit dfee400
Show file tree
Hide file tree
Showing 9 changed files with 57 additions and 10 deletions.
2 changes: 1 addition & 1 deletion examples/kitchen-sink-example/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ import { withMiddlewareAuthRequired } from '@auth0/nextjs-auth0/middleware';
export default withMiddlewareAuthRequired();

export const config = {
matcher: '/profile-mw'
matcher: ['/profile-mw', '/api/hello-world-mw']
};
3 changes: 3 additions & 0 deletions examples/kitchen-sink-example/pages/api/hello-world-mw.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default async function helloWorld(req, res) {
res.status(200).json({ text: 'Hello World!' });
}
4 changes: 3 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ export interface NextConfig extends Pick<BaseConfig, 'identityClaimFilter'> {
routes: {
callback: string;
login: string;
unauthorized: string;
};
}

Expand Down Expand Up @@ -522,7 +523,8 @@ export const getConfig = (params: ConfigParameters = {}): { baseConfig: BaseConf
const nextConfig = {
routes: {
...baseConfig.routes,
login: baseParams.routes?.login || getLoginUrl()
login: baseParams.routes?.login || getLoginUrl(),
unauthorized: baseParams.routes?.unauthorized || '/api/auth/401'
},
identityClaimFilter: baseConfig.identityClaimFilter,
organization: organization || AUTH0_ORGANIZATION
Expand Down
19 changes: 17 additions & 2 deletions src/handlers/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,13 @@ type ErrorHandlers = {
* export default handleAuth();
* ```
*
* This will create 4 handlers for the following urls:
* This will create 5 handlers for the following urls:
*
* - `/api/auth/login`: log the user in to your app by redirecting them to your identity provider.
* - `/api/auth/callback`: The page that your identity provider will redirect the user back to on login.
* - `/api/auth/logout`: log the user out of your app.
* - `/api/auth/me`: View the user profile JSON (used by the {@link UseUser} hook)
* - `/api/auth/me`: View the user profile JSON (used by the {@link UseUser} hook).
* - `/api/auth/unauthorized`: Returns a 401 for use by {@link WithMiddlewareAuthRequired} when protecting API routes.
*
* @category Server
*/
Expand Down Expand Up @@ -123,6 +124,19 @@ const defaultOnError: OnError = (_req, res, error) => {
res.status(error.status || 500).end();
};

/**
* This is a handler for use by {@link WithMiddlewareAuthRequired} when protecting an API route.
* Middleware can't return a response body, so an unauthorized request for an API route
* needs to rewrite to this handler.
* @ignore
*/
const unauthorized: NextApiHandler = (_req, res) => {
res.status(401).json({
error: 'not_authenticated',
description: 'The user does not have an active session or is not authenticated'
});
};

/**
* @ignore
*/
Expand All @@ -143,6 +157,7 @@ export default function handlerFactory({
logout: handleLogout,
callback: handleCallback,
me: (handlers as ApiHandlers).profile || handleProfile,
401: unauthorized,
...handlers
};
return async (req, res): Promise<void> => {
Expand Down
7 changes: 5 additions & 2 deletions src/helpers/with-middleware-auth-required.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,14 @@ export type WithMiddlewareAuthRequired = (middleware?: NextMiddleware) => NextMi
* @ignore
*/
export default function withMiddlewareAuthRequiredFactory(
{ login, callback }: { login: string; callback: string },
{ login, callback, unauthorized }: { login: string; callback: string; unauthorized: string },
getSessionCache: () => SessionCache<NextRequest, NextResponse>
): WithMiddlewareAuthRequired {
return function withMiddlewareAuthRequired(middleware?): NextMiddleware {
return async function wrappedMiddleware(...args) {
const [req] = args;
const { pathname, origin } = req.nextUrl;
const ignorePaths = [login, callback, '/_next', '/favicon.ico'];
const ignorePaths = [login, callback, unauthorized, '/_next', '/favicon.ico'];
if (ignorePaths.some((p) => pathname.startsWith(p))) {
return;
}
Expand All @@ -66,6 +66,9 @@ export default function withMiddlewareAuthRequiredFactory(
const authRes = NextResponse.next();
const session = await sessionCache.get(req, authRes);
if (!session?.user) {
if (pathname.startsWith('/api')) {
return NextResponse.rewrite(new URL(unauthorized, origin), { status: 401 });
}
return NextResponse.redirect(
new URL(`${login}?returnTo=${encodeURIComponent(req.nextUrl.toString())}`, origin)
);
Expand Down
3 changes: 2 additions & 1 deletion tests/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ describe('config params', () => {
routes: {
login: '/api/auth/login',
callback: '/api/auth/callback',
postLogoutRedirect: ''
postLogoutRedirect: '',
unauthorized: '/api/auth/401'
},
organization: undefined
});
Expand Down
6 changes: 6 additions & 0 deletions tests/handlers/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ describe('auth handler', () => {
global.handleAuth = initAuth0(withoutApi).handleAuth;
await expect(get(baseUrl, '/api/auth/__proto__')).rejects.toThrow('Not Found');
});

test('return unauthorized for /401 route', async () => {
const baseUrl = await setup(withoutApi);
global.handleAuth = initAuth0(withoutApi).handleAuth;
await expect(get(baseUrl, '/api/auth/401')).rejects.toThrow('Unauthorized');
});
});

describe('custom error handler', () => {
Expand Down
15 changes: 15 additions & 0 deletions tests/helpers/with-middleware-auth-required.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,21 @@ describe('with-middleware-auth-required', () => {
expect(redirect.searchParams.get('returnTo')).toEqual('http://example.com/');
});

test('require auth on anonymous requests to api routes', async () => {
const res = await setup({ url: 'http://example.com/api/foo' });
expect(res.status).toEqual(401);
expect(res.headers.get('x-middleware-rewrite')).toEqual('http://example.com/api/auth/401');
});

test('require auth on anonymous requests to api routes with custom 401', async () => {
const res = await setup({
url: 'http://example.com/api/foo',
config: { ...withoutApi, routes: { unauthorized: '/api/foo-401' } }
});
expect(res.status).toEqual(401);
expect(res.headers.get('x-middleware-rewrite')).toEqual('http://example.com/api/foo-401');
});

test('return to previous url', async () => {
const res = await setup({ url: 'http://example.com/foo/bar?baz=hello' });
const redirect = new URL(res.headers.get('location') as string);
Expand Down
8 changes: 5 additions & 3 deletions tests/session/session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { fromJson, fromTokenSet } from '../../src/session';
import { makeIdToken } from '../auth0-session/fixtures/cert';
import { Session } from '../../src';

const routes = { login: '', callback: '', postLogoutRedirect: '', unauthorized: '' };

describe('session', () => {
test('should construct a session with a user', async () => {
expect(new Session({ foo: 'bar' }).user).toEqual({ foo: 'bar' });
Expand All @@ -13,7 +15,7 @@ describe('session', () => {
expect(
fromTokenSet(new TokenSet({ id_token: await makeIdToken({ foo: 'bar', bax: 'qux' }) }), {
identityClaimFilter: ['baz'],
routes: { login: '', callback: '', postLogoutRedirect: '' }
routes
}).user
).toEqual({
aud: '__test_client_id__',
Expand All @@ -32,7 +34,7 @@ describe('session', () => {
expect(
fromTokenSet(new TokenSet({ id_token: await makeIdToken({ foo: 'bar' }) }), {
identityClaimFilter: ['baz'],
routes: { login: '', callback: '', postLogoutRedirect: '' }
routes
}).idToken
).toBeUndefined();
});
Expand All @@ -49,7 +51,7 @@ describe('session', () => {
cookie: { transient: false, httpOnly: false, sameSite: 'lax' }
},
identityClaimFilter: ['baz'],
routes: { login: '', callback: '', postLogoutRedirect: '' }
routes
}).idToken
).not.toBeUndefined();
});
Expand Down

0 comments on commit dfee400

Please sign in to comment.