Is custom JWT Validation available in V4?

System Information
  • 4.0.2:
  • MacOS 11.6:
  • PG 8.6.0:

Hi,

Is there something similar we can do with strapi v4, I saw this guide for v3, but I am not sure it is working for v4.

Any direction is much appreciated. Thanks!

6 Likes

Hi there,

Just a community member here facing a similar challenge.

Based on reading the source code, I believe this should work:

const { UnauthorizedError, ForbiddenError } = require('@strapi/utils').errors;
// See default implementation packages/core/admin/server/strategies/admin.js
//
// the first param of the `register` method is the "type" of endpoints to protect:
// 1. 'admin' - admin UI routes
// 2. 'content-api' - api routes
strapi.container.get('auth').register('admin', {
  name: 'your-custom-jwt-verifier',
  async authenticate(ctx, next): {
     // Get JWT from context and validate.
     const { authorization } = ctx.request.header;

    if (!authorization) {
       return { authenticated: false };
    }

    const parts = authorization.split(/\s+/);
    if (parts[0].toLowerCase() !== 'bearer' || parts.length !== 2) {
      return { authenticated: false };
    }

    const token = parts[1];
    const { payload, isValid } = validateJwtSomehow(token);


      if (!isValid) {
        return { authenticated: false };
      }

      let user = await strapi
        .query('admin::user')
        .findOne({ where: { id: payload.id }, populate: ['roles'] });

     // handle missing user
     if (!user) {
     }

     ctx.state.user = user;
      ctx.state.userAbility = await strapi.service('admin::permission').engine.generateUserAbility(user);
      
      return { authenticated: true, credentials: user };
  },
  async verify(ctx, next) {
    const { credentials } = ctx.state.auth;
    if (!checkIfCanAccessAdminTools()) {
      throw new ForbiddenError();
    }
    return 
  }
});

I’ll circle back once I’ve validated if this approach works, and probably submit a PR to update the docs.

Any update on this @sjones6?

I’m not sure where to place the code snippet you posted?

Having real trouble getting my custom jwt validation working after migrating to Strapi 4.
Might need to remain on v3 for the forseeable :frowning_face:

@Eli_Nathan another random strapi user here so take this with a grain of salt, but I was able to use @sjones6 response here to point me in the right direction and I was able to succesfully register and call a custom authStrategy with it.

The missing element is that you need to put this register code bit in the index.js file in the root of the src directory strapi generates (I imagine there is a better place for it, but whatever, this works). We are using the users-permissions plugin so what happens is they plugin runs first to check for the normal JWT strapi issues and if (and only if) it fails, then the custom strategy gets called. so you would just need to implement the verify and authenticate functions for your set up (we are using firebase auth) and use the users-permissions strategy source code as a rough template to follow for ‘things your custom function should mirror’. → link

Hope it helps. If I get a full working version with firebase auth I will share some better code. until then, here is a snippet of a slightly refactored (and radically simplified) set that at least runs. (index.js file)

"use strict";

const { getService } = require("@strapi/plugin-users-permissions/server/utils");
module.exports = {
  /**
   * An asynchronous register function that runs before
   * your application is initialized.
   *
   * This gives you an opportunity to extend code.
   */
  register({ strapi }) {
    strapi.container.get("auth").register("content-api", {
      name: "your-custom-jwt-verifier",
      authenticate: async function (ctx) {
        // Get JWT from context and validate.
        const { authorization } = ctx.request.header;

        if (!authorization) {
          return { authenticated: false };
        }
        //This is a hardcoded user id for now, would change this to fetch based off email from the firebase id token validation phase
        const user = await getService("user").fetchAuthenticatedUser(1);

        if (!user) {
          return { error: "Invalid credentials" };
        }

        ctx.state.user = user;

        return {
          authenticated: true,
          credentials: user,
        };
      },
      verify: async function (ctx) {
        console.log("arrived to do things --> need to check permissions access here");
        // const { credentials } = ctx.state.auth;
        // just always pass for testing purposes now
        return;
      },
    });
  },

  /**
   * An asynchronous bootstrap function that runs before
   * your application gets started.
   *
   * This gives you an opportunity to set up your data model,
   * run jobs, or perform some special logic.
   */
  bootstrap({ strapi }) {
    const admin = require("firebase-admin");
    const serviceAccount = require("../keys/specialKey.json");
    admin.initializeApp({
      credential: admin.credential.cert(serviceAccount),
    });

    strapi.firebase = admin;
  },
};

4 Likes

@gfritz I was just about to come here to write this exact thing. Did some digging and found where to put the code yesterday! :slight_smile:

Thanks so much!

I’m currently trying to figure out how to use the users-permissions fetchAuthenticatedUser method as my users never actually log in with the /auth/xxx endpoints as I’m building a React native app so I don’t have the luxury of callback URLs.

I am able to call my Auth0 domain int hat block to verify that the user is properly authenticated though so that’s likely a separate issue.

Thanks again, been stuck for days!

@Eli_Nathan Can you please share the location to add the custom validation? also a sample snippet would help out a lot. Thank you.

2 Likes

@gfritz Thanks for your reply, it helped me a lot in creating authorization through Firebase Phone Auth. But I’m stuck at the verify stage. The authenticate function works correctly. Could you help with writing the verify function? After the code, I’ll provide the context that goes into it.

"use strict";
const { ForbiddenError } = require('@strapi/utils').errors;
const jwt = require('jsonwebtoken');

module.exports = {
  /**
   * An asynchronous register function that runs before
   * your application is initialized.
   *
   * This gives you an opportunity to extend code.
   */
  register({ strapi }) {
    strapi.container.get('auth').register('content-api', {
      name: 'firebase-jwt-verifier',
      
      async authenticate(ctx) {
        const { authorization } = ctx.request.header;
      
        // Check for the presence of a JWT token in the header
        if (authorization) {
          const parts = authorization.split(/\s+/);
          if (parts[0].toLowerCase() === 'bearer' && parts.length === 2) {
            const token = parts[1];
      
            try {
              // Verify and decode the Firebase JWT token
              const decodedToken = await strapi.firebase.auth().verifyIdToken(token);
      
              // Check if the phone number in the JWT token matches the incoming request
              if (decodedToken.phone_number === ctx.request.body.phoneNumber) {
                // Get the user or create a new user based on the phone number
                let user = await strapi.db.query('plugin::users-permissions.user').findOne({
                  where: { phoneNumber: ctx.request.body.phoneNumber }
                });
      
                if (!user) {
                  // If the user does not exist, create a new user
                  user = await strapi.db.query('plugin::users-permissions.user').create({ phoneNumber: ctx.request.body.phoneNumber });
                }
      
                // Set the user in the context state
                ctx.state.user = user;

                // Generate Strapi JWT token
                const jwtToken = await strapi.service('plugin::users-permissions.jwt').issue({ id: user.id });
                ctx.state.user.jwt = jwtToken
      
                // Return successful authentication and user information
                return { authenticated: true, credentials: user };
              }
            } catch (error) {
              // Handle error when verifying or decoding the JWT token
              console.error('Error while verifying Firebase JWT token:', error);
            }
          }
        }
      
        // If authentication fails, return authentication error
        return { authenticated: false };
      },      

      async verify(ctx) {
        try {
          // Check for jwt token in ctx.state.auth
          if (ctx.credentials && ctx.credentials.jwt) {
            const tokenPayload = await strapi.service('plugin::users-permissions.jwt').verify(ctx.credentials.jwt);
            return tokenPayload;
          }
        } catch (error) {
          console.error('Error while verifying JWT token:', error);
        }
        throw new ForbiddenError('Invalid JWT token');
      }       
      
    });
  },

  /**
   * An asynchronous bootstrap function that runs before
   * your application gets started.
   *
   * This gives you an opportunity to set up your data model,
   * run jobs, or perform some special logic.
   */
  bootstrap({ strapi }) {
    const admin = require("firebase-admin");
    const serviceAccount = require("../private/firebase/serviceAccountKey.json");
    admin.initializeApp({
      credential: admin.credential.cert(serviceAccount),
    });

    strapi.firebase = admin;
  },
};

Ctx in verify:

{
  strategy: {
    name: 'firebase-jwt-verifier',
    authenticate: [AsyncFunction: authenticate],
    verify: [AsyncFunction: verify]
  },
  credentials: {
    id: 1,
    username: 'ivstepin',
    email: '...',
    provider: 'local',
    password: '...',
    resetPasswordToken: null,
    confirmationToken: null,
    confirmed: true,
    blocked: false,
    createdAt: '2023-06-04T03:18:58.578Z',
    updatedAt: '2023-06-06T20:00:36.307Z',
    phoneNumber: '...',
    jwt: 'eyJhbGciO ... ePFc'
  },
  ability: null
}