Advertisement
AdzeB

logto-fastify.ts

Mar 9th, 2025
171
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
  2. import { createRemoteJWKSet, jwtVerify } from "jose";
  3. import fp from "fastify-plugin";
  4. import {
  5.   createErrorResponse,
  6.   InternalMessageCode,
  7.   MessageCode,
  8. } from "../types/api-resource";
  9.  
  10. interface LogtoFastifyOptions {
  11.   jwksUrl: string;
  12.   issuer: string;
  13.   audience: string;
  14. }
  15.  
  16. export interface LogtoUser {
  17.   sub: string;
  18.   scope: string;
  19.   user_id: string | null;
  20.   is_staff: boolean;
  21.   role: string;
  22.   role_id: string;
  23.   site_id: string;
  24.  
  25.   [key: string]: any;
  26. }
  27.  
  28. declare module "fastify" {
  29.   interface FastifyRequest {
  30.     logtoUser?: LogtoUser;
  31.     user?: {
  32.       id: string | null;
  33.       logtoId: string;
  34.       email: string | undefined;
  35.       isStaff: boolean;
  36.       claims: LogtoUser;
  37.       role: string;
  38.       role_id: string;
  39.       site_id: string | undefined;
  40.     };
  41.   }
  42. }
  43.  
  44. const extractBearerTokenFromHeaders = (authorization?: string) => {
  45.   if (!authorization) {
  46.     throw new Error("Authorization header is missing");
  47.   }
  48.  
  49.   if (!authorization.startsWith("Bearer ")) {
  50.     throw new Error("Authorization header is not in the Bearer scheme");
  51.   }
  52.  
  53.   return authorization.slice(7); // The length of 'Bearer ' is 7
  54. };
  55.  
  56. export const logtoFastifyPlugin = fp(
  57.   async (fastify: FastifyInstance, options: LogtoFastifyOptions) => {
  58.     const { jwksUrl, issuer, audience } = options;
  59.  
  60.     // Generate a JWKS using jwks_uri obtained from the Logto server
  61.     const jwks = createRemoteJWKSet(new URL(jwksUrl));
  62.  
  63.     // Decorator to verify access token
  64.     fastify.decorate(
  65.       "verifyLogtoToken",
  66.       async (request: FastifyRequest, reply: FastifyReply) => {
  67.         try {
  68.           const token = extractBearerTokenFromHeaders(
  69.             request.headers.authorization
  70.           );
  71.  
  72.           // Log token details for debugging (remove in production)
  73.           console.log(
  74.             `Verifying token with issuer: ${issuer}, audience: ${audience}`
  75.           );
  76.  
  77.           const { payload } = await jwtVerify(token, jwks, {
  78.             issuer,
  79.             audience,
  80.             clockTolerance: "10 hours",
  81.           });
  82.  
  83.           // Add user info to request
  84.           request.logtoUser = payload as LogtoUser;
  85.  
  86.           return true;
  87.         } catch (error) {
  88.           // Provide more detailed error information
  89.           console.error("Token verification failed:", error);
  90.  
  91.           let errorMessage = "Invalid token";
  92.           if (error instanceof Error) {
  93.             // More specific error messages based on error type
  94.             if (error.message.includes("expired")) {
  95.               errorMessage = "Token has expired";
  96.             } else if (error.message.includes("audience")) {
  97.               errorMessage = "Invalid token audience";
  98.             } else if (error.message.includes("issuer")) {
  99.               errorMessage = "Invalid token issuer";
  100.             } else {
  101.               errorMessage = error.message;
  102.             }
  103.           }
  104.           return reply
  105.             .status(401)
  106.             .send(
  107.               createErrorResponse(
  108.                 error instanceof Error ? error.message : "Invalid token",
  109.                 MessageCode.UNAUTHORIZED,
  110.                 401,
  111.                 InternalMessageCode.UNAUTHORIZED_REQUEST
  112.               )
  113.             );
  114.         }
  115.       }
  116.     );
  117.  
  118.     // Helper to check if user has required scopes
  119.     fastify.decorate("requireScopes", (scopes: string[]) => {
  120.       return async (request: FastifyRequest, reply: FastifyReply) => {
  121.         if (!request.logtoUser) {
  122.           reply
  123.             .status(401)
  124.             .send(
  125.               createErrorResponse(
  126.                 "Unauthorized",
  127.                 MessageCode.UNAUTHORIZED,
  128.                 401,
  129.                 InternalMessageCode.UNAUTHORIZED_REQUEST
  130.               )
  131.             );
  132.           return false;
  133.         }
  134.  
  135.         const userScopes = request.logtoUser.scope?.split(" ") || [];
  136.  
  137.         const hasRequiredScopes = scopes.every((scope) =>
  138.           userScopes.includes(scope)
  139.         );
  140.  
  141.         if (!hasRequiredScopes) {
  142.           reply
  143.             .status(403)
  144.             .send(
  145.               createErrorResponse(
  146.                 "Insufficient permissions",
  147.                 MessageCode.FORBIDDEN,
  148.                 403,
  149.                 InternalMessageCode.FORBIDDEN_REQUEST
  150.               )
  151.             );
  152.           return false;
  153.         }
  154.  
  155.         return true;
  156.       };
  157.     });
  158.  
  159.     // Helper to check if user has required roles
  160.     fastify.decorate("requireRoles", (roles: string[]) => {
  161.       return async (request: FastifyRequest, reply: FastifyReply) => {
  162.         if (!request.logtoUser) {
  163.           reply
  164.             .status(401)
  165.             .send(
  166.               createErrorResponse(
  167.                 "Unauthorized",
  168.                 MessageCode.UNAUTHORIZED,
  169.                 401,
  170.                 InternalMessageCode.UNAUTHORIZED_REQUEST
  171.               )
  172.             );
  173.           return false;
  174.         }
  175.  
  176.         // Change this line to use the single role property
  177.         const userRole = request.logtoUser.role;
  178.  
  179.         // Check if the user's role is in the allowed roles array
  180.         const hasRequiredRoles = roles.includes(userRole);
  181.  
  182.         if (!hasRequiredRoles) {
  183.           reply
  184.             .status(403)
  185.             .send(
  186.               createErrorResponse(
  187.                 "Insufficient permissions",
  188.                 MessageCode.FORBIDDEN,
  189.                 403,
  190.                 InternalMessageCode.FORBIDDEN_REQUEST
  191.               )
  192.             );
  193.           return false;
  194.         }
  195.  
  196.         return true;
  197.       };
  198.     });
  199.  
  200.     // Helper to get claims from JWT
  201.     fastify.decorate("getLogtoUserClaims", (request: FastifyRequest) => {
  202.       if (!request.logtoUser) {
  203.         return null;
  204.       }
  205.       return request.logtoUser;
  206.     });
  207.  
  208.     // Helper to get a specific claim from JWT
  209.     fastify.decorate(
  210.       "getLogtoUserClaim",
  211.       (request: FastifyRequest, claimName: string) => {
  212.         if (!request.logtoUser) {
  213.           return null;
  214.         }
  215.         return request.logtoUser[claimName];
  216.       }
  217.     );
  218.   }
  219. );
  220.  
  221. // Middleware to verify token
  222. export const verifyLogtoToken = async (
  223.   request: FastifyRequest,
  224.   reply: FastifyReply
  225. ) => {
  226.   return request.server.verifyLogtoToken(request, reply);
  227. };
  228.  
  229. // Middleware to require specific scopes
  230. export const requireScopes = (scopes: string[]) => {
  231.   return async (request: FastifyRequest, reply: FastifyReply) => {
  232.     return request.server.requireScopes(scopes)(request, reply);
  233.   };
  234. };
  235.  
  236. // Middleware to require specific roles
  237. export const requireRoles = (roles: string[]) => {
  238.   return async (request: FastifyRequest, reply: FastifyReply) => {
  239.     return request.server.requireRoles(roles)(request, reply);
  240.   };
  241. };
  242.  
  243. // Helper to get all claims from JWT
  244. export const getLogtoUserClaims = (request: FastifyRequest) => {
  245.   return request.server.getLogtoUserClaims(request);
  246. };
  247.  
  248. // Helper to get a specific claim from JWT
  249. export const getLogtoUserClaim = (
  250.   request: FastifyRequest,
  251.   claimName: string
  252. ) => {
  253.   return request.server.getLogtoUserClaim(request, claimName);
  254. };
  255.  
  256. export const extractLogtoUserClaims = async (
  257.   request: FastifyRequest,
  258.   reply: FastifyReply
  259. ) => {
  260.   // Skip if no user is authenticated
  261.   if (!request.logtoUser) {
  262.     return;
  263.   }
  264.  
  265.   // Extract common claims and add them directly to the request
  266.   request.user = {
  267.     id: request.logtoUser.user_id,
  268.     logtoId: request.logtoUser.sub,
  269.     email: request.logtoUser.email,
  270.     isStaff: request.logtoUser.is_staff || false,
  271.     role: request.logtoUser.role,
  272.     role_id: request.logtoUser.role_id,
  273.     site_id: request.logtoUser.site_id,
  274.     claims: request.logtoUser,
  275.   };
  276. };
  277.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement