How to integrate OpenID connect to Nest JS application - node.js

I used this documentation(https://github.com/panva/node-oidc-provider/blob/main/docs/README.md#accounts)for implementing OpenID to Nest JS. In this documentation he mentioned client_id and client secret and redirect URLS. How to get this Information's and Integrating

One option is to create an oidc strategy for passport.
It's a lengthy process, and rather than copying/pasting an entire tutorial, I'll add a link and hope it doesn't break.
https://sdoxsee.github.io/blog/2020/02/05/cats-nest-nestjs-mongo-oidc.html
Here's the strategy implementation, but there are several other components that need to be configured.
// auth/oidc.strategy.ts
import { UnauthorizedException } from '#nestjs/common';
import { PassportStrategy } from '#nestjs/passport';
import { Strategy, Client, UserinfoResponse, TokenSet, Issuer } from 'openid-client';
import { AuthService } from './auth.service';
export const buildOpenIdClient = async () => {
const TrustIssuer = await Issuer.discover(`${process.env.OAUTH2_CLIENT_PROVIDER_OIDC_ISSUER}/.well-known/openid-configuration`);
const client = new TrustIssuer.Client({
client_id: process.env.OAUTH2_CLIENT_REGISTRATION_LOGIN_CLIENT_ID,
client_secret: process.env.OAUTH2_CLIENT_REGISTRATION_LOGIN_CLIENT_SECRET,
});
return client;
};
export class OidcStrategy extends PassportStrategy(Strategy, 'oidc') {
client: Client;
constructor(private readonly authService: AuthService, client: Client) {
super({
client: client,
params: {
redirect_uri: process.env.OAUTH2_CLIENT_REGISTRATION_LOGIN_REDIRECT_URI,
scope: process.env.OAUTH2_CLIENT_REGISTRATION_LOGIN_SCOPE,
},
passReqToCallback: false,
usePKCE: false,
});
this.client = client;
}
async validate(tokenset: TokenSet): Promise<any> {
const userinfo: UserinfoResponse = await this.client.userinfo(tokenset);
try {
const id_token = tokenset.id_token
const access_token = tokenset.access_token
const refresh_token = tokenset.refresh_token
const user = {
id_token,
access_token,
refresh_token,
userinfo,
}
return user;
} catch (err) {
throw new UnauthorizedException();
}
}
}

You get the client-id and secret from the openid connect provider. You add/register the client there.
Redirect URL is the URL to the openid connect client, to what URL the authorization code should be sent to after a successful authentication. This URL is hardcoded in the provider.

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.

Nestjs Passport google Oauth2 with custom JWT

What I need to achieve -
I need to have a dynamic redirect URL (not google's refer Current Flow last step) based on the query param sent by Frontend.
I need to send my custom JWT token instead of google token which can have roles and permission in it. (Not sure if we can add claims to google token as well)
In my app, I have 2 roles - candidate, and recruiter. I need to use Gmail auth and create a user in my DB according to roles, which again I could achieve via query param pass by Frontend.
Current Flow -
Frontend calls google/login -> GoogleAuthGaurd -> GoogleStrategy -> google/redirect -> Generate custom JWT token -> redirect to frontend with access token and refresh token in URL.
Problem -
In Passport, we have GoogleAuthGaurd, and GoogleStrategy. I have read somewhere that Auth Gaurd decides which strategy to be used and it internally calls the strategy and further execution.
If I pass query param to google/login it totally ignores it and redirects to strategy. We can access contecxt (ExecutionContext) in AuthGaurd, so we can get query param there but how to pass it to strategy? or may be invoke custom strategy from auth guard not sure if we can.
Is there any way I could pass the query param to strategy then I could write a logic to update the redirect URI or roles?
import { TokenResponsePayload } from '#identity/payloads/responses/token-response.payload';
import { Controller, Get, Inject, Req, Res, UseGuards } from '#nestjs/common';
import { ApiTags } from '#nestjs/swagger';
import { Request, Response } from 'express';
import {
AuthServiceInterface,
AuthServiceSymbol,
} from '../interfaces/services/auth-service.interface';
import { AccessTokenGaurd } from '../utils/access-token.guard';
import { GoogleAuthGaurd } from '../utils/google-auth-guard';
import { RefreshTokenGuard } from '../utils/refresh-token.guard';
#ApiTags('Auth')
#Controller('auth')
export class AuthController {
constructor(
#Inject(AuthServiceSymbol)
private authService: AuthServiceInterface,
) {}
#Get('google/login')
#UseGuards(GoogleAuthGaurd)
handleGoogleLogin() {}
#Get('google/redirect')
#UseGuards(GoogleAuthGaurd)
async handleGoogleRedirect(#Req() req, #Res() res: Response) {
const tokens = await this.authService.signInWithGoogle(req);
res.redirect(302,`http://127.0.0.1:4200?access_token=${tokens.accessToken}&refresh_token=${tokens.refreshToken}`)
}
#Get('logout')
#UseGuards(AccessTokenGaurd)
async remove(#Req() req: Request): Promise<void> {
return this.authService.removeSession(req.user['sessionId']);
}
#UseGuards(RefreshTokenGuard)
#Get('refresh')
async refreshToken(#Req() req: Request): Promise<TokenResponsePayload> {
const sessionId = req.user['sessionId'];
const refreshToken = req.user['refreshToken'];
return this.authService.refreshTokens(sessionId, refreshToken);
}
}
import { Injectable } from '#nestjs/common'; import { AuthGuard } from '#nestjs/passport';
#Injectable() export class GoogleAuthGaurd extends AuthGuard('google') {}
import { CalConfigService, ConfigEnum } from '#cawstudios/calibrate.common';
import { Injectable } from '#nestjs/common';
import { PassportStrategy } from '#nestjs/passport';
import { Profile, Strategy } from 'passport-google-oauth20';
import { VerifiedCallback } from 'passport-jwt';
const configService = new CalConfigService();
#Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
clientID: configService.get(ConfigEnum.CLIENT_ID),
clientSecret: configService.get(ConfigEnum.CLIENT_SECRET),
callbackURL: configService.get('CALLBACK_URL'),
scope: ['profile', 'email'],
});
}
async validate(
accessToken: string,
refreshToken: string,
profile: Profile,
done: VerifiedCallback,
): Promise<any> {
const email = profile.emails[0].value;
done(null, email);
}
}

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

Nestjs + Passport: Prevent user 1 to access information of user 2

How can I prevent user 1 to access information of user 2 using passport in a Nesjs app ?
I already have 2 strategies:
the local strategy which validate a user with email/password. The route protected by this strategy return a jwt token.
the jwt strategy which validate the given jwt token.
Now, I want to restrict access to routes such as users/:id to jwt token which actually have the same userId encrypted.
How to do that ?
EDIT
I was mixing Authentication and Authorization: what I want to achieve is about authorization, once the user has been authenticated.
I had to use Guard:
own.guard.ts
#Injectable()
export class OwnGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const req = context.switchToHttp().getRequest();
return req.user.id === req.params.id;
}
}
Then use it in my route:
#Get(':id')
#UseGuards(OwnGuard)
async get(#Param('id') id: string) {
return await this.usersService.get(id);
}
ORIGINAL ANSWER
What I did was to create a third strategy based on the jwt one:
#Injectable()
export class OwnStrategy extends PassportStrategy(Strategy, 'own') {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: SECRET,
passReqToCallback: true
});
}
async validate(req: Request, payload: { sub: string }) {
if (req.params.id !== payload.sub) {
throw new UnauthorizedException();
}
return { userId: payload.sub };
}
}
Note how I pass the custom name 'own' as second parameter of PassportStrategy to differentiate it from the 'jwt' one. Its guard:
#Injectable()
export class OwnAuthGuard extends AuthGuard('own') {}
This works but I wonder if it is the good way of doing it...
What if later I want to able user modification for admin users ?
Should I create a forth strategy which check if role === Role.ADMIN || req.params.id === payload.sub ?
I think I'm missing something. There should be a way to create a strategy which validate only the jwt, another one only the userId, another one only the role, and combine them as I want when applying guards to my routes.
same case. you can use handleRequest method in guard.
here you can access user auth and req, then doing validation for resource appropriate. check out my code
#Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
canActivate(context: ExecutionContext) {
return super.canActivate(context);
}
handleRequest(err, user, info, context: ExecutionContext) {
const request = context.switchToHttp().getRequest<Request>();
const params = request.params;
if (user.id !== +params.id) {
throw new ForbiddenException();
}
return user;
}
}
look more here https://docs.nestjs.com/security/authentication#extending-guards

I'm using a passport-jwt auth strategy in my nestJS app (with authGuard), how to get access to the token payload in my controller?

I'm trying to get access to the jwt payload in a route that is protected by an AuthGuard.
I'm using passport-jwt and the token payload is the email of the user.
I could achieve this by runing the code bellow:
import {
Controller,
Headers,
Post,
UseGuards,
} from '#nestjs/common';
import { JwtService } from '#nestjs/jwt';
import { AuthGuard } from '#nestjs/passport';
#Post()
#UseGuards(AuthGuard())
async create(#Headers() headers: any) {
Logger.log(this.jwtService.decode(headers.authorization.split(' ')[1]));
}
I want to know if there's a better way to do it?
Your JwtStrategy has a validate method. Here you have access to the JwtPayload. The return value of this method will be attached to the request (by default under the property user). So you can return whatever you need from the payload here:
async validate(payload: JwtPayload) {
// You can fetch additional information if needed
const user = await this.userService.findUser(payload);
if (!user) {
throw new UnauthorizedException();
}
return {user, email: payload.email};
}
And then access it in you controller by injecting the request:
#Post()
#UseGuards(AuthGuard())
async create(#Req() request) {
Logger.log(req.user.email);
}
You can make this more convenient by creating a custom decorator:
import { createParamDecorator } from '#nestjs/common';
export const User = createParamDecorator((data, req) => {
return req.user;
});
and then inject #User instead of #Req.

Resources