keycloak / keycloak-nodejs-connect

Apache License 2.0
676 stars 421 forks source link

Proposal: use `jose` package as alternative to `keycloak-nodejs-connect` #492

Open valerii15298 opened 1 year ago

valerii15298 commented 1 year ago

Description

Since this library is deprecated I would like to propose one of the possible alternatives => jose It contains quite useful functions: createRemoteJWKSet and jwtVerify as described here: https://github.com/panva/jose/blob/main/docs/functions/jwks_remote.createRemoteJWKSet.md#function-createremotejwkset

Example verification with jose looks like this:

import { JWTPayload, createRemoteJWKSet, jwtVerify } from "jose";

const auth_server_url = "http://keycloak.localhost:8080";
const jwks = createRemoteJWKSet(new URL(`${auth_server_url}/realms/${realmName}/protocol/openid-connect/certs`));
const { payload } = await jwtVerify(token, jwks); // this line will throw on invalid token
console.log(payload.sub);

jose automatically fetches public keys from endpoint if previous ones are not valid.

The JSON Web Key Set is fetched when no key matches the selection process

Jose library seems to cover significant part of keycloak-nodejs-connect functionality. I already using it in my app and DX is even more enjoyable. I have more control and can create express middleware myself, or integrate it in any kind of app bcs it is not tightly coupled to expressjs.

@jonkoops @abstractj I would really appreciate your review about using jose, seems like it can be recommended as keycloak-nodejs-connect alternative or at least one of alternatives. In any case would be very helpful to hear your thoughts on it, whether you think it is good replacement or not. Tagging both of you since you guys seem to be active ones in this repo from maintainers.

Btw, also, as I checked another library named jose is used under the hood in keycloak Java source code too.

jonkoops commented 1 year ago

Thanks for this suggestion, I will take a look and see if this could be a library to recommend.

m1212e commented 1 year ago

@valerii15298 can you post a link to the code of the app you tested it in?

valerii15298 commented 1 year ago

@m1212e sure, I used it in Nest.js app but I can easily give you StackBlitz example with express(or Nest.js), what do you prefer?

m1212e commented 1 year ago

Express sounds good, but I thought it may be a public repo you were referring to. Whatever is less work for you <3

valerii15298 commented 1 year ago

@m1212e I thought that simplest example would be easier to give here, since you cannot start keycloak in StackBlitz without docker-compose anyway...

So the simplest authorization would be like this:

import { createRemoteJWKSet, jwtVerify } from "jose";
import express, { Request } from "express";

const app = express();

const auth_server_url = "http://keycloak.localhost:8080";
const realmName = "master";
const jwks = createRemoteJWKSet(
  new URL(
    `${auth_server_url}/realms/${realmName}/protocol/openid-connect/certs`,
  ),
);
function extractTokenFromHeader(req: Request) {
  const [type, token] = req.headers.authorization?.split(" ") ?? [];
  return type === "Bearer" ? token : undefined;
}
app.use((req, res, next) => {
  const token = extractTokenFromHeader(req);
  if (!token) {
    return res.status(401);
  }
  jwtVerify(token, jwks)
    .then(({ payload }) => {
      // @ts-ignore
      req.authPayload = payload;
      console.log({
        userId: payload.sub,
        useEmail: payload.email,
        userRealmRoles: payload.realm_access.roles,
        // and so on... simply check what is available in payload
      });
      next();
    })
    .catch(() => res.status(401));
});
dlaraf commented 1 year ago

May be is a dummy question but how is this implemented currently? Is the token checked in the Keycloak service?. I mean after a logout, for example. In this case I don't see any server side check so anyone with that "old"/"not valid" token could use the protected endpoint. Is it? I guess the bearer token from the frontend app should be checked against the server to ensure is not expired/revoked.

valerii15298 commented 1 year ago

@dlaraf There are to ways to verify keycloak token: Online and Offline, both having their own tradeoffs and benefits.

Online validation is quite expensive, you will need to make requests to your keycloak server every time which could slow down your app, but it will always give you the most precise and valid result.

Offline validation on the other hand is less expensive but if the user logged out, the token will be valid in the remaining time(usually 5 minutes depending which configuration you chose for token to be valid for how long).

When using jose the verification will be valid for most cases except cases like user logout for which jose will verify token without error(while token will be revoked already) until the end of its lifespan.

In the case of token expiration, jwtVerify will throw an error always stating that token is expired which is correct behaviour, since expiration time is encoded in the token itself(so no need to make requests to the server).

And are you asking about implementation with jose or about current implementation of keycloak-nodejs-connect?

dlaraf commented 1 year ago

Hi @valerii15298! Thank you for the clarification. Very helpful. I was asking about the current verification in keycloak-nodejs-connect. I guess offline method is the one used with the bearerOnly:true and the other(online) is using the client_secret? If you wants to use both(frontend login with bearer token and check that token online on the API side(backend), you need two clients? one for bearer flow (frontend) and another one using secret code (backend)?

I'm trying to do a secure app with Keycloak so I was following this guide reactjs express rest api Keycloak . Another good example I've seen is this one that I guess uses the offline method as well with Passport Keycloak Nestjs React example

Thank you @valerii15298

late-e commented 1 year ago

@dlaraf You don't need two clients if you just want to verify the token that backend received from frontend. You can do a request on backend to keycloak realm endpoint where you get the public key (jwks endpoint), and with that you can verify the validity of the token.

dav-bisc commented 1 year ago

Sorry if my question seems stupid, I'm new to this. Assuming that I'm using jose, should I check the role permissions using some custom code or is there a standardized way to do this? I was thinking of just checking whether the payload.realm_access.roles contains the necessary role to access a certain resource, but I'm unsure whether this is the correct way to go about it.

late-e commented 1 year ago

@dav-bisc

Hi,

Yes you can just check if the payload.realm_access.roles contains the necessary role. You can check the code from this library too, it's very simple.

Dany-C commented 1 year ago

Any news about the "official" potential replacement? 🤔

BasilaryGroup commented 2 weeks ago

Alternatives recommended or hinted at by the Keycloak team are mentioned in Deprecation of Keycloak adapters.