In a keycloak context I am using Insomnia to get a token and send a post request to my nestjs app. I use this function to validate the token
async validateToken(token: string): Promise<boolean> {
try {
// Make a request to the Keycloak token endpoint to validate the token
// Decode the token to extract its claims
const decodedToken: CustomJwtPayload = jwtDecode(token);
console.log(Date());
console.log(decodedToken);
// Validate the token signature (verify it with Keycloak public key)
const publicKeyResponse = await axios.get<PublicKeyResponse>(
`${process.env.KEYCLOAK_BASE_URL}/protocol/openid-connect/certs`,
);
const publicKey = publicKeyResponse.data.keys[0].x5c[0];
// Verify the token signature with the public key
const verifiedToken = jwt.verify(token, publicKey, {
algorithms: ['RS256'],
});
console.log(verifiedToken);
if (!verifiedToken) {
return false;
}
// Check token expiry
const currentTime = Math.floor(Date.now() / 1000);
if (decodedToken.exp && decodedToken.exp < currentTime) {
return false; // Token expired
}
// Verify token issuer
if (decodedToken.notes.iss !== `${process.env.KEYCLOAK_BASE_URL}`) {
return false; // Invalid issuer
}
// Check token audience
if (decodedToken.aud !== process.env.KEYCLOAK_CLIENT_ID) {
return false; // Invalid audience
}
// Token is valid
return true;
} catch (error) {
console.error('Error validating token:', error);
return false;
}
}
}
Yet I am having this error
Error validating token: JsonWebTokenError: invalid token
it's not verbose enough and I can not get my mind around it.
Does anyone have any idea how to get through ?
EDIT: now that both my db and keycloak are running on the same Docker, here is my app structure
my-nestjs-project/
│
├── src/
│ ├── auth/
│ ├── catApi/
│ │ ├── catApi.controller.spec.ts
│ │ ├── catApi.controller.ts
│ │ ├── catApi.module.ts
│ │ ├── catApi.service.spec.ts
│ │ └── catApi.service.ts
│ ├── job/
│ │ ├── dto
│ │ ├── job.controller.ts
│ │ ├── job.module.ts
│ │ └── job.service.ts
│ ├── keycloak/
│ │ ├── keycloak.guard.spec.ts # ??? nothing is happening here
│ │ ├── keycloak.guard.ts # guarding routes in the backend
│ │ ├── keycloak.module.ts
│ │ └── keycloak.service.ts # Keycloak logic to discover keycloak issuer conf, validatetoken
│ ├── prisma
│ ├── webapp
│ │ ├── dto
│ │ ├── job.controller.ts
│ │ ├── job.module.ts
│ │ └── job.service.ts
│ │
│ └── ... # Other pages (e.g., app.module.ts, main.ts, etc.)
│
├── node_modules/ # Node modules (not manually edited)
│
├── .env.local # Environment variables (e.g., Keycloak URL, client ID)
│
├── styles/ # Global styles, CSS modules, etc.
│
├── docker-compose.yml # Keycloak Docker Compose File
│
├── package.json # Project metadata and dependencies
│
└── ...
Job and Webapp are both business logic and have db tables, they will be the protected routes when I succeed. I am using catApi to hit catfacts and get some fun and quirky content with Insomnia, this route is currently protected
@Get('facts/cats')
@UseGuards(KeycloakAuthGuard)
getCatFacts() {
this.apiService.getCatFactsWithAxiosLib();
}
and ask for cat fact like this
and that's when it runs through my function above and send me back the error
Decode the Token: The JWT token is decoded to extract its payload, which includes information like the issuer, audience, and expiry time.
Verify Issuer and Audience: The code checks if the token's issuer (iss) and authorized party (azp) match the expected values, ensuring it's from a trusted source and intended for the correct audience.
Check Expiry: The token's expiry time (exp) is compared against the current time to ensure it hasn't expired.
Fetch Public Key: The code retrieves the public key from the Keycloak server, which is necessary for the next step.
Signature Verification: Using the obtained public key, the code verifies the token's signature to ensure it's valid and hasn't been tampered with.
This code will work
Token generator by Keycloak
Install Docker Desktop in Windows
Or In Mac
Save as docker-compose.yml
More detail information in here
version: '3.7'
services:
postgres:
image: postgres
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: password
keycloak:
image: quay.io/keycloak/keycloak:latest # Update to the latest Keycloak image
command: start-dev
environment:
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://postgres/keycloak
KC_DB_USERNAME: keycloak
KC_DB_PASSWORD: password
KC_HTTP_ENABLED: true # Enable HTTP if you're not using HTTPS
KC_HEALTH_ENABLED: true
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin
ports:
- 8080:8080
restart: always
depends_on:
- postgres
volumes:
postgres_data:
driver: local
In root directory
docker compose up
http://localhost:8080/
Token Endpoint
POST http://localhost:8080/realms/my-nestjs-app/protocol/openid-connect/token
At x-www-form-urlencoded
tab
client_id : admin-cli
username : admin
password : 1234
grant_type : password
Each file code
auth.controller.ts
import {Body, Controller, Post} from '@nestjs/common';
import {KeycloakService} from '../keycloak/keycloak.service';
@Controller('auth')
export class AuthController {
constructor(private keycloakService: KeycloakService) {}
@Post('validateToken')
async validateToken(@Body() body) {
const {token} = body;
return await this.keycloakService.validateToken(token);
}
}
auth.module.ts
import {Module} from '@nestjs/common';
import {KeycloakModule} from '../keycloak/keycloak.module';
import {AuthController} from './auth.controller';
import {AuthService} from './auth.service';
@Module({
imports: [KeycloakModule],
providers: [AuthService],
controllers: [AuthController]
})
export class AuthModule {
}
cat-api.module
import {HttpModule} from '@nestjs/axios';
import {Module} from '@nestjs/common';
import {KeycloakModule} from '../keycloak/keycloak.module';
import {CatApiController} from './catApi.controller';
import {CatApiService} from './catApi.service';
@Module({
imports: [
KeycloakModule,
HttpModule,
],
controllers: [CatApiController],
providers: [CatApiService],
})
export class CatApiModule {
}
catApi.controller.ts
import { Controller, Get, UseGuards } from '@nestjs/common';
import { CatApiService } from './catApi.service';
import { KeycloakAuthGuard } from '../keycloak/keycloak.guard';
@Controller('facts')
export class CatApiController {
constructor(private catApiService: CatApiService) {}
@Get('cats')
@UseGuards(KeycloakAuthGuard)
async getCatFacts() {
return this.catApiService.getCatFactsWithAxiosLib();
}
}
catApi.service.ts
import {HttpService} from '@nestjs/axios';
import {Injectable} from '@nestjs/common';
import {AxiosResponse} from 'axios';
import {Observable} from 'rxjs';
import {map} from 'rxjs/operators';
@Injectable()
export class CatApiService {
constructor(private httpService: HttpService) {}
async getCatFactsWithAxiosLib(): Promise<any> {
try {
// Replace 'external-api-url' with the actual API URL
const response: Observable<AxiosResponse<any>> =
this.httpService.get('https://catfact.ninja/breeds?limit=1');
return response
.pipe(map((axiosResponse) => {
return {
data: axiosResponse.data,
status: axiosResponse.status,
};
}))
.toPromise();
} catch (error) {
// Handle error (e.g., log it, format it, etc.)
console.error('Error fetching cat facts:', error);
throw new Error('Failed to fetch cat facts');
}
}
}
keycloak.guard.ts
import {CanActivate, ExecutionContext, Injectable} from '@nestjs/common';
import {Observable} from 'rxjs';
import {KeycloakService} from './keycloak.service';
@Injectable()
export class KeycloakAuthGuard implements CanActivate {
constructor(private keycloakService: KeycloakService) {}
canActivate(
context: ExecutionContext,
): boolean|Promise<boolean>|Observable<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.keycloakService.extractToken(request);
if (!token) {
// Handle the case where the token is not provided
return false;
}
return this.keycloakService.validateToken(token);
}
}
keycloak.module.ts
import { HttpModule } from '@nestjs/axios';
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { KeycloakService } from './keycloak.service';
@Module({
imports: [HttpModule, ConfigModule],
providers: [KeycloakService],
exports: [KeycloakService],
})
export class KeycloakModule {
}
keycloak.service.ts
import {HttpService} from '@nestjs/axios';
import {Injectable} from '@nestjs/common';
import axios from 'axios';
import * as jwt from 'jsonwebtoken';
@Injectable()
export class KeycloakService {
constructor(private httpService: HttpService) {}
extractToken(request: any): string|null {
const authHeader = request.headers.authorization;
if (authHeader) {
return authHeader.split(' ')[1]; // Assumes Bearer token format
}
return null;
}
async validateToken(token: string): Promise<boolean> {
const keycloakUrl = 'http://localhost:8080';
const realm = 'my-nestjs-app';
const clientId = 'admin-cli';
try {
if (!token) {
console.error('Token not provided');
return false;
}
const decodedToken = jwt.decode(token, {complete: true});
const publicKeyResponse = await axios.get(
`${keycloakUrl}/realms/${realm}/protocol/openid-connect/certs`);
const signingKey = publicKeyResponse.data.keys.find(
key => key.use === 'sig' && key.alg === 'RS256');
if (!signingKey) {
return false; // Signing key not found
}
const pemStart = '-----BEGIN CERTIFICATE-----\n';
const pemEnd = '\n-----END CERTIFICATE-----';
const pem = pemStart + signingKey.x5c[0] + pemEnd;
if (decodedToken.payload.iss !== `${keycloakUrl}/realms/${realm}`) {
return false; // Invalid issuer
}
if (decodedToken.payload.azp !== clientId) {
return false; // Invalid audience
}
const currentTime = Math.floor(Date.now() / 1000);
if (decodedToken.payload.exp < currentTime) {
return false; // Token expired
}
jwt.verify(token, pem, {algorithms: ['RS256']});
return true;
} catch (error) {
console.error('Error validating token:', error);
return false; // Error in validating token
}
}
}
app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AuthModule } from './auth/auth.module';
import { CatApiModule } from './cat-api/cat-api.module';
import { JobModule } from './job/job.module';
import { WebappModule } from './webapp/webapp.module';
import { KeycloakModule } from './keycloak/keycloak.module';
@Module({
imports: [AuthModule, CatApiModule, JobModule, WebappModule, KeycloakModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableCors(); // Enables CORS for all routes
await app.listen(3000);
}
bootstrap();
npm install axios jsonwebtoken reflect-metadata rxjs
npm run start
Get Call with token.
GET http://localhost:3000/facts/cats
The response body data get from this URL'data
'https://catfact.ninja/breeds?limit=1'
getCatFactsWithAxiosLib() at catApi.service.ts
file
Forwarding by this code.
@Controller('facts')
export class CatApiController {
constructor(private catApiService: CatApiService) {}
@Get('cats')
@UseGuards(KeycloakAuthGuard)
async getCatFacts() {
return this.catApiService.getCatFactsWithAxiosLib();
}
}
Retrieve JWKS URI: The code accesses the JWKS (JSON Web Key Set) URI, a standard URL provided by the authentication server (like Keycloak), which contains the public keys.
Fetch Public Keys: It then makes an HTTP request to this URI to fetch the set of public keys, one of which will be used to verify the JWT token's signature.
http://localhost:8080/realms/master/protocol/openid-connect/certs
{
"issuer": "http://localhost:8080/realms/my-nestjs-app",
"authorization_endpoint": "http://localhost:8080/realms/my-nestjs-app/protocol/openid-connect/auth",
"token_endpoint": "http://localhost:8080/realms/my-nestjs-app/protocol/openid-connect/token",
"introspection_endpoint": "http://localhost:8080/realms/my-nestjs-app/protocol/openid-connect/token/introspect",
"userinfo_endpoint": "http://localhost:8080/realms/my-nestjs-app/protocol/openid-connect/userinfo",
"end_session_endpoint": "http://localhost:8080/realms/my-nestjs-app/protocol/openid-connect/logout",
"frontchannel_logout_session_supported": true,
"frontchannel_logout_supported": true,
"jwks_uri": "http://localhost:8080/realms/my-nestjs-app/protocol/openid-connect/certs",
"check_session_iframe": "http://localhost:8080/realms/my-nestjs-app/protocol/openid-connect/login-status-iframe.html",
"grant_types_supported": [
"authorization_code",
"implicit",
"refresh_token",
"password",
"client_credentials",
"urn:openid:params:grant-type:ciba",
"urn:ietf:params:oauth:grant-type:device_code"
],
// cut-off
Define Certificate Boundaries: The code defines the starting (pemStart) and ending (pemEnd) boundaries of a PEM formatted certificate.
Extract Key: It extracts the first certificate (x5c[0]) from the signingKey object, which is part of the JSON Web Key Set (JWKS) retrieved from the authentication server.
Create PEM Format: These components are concatenated to create a full PEM-formatted public key (pem), which is used for verifying JWT signatures.
const pemStart = '-----BEGIN CERTIFICATE-----\n';
const pemEnd = '\n-----END CERTIFICATE-----';
const pem = pemStart + signingKey.x5c[0] + pemEnd;