Implement keycloak connect in express application - node.js

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.

Related

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

Authentication with passport.js and HttpOnly cookies with supertest is not working

and thanks for the tool 😊 .
I'm working on a Nestjs (v8.x) application relying on Passport.js with a JWT strategy through HttpOnly cookies. For testing the different endpoints of my app, I'm using supertest (v6.1.x).
In order to reach certain endpoints, I need to get an authentication cookie set after submitting credentials. When playing with the UI, everything is working correctly and I can get the data I want. However, when trying to create an automated test based on that, it does not work (at all).
My tests looks like following:
it('gives the current user when the token is valid', async () => {
const cookieToken = await authenticate(app);
const { body: user } = await request(app.getHttpServer())
.get('/users/me')
.set('Cookie', cookieToken);
expect(user.email).toEqual('joe.doe#gmail.com');
expect(user.fullname).toEqual('Joe Doe');
expect(user.uuid).toBeTruthy();
expect(user.password).toBeFalsy();
});
The authenticate function is a helper that looks like:
export const authenticate = async (
app: INestApplication,
username = 'joe.doe#gmail.com',
password = 'password',
) => {
const res = await request(app.getHttpServer()).post('/auth/login').send({
username,
password,
});
// This cookie resolves correctly
const cookieWithToken = res.headers['set-cookie'];
return cookieWithToken;
};
This tests fails and I got a 401 response from my JWT strategy. The code for this strategy looks like:
#Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromExtractors([
(request: Request) => {
let data = request?.cookies?.['auth-cookie'];
// In the test scenario, request.cookies is always undefined
if (!data) {
throw new UnauthorizedException();
}
return data.access_token;
},
]),
ignoreExpiration: false,
secretOrKey: jwtConstants.secret,
});
}
async validate(payload: any) {
// Not important for this issue
}
}
During the test run, request?.cookies is undefined and never set, thus throws the 401 error to the caller.
I don't know what is going but it looks like something is wrong on the way I set it.
I've tried the following approaches:
Adding withCredentials (cuz, who knows...)
Nesting the second call inside the callback resolving the login call in case there's a shared context between the requests
Relying on the same agent to make both the calls instead of directly calling superagent
But still, without success :(
Do you have any ideas?

how to i access the request.user nestjs microservice?

I have a microservice setup with an api gateway accepting http requests and passing them to miscroservices (media-service, payment-service, etc.) using message pattern.
I can access the request.user in the api-gateway which gives me the authenticated user from the decoded jwt.
What i would like is, to pass this to the media microservice and access it within the functions in media service.
How do i achieve this?
controller in api-gateway
export class MediasController {
constructor(#Inject('MEDIA_SERVICE') private client: ClientProxy) { }
#Get()
#UseGuards(AuthGuard('jwt'))
async find(#Query() query: QueryArgs): Promise<any> {
const pattern = { cmd: 'media-find' };
return await this.client.send<any>(pattern, query)
}
}
controller & service in media microservice
medias.controller.ts
#Controller({ path: 'medias', scope: Scope.REQUEST })
export class MediasController {
constructor(private readonly mediasService: MediasService) { }
#MessagePattern({ cmd: 'media-find' })
async find(query: any): Promise<any> {
return await this.mediasService.find(query)
}
}
medias.service.ts
export class MediasService {
constructor(#InjectRepository(Media) private readonly mediaRepository: Repository<Media>) {
}
async find(query): Promise<Media[]> {
// access request.user here
return await this.mediaRepository.find(query);
}
}

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.

Inject HttpContext into InversifyJS middleware

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.

Resources