Skip to content

Commit 82a7d21

Browse files
authored
Add JWK Auth middleware (#479)
1 parent 30a7ad0 commit 82a7d21

File tree

6 files changed

+580
-53
lines changed

6 files changed

+580
-53
lines changed

README.md

+170-17
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Additional optional dependencies may be needed, all optional dependencies are:
1212

1313
- `react-router`
1414
- `@edgefirst-dev/batcher`
15+
- `@edgefirst-dev/jwt`
1516
- `@edgefirst-dev/server-timing`
1617
- `@oslojs/crypto`
1718
- `@oslojs/encoding`
@@ -25,7 +26,7 @@ The utils that require an extra optional dependency mention it in their document
2526
If you want to install them all run:
2627

2728
```sh
28-
npm add @edgefirst-dev/batcher @edgefirst-dev/server-timing @oslojs/crypto @oslojs/encoding is-ip intl-parse-accept-language zod
29+
npm add @edgefirst-dev/batcher @edgefirst-dev/jwt @edgefirst-dev/server-timing @oslojs/crypto @oslojs/encoding is-ip intl-parse-accept-language zod
2930
```
3031

3132
React and React Router packages should be already installed in your project.
@@ -2089,15 +2090,15 @@ let [sessionMiddleware, getSession] =
20892090
Then you can use the `sessionMiddleware` in your `app/root.tsx` function.
20902091

20912092
```ts
2092-
import { sessionMiddleware } from "~/session.server";
2093+
import { sessionMiddleware } from "~/middleware/session.server";
20932094

20942095
export const unstable_middleware = [sessionMiddleware];
20952096
```
20962097

20972098
And you can use the `getSession` function in your loaders to get the session object.
20982099

20992100
```ts
2100-
import { getSession } from "~/session.server";
2101+
import { getSession } from "~/middleware/session.server";
21012102

21022103
export async function loader({ context }: Route.LoaderArgs) {
21032104
let session = await getSession(context);
@@ -2153,7 +2154,7 @@ export const [loggerMiddleware] = unstable_createLoggerMiddleware();
21532154
To use it, you need to add it to the `unstable_middleware` array in your `app/root.tsx` file.
21542155

21552156
```ts
2156-
import { loggerMiddleware } from "~/logger.server";
2157+
import { loggerMiddleware } from "~/middleware/logger.server";
21572158
export const unstable_middleware = [loggerMiddleware];
21582159
```
21592160

@@ -2190,15 +2191,15 @@ export const [serverTimingMiddleware, getTimingCollector] =
21902191
To use it, you need to add it to the `unstable_middleware` array in your `app/root.tsx` file.
21912192

21922193
```ts
2193-
import { serverTimingMiddleware } from "~/server-timing.server";
2194+
import { serverTimingMiddleware } from "~/middleware/server-timing.server";
21942195

21952196
export const unstable_middleware = [serverTimingMiddleware];
21962197
```
21972198

21982199
And you can use the `getTimingCollector` function in your loaders and actions to add timings to the response.
21992200

22002201
```ts
2201-
import { getTimingCollector } from "~/server-timing.server";
2202+
import { getTimingCollector } from "~/middleware/server-timing.server";
22022203

22032204
export async function loader({ request }: LoaderFunctionArgs) {
22042205
let collector = getTimingCollector();
@@ -2229,14 +2230,14 @@ export const [singletonMiddleware, getSingleton] =
22292230
To use it, you need to add it to the `unstable_middleware` array in the route where you want to use it.
22302231

22312232
```ts
2232-
import { singletonMiddleware } from "~/singleton.server";
2233+
import { singletonMiddleware } from "~/middleware/singleton.server";
22332234
export const unstable_middleware = [singletonMiddleware];
22342235
```
22352236

22362237
And you can use the `getSingleton` function in your loaders to get the singleton object.
22372238

22382239
```ts
2239-
import { getSingleton } from "~/singleton.server";
2240+
import { getSingleton } from "~/middleware/singleton.server";
22402241

22412242
export async function loader({ request }: LoaderFunctionArgs) {
22422243
let singleton = getSingleton();
@@ -2269,7 +2270,7 @@ And use it in a route like this.
22692270
import {
22702271
singletonMiddleware,
22712272
anotherSingletonMiddleware,
2272-
} from "~/singleton.server";
2273+
} from "~/middleware/singleton.server";
22732274

22742275
export const unstable_middleware = [
22752276
singletonMiddleware,
@@ -2296,14 +2297,14 @@ export const [batcherMiddleware, getBatcher] =
22962297
To use it, you need to add it to the `unstable_middleware` array in the route where you want to use it.
22972298

22982299
```ts
2299-
import { batcherMiddleware } from "~/batcher.server";
2300+
import { batcherMiddleware } from "~/middleware/batcher.server";
23002301
export const unstable_middleware = [batcherMiddleware];
23012302
```
23022303

23032304
And you can use the `getBatcher` function in your loaders to get the batcher object.
23042305

23052306
```ts
2306-
import { getBatcher } from "~/batcher.server";
2307+
import { getBatcher } from "~/middleware/batcher.server";
23072308

23082309
export async function loader({ request }: LoaderFunctionArgs) {
23092310
let batcher = getBatcher();
@@ -2328,15 +2329,15 @@ export const [contextStorageMiddleware, getContext, getRequest] =
23282329
To use it, you need to add it to the `unstable_middleware` array in your `app/root.tsx` file.
23292330

23302331
```ts
2331-
import { contextStorageMiddleware } from "~/context-storage.server";
2332+
import { contextStorageMiddleware } from "~/middleware/context-storage.server";
23322333

23332334
export const unstable_middleware = [contextStorageMiddleware];
23342335
```
23352336

23362337
And you can use the `getContext` and `getRequest` functions in your function to get the context and request objects.
23372338

23382339
```ts
2339-
import { getContext, getRequest } from "~/context-storage.server";
2340+
import { getContext, getRequest } from "~/middleware/context-storage.server";
23402341

23412342
export async function doSomething() {
23422343
let context = getContext();
@@ -2361,15 +2362,15 @@ export const [requestIDMiddleware, getRequestID] =
23612362
To use it, you need to add it to the `unstable_middleware` array in your `app/root.tsx` file.
23622363

23632364
```ts
2364-
import { requestIDMiddleware } from "~/request-id.server";
2365+
import { requestIDMiddleware } from "~/middleware/request-id.server";
23652366

23662367
export const unstable_middleware = [requestIDMiddleware];
23672368
```
23682369

23692370
And you can use the `getRequestID` function in your loaders, actions, and other middleware to get the request ID.
23702371

23712372
```ts
2372-
import { getRequestID } from "~/request-id.server";
2373+
import { getRequestID } from "~/middleware/request-id.server";
23732374

23742375
export async function loader({ request }: LoaderFunctionArgs) {
23752376
let requestID = getRequestID();
@@ -2410,7 +2411,7 @@ export const [requestIDMiddleware, getRequestID] =
24102411
24112412
The Basic Auth middleware let's you add a basic authentication to your routes, this can be useful to protect routes that need to be private.
24122413

2413-
> [!WARN]
2414+
> [!WARNING]
24142415
> Basic Auth is not secure by itself, it should be used with HTTPS to ensure the username and password are encrypted. Do not use it to protect sensitive data, use a more secure method instead.
24152416
24162417
```ts
@@ -2424,7 +2425,7 @@ export const [basicAuthMiddleware] = unstable_createBasicAuthMiddleware({
24242425
To use it, you need to add it to the `unstable_middleware` array in the route where you want to use it.
24252426

24262427
```ts
2427-
import { basicAuthMiddleware } from "~/basic-auth.server";
2428+
import { basicAuthMiddleware } from "~/middleware/basic-auth.server";
24282429
export const unstable_middleware = [basicAuthMiddleware];
24292430
```
24302431

@@ -2522,6 +2523,158 @@ WWW-Authenticate: Basic realm="My Realm"
25222523
{"message":"Invalid username or password"}
25232524
```
25242525

2526+
#### JWK Auth Middleware
2527+
2528+
> [!NOTE]
2529+
> This depends on `@edgefirst-dev/jwt`.
2530+
2531+
The JWK Auth middleware let's you add a JSON Web Key authentication to your routes, this can be useful to protect routes that need to be private and will be accessed by other services.
2532+
2533+
> [!WARNING]
2534+
> JWK Auth is more secure than Basic Auth, but it should be used with HTTPS to ensure the token is encrypted.
2535+
2536+
```ts
2537+
import { unstable_createJWKAuthMiddleware } from "remix-utils/middleware/jwk-auth";
2538+
2539+
export const [jwkAuthMiddleware, getJWTPayload] =
2540+
unstable_createJWKAuthMiddleware({
2541+
jwksUri: "https://auth.example.com/.well-known/jwks.json",
2542+
});
2543+
```
2544+
2545+
The `jwksUri` option let's you set the URL to the JWKS endpoint, this is the URL where the public keys are stored.
2546+
2547+
To use the middleware, you need to add it to the `unstable_middleware` array in the route where you want to use it.
2548+
2549+
```ts
2550+
import { jwkAuthMiddleware } from "~/middleware/jwk-auth";
2551+
export const unstable_middleware = [jwkAuthMiddleware];
2552+
```
2553+
2554+
Now, when you access the route it will check the JWT token in the `Authorization` header.
2555+
2556+
In case of an invalid token the middleware will return a `401` status code with a `WWW-Authenticate` header.
2557+
2558+
```http
2559+
HTTP/1.1 401 Unauthorized
2560+
WWW-Authenticate: Bearer realm="Secure Area"
2561+
2562+
Unauthorized
2563+
```
2564+
2565+
The `realm` option let's you set the realm for the authentication, this is the name of the protected area.
2566+
2567+
```ts
2568+
import { unstable_createJWKAuthMiddleware } from "remix-utils/middleware/jwk-auth";
2569+
2570+
export const [jwkAuthMiddleware] = unstable_createJWKAuthMiddleware({
2571+
realm: "My Realm",
2572+
jwksUri: "https://auth.example.com/.well-known/jwks.json",
2573+
});
2574+
```
2575+
2576+
If you want to customize the message sent when the token is invalid you can use the `invalidTokenMessage` option.
2577+
2578+
```ts
2579+
import { unstable_createJWKAuthMiddleware } from "remix-utils/middleware/jwk-auth";
2580+
2581+
export const [jwkAuthMiddleware] = unstable_createJWKAuthMiddleware({
2582+
invalidTokenMessage: "Invalid token",
2583+
jwksUri: "https://auth.example.com/.well-known/jwks.json",
2584+
});
2585+
```
2586+
2587+
And this will be the response when the token is invalid.
2588+
2589+
```http
2590+
HTTP/1.1 401 Unauthorized
2591+
WWW-Authenticate: Bearer realm="Secure Area"
2592+
2593+
Invalid token
2594+
```
2595+
2596+
You can also customize the `invalidTokenMessage` by passing a function which will receive the Request and context objects.
2597+
2598+
```ts
2599+
import { unstable_createJWKAuthMiddleware } from "remix-utils/middleware/jwk-auth";
2600+
2601+
export const [jwkAuthMiddleware] = unstable_createJWKAuthMiddleware({
2602+
invalidTokenMessage({ request, context }) {
2603+
// do something with request or context here
2604+
return { message: `Invalid token` };
2605+
},
2606+
jwksUri: "https://auth.example.com/.well-known/jwks.json",
2607+
});
2608+
```
2609+
2610+
In both cases, with a hard-coded value or a function, the invalid message can be a string or an object, if it's an object it will be converted to JSON.
2611+
2612+
```http
2613+
HTTP/1.1 401 Unauthorized
2614+
WWW-Authenticate: Bearer realm="Secure Area"
2615+
2616+
{"message":"Invalid token"}
2617+
```
2618+
2619+
If you want to get the JWT payload in your loaders, actions, or other middleware you can use the `getJWTPayload` function.
2620+
2621+
```ts
2622+
import { getJWTPayload } from "~/middleware/jwk-auth.server";
2623+
2624+
export async function loader({ request }: LoaderFunctionArgs) {
2625+
let payload = getJWTPayload();
2626+
// ...
2627+
}
2628+
```
2629+
2630+
And you can use the payload to get the subject, scope, issuer, audience, or any other information stored in the token.
2631+
2632+
##### With a Custom Header
2633+
2634+
If your app receives the JWT in a custom header instead of the `Authorization` header you can tell the middleware to look for the token in that header.
2635+
2636+
```ts
2637+
import { unstable_createJWKAuthMiddleware } from "remix-utils/middleware/jwk-auth";
2638+
2639+
export const [jwkAuthMiddleware, getJWTPayload] =
2640+
unstable_createJWKAuthMiddleware({ header: "X-API-Key" });
2641+
```
2642+
2643+
Now use the middleware as usual, but now instead of looking for the token in the `Authorization` header it will look for it in the `X-API-Key` header.
2644+
2645+
```ts
2646+
import { jwkAuthMiddleware } from "~/middleware/jwk-auth";
2647+
2648+
export const unstable_middleware = [jwkAuthMiddleware];
2649+
```
2650+
2651+
##### With a Cookie
2652+
2653+
If you save a JWT in a cookie using React Router's Cookie API, you can tell the middleware to look for the token in the cookie instead of the `Authorization` header.
2654+
2655+
```ts
2656+
import { unstable_createJWKAuthMiddleware } from "remix-utils/middleware/jwk-auth";
2657+
import { createCookie } from "react-router";
2658+
2659+
export const cookie = createCookie("jwt", {
2660+
path: "/",
2661+
sameSite: "lax",
2662+
httpOnly: true,
2663+
secure: process.env.NODE_ENV === "true",
2664+
});
2665+
2666+
export const [jwkAuthMiddleware, getJWTPayload] =
2667+
unstable_createJWKAuthMiddleware({ cookie });
2668+
```
2669+
2670+
Then use the middleware as usual, but now instead of looking for the token in the `Authorization` header it will look for it in the cookie.
2671+
2672+
```ts
2673+
import { jwkAuthMiddleware } from "~/middleware/jwk-auth";
2674+
2675+
export const unstable_middleware = [jwkAuthMiddleware];
2676+
```
2677+
25252678
## Author
25262679

25272680
- [Sergio Xalambrí](https://sergiodxa.com)

bun.lockb

2.21 KB
Binary file not shown.

package.json

+12-8
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@
66
"node": ">=20.0.0"
77
},
88
"type": "module",
9-
"funding": [
10-
"https://github.com/sponsors/sergiodxa"
11-
],
9+
"funding": ["https://github.com/sponsors/sergiodxa"],
1210
"exports": {
1311
"./package.json": "./package.json",
1412
"./middleware/basic-auth": {
@@ -23,6 +21,10 @@
2321
"types": "./build/server/middleware/context-storage.d.ts",
2422
"default": "./build/server/middleware/context-storage.js"
2523
},
24+
"./middleware/jwk-auth": {
25+
"types": "./build/server/middleware/jwk-auth.d.ts",
26+
"default": "./build/server/middleware/jwk-auth.js"
27+
},
2628
"./middleware/logger": {
2729
"types": "./build/server/middleware/logger.d.ts",
2830
"default": "./build/server/middleware/logger.js"
@@ -234,6 +236,7 @@
234236
],
235237
"peerDependencies": {
236238
"@edgefirst-dev/batcher": "^1.0.0",
239+
"@edgefirst-dev/jwt": "^1.2.0",
237240
"@edgefirst-dev/server-timing": "^0.0.1",
238241
"@oslojs/crypto": "^1.0.1",
239242
"@oslojs/encoding": "^1.1.0",
@@ -247,6 +250,9 @@
247250
"@edgefirst-dev/batcher": {
248251
"optional": true
249252
},
253+
"@edgefirst-dev/jwt": {
254+
"optional": true
255+
},
250256
"@edgefirst-dev/server-timing": {
251257
"optional": true
252258
},
@@ -276,8 +282,10 @@
276282
"@arethetypeswrong/cli": "^0.17.4",
277283
"@biomejs/biome": "^1.7.2",
278284
"@edgefirst-dev/batcher": "^1.0.1",
285+
"@edgefirst-dev/jwt": "^1.2.0",
279286
"@edgefirst-dev/server-timing": "^0.0.1",
280287
"@happy-dom/global-registrator": "^17.4.3",
288+
"@mjackson/file-storage": "^0.6.1",
281289
"@oslojs/crypto": "^1.0.1",
282290
"@oslojs/encoding": "^1.1.0",
283291
"@testing-library/jest-dom": "^6.1.3",
@@ -300,9 +308,5 @@
300308
"dependencies": {
301309
"type-fest": "^4.37.0"
302310
},
303-
"files": [
304-
"build",
305-
"package.json",
306-
"README.md"
307-
]
311+
"files": ["build", "package.json", "README.md"]
308312
}

0 commit comments

Comments
 (0)