Keycloak: Access token validation end point
Asked Answered
D

7

29

Running keycloak on standalone mode.and created a micro-service by using node.js adapter for authenticating api calls.

jwt token from the keyclaok is sending along with each api calls. it will only respond if the token sent is a valid one.

  • how can i validate the access token from the micro service?
  • is there any token validation availed by keycloak?
Deaconess answered 16/1, 2018 at 4:45 Comment(0)
T
40

To expand on troger19's answer:

Question 1: How can I validate the access token from the micro service?

Implement a function to inspect each request for a bearer token and send that token off for validation by your keycloak server at the userinfo endpoint before it is passed to your api's route handlers.

You can find your keycloak server's specific endpoints (like the userinfo route) by requesting its well-known configuration.

If you are using expressjs in your node api this might look like the following:

const express = require("express");
const request = require("request");

const app = express();

/*
 * additional express app config
 * app.use(bodyParser.json());
 * app.use(bodyParser.urlencoded({ extended: false }));
 */

const keycloakHost = 'your keycloak host';
const keycloakPort = 'your keycloak port';
const realmName = 'your keycloak realm';

// check each request for a valid bearer token
app.use((req, res, next) => {
  // assumes bearer token is passed as an authorization header
  if (req.headers.authorization) {
    // configure the request to your keycloak server
    const options = {
      method: 'GET',
      url: `https://${keycloakHost}:${keycloakPort}/auth/realms/${realmName}/protocol/openid-connect/userinfo`,
      headers: {
        // add the token you received to the userinfo request, sent to keycloak
        Authorization: req.headers.authorization,
      },
    };

    // send a request to the userinfo endpoint on keycloak
    request(options, (error, response, body) => {
      if (error) throw new Error(error);

      // if the request status isn't "OK", the token is invalid
      if (response.statusCode !== 200) {
        res.status(401).json({
          error: `unauthorized`,
        });
      }
      // the token is valid pass request onto your next function
      else {
        next();
      }
    });
  } else {
    // there is no token, don't process request further
    res.status(401).json({
    error: `unauthorized`,
  });
});

// configure your other routes
app.use('/some-route', (req, res) => {
  /*
  * api route logic
  */
});


// catch 404 and forward to error handler
app.use((req, res, next) => {
  const err = new Error('Not Found');
  err.status = 404;
  next(err);
});

Question 2: Is there any token validation availed by Keycloak?

Making a request to Keycloak's userinfo endpoint is an easy way to verify that your token is valid.

Userinfo response from valid token:

Status: 200 OK

{
    "sub": "xxx-xxx-xxx-xxx-xxx",
    "name": "John Smith",
    "preferred_username": "jsmith",
    "given_name": "John",
    "family_name": "Smith",
    "email": "[email protected]"
}

Userinfo response from invalid valid token:

Status: 401 Unauthorized

{
    "error": "invalid_token",
    "error_description": "Token invalid: Token is not active"
}

Additional Information:

Keycloak provides its own npm package called keycloak-connect. The documentation describes simple authentication on routes, requiring users to be logged in to access a resource:

app.get( '/complain', keycloak.protect(), complaintHandler );

I have not found this method to work using bearer-only authentication. In my experience, implementing this simple authentication method on a route results in an "access denied" response. This question also asks about how to authenticate a rest api using a Keycloak access token. The accepted answer recommends using the simple authentication method provided by keycloak-connect as well but as Alex states in the comments:

"The keyloak.protect() function (doesn't) get the bearer token from the header. I'm still searching for this solution to do bearer only authentication – alex Nov 2 '17 at 14:02

Tubuliflorous answered 26/6, 2018 at 16:24 Comment(8)
Hi. But when I hit the userInfo endpoint. I recieve this response everytime. { "sub": "xxxxxxxxxxx", "email_verified": false, "preferred_username": "service-account-testclient" }Latchkey
But I have the username as 'user'. Can anyone explain why?Latchkey
Above code snippet worked in my setup. Tried with kecloak-connect, but it did not work as expected.Artieartifact
Making a request to keycloak server before each request, isn't it slow down the response and hence the app?Soul
What is the "well-known-configuration" you are referring to?Toweling
@Toweling openid.net/specs/…, e.g. for Keycloak maybe URL/auth/realms/REALM/.well-known/openid-configuration.Winnifredwinning
In my opinion the offline token validation described in Markus's answer is a much better practice. Every security library I've used (Node, Java, C#, Drupal, Moodle) supports it. Calling the userinfo or introspection endpoint creates unnecessary traffic, and one of the main design goals of JWT authentication is to mitigate the scaling problems of server-side session management. I have seen real-life clustered Keycloak (RedHat SSO) systems impacted by misuse of online token validation.Winnifredwinning
Hi, I have tried your answer, If I am not providing the access_token in the header it throws the unauthorized response without an error message. And if I provide the valid token in header authorization it throws a forbidden error without an error message, Can you tell me why it throws forbidden even thouth the token is valid?Undersecretary
T
24

There are two ways to validate a token:

  • Online
  • Offline

The variant described above is the Online validation. This is of course quite costly, as it introduces another http/round trip for every validation.

Much more efficient is offline validation: A JWT Token is a base64 encoded JSON object, that already contains all information (claims) to do the validation offline. You only need the public key and validate the signature (to make sure that the contents is "valid"):

There are several libraries (for example keycloak-backend) that do the validation offline, without any remote request. Offline validation can be as easy as that:

token = await keycloak.jwt.verifyOffline(someAccessToken, cert);
console.log(token); //prints the complete contents, with all the user/token/claim information...

Why not use the official keycloak-connect node.js library (and instead use keycloak-backend)? The official library is more focused on the express framework as a middleware and does (as far as I have seen) not directly expose any validation functions. Or you can use any arbitrary JWT/OICD library as validation is a standardized process.

Trend answered 14/12, 2019 at 3:3 Comment(7)
Yes, online validation is costly, but if you use purely offline validation, how would we know if the token was not invalidated by a logout?Dynast
Hello alabid, you are absolutely right. It is a decision and trade off to make. JWTs should anyway be rather short lived. An alternative is some kind of "logout event" pushed to an in memory invalidation store: So you do check every token, but not to a remote service, only to an process/system internal cache that contains pushed invalidations. But I do not know any library that implements this.Trend
Indeed. I am quiet surprised that there's no library that actually does that, makes a good idea for one imho. Anyway I think a short lived token would be enough. One more question please, when it expires we use the refresh token to generate a new one right?Dynast
@alabid: Yes your are absolutely right too! As you have written, you use use the "refresh token" to get a new "access token". I think some servers even return a new refresh token, when you query for a new "access token". This is some kind of "refresh token rotation".Trend
I'm using keycloak-connect library for my node-rest-api, but when I logout from react-app or close all session in keycloak admin console before that the token expire, I still can call rest api backend using the previous token generated at login moment (ex. with postman). Are there some method in keycloak library that validate token ?Runstadler
@ala Typical JWT lifetime is around 3-5 minutes. I'd have to declare that the majority of systems are not so critical that they need less than 3 minute certainty that the user hasn't logged out somewhere else.Winnifredwinning
If the user is not the admin in keyclok, then the following API throws 403 error, https://${keycloakHost}:${keycloakPort}/auth/realms/${realmName}/protocol/openid-connect/userinfoUndersecretary
Z
8

I would use this UserInfo endpoint for that, with which you can also check other attributes like email as well as what you defined in mappers. You must send access token in header attributes with Bearer Authorization : Bearer access_token

http://localhost:8081/auth/realms/demo/protocol/openid-connect/userinfo

Zosima answered 16/4, 2018 at 12:0 Comment(0)
S
3

You must use the introspect token service

Here is an example with CURL and Postman

curl --location --request POST 'https://HOST_KEYCLOAK/realms/master/protocol/openid-connect/token/introspect' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=oficina-virtual' \
--data-urlencode 'client_secret=4ZeE2v' \
--data-urlencode 

'token=eyJhbGciKq4n_8Bn2vvy2WY848toOFxEyWuKiHrGHuJxgoU2DPGr9Mmaxkqq5Kg'

Or with Postman

enter image description here

NOTE: Use the variables exp, iat and active to validate the AccessToken

Sacerdotal answered 6/9, 2022 at 18:39 Comment(0)
R
1

@kfrisbie Thanks for you response, with your example I could refactor your code using the keycloak connect adapter:

// app.js
app.use(keycloakConfig.validateTokenKeycloak); // valid token with keycloak server

// add routes
const MyProtectedRoute = require('./routes/protected-routes'); // routes using keycloak.protect('some-role')
app.use('/protected', MyProtectedRoute);

So when authorization header is sended, I can verify that token is still valid against keycloak server, so in case of any logout from admin console, or front spa before expire token, my rest api throws 401 error, in other cases keycloak protect method is used.

// keycloak.config.js
let memoryStore = new session.MemoryStore();
let _keycloak = new Keycloak({ store: memoryStore });

async function validateTokenKeycloak(req, res, next) {
    if (req.kauth && req.kauth.grant) {        
        console.log('--- Verify token ---');
        try {
            var result = await _keycloak.grantManager.userInfo(req.kauth.grant.access_token);
            //var result = await _keycloak.grantManager.validateAccessToken(req.kauth.grant.access_token);
            if(!result) {
                console.log(`result:`,  result); 
                throw Error('Invalid Token');
            }                        
        } catch (error) {
            console.log(`Error: ${error.message}`);
            return next(createError.Unauthorized());
        }
    }
    next();  
}

module.exports = {
    validateTokenKeycloak
};
Runstadler answered 23/8, 2020 at 17:16 Comment(0)
P
1

To solve this problem, Keycloak implements JWKS.

Below is an example code snippet in typescript that uses this feature.

import jwt, { JwtPayload, VerifyCallback } from 'jsonwebtoken';
import AccessTokenDecoded from './AccessTokenDecoded';
import jwksClient = require('jwks-rsa');

function getKey(header: jwt.JwtHeader, callback: jwt.SigningKeyCallback): void {
  const jwksUri = process.env.AUTH_SERVICE_JWKS || '';
  const client = jwksClient({ jwksUri, timeout: 30000 });

  client
    .getSigningKey(header.kid)
    .then((key) => callback(null, key.getPublicKey()))
    .catch(callback);
}

export function verify(token: string): Promise<AccessTokenDecoded> {
  return new Promise(
    (
      resolve: (decoded: AccessTokenDecoded) => void,
      reject: (error: Error) => void
    ) => {
      const verifyCallback: VerifyCallback<JwtPayload | string> = (
        error: jwt.VerifyErrors | null,
        decoded: any
      ): void => {
        if (error) {
          return reject(error);
        }
        return resolve(decoded);
      };

      jwt.verify(token, getKey, verifyCallback);
    }
  );
}

import AccessTokenDecoded from './AccessTokenDecoded'; is just an interface of the decoded token.

AUTH_SERVICE_JWKS=http://localhost:8080/realms/main/protocol/openid-connect/certs is an environment variable with the URI of certificates provided by Keycloak.

Purree answered 17/11, 2022 at 13:10 Comment(0)
P
0

For express.js and keycloak 23.0.6 i use this middleware to validate (and decode, if needed) my JWTs from keycloak

      // keycloak JWT which can be obtained from keycloak like this:
      // curl -X POST 'http://localhost:8080/realms/<REALM>/protocol/openid-connect/token' -H 'Content-Type: application/x-www-form-urlencoded' -d 'client_id=<CLIENT_ID>' -d 'username=<USER>' -d 'password=<PASS>' -d 'grant_type=password' -d 'scope=email profile'
      if (req.headers.authorization) {
        const token: string = req.headers.authorization?.split(' ')[1];

        if (!req.app.locals.publicKey || req.app.locals.publicKeyExpire < Date.now()) {
          try {
            const response: AxiosResponse = await axios.get(process.env.KEYCLOAK_CLIENT_ISSUER); // http://localhost:8080/realms/<REALM>
            const pubKey: string = `-----BEGIN PUBLIC KEY-----\n${response.data.public_key}\n-----END PUBLIC KEY-----`;
            req.app.locals.publicKey = pubKey;
            req.app.locals.publicKeyExpire = Date.now() + 5 * 60 * 1000; // 5 min
          } catch (error) {
            console.error('Failed to fetch public key', error);

            return res.status(500).json({error, logout: true});
          }
        }

        try {
          jwt.verify(token, req.app.locals.publicKey, {algorithms: ['RS256']});
          // const decoded: {} = jwt.decode(token);
          next();
        } catch (error) {
          res.status(401).json(error);
        }
      } else {
        return res.status(401).json({
          error: 'Unauthorized',
          logout: true,
        });
      }
Prudhoe answered 23/7 at 11:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.