import * as fs from "node:fs/promises";
import { Controller, Get, Param, Req } from "@nestjs/common";
import { getUser } from "./auth";
@Controller("api")
export class TodosController {
@Get("todos")
async getTodos(@Req() req) {
const user = await getUser({ req });
const json = await fs.readFile(`todos_${user.id}.json`, "utf8");
return JSON.parse(json);
}
@Get("todos-for-support/:userId")
async getTodosForSupportStaff(
@Req() req,
@Param("userId") userId: string
) {
// Will reject the request if user making the request
// doesn't have "support-staff" role.
await getUser({ req, requiredRole: "support-staff" });
const json = await fs.readFile(`todos_${userId}.json`, "utf8");
return JSON.parse(json);
}
}
src/auth.ts
import { BadRequestException, ForbiddenException, UnauthorizedException } from "@nestjs/common";
import { oidcSpa, extractRequestAuthContext, type AnyRequest } from "oidc-spa/server";
import { z } from "zod";
const { bootstrapAuth, validateAndDecodeAccessToken } = oidcSpa
.withExpectedDecodedAccessTokenShape({
decodedAccessTokenSchema: z.object({
sub: z.string(),
// Keycloak specific, convention to manage authorization.
realm_access: z
.object({
roles: z.array(z.string())
})
.optional()
})
})
.createUtils();
export { bootstrapAuth };
export type User = {
id: string;
};
export async function getUser(params: {
// This can be an Express Request object, a FastifyRequest object
// or really any well know object that represent a request,
// oidc-spa will normalize the representation internally.
// so this function will work regardless of the HTTP framework
// you're using to bootstrap your NestJS app.
req: AnyRequest;
requiredRole?: "realm-admin" | "support-staff";
}): Promise<User> {
const { req, requiredRole } = params;
const requestAuthContext = extractRequestAuthContext({
request: req,
// Set this to false only if you don't have a reverse HTTP proxy in front of your
// server. (Almost never the case in modern deployments).
trustProxy: true
});
if (!requestAuthContext) {
console.warn("Anonymous request");
throw new UnauthorizedException();
}
if (!requestAuthContext.isWellFormed) {
console.warn(requestAuthContext.debugErrorMessage);
throw new BadRequestException();
}
const { isSuccess, debugErrorMessage, decodedAccessToken } =
await validateAndDecodeAccessToken(
requestAuthContext.accessTokenAndMetadata
);
if (!isSuccess) {
console.warn(debugErrorMessage);
throw new UnauthorizedException();
}
if (requiredRole) {
if (!decodedAccessToken.realm_access?.roles.includes(requiredRole)) {
console.warn(`User missing role: ${requiredRole}`);
throw new ForbiddenException();
}
}
return { id: decodedAccessToken.sub };
}