Skip to content

Commit

Permalink
feat(express): Make requireAuth middleware flexible (#4159)
Browse files Browse the repository at this point in the history
  • Loading branch information
wobsoriano authored Sep 13, 2024
1 parent d0960c4 commit d895494
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 28 deletions.
5 changes: 5 additions & 0 deletions .changeset/popular-coats-turn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@clerk/express": minor
---

Make `requireAuth` middleware more flexible
18 changes: 13 additions & 5 deletions packages/express/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,12 @@ app.use(clerkMiddleware(handler, options));

### `requireAuth`

`requireAuth` is a middleware function that you can use to protect routes in your Express.js application. This function checks if the user is authenticated, and returns a 401 status code if they are not.
`requireAuth` is a middleware function that you can use to protect routes in your Express.js application. This function checks if the user is authenticated, and passes an `UnauthorizedError` to the next middleware if they are not.

`clerkMiddleware()` is required to be set in the middleware chain before this util is used.

```js
import { clerkMiddleware, requireAuth } from '@clerk/express';
import { clerkMiddleware, requireAuth, UnauthorizedError } from '@clerk/express';
import express from 'express';

const app = express();
Expand All @@ -104,6 +104,14 @@ app.get('/protected', requireAuth, (req, res) => {
res.send('This is a protected route');
});

app.use((err, req, res, next) => {
if (err instanceof UnauthorizedError) {
res.status(401).send('Unauthorized');
} else {
next(err);
}
});

// Start the server and listen on the specified port
app.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`);
Expand All @@ -115,7 +123,7 @@ app.listen(port, () => {
The `getAuth()` helper retrieves authentication state from the request object. See the [Next.js reference documentation](https://clerk.com/docs/references/nextjs/get-auth) for more information on how to use it.

```js
import { clerkMiddleware, getAuth } from '@clerk/express';
import { clerkMiddleware, getAuth, ForbiddenError } from '@clerk/express';
import express from 'express';

const app = express();
Expand All @@ -130,8 +138,8 @@ hasPermission = (request, response, next) => {

// Handle if the user is not authorized
if (!auth.has({ permission: 'org:admin:testpermission' })) {
response.status(403).send('Forbidden');
return;
// Catch this inside an error-handling middleware
return next(new ForbiddenError());
}

return next();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

exports[`module exports should not change unless explicitly set 1`] = `
[
"ForbiddenError",
"UnauthorizedError",
"clerkClient",
"clerkMiddleware",
"createClerkClient",
Expand Down
46 changes: 35 additions & 11 deletions packages/express/src/__tests__/requireAuth.test.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,55 @@
import { requireAuth } from '../requireAuth';
import type { NextFunction, Request as ExpressRequest, Response as ExpressResponse } from 'express';

import { requireAuth, UnauthorizedError } from '../requireAuth';
import { mockRequest, mockRequestWithAuth, mockResponse } from './helpers';

// This middleware is used to handle the UnauthorizedError thrown by requireAuth
// See https://expressjs.com/en/guide/error-handling.html for handling errors in Express
const errorHandler = (err: Error, _req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
if (err instanceof UnauthorizedError) {
return res.status(401).send('Unauthorized');
}

return next(err);
};

describe('requireAuth', () => {
it('throws error if clerkMiddleware is not executed before this middleware', async () => {
expect(() => requireAuth(mockRequest(), mockResponse(), () => undefined)).toThrow(
/The "clerkMiddleware" should be registered before using "requireAuth"/,
);
});

it('make application require auth - returns 401 Unauthorized for signed-out', async () => {
it('passes UnauthorizedError to next for unauthenticated requests', () => {
const request = mockRequestWithAuth();
const response = mockResponse();
const nextFn = jest.fn();
const next = jest.fn();

requireAuth(request, response, next);

requireAuth(mockRequestWithAuth(), response, nextFn);
// Simulate how Express would call the error middleware
const error = next.mock.calls[0][0];
errorHandler(error, request, response, next);

expect(response.status).toHaveBeenCalledWith(401);
expect(nextFn).not.toHaveBeenCalled();
expect(response.send).toHaveBeenCalledWith('Unauthorized');
});

it('make application require auth - proceed with next middlewares for signed-in', async () => {
const response = mockResponse();
const nextFn = jest.fn();
it('allows access for authenticated requests', async () => {
const request = mockRequestWithAuth({ userId: 'user_1234' });
const response = mockResponse();
const next = jest.fn();

requireAuth(request, response, next);

// Simulate a protected route
const protectedRoute = (_req: ExpressRequest, res: ExpressResponse) => {
res.status(200).send('Welcome, user_1234');
};

requireAuth(request, response, nextFn);
protectedRoute(request, response);

expect(response.status).not.toHaveBeenCalled();
expect(nextFn).toHaveBeenCalled();
expect(response.status).toHaveBeenCalledWith(200);
expect(response.send).toHaveBeenCalledWith('Welcome, user_1234');
});
});
2 changes: 1 addition & 1 deletion packages/express/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ export { clerkClient } from './clerkClient';
export type { ClerkMiddleware, ExpressRequestWithAuth } from './types';
export { clerkMiddleware } from './clerkMiddleware';
export { getAuth } from './getAuth';
export { requireAuth } from './requireAuth';
export { requireAuth, ForbiddenError, UnauthorizedError } from './requireAuth';
94 changes: 83 additions & 11 deletions packages/express/src/requireAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,106 @@ import { getAuth } from './getAuth';
import { requestHasAuthObject } from './utils';

/**
* Middleware to require auth requests for user authenticated or authorized requests.
* An HTTP 401 status code is returned for unauthenticated requests.
* This error is typically thrown by the `requireAuth` middleware when
* a request is made to a protected route without proper authentication.
*
* @class
* @extends Error
*
* @example
* // This error is usually handled in an Express error handling middleware
* app.use((err, req, res, next) => {
* if (err instanceof UnauthorizedError) {
* res.status(401).send('Unauthorized');
* } else {
* next(err);
* }
* });
*/
export class UnauthorizedError extends Error {
constructor() {
super('Unauthorized');
this.name = 'UnauthorizedError';
}
}

/**
* This error is typically used when a user is authenticated but lacks the necessary permissions
* to access a resource or perform an action.
*
* @class
* @extends Error
*
* @example
* // This error can be used in custom authorization middleware
* const checkPermission = (req, res, next) => {
* const auth = getAuth(req)
* if (!auth.has({ permission: 'permission' })) {
* return next(new ForbiddenError());
* }
* next();
* };
*
* @example
* // This error is usually handled in an Express error handling middleware
* app.use((err, req, res, next) => {
* if (err instanceof ForbiddenError) {
* res.status(403).send('Forbidden');
* } else {
* next(err);
* }
* });
*/
export class ForbiddenError extends Error {
constructor() {
super('Forbidden');
this.name = 'ForbiddenError';
}
}

/**
* Middleware to require authentication for user requests.
* Passes an UnauthorizedError to the next middleware for unauthenticated requests,
* which should be handled by an error middleware.
*
* @example
* // Basic usage
* import { requireAuth, UnauthorizedError } from '@clerk/express'
*
* router.get('/path', requireAuth, getHandler)
* //or
* router.use(requireAuth)
*
* router.use((err, req, res, next) => {
* if (err instanceof UnauthorizedError) {
* res.status(401).send('Unauthorized')
* } else {
* next(err)
* }
* })
*
* @example
* hasPermission = (request, response, next) => {
* const auth = getAuth(request);
* // Combining with permission check
* import { requireAuth, ForbiddenError } from '@clerk/express'
*
* const hasPermission = (req, res, next) => {
* const auth = getAuth(req)
* if (!auth.has({ permission: 'permission' })) {
* response.status(403).send('Forbidden');
* return;
* return next(new ForbiddenError())
* }
* return next();
* return next()
* }
* router.get('/path', requireAuth, hasPermission, getHandler)
*
* @throws {Error} `clerkMiddleware` is required to be set in the middleware chain before this util is used.
* @throws {Error} If `clerkMiddleware` is not set in the middleware chain before this util is used.
*/
export const requireAuth: RequestHandler = (request, response, next) => {
export const requireAuth: RequestHandler = (request, _response, next) => {
if (!requestHasAuthObject(request)) {
throw new Error(middlewareRequired('requireAuth'));
}

if (!getAuth(request).userId) {
response.status(401).send('Unauthorized');
return;
return next(new UnauthorizedError());
}

return next();
Expand Down

0 comments on commit d895494

Please sign in to comment.