UNPKG

remix-utils

Version:

This package contains simple utility functions to use with [React Router](https://reactrouter.com/).

233 lines (232 loc) 8.35 kB
/** * > 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 { type Cookie, type unstable_MiddlewareFunction, unstable_RouterContextProvider } from "react-router"; import type { unstable_MiddlewareGetter } from "./utils.js"; export declare function unstable_createJWKAuthMiddleware({ jwksUri, realm, alg, invalidUserMessage, ...options }: unstable_createBearerAuthMiddleware.Options): unstable_createBearerAuthMiddleware.ReturnType; export declare namespace unstable_createBearerAuthMiddleware { type Args = { request: Request; context: unstable_RouterContextProvider; }; type MessageFunction = (args: Args) => string | object | Promise<string | object>; interface BaseOptions { /** * The URL of the JWKS endpoint. * @example * "https://auth.example.com/.well-known/jwks.json" */ jwksUri: ConstructorParameters<typeof URL>[0]; /** * The algorithm to use for verifying the JWT signature. * @default "ES256" */ alg?: JWK.Algoritm; /** * The message to return when the user is invalid. * * If a function is provided, it will be called with the request and context * as arguments. * * If the function returns a string, it will be used as the message. * * If the function returns an object, it will be serialized as JSON and used * as the response body. * * @default "Unauthorized" * @example * "Invalid user" * (args) => `Invalid user: ${args.request.headers.get("X-User")}` * async (args) => { * let user = await getUser(args.context); * return `Invalid user: ${user}`; * } * { error: "Invalid user" } * (args) => ({ * error: `Invalid user: ${args.request.headers.get("X-User")}` * }) * async (args) => { * let user = await getUser(args.context); * return { error: `Invalid user: ${user}` }; * } */ invalidUserMessage?: string | object | MessageFunction; /** * The domain name of the realm, as part of the returned WWW-Authenticate * challenge header. * * @default "Secure Area" */ realm?: string; verifyOptions?: JWT.VerifyOptions; } interface HeaderOptions extends BaseOptions { /** * The name of the header to use for the bearer token. * @default "Authorization" */ headerName?: string; } interface CookieOptions extends BaseOptions { /** * The cookie to use for the bearer token. * * If provided the cookie will be parsed to try to extract the JWT. */ cookie: Cookie; } type Options = HeaderOptions | CookieOptions; type ReturnType = [ unstable_MiddlewareFunction<Response>, unstable_MiddlewareGetter<JWT> ]; }