JWT Authentication

Created on 25 July 2023, 11 months ago
Updated 30 May 2024, 29 days ago

Problem/Motivation

When discussing authentication methods to support, we've had requests for JWT support.

The most mature Drupal implementation seems to be the https://www.drupal.org/project/jwt → module, so we should focus on providing tools for that implementation. But I'd imagine this will also help with other JWT based solutions.

Proposed resolution

I'm learning about JWT along the way here, but I see two main flows:

1. Authenticating with tokens issued by Drupal.
* Get a token from /jwt/token on your Drupal site. Somewhat ironically the first request would require using a different auth method to get the token, at that point subsequent requests can authenticate using the token as a bearer token.
* We could also check token expiration and negotiate refreshing the token (not unlike what we do for oauth)

2. Authentication with tokens signed by a JavaScript application.
* The JS app signs a token that is compatible with the Drupal JWT module implementation.
* Requests to Drupal authenticate using this token as a bearer token.
* We could still negotiate refreshing the token via the Drupal endpoint.

As I experiment with these cases, I think what the client needs to implement is the overlap. We assume that you already have a valid JWT and you provide that to the client. The client then handles adding the necessary header, and negotiating new tokens when they expire.

We can document how to issue the initial JWT - either from Drupal's endpoint, or within the JS application. We also might be able to provide some small utilities to help with this.

Related tasks:
* We could also update the local Drupal environment to install and configure JWT.

Remaining tasks

API changes

✨ Feature request
Status

Active

Component

Code

Created by

🇺🇸United States brianperry

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

  • Issue created by @brianperry
  • Status changed to Active about 2 months ago
  • 🇺🇸United States brianperry
  • 🇺🇸United States brianperry
  • Assigned to brianperry
  • 🇺🇸United States brianperry
  • 🇺🇸United States brianperry

    The current client can authenticate using Drupal issued JWT with something like this:

      // Get a JWT token using basic auth
      const headers = new Headers();
      const encodedCredentials = stringToBase64(`admin:admin`);
      headers.set("Authorization", `Basic ${encodedCredentials}`);
    
      const tokenRequest = await fetch(
        "https://drupal-client.ddev.site/jwt/token",
        { headers },
      ).then((response) => response.json());
    
      // Use the JWT with custom auth
      const jsonApiClient = new JsonApiClient(baseUrl, {
        debug: true,
        authentication: {
          type: "Custom",
          credentials: {
            value: `Bearer ${tokenRequest.token}`,
          },
        },
      });
      const actions = await jsonApiClient.getCollection("action--action");
    

    It is also possible to refresh your JWT using the existing JWT:

      // Refresh your token using the existing token
      const jwtHeaders = new Headers();
      jwtHeaders.set("Authorization", `Bearer ${tokenRequest.token}`);
    
      const jwtTokenRequest = await fetch(
        "https://drupal-client.ddev.site/jwt/token",
        { headers: jwtHeaders },
      ).then((response) => response.json());
    

    Next I'm going to try to work up an example where the token is issued by the JS app, but honored by Drupal.

    This has been helpful though - I think what we need to implement here is the overlap between these two flows. Basically the client assumes that you have a valid JWT to start, and then uses (and updates) it from there. We can document how you'd get the initial token, or maybe create small helpers.

  • 🇺🇸United States brianperry
  • 🇮🇳India Ruturaj Chaubey Pune, India

    @brianperry

    The example you shared in #5 → looks pretty solid.

    For the second scenario, I was able to issue a JWT from a JS app and send it to the Drupal back-end.

    On Drupal front, I tried to consume the token using a custom controller but its still in progress.

    Is this the kind of strategy we want to use in this case?

  • 🇮🇳India Ruturaj Chaubey Pune, India

    The back-end code looks something like this

     public function success(Request $request) {
        $authHeader = $request->headers->get('Authorization');
    
        if ($authHeader && preg_match('/Bearer\s(\S+)/', $authHeader, $matches)) {
            $jwt = $matches[1];
            try {
              $decoded = JWT::decode($jwt, 'dummy-jwt-secret');
      
              // Assuming the payload has a userId.
              $uid = $decoded->userId;
              $user = User::load($uid);
      
              if ($user) {
                return new JsonResponse([
                  'status' => 'success',
                  'user' => [
                    'uid' => $user->id(),
                    'name' => $user->getUsername(),
                  ],
                ]);
              }
            } catch (\Exception $e) {
              $this->logger->error('JWT validation failed: @message', ['@message' => $e->getMessage()]);
              return new JsonResponse(['status' => 'error', 'message' => 'Invalid JWT token'], 401);
            }
          }
      
    
        $data = [
          'status' => 'success',
          'message' => 'The request was successful.',
          'value' => $authHeader
        ];
        return new JsonResponse($data);
      }
    
  • 🇺🇸United States brianperry

    Thanks for checking in on this @ruturaj-chaubey!

    That back end code makes sense at a glance, but my assumption was that we could use https://www.drupal.org/project/jwt → for essentially the same thing.

    I tried an example that issues a token in node.js that should be compatible with what the JWT module is expecting (uses the same key, has the same payload, etc), but if I try to authorize using the token I sign in node, it doesn't seem to work.

    import jwt from "jsonwebtoken";
    
    const token = jwt.sign(
      {
        drupal: {
          uid: "1",
        },
      },
      privateKey,
      { algorithm: "HS512", expiresIn: 60 * 60 },
    );
    
    const headers = new Headers();
    headers.set("Authorization", `Bearer ${token}`);
    
    fetch("https://drupal-client.ddev.site/jsonapi/action/action", { headers })
      .then((response) => response.json())
      .then((data) => console.log(data))
      .catch((error) => console.error("Error:", error));

    My best guess based on inspecting tokens using the debugger at https://jwt.io/ is that it is a problem related to base64 encoding. I'll keep poking at this, but wondering if I'm missing something obvious, or if the JWT module can't handle this use case.

Production build 0.69.0 2024