Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WithMiddlewareAuthRequired should return 401 for /api routes #909

Merged
merged 1 commit into from
Nov 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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