简体   繁体   中英

Passing Keycloak bearer token to express backend?

We have a frontend application that uses Vue3 and a backend that uses nodejs+express.

We are trying to make it so once the frontend application is authorised by keycloak it can then pass a bearer token to the backend (which is also protected by keycloak in the same realm), to make the API calls.

Can anyone suggest how we should be doing this?

Follows is what we are trying and seeing as a result.

The error thrown back is simply 'Access Denied', with no other details Running the debugger we see a 'invalid token (wrong audience)' error thrown in the GrantManager.validateToken function (which unfortunately doesn't bubble up).

In the webapp startup we initialise axios as follows, which passes the bearer token to the backend server

  const axiosConfig: AxiosRequestConfig = {
    baseURL: 'http://someurl'
  };
  api = axios.create(axiosConfig);

  // include keycloak token when communicating with API server
  api.interceptors.request.use(
    (config) => {
      if (app.config.globalProperties.$keycloak) {
        const keycloak = app.config.globalProperties.$keycloak;
        const token = keycloak.token as string;
        const auth = 'Authorization';
        if (token && config.headers) {
          config.headers[auth] = `Bearer ${token}`;
        }
      }

      return config;
    }
  );

  app.config.globalProperties.$api = api;

On the backend, during the middleware initialisation:

const keycloak = new Keycloak({});
app.keycloak = keycloak;

app.use(keycloak.middleware({
  logout: '/logout',
  admin: '/'
}));

Then when protecting the endpoints:

const keycloakJson = keystore.get('keycloak');
const keycloak = new KeycloakConnect ({
  cookies: false
}, keycloakJson);
router.use('/api', keycloak.protect('realm:staff'), apiRoutes);

We have two client configured in Keycloak:

  • app-frontend, set to use access type 'public'
  • app-server, set to use access type 'bearer token'

Trying with $keycloak.token gives us the 'invalid token (wrong audience)' error, but if we try with $keycloak.idToken instead, then we get 'invalid token (wrong type)'

In the first case it is comparing token.content.aud of value 'account', with a clientId of app-server . In the second case it is comparing token.content.typ , of value 'ID' with an expected type of 'Bearer'.

Upon discussion with a developer on another projects, it turns out my approach is wrong on the server and that keycloak-connect is the wrong tool for the job. The reasoning is that keycloak-connect is wanting to do its own authentication flow, since the front-end token is incompatible.

The suggested approach is to take the bearer token provided in the header and use the jwt-uri for my keycloak realm to verify the token and then use whatever data I need in the token.

Follows is an early implementation (it works, but it needs refinement) of the requireApiAuthentication function I am using to protect our endpoints:

import jwksClient from 'jwks-rsa';
import jwt, { Secret, GetPublicKeyOrSecret } from 'jsonwebtoken';

// promisify jwt.verify, since it doesn't do promises
async function jwtVerify (token: string, secretOrPublicKey: Secret | GetPublicKeyOrSecret): Promise<any> {
    return new Promise<any>((resolve, reject) => {
        jwt.verify(token, secretOrPublicKey, (err: any, decoded: object | undefined) => {
            if (err) {
                reject(err);
            } else {
                resolve(decoded);
            }
        });
    });
}

function requireApiAuthentication (requiredRole: string) {

    // TODO build jwksUri based on available keycloak configuration;
    const baseUrl = '...';
    const realm = '...';

    const client = jwksClient({
        jwksUri: `${baseUrl}/realms/${realm}/protocol/openid-connect/certs`
    });

    function getKey (header, callback) {
        client.getSigningKey(header.kid, (err: any, key: Record<string, any>) => {
            const signingKey = key.publicKey || key.rsaPublicKey;
            callback(null, signingKey);
        });
    }

    return async (req: Request, res: Response, next: NextFunction) => {
        const authorization = req.headers.authorization;
        if (authorization && authorization.toLowerCase().startsWith('bearer ')) {
            const token = authorization.split(' ')[1];
            const tokenDecoded = await jwtVerify(token, getKey);

            if (tokenDecoded.realm_access && tokenDecoded.realm_access.roles) {
                const roles = tokenDecoded.realm_access.roles;
                if (roles.indexOf(requiredRole) > -1) {
                    next();
                    return;
                }
            }
        }

        next(new Error('Unauthorized'));
    };
}

and then used as follows:

router.use('/api', requireApiAuthentication('staff'), apiRoutes);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM