Inject HttpContext into InversifyJS middleware - node.js

I have the following controller.
#controller('/users')
class UsersController {
#httpGet('/', authMiddleware({ role: 'ADMIN' }))
public get() { ... }
}
I have implemented a custom AuthenticationProvider, which returns a principal containing details about the currently authenticated user, including the user's roles.
....
return new Principal({
firstName: "John",
lastName: "Smit",
roles: ["ADMIN"]
});
...
This all works fine, but I am wondering how I can retrieve the principal from the authMiddleware which is used by the above GET route.
For now I have an ugly hack which uses internals of InversifyJS.
function authMiddlewareFactory() {
return (config: { role: string }) => {
return (
req: express.Request,
res: express.Response,
next: express.NextFunction
): void => {
const httpContext: interfaces.HttpContext =
Reflect.getMetadata(
"inversify-express-utils:httpcontext",
req
);
const principal: interfaces.Principal = httpContext.user;
if (!principal.isInRole(config.role)) {
res.sendStatus(HttpStatus.UNAUTHORIZED);
return;
}
next();
};
};
}
The custom authentication provider uses the authorization header to authenticate the user and returns a principal. I don't want to do this work again in the middleware, I just want to retrieve the principal.
This hack works, but I was wondering if someone knows a cleaner way of obtaining the HttpContext in this middleware.
I know you can access the HttpContext and thus the principal (user) if you extend from the BaseMiddleware, but then it's not clear to me how you pass configuration (parameters) to it, such as the desired role. Related to the following issue on InversifyJS.
https://github.com/inversify/InversifyJS/issues/673

This is not supported, but I can see why it is needed. We cannot pass the httpContext to the middleware as an argument because we want to keep the standard Express middleware compatible. This means that the only option is doing something like what you have done but ideally we should encapsulate it using some helper.
We need to implement something like the following getHttpContext function:
import * as express from "express";
import { getHttpContext } from "inversify-express-utils";
function authMiddlewareFactory() {
return (config: { role: string }) => {
return (
req: express.Request,
res: express.Response,
next: express.NextFunction
): void => {
const httpContext = getHttpContext(req);
const principal: interfaces.Principal = httpContext.user;
if (!principal.isInRole(config.role)) {
res.sendStatus(HttpStatus.UNAUTHORIZED);
return;
}
next();
};
};
}
Until this is implemented I don't see any problems with your implementation other than the information leakage of the inversify internals.

Related

Fastify pass custom parameter to preHandler

I'm just learning fastify and I'm not sure how to achieve what I want:
I have this route:
this.fastify.get('/ping', {
preHandler: [
this.fastify.jwtVerify,
],
}, this.configHandler.getConfiguration.bind(this.configHandler));
The pre handler does get executed and contains the known parameters like the request and the reply.
I want to pass a custom parameter to my preHandler function. Currently the preHandler is verifying the jwt token passed in the auth header. What I want to achieve is to pass scopes to the handler which may also be checked.
My preHandler currently is a plugin registered like this:
const jwtVerifyPlugin: FastifyPluginAsync = async (fastify: FastifyInstance, options: FastifyPluginOptions) => {
fastify.decorate('jwtVerify', async function (request: FastifyRequest, reply: FastifyReply) {
//jwtVerficiation happens here
//scope verification should follow
})
}
So overall: I have to add scopes somewhere at the route and I have to get those scopes somwhere inside my preHandler.
Any idea how I can do that?
Thanks!
You can define your decorate function like this:
const jwtVerifyPlugin: FastifyPluginAsync = async (fastify: FastifyInstance, options: FastifyPluginOptions) => {
fastify.decorate('jwtVerify', function (options?: { scopes?: string[] }) {
return async function (request: FastifyRequest, reply: FastifyReply, done: HookHandlerDoneFunction) {
if (options?.scopes) {
// access scopes here
}
done();
};
})
}
and then use it like this:
this.fastify.get('/ping', {
preHandler: [
this.fastify.jwtVerify({ scopes: ['admin'] }),
],
}, this.configHandler.getConfiguration.bind(this.configHandler));

Implement keycloak connect in express application

Im using Keycloak server for authorization(microservice).
I cloned this project https://github.com/w3tecch/express-typescript-boilerplate/tree/master
In my express project I'm using keycloak-connect(keycloak adapter) library
I want to implement code below in my new express-nodejs project.
reference: https://github.com/keycloak/keycloak-quickstarts/blob/latest/service-nodejs/app.js
app.get('/service/secured', keycloak.protect(), function (req, res) {
res.json({message: 'secured'});
});
I'm using routing-controllers library for express. I don't know how to use keycloak.protect() in this project. any suggestions?
So far what I managed to do(Im not sure if this is the right way):
authorizationChecker.ts
export function authorizationChecker(connection: Connection): (action: Action, roles: any[]) => Promise<boolean> | boolean {
return async function innerAuthorizationChecker(action: Action, roles: string[]): Promise<any> {
try {
const grant = await KeycloakService.keycloak.getGrant(action.request, action.response);
action.request.auth = await KeycloakService.keycloak.grantManager.userInfo(grant.access_token);
return true;
} catch (e) {
console.log(e)
return false;
}
};
}
and than I use annotation #Authorized() in controller:
#Authorized()
#JsonController('/users')
export class UserController {
}
Im using postman to test api. Im able to obtain token and i can make few requests on my api before i got authentication error.
Expire time on token is not set.

facebook-passport with NestJS

I have looked into both passport-facebook and passport-facebook-token integration with NestJS. The problem is that NestJS abstracts passport implementation with its own utilities such as AuthGuard.
Because of this, ExpressJS style implementation that's documented will not work with NestJS. This for instance is not compliant with the #nestjs/passport package:
var FacebookTokenStrategy = require('passport-facebook-token');
passport.use(new FacebookTokenStrategy({
clientID: FACEBOOK_APP_ID,
clientSecret: FACEBOOK_APP_SECRET
}, function(accessToken, refreshToken, profile, done) {
User.findOrCreate({facebookId: profile.id}, function (error, user) {
return done(error, user);
});
}
));
This blog post shows one strategy for implementing passport-facebook-token using an unfamiliar interface that isn't compliant with AuthGuard.
#Injectable()
export class FacebookStrategy {
constructor(
private readonly userService: UserService,
) {
this.init();
}
init() {
use(
new FacebookTokenStrategy(
{
clientID: <YOUR_APP_CLIENT_ID>,
clientSecret: <YOUR_APP_CLIENT_SECRET>,
fbGraphVersion: 'v3.0',
},
async (
accessToken: string,
refreshToken: string,
profile: any,
done: any,
) => {
const user = await this.userService.findOrCreate(
profile,
);
return done(null, user);
},
),
);
}
}
The problem here is that this seems to be completely unconventional to how NestJS expects you to handle a passport strategy. It is hacked together. It could break in future NestJS updates as well. There's also no exception handling here; I have no way to capture exceptions such as InternalOAuthError which gets thrown by passport-facebook-token because of the callback nature that's being utilized.
Is there a clean way to implement either one of passport-facebook or passport-facebook-token so that it'll use #nestjs/passport's validate() method? From the documentation: For each strategy, Passport will call the verify function (implemented with the validate() method in #nestjs/passport). There should be a way to pass a clientId, clientSecret in the constructor and then put the rest of the logic into the validate() method.
I would imagine the final result to look something similar to the following (this does not work):
import { Injectable } from "#nestjs/common";
import { PassportStrategy } from "#nestjs/passport";
import FacebookTokenStrategy from "passport-facebook-token";
#Injectable()
export class FacebookStrategy extends PassportStrategy(FacebookTokenStrategy, 'facebook')
{
constructor()
{
super({
clientID : 'anid', // <- Replace this with your client id
clientSecret: 'secret', // <- Replace this with your client secret
})
}
async validate(request: any, accessToken: string, refreshToken: string, profile: any, done: Function)
{
try
{
console.log(`hey we got a profile: `, profile);
const jwt: string = 'placeholderJWT'
const user =
{
jwt
}
done(null, user);
}
catch(err)
{
console.log(`got an error: `, err)
done(err, false);
}
}
}
In my particular case, I am not interested in callbackURL. I am just validating an access token that the client has forwarded to the server. I just put the above to be explicit.
Also if you are curious, the code above produces an InternalOAuthError but I have no way of capturing the exception in the strategy to see what the real problem is because it isn't implemented correctly. I know that in this particular case the access_token I am passing is invalid, if I pass a valid one, the code works. With a proper implementation though I would be able to capture the exception, inspect the error, and be able to bubble up a proper exception to the user, in this case an HTTP 401.
InternalOAuthError: Failed to fetch user profile
It seems clear that the exception is being thrown outside of the validate() method, and that's why our try/catch block is not capturing the InternalOAuthError. Handling this exception is critical for normal user experience and I am not sure what the NestJS way of handling it is in this implementation or how error handling should be done.
You're on the right track with the Strategy using extends PassportStrategy() class setup you have going. In order to catch the error from passport, you can extend the AuthGuard('facebook') and add some custom logic to handleRequest(). You can read more about it here, or take a look at this snippet from the docs:
import {
ExecutionContext,
Injectable,
UnauthorizedException,
} from '#nestjs/common';
import { AuthGuard } from '#nestjs/passport';
#Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
canActivate(context: ExecutionContext) {
// Add your custom authentication logic here
// for example, call super.logIn(request) to establish a session.
return super.canActivate(context);
}
handleRequest(err, user, info) {
// You can throw an exception based on either "info" or "err" arguments
if (err || !user) {
throw err || new UnauthorizedException();
}
return user;
}
}
Yes, this is using JWT instead of Facebook, but the underlying logic and handler are the same so it should still work for you.
In my case, I used to use the passport-facebook-token with older version of nest. To upgrade, the adjustment of the strategy was needed. I am also not interested in the callback url.
This is a working version with passport-facebook-token that uses nest conventions and benefits from dependency injection:
import { Injectable } from '#nestjs/common'
import { PassportStrategy } from '#nestjs/passport'
import * as FacebookTokenStrategy from 'passport-facebook-token'
import { UserService } from '../user/user.service'
import { FacebookUser } from './types'
#Injectable()
export class FacebookStrategy extends PassportStrategy(FacebookTokenStrategy, 'facebook-token') {
constructor(private userService: UserService) {
super({
clientID: process.env.FB_CLIENT_ID,
clientSecret: process.env.FB_CLIENT_SECRET,
})
}
async validate(
accessToken: string,
refreshToken: string,
profile: FacebookTokenStrategy.Profile,
done: (err: any, user: any, info?: any) => void,
): Promise<any> {
const userToInsert: FacebookUser = {
...
}
try {
const user = await this.userService.findOrCreateWithFacebook(userToInsert)
return done(null, user.id) // whatever should get to your controller
} catch (e) {
return done('error', null)
}
}
}
This creates the facebook-token that can be used in the controller.

NestJS / NodeJS / Passport / JWT - Stock current user

I have a NestJS backend, secured by JWT.
I would like to know what is the best way to store the actual user or the best way to pass it to my services?
I have a JwtAuthGuard
#Injectable()
export class JwtAuthGuard extends AuthGuard( 'jwt' ) {
canActivate(context: ExecutionContext) {
return super.canActivate( context );
}
handleRequest(err, user, info) {
if ( err || !user ) {
throw err || new UnauthorizedException();
}
return user;
}
}
My actual user id is in user var in handleRequest but I don't know where to "stock" it to be able to reach it in some modules.
Does anyone can help me ?
Thanks
The JWT itself is where you store the user id (or any identifying details of the user).
If you create the JWT payload with the user id ({ id: 123, ... }) the passport will set the user member to the request object.
Important: Don't store sensitive data in the JWT.
#AuthGuard( 'jwt' )
#Get('profile')
getUserId(#Request() req: any) {
return req.user.id;
}
You can pass the req.user.id to services as needed.
See: https://docs.nestjs.com/techniques/authentication#implement-protected-route-and-jwt-strategy-guards
One last thing:
If you like to have types for the request object you can do something like this
import { Request as HttpRequest } from 'express';
interface UserJwtPayload {
id: string,
}
type AuthRequest = HttpRequest & { user: UserJwtPayload }

Social Login in NestJS using #AuthGuard, Passport

So, I have almost finished attempt to implement social login in NestJS powered app. I have some problems though:
First things first. I have AuthModule and in there is provider TwitterGuard:
const twitterOptions: IStrategyOptionWithRequest = {
consumerKey: process.env[ENV.SOCIAL.TWITTER_CONSUMER_KEY],
consumerSecret: process.env[ENV.SOCIAL.TWITTER_CONSUMER_SECRET],
callbackURL: process.env[ENV.SOCIAL.TWITTER_CALLBACK_URL],
passReqToCallback: true,
includeEmail: true,
skipExtendedUserProfile: false,
};
export class TwitterGuard extends PassportStrategy(Strategy, 'twitter') {
constructor() {
super(twitterOptions);
}
// Magical nest implementation, eq to passport.authenticate
validate(req: Request, accessToken: string, refreshToken: string, profile: Profile, done: (error: any, user?: any) => void) {
const user: SocialAuthUser = {
id: profile.id,
nick: profile.username,
name: profile.displayName,
};
if (profile.emails) {
user.email = profile.emails.shift().value;
}
if (profile.photos) {
user.avatar = profile.photos.shift().value;
}
done(null, user);
}
}
as well as AuthController:
#Controller('auth')
#ApiUseTags('auth')
export class SocialAuthController {
constructor(private us: UserService) {
}
#Get('twitter')
#UseGuards(AuthGuard('twitter'))
twitter() {
throw new UnauthorizedException();
}
#Get('twitter/callback')
#UseGuards(AuthGuard('twitter'))
async twitterCallback(#ReqUser() socialUser: SocialAuthUser, #Res() response) {
const user = await this.us.registerSocialUser(socialUser);
if (user) {
// console.log('Redirect', '/some-client-route/token');
response.redirect(`${SITE_URL}/activate/${user.token}`);
}
response.sendStatus(401);
}
}
When I am calling URL /auth/twitter the guard kicks in and reroutes to Twitter page asking user to grant access to Twitter app.
If the user grants access, everything is fine, on the callback route (/auth/twitter/callback) the TwitterGuard kicks in again and processes user in validate, stores to request and I can access that further in controller. So far so good.
However if user denies access to Twitter app, the guard returns 401 on the callback route even before any of my methods are hit.
I tried to play with authenticate method that is called (now commented out in the code) where I could somehow maybe tweak this but have no idea what to return or do. If that is a way to go, how do I redirect from there to twitter auth page like passport strategy does? What to return on callback to keep going and set some flag that access was denied?
Is there any other way to do it? What am I missing?
Thanks in advance.
Edit: If you have questions what does #ReqUser() do, here it is:
export const ReqUser = createParamDecorator((data, req): any => {
return req.user;
});
Nevermind, I found a solution, this answer helped a lot. Posting here in case someone else would get into the same trouble.
I created TwitterAuthGuard:
export class TwitterAuthGuard extends AuthGuard('twitter') {
handleRequest(err, user, info, context) {
return user;
}
}
and used it at callback route:
#Get('twitter/callback')
#UseGuards(TwitterAuthGuard)
async twitterCallback(#ReqUser() socialUser: SocialAuthUser, #Res() response) {
if (socialUser) {
const user = await this.us.registerSocialUser(socialUser);
if (user) {
response.redirect(`...url`);
return;
}
}
response.redirect(SocialAuthController.authFailedUrl(LoginMethod.TWITTER));
}
Now, when Twitter calls the callback route, it gets into TwitterAuthGuard handleRequest method.
If the access was granted, user parameter contains data from user profile and is passed further down the chain to TwitterGuard validate method (see above in the question).
If the access was denied then user parameter is false.
Therefore in the controller callback route method I get either normalized user data or false in user parameter therefore I can check whether it failed or not and act accordingly.

Resources