Passing Keycloak bearer token to express backend? - node.js

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).
The frontend makes use of #dsb-norge/vue-keycloak-js which leverages keycloak-js.
The backend makes use of keycloak-connect. Its endpoints are REST based.
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);

Related

How can I verify a token from Auth0 in nodejs backend with jwt? JsonWebTokenError

I have a react frontend with auth0 to log users, once a user is logged in I get the token with getAccessTokenSilently() and send it to the backend like this:
const { user, isAuthenticated, getAccessTokenSilently } = useAuth0()
useEffect(() => {
if (user) getTickets()
}, [user])
async function getTickets() {
const token = await getAccessTokenSilently()
const response = await fetch('http://localhost:4000/api/gettickets', {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ user, bar }),
})
const data = await response.json()
}
Once I have the token in my backend I try to verify it jsonwebtoken like this:
import express from 'express'
import jwt from 'jsonwebtoken'
import dotenv from 'dotenv'
dotenv.config()
const routerGetTickets = express.Router()
routerGetTickets.post('/', async (req, res) => {
const PUBKEY = process.env.PUBKEY
const token = req.headers.authorization?.split(' ')[1]
if (token && PUBKEY) {
jwt.verify(token, PUBKEY, { algorithms: ['RS256'] }, (err, data) => {
console.log('token :>> ', token)
if (err) {
res.sendStatus(403)
console.log('err :>> ', err)
return
} else {
console.log('everything ok')
}
})
}
})
export default routerGetTickets
If I'm not wrong, with algorithm RS256 I have to provide the public key witch I got using openssl with the signin certification I downloaded from my aplication in Auth0 dashboard.
This is the error I get: err :>> JsonWebTokenError: secretOrPublicKey must be an asymmetric key when using RS256
And this is my index.ts:
import React from 'react'
import ReactDOM from 'react-dom/client'
import './sass/index.scss'
import App from './App'
import { BrowserRouter } from 'react-router-dom'
import { Auth0Provider } from '#auth0/auth0-react'
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
root.render(
<React.StrictMode>
<BrowserRouter basename='/tombola'>
<Auth0Provider
domain='*******.uk.auth0.com'
clientId='*****************************'
authorizationParams={{
redirect_uri: `${window.location.origin}/tombola/callback`,
audience: 'https://******.uk.auth0.com/api/v2/',
scope: 'read:current_user update:current_user_metadata'
}}
>
<App />
</Auth0Provider>
</BrowserRouter>
</React.StrictMode>
)
I had this issue in the verify jwt method also using Auth0, the problem is with the public key.
You didn't show your .env file where the PUBKEY variable are but I imagine you got it from the Application -> settings -> advanced settings -> certificates, as the Auth0 docs says, but thats the certificate not the pubkey.
To get the public key you need to use the "jwks-rsa" library, it retrieves the publickey using the certificate, getting it from your Auth0 application domain, and for this it needs two things:
1- The domain to construct the url that Auth0 serve the keys.
2- the "kid" header of your jwt token, that identified the correct key.
The code look like this:
async function HandleRequest(req, res) {
const cookie = req.headers.cookie;
const token = cookie.split(";")
.find(c => c.trim().startsWith("access_token="))
.split("=")[1]
const kid = decode(token, { complete: true }).header.kid;
const publicKey = (await JwksRsa({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: `https://${process.env.DOMAIN}/.well-known/jwks.json`
}).getSigningKey(kid)).getPublicKey()
if (cookie && cookie.includes("access_token")) {
verify(
token,
publicKey,
{
algorithms: ["RS256"],
},
(err, decoded) => {
if (err) {
res.write(JSON.stringify(err));
} else {
res.write(JSON.stringify(decoded));
}
}
);
} else {
res.write("No access token found")
}
res.end();
return
}
Note that in the example, I'm in a api route, dealing with a request and getting the jwt token in the headers, my jwt is in the headers as "access_token", yours could be different, then decoding it, extracting the kid identifier, passing it to the jwks-rsa alongside the domain constructed url, and then the jwks-rsa library is dealing with all request to get the certificate from your Auth0 application, and all the cryptography work of retrieving the public key from the certificate.
You can retrieve from the certificate using "crypto" node module, but I tried and it was a mess because it works bettem with .pem files than with strings, but it's a useful thing to know.
Hope this helps you, I know that Auth0 can mess with our heads while learning.
Thanks! I had the same issue and resolved it as you suggested.

How to check wheter the Azure AD token send by React to Node.js is valid

Hi I have a code from https://github.com/Azure-Samples/ms-identity-javascript-react-spa
I changed it a little bit, so instead calling an Microsoft Graph API endpoint, I call mine endpoint on localhost:7000.
So it basically starts with me logging in (here i did not change enything). Then there is this function which acquires token:
const { instance, accounts } = useMsal();
const [graphData, setData] = useState(null);
function RequestProfileData() {
// Silently acquires an access token which is then attached to a request for MS Graph data
instance
.acquireTokenSilent({
...loginRequest,
account: accounts[0],
})
.then((response) => {
callMyEndpoint(response.accessToken).then((response) =>
setData(response)
);
});
}
it uses function callMyEndpoint which looks like this:
export async function callMyEndpoint(accessToken) {
const headers = new Headers();
const bearer = `Bearer ${accessToken}`;
headers.append("Authorization", bearer);
const options = {
method: "POST",
headers: headers,
};
return fetch("http://localhost:7000/myendpoint", options)
.then((response) => response.json())
.catch((error) => console.log(error)) // if the user is not logged in- catch an error;
}
Now, onto my Node.js backend application where the http://localhost:7000/myendpoint is served.
app.post("/myendpoint", async (req, res) => {
console.log("TOKEN", req.headers.authorization); // it is being printed here, everything seems fine.
// here i would like to check whether the token is valid
// if req.headers.authorization == AZURE_TOKEN?
// How to do this?
});
And now the question is? How to check in backend if the token send from frontend is valid for the user, so only logged users, or users which are added in my app registration in azure can post onto this request?
You can use the libraries such as validate-azure-ad-token or you can write your own logic using jsonwebtoken
Here I have my custom logic for that first you will need client_id , tenat_id and scope name.
I am assuming you already have client and tenant id and for scope name it will be available in the Expose Api tab of your app registration.
Here I have console app which will take your token and try to validate it.
var jwt = require('jsonwebtoken');
var token = 'your Token';
var clientid = '' ;
var tenantid = "" ;
var scope = "";
// Create an audiance variable
var audiance = 'api://'+clientid;
// decoded token
var decodedToken = jwt.decode(token , {complete :true});
if((decodedToken.payload.aud==audi)&&(decodedToken.payload.scp==scope)&&(decodedToken.payload.tid==tenantid))
{
console.log("The token is valid");
}
else
{
console.log("The Token is invalid")
}
Output :

How to clear an set cookies with Apollo Server

I recently just switched from using express with apollo server to just using apollo server since the subscriptions setup seemed more current and easier to setup. The problem I'm having now is I was saving a cookie with our refresh token for login and clearing the cookie on logout. This worked when I was using express.
const token = context.req.cookies[process.env.REFRESH_TOKEN_NAME!];
context.res.status(401);
Since switching from express/apollo to just apollo server I don't have access to req.cookies even when i expose the req/res context on apollo server.
I ended up switching to this (which is hacky) to get the cookie.
const header = context.req.headers.cookie
var cookies = header.split(/[;] */).reduce(function(result: any, pairStr: any) {
var arr = pairStr.split('=');
if (arr.length === 2) { result[arr[0]] = arr[1]; }
return result;
}, {});
This works but now I can't figure out how to delete the cookies. With express I was doing
context.res.clearCookie(process.env.REFRESH_TOKEN_NAME!);
Not sure how I can clear cookies now since res.clearCookie doesn't exist.
You do not have to specifically clear the cookies. The expiresIn cookie key does that for you. Here is the snippet which i used to set cookies in browser from apollo-server-lambda. Once the expiresIn date values has passed the current date time then the cookies wont be valid for that host/domain. You need to revoke access token for the user again or logout the user from the application
import { ApolloServer, AuthenticationError } from "apollo-server-lambda";
import resolvers from "./src/graphql/resolvers";
import typeDefs from "./src/graphql/types";
const { initConnection } = require("./src/database/connection");
const { validateAccessToken, hasPublicEndpoint } = require("./src/bll/user-adapter");
const { addHoursToDate } = require("./src/helpers/utility");
const corsConfig = {
cors: {
origin: "http://localhost:3001",
credentials: true,
allowedHeaders: [
"Content-Type",
"Authorization"
],
},
};
// creating the server
const server = new ApolloServer({
// passing types and resolvers to the server
typeDefs,
resolvers,
context: async ({ event, context, express }) => {
const cookies = event.headers.Cookie;
const accessToken = ("; " + cookies).split(`; accessToken=`).pop().split(";")[0];
const accessLevel = ("; " + cookies).split(`; accessLevel=`).pop().split(";")[0];
const expiresIn = ("; " + cookies).split(`; expiresIn=`).pop().split(";")[0];
const { req, res } = express;
const operationName = JSON.parse(event.body).operationName;
if (await hasPublicEndpoint(operationName)) {
console.info(operationName, " Is a public endpoint");
} else {
if (accessToken) {
const jwtToken = accessToken.split(" ")[1];
try {
const verifiedUser = await validateAccessToken(jwtToken);
console.log("verifiedUser", verifiedUser);
if (verifiedUser) {
return {
userId: verifiedUser,
};
} else {
console.log();
throw new AuthenticationError("Your token does not verify!");
}
} catch (err) {
console.log("error", err);
throw new AuthenticationError("Your token does not verify!");
}
}
}
return {
headers: event.headers,
functionName: context.functionName,
event,
context,
res,
};
},
cors: corsConfig,
formatResponse: (response, requestContext) => {
if (response.data?.authenticateUser || response.data?.revokeAccessToken) {
// console.log(requestContext.context);
const { access_token, user_type, access_token_generated_on, email } =
response.data.authenticateUser || response.data.revokeAccessToken;
const expiresIn = addHoursToDate(new Date(access_token_generated_on), 12);
requestContext.context.res.set("set-cookie", [
`accessToken=Bearer ${access_token}`,
`accessLevel=${user_type}`,
`expiresIn=${new Date(access_token_generated_on)}`,
`erUser=${email}`,
]);
}
if (response.data?.logoutUser) {
console.log("Logging out user");
}
return response;
},
});
Simply send back the exact same cookie to the client with an Expires attribute set to some date in the past. Note that everything about the rest of the cookie has to be exactly the same, so be sure to keep all the original cookie attributes, too.
And, here's a link to the RFC itself on this topic:
Finally, to remove a cookie, the server returns a Set-Cookie header
with an expiration date in the past. The server will be successful
in removing the cookie only if the Path and the Domain attribute in
the Set-Cookie header match the values used when the cookie was
created.
As to how to do this, if you're using Node's http module, you can just use something like this (assuming you have a response coming from the callback passed to http.createServer):
context.response.writeHead(200, {'Set-Cookie': '<Your Cookie Here>', 'Content-Type': 'text/plain'});
This is assuming that your context has access to that http response it can write to.
For the record, you can see how Express does it here and here for clarity.

Secure a GraphQL API with passport + JWT's or sessions? (with example)

To give a bit of context: I am writing an API to serve a internal CMS in React that requires Google login and a React Native app that should support SMS, email and Apple login, I am stuck on what way of authentication would be the best, I currently have an example auth flow below where a team member signs in using Google, a refresh token gets sent in a httpOnly cookie and is stored in a variable in the client, then the token can be exchanged for an accessToken, the refresh token in the cookie also has a tokenVersion which is checked before sending an accessToken which does add some extra load to the database but can be incremented if somebody got their account stolen, before any GraphQL queries / mutations are allowed, the user's token is decoded and added to the GraphQL context so I can check the roles using graphql-shield and access the user for db operations in my queries / mutations if needed
Because I am still hitting the database even if it's only one once on page / app load I wonder if this is a good approach or if I would be better off using sessions instead
// index.ts
import "./passport"
const main = () => {
const server = fastify({ logger })
const prisma = new PrismaClient()
const apolloServer = new ApolloServer({
schema: applyMiddleware(schema, permissions),
context: (request: Omit<Context, "prisma">) => ({ ...request, prisma }),
tracing: __DEV__,
})
server.register(fastifyCookie)
server.register(apolloServer.createHandler())
server.register(fastifyPassport.initialize())
server.get(
"/auth/google",
{
preValidation: fastifyPassport.authenticate("google", {
scope: ["profile", "email"],
session: false,
}),
},
// eslint-disable-next-line #typescript-eslint/no-empty-function
async () => {}
)
server.get(
"/auth/google/callback",
{
preValidation: fastifyPassport.authorize("google", { session: false }),
},
async (request, reply) => {
// Store user in database
// const user = existingOrCreatedUser
// sendRefreshToken(user, reply) < send httpOnly cookie to client
// const accessToken = createAccessToken(user)
// reply.send({ accessToken, user }) < send accessToken
}
)
server.get("/refresh_token", async (request, reply) => {
const token = request.cookies.fid
if (!token) {
return reply.send({ accessToken: "" })
}
let payload
try {
payload = verify(token, secret)
} catch {
return reply.send({ accessToken: "" })
}
const user = await prisma.user.findUnique({
where: { id: payload.userId },
})
if (!user) {
return reply.send({ accessToken: "" })
}
// Check live tokenVersion against user's one in case it was incremented
if (user.tokenVersion !== payload.tokenVersion) {
return reply.send({ accessToken: "" })
}
sendRefreshToken(user, reply)
return reply.send({ accessToken: createAccessToken(user) })
})
server.listen(port)
}
// passport.ts
import fastifyPassport from "fastify-passport"
import { OAuth2Strategy } from "passport-google-oauth"
fastifyPassport.registerUserSerializer(async (user) => user)
fastifyPassport.registerUserDeserializer(async (user) => user)
fastifyPassport.use(
new OAuth2Strategy(
{
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: "http://localhost:4000/auth/google/callback",
},
(_accessToken, _refreshToken, profile, done) => done(undefined, profile)
)
)
// permissions/index.ts
import { shield } from "graphql-shield"
import { rules } from "./rules"
export const permissions = shield({
Mutation: {
createOneShopLocation: rules.isAuthenticatedUser,
},
})
// permissions/rules.ts
import { rule } from "graphql-shield"
import { Context } from "../context"
export const rules = {
isAuthenticatedUser: rule()(async (_parent, _args, ctx: Context) => {
const authorization = ctx.request.headers.authorization
if (!authorization) {
return false
}
try {
const token = authorization.replace("Bearer", "")
const payload = verify(token, secret)
// mutative
ctx.payload = payload
return true
} catch {
return false
}
}),
}
To answer your question directly, you want to be using jwts for access and that's it. These jwts should be created tied to a user session, but you don't want to have to manage them. You want a user identity aggregator to do it.
You are better off removing most of the code to handle user login/refresh and use a user identity aggregator. You are running into common problems of the complexity when handling the user auth flow which is why these exist.
The most common is Auth0, but the price and complexity may not match your expectations. I would suggest going through the list and picking the one that best supports your use cases:
Auth0
Okta
Firebase
Cognito
Authress
Or you can check out this article which suggests a bunch of different alternatives as well as what they focus on

In firebase, create a custom token with specific exp?

I notice that the docs specify that I can create a token to expire up to 3600 seconds later[1] But I don't see how to do that with auth().createCustomToken ... I can manually do it with jsonwektoken, but it seems like this should be addressable directly with firebase-admin library.
Another question is, what is the secret I need to verify my own token generated in this way, the uid ?
index.js
// demo server generating custom auth for firebase
import Koa from 'koa'
import Koajwt from 'koa-jwt'
import Token from './token'
const app = new Koa()
// Custom 401 handling if you don't want to expose koa-jwt errors to users
app.use(function(ctx, next){
return next().catch((err) => {
if (401 == err.status) {
ctx.status = 401
ctx.body = 'Protected resource, use Authorization header to get access\n'
} else {
throw err
}
})
})
// Unprotected middleware
app.use(function(ctx, next){
if (ctx.url.match(/^\/login/)) {
// use router , post, https to securely send an id
const conf = {
uid: 'sample-user-uid',
claims: {
// Optional custom claims to include in the Security Rules auth / request.auth variables
appid: 'sample-app-uid'
}
}
ctx.body = {
token: Token.generateJWT(conf)
}
} else {
return next();
}
});
// Middleware below this line is only reached if JWT token is valid
app.use(Koajwt({ secret: 'shared-secret' }))
// Protected middleware
app.use(function(ctx){
if (ctx.url.match(/^\/api/)) {
ctx.body = 'protected\n'
}
})
app.listen(3000);
token.js
//import jwt from 'jsonwebtoken'
import FirebaseAdmin from 'firebase-admin'
import serviceAccount from 'demo-admin-firebase-adminsdk-$$$$-$$$$$$.json'
export default {
isInitialized: false,
init() {
FirebaseAdmin.credential.cert(serviceAccount)
isInitialized = true
},
/* generateJWTprimiative (payload, signature, conf) {
// like: jwt.sign({ data: 'foobar' }, 'secret', { expiresIn: '15m' })
jwt.sign(payload, signature, conf)
} */
generateJWT (conf) {
if(! this.isInitialized)
init()
FirebaseAdmin.auth().createCustomToken(conf.uid, conf.claims)
.then(token => {
return token
})
.catch(err => {
console.log('no token generate because', err)
})
}
}
[1] https://firebase.google.com/docs/auth/admin/create-custom-tokens
You can't change the token expiration. The docs you found includes the words:
Firebase tokens comply with the OpenID Connect JWT spec, which means
the following claims are reserved and cannot be specified within the
additional claims:
... exp ...
This is further backed up by inspecting the Firebase Admin SDK source code on GitHub.
In this section:
public createCustomToken(uid: string, developerClaims?: {[key: string]: any}): Promise<string> {
// .... cut for length ....
const header: JWTHeader = {
alg: ALGORITHM_RS256,
typ: 'JWT',
};
const iat = Math.floor(Date.now() / 1000);
const body: JWTBody = {
aud: FIREBASE_AUDIENCE,
iat,
exp: iat + ONE_HOUR_IN_SECONDS,
iss: account,
sub: account,
uid,
};
if (Object.keys(claims).length > 0) {
body.claims = claims;
}
// .... cut for length ....
You can see the exp property is hard coded to be iat + ONE_HOUR_IN_SECONDS where the constant is defined elsewhere in the code as 60 * 60...
If you want to customize the expiration time, you will HAVE to create your own token via a 3rd party JWT package.
To your 2nd question, a secret is typically stored in the server environment variables, and is a pre-set string or password. Technically you could use the UID as the secret, but that would be a TERRIBLE idea security wise - please don't do this. Your secret should be like your password, keep it secure and don't upload it with your source code to GitHub. You can read more about setting and retrieving environment variables in Firebase in these docs here

Resources