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

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

Related

Firebase functions '__session' cookie stopped working

I am using firebase functions for instagram authentication for my android app.
This is my flow:
User access my "connect" api with param that states its user name (..... ?username=someusername)
"Connect" saves the username as a cookie called "__session" (as this is the only cookie allowed by functions).
"connect" redirects 'https://api.instagram.com' which asks the user for permission to give me their details.
After user agrees, Instagram redirects to my function called "onredirect"
I store the user token in realtime data base along side with the user name found in "__session" cookie.
All of this worked all in well for a month or two but suddenly "redirect" function can no longer retrieve the "__session" cookie (it's empty).
I'm not sure what has changed or possibly what im doing wrong (it used to work and the code has not changed), would be happy for you help.
Code:
exports.connectinsta = functions.https.onRequest(async (req, res) => {
functions.logger.log('connectinsta query:', req.query);
functions.logger.log('connectinsta query:', req.query.account);
if(!req.query.account){
res.send("No user account provided in query")
} else {
cookieParser()(req, res, () => {
const oauth2 = instagramOAuth2Client2();
res.setHeader('Cache-Control', 'private');
const redirectUri = oauth2.authorizationCode.authorizeURL({
redirect_uri: OAUTH_REDIRECT_URI_Y,
scope: OAUTH_SCOPES,
client_id: functions.config().instagram.client_id,
});
res.cookie('__session', req.query.account, {
maxAge: 3600000,
secure: true,
httpOnly: true,
});
res.redirect(redirectUri)
});
}
});
exports.onredirect = functions.https.onRequest(async (req, res) => {
try {
cookieParser()(req, res, async () => {
functions.logger.log('onredirect cookies:', req.cookies);
functions.logger.log('onredirect cookies session:', req.cookies.__session);
const session_id = req.cookies.__session <--- NO LONGER WORKS
const oauth2 = instagramOAuth2Client();
const results = await oauth2.authorizationCode.getToken({
client_id: functions.config().instagram.client_id,
client_secret: functions.config().instagram.client_secret,
grant_type: "authorization_code",
code: req.query.code,
redirect_uri: OAUTH_REDIRECT_URI_Y
});
const accessToken = results.access_token;
functions.logger.log('EExperiment:', accessToken);
var result_long = await get_long_token_promise(accessToken)
functions.logger.log('Long EExperiment:', result_long.access_token);
save_code(session_id, result_long.access_token)
res.send("Success! Return to app")
});
} catch(error) {
functions.logger.error("onredirect error1", error)
return res.jsonp({
error: error.toString(),
});
}
});

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).
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);

Passing JWT to UI application after Google Oauth2 login

I have created a MERN application with a separate backend and frontend. I have added support for Google Oauth2 login using passport-google-oauth20 npm package.
So I have exposed an end point in the backend as follows:
class AccountAPIs {
constructor() { }
redirectToSuccess(req, res) {
const accountServiceInst = AccountService.getInst();
let account = req.session.passport.user;
let filePath = path.join(__dirname + '../../../public/views/loginSuccess.html');
let jwt = accountServiceInst.generateJWT(account);
// how do I send this jwt to ui application
res.sendFile(filePath);
}
loadMappings() {
return {
'/api/auth': {
'/google': {
get: {
callbacks: [
passport.authenticate('google', { scope: ['profile', 'email'] })
]
},
'/callback': {
get: {
callbacks: [
passport.authenticate('google', { failureRedirect: '/api/auth/google/failed' }),
this.redirectToSuccess
]
}
},
'/success': {
get: {
callbacks: [this.successfulLogin]
}
}
}
}
};
}
}
Here is the passport setup for reference:
let verifyCallback = (accessToken, refreshToken, profile, done) => {
const accountServiceInst = AccountService.getInst();
return accountServiceInst.findOrCreate(profile)
.then(account => {
return done(null, account);
})
.catch(err => {
return done(err);
});
};
let googleStrategyInst = new GoogleStrategy({
clientID: serverConfig.auth.google.clientId,
clientSecret: serverConfig.auth.google.clientSecret,
callbackURL: 'http://localhost/api/auth/google/callback'
}, verifyCallback);
passport.use(googleStrategyInst);
In the UI application, on button click I am opening a new window which opens the '/api/auth/google' backend API. After authenticating with a google account, the window redirects to the '/api/auth/google/callback' backend API where I am able to generate a JWT. I am unsure about how to transfer this JWT to the frontend application since this is being opened in a separate window.
I know that res.cookie('jwt', jwt) is one way to do it. Please suggest the best practices here..
There are two ways to pass the token to the client :
1- you put the token into a cookie as you have mentioned
2-you pass the token in the redirect URL to the client as a parameter "CLIENT_URL/login/token", that you can extract the token in your front-end client

i want to get my jwt value from cookies in browser

i have now stored my jwt in cookies when user sign in or sign up but the data don't stay so i made a function to handle this but i need the value of the token to make it work
this is the function that i need token value for
const setAuthToken = (token) => {
if (token) {
axios.defaults.headers.common['x-auth-token'] = token;
} else {
delete axios.defaults.headers.common['x-auth-token'];
}
};
and this is my action that i use in react to send the token value to this function i tried to use js-cookies for that but it give me undefined
import Cookies from 'js-cookie';
//load user
export const loadUser = () => async (dispatch) => {
const token = Cookies.get('access_token');
console.log(token);
// if (cookie.access_token) {
// setAuthToken(cookie.access_token);
// }
try {
const res = await axios.get('/user/me');
dispatch({
type: USER_LOADED,
payload: res.data,
});
} catch (err) {
dispatch({
type: AUTH_ERROR,
});
}
};
and this is my recieved cookie in browser
If you take a close look at your screenshot, you can see that the cookie is sent by the server as HttpOnly. This is a security measure, and therefore the cookie isn't accessible to any JavaScript code by design.
See https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#Restrict_access_to_cookies
If you are in control of the server, you could change it accordingly, if not you will have to make a deal :-)
res.cookie('x-auth-token',token,{
maxAge: 3600,
httpOnly: true,
secure:true
})

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