remix-utils
Version:
This package contains simple utility functions to use with [React Router](https://reactrouter.com/).
207 lines • 7.74 kB
JavaScript
/**
* > This depends on `@edgefirst-dev/jwt`.
*
* 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.
*
* > **Warning**: JWK Auth is more secure than Basic Auth, but it should be used with HTTPS to ensure the token is encrypted.
*
* ```ts
* import { unstable_createJWKAuthMiddleware } from "remix-utils/middleware/jwk-auth";
*
* export const [jwkAuthMiddleware, getJWTPayload] =
* unstable_createJWKAuthMiddleware({
* jwksUri: "https://auth.example.com/.well-known/jwks.json",
* });
* ```
*
* The `jwksUri` option let's you set the URL to the JWKS endpoint, this is the URL where the public keys are stored.
*
* To use the middleware, you need to add it to the `unstable_middleware` array in the route where you want to use it.
*
* ```ts
* import { jwkAuthMiddleware } from "~/middleware/jwk-auth";
* export const unstable_middleware = [jwkAuthMiddleware];
* ```
*
* Now, when you access the route it will check the JWT token in the `Authorization` header.
*
* In case of an invalid token the middleware will return a `401` status code with a `WWW-Authenticate` header.
*
* ```http
* HTTP/1.1 401 Unauthorized
* WWW-Authenticate: Bearer realm="Secure Area"
*
* Unauthorized
* ```
*
* The `realm` option let's you set the realm for the authentication, this is the name of the protected area.
*
* ```ts
* import { unstable_createJWKAuthMiddleware } from "remix-utils/middleware/jwk-auth";
*
* export const [jwkAuthMiddleware] = unstable_createJWKAuthMiddleware({
* realm: "My Realm",
* jwksUri: "https://auth.example.com/.well-known/jwks.json",
* });
* ```
*
* If you want to customize the message sent when the token is invalid you can use the `invalidTokenMessage` option.
*
* ```ts
* import { unstable_createJWKAuthMiddleware } from "remix-utils/middleware/jwk-auth";
*
* export const [jwkAuthMiddleware] = unstable_createJWKAuthMiddleware({
* invalidTokenMessage: "Invalid token",
* jwksUri: "https://auth.example.com/.well-known/jwks.json",
* });
* ```
*
* And this will be the response when the token is invalid.
*
* ```http
* HTTP/1.1 401 Unauthorized
* WWW-Authenticate: Bearer realm="Secure Area"
*
* Invalid token
* ```
*
* You can also customize the `invalidTokenMessage` by passing a function which will receive the Request and context objects.
*
* ```ts
* import { unstable_createJWKAuthMiddleware } from "remix-utils/middleware/jwk-auth";
*
* export const [jwkAuthMiddleware] = unstable_createJWKAuthMiddleware({
* invalidTokenMessage({ request, context }) {
* // do something with request or context here
* return { message: `Invalid token` };
* },
* jwksUri: "https://auth.example.com/.well-known/jwks.json",
* });
* ```
*
* 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.
*
* ```http
* HTTP/1.1 401 Unauthorized
* WWW-Authenticate: Bearer realm="Secure Area"
*
* {"message":"Invalid token"}
* ```
*
* If you want to get the JWT payload in your loaders, actions, or other middleware you can use the `getJWTPayload` function.
*
* ```ts
* import { getJWTPayload } from "~/middleware/jwk-auth.server";
*
* export async function loader({ request }: LoaderFunctionArgs) {
* let payload = getJWTPayload();
* // ...
* }
* ```
*
* And you can use the payload to get the subject, scope, issuer, audience, or any other information stored in the token.
*
* ## With a Custom Header
*
* 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.
*
* ```ts
* import { unstable_createJWKAuthMiddleware } from "remix-utils/middleware/jwk-auth";
*
* export const [jwkAuthMiddleware, getJWTPayload] =
* unstable_createJWKAuthMiddleware({ header: "X-API-Key" });
* ```
*
* 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.
*
* ```ts
* import { jwkAuthMiddleware } from "~/middleware/jwk-auth";
*
* export const unstable_middleware = [jwkAuthMiddleware];
* ```
*
* ## With a Cookie
*
* 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.
*
* ```ts
* import { unstable_createJWKAuthMiddleware } from "remix-utils/middleware/jwk-auth";
* import { createCookie } from "react-router";
*
* export const cookie = createCookie("jwt", {
* path: "/",
* sameSite: "lax",
* httpOnly: true,
* secure: process.env.NODE_ENV === "true",
* });
*
* export const [jwkAuthMiddleware, getJWTPayload] =
* unstable_createJWKAuthMiddleware({ cookie });
* ```
*
* 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.
*
* ```ts
* import { jwkAuthMiddleware } from "~/middleware/jwk-auth";
*
* export const unstable_middleware = [jwkAuthMiddleware];
* ```
* @author [Sergio Xalambrí](https://sergiodxa.com)
* @module Middleware/JWK Auth
*/
import { JWK, JWT } from "@edgefirst-dev/jwt";
import { unstable_RouterContextProvider, unstable_createContext, } from "react-router";
export function unstable_createJWKAuthMiddleware({ jwksUri, realm = "Secure Area", alg = JWK.Algoritm.ES256, invalidUserMessage = "Unauthorized", ...options }) {
const tokenContext = unstable_createContext();
const remote = JWK.importRemote(new URL(jwksUri), { alg });
const cookieInOptions = "cookie" in options;
return [
async function jwkAuthMiddleware({ request, context }, next) {
let token = null;
if (cookieInOptions) {
token = await options.cookie.parse(request.headers.get("Cookie"));
}
if (!cookieInOptions) {
let authorization = request.headers.get(options.headerName ?? "Authorization");
if (!authorization)
throw await unauthorized(request, context);
let [type, ...rest] = authorization.split(" ");
if (type?.toLowerCase() !== "bearer") {
throw await unauthorized(request, context);
}
token = rest[0] ?? null;
}
if (!token)
throw await unauthorized(request, context);
try {
context.set(tokenContext, await JWT.verify(token, await remote, options.verifyOptions));
}
catch {
throw await unauthorized(request, context);
}
return await next();
},
function getJWTPayload(context) {
return context.get(tokenContext);
},
];
async function getInvalidUserMessage(args) {
if (invalidUserMessage === undefined)
return "Unauthorized";
if (typeof invalidUserMessage === "string")
return invalidUserMessage;
if (typeof invalidUserMessage === "function") {
return await invalidUserMessage(args);
}
return invalidUserMessage;
}
async function unauthorized(request, context) {
let message = await getInvalidUserMessage({ request, context });
return Response.json(message, {
status: 401,
statusText: "Unauthorized",
headers: { "WWW-Authenticate": `Bearer realm="${realm}"` },
});
}
}
//# sourceMappingURL=jwk-auth.js.map