I am encountering an issue with Keycloak integration in my NestJS application. My environment uses Docker for orchestrating services. Here is an overview of my configuration and relevant files:
Context
-
Dockerfile:
FROM node:18-alpine WORKDIR /app COPY package.json ./ COPY package-lock.json ./ RUN npm install COPY . . RUN npx prisma generate RUN npm run build EXPOSE 3000 CMD ["node", "dist/main.js"]
-
docker-compose.yml:
version: '3.8' services: nest-app: build: context: . dockerfile: Dockerfile ports: - "3000:3000" environment: DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/mydatabase" depends_on: postgres: condition: service_healthy networks: - my-network postgres: image: postgres:latest environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: mydatabase ports: - "5432:5432" volumes: - postgres_data:/var/lib/postgresql/data networks: - my-network healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 10s timeout: 5s retries: 5 keycloak: image: quay.io/keycloak/keycloak:latest environment: DB_VENDOR: h2 KEYCLOAK_ADMIN: admin KEYCLOAK_ADMIN_PASSWORD: admin KEYCLOAK_IMPORT: /tmp/realm.json ports: - "8081:8080" volumes: - ./realm.json:/tmp/realm.json entrypoint: ["/opt/keycloak/bin/kc.sh"] command: ["start-dev"] networks: - my-network volumes: postgres_data: networks: my-network: driver: bridge
-
app.module.ts:
import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { AuthGuard, KeycloakConnectModule, ResourceGuard, RoleGuard, } from 'nest-keycloak-connect'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { PrismaModule } from './prisma.module'; import { APP_GUARD } from '@nestjs/core'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, }), PrismaModule, KeycloakConnectModule.register({ authServerUrl: 'http://127.0.0.1:8081/', realm: 'test', clientId: 'my-nest-project', secret: 'GSLUjhMMctvip6B01q3g7xu6P8SJBvT6', // or `credentials.secret` if used }), ], controllers: [AppController], providers: [ AppService, { provide: APP_GUARD, useClass: AuthGuard, }, { provide: APP_GUARD, useClass: ResourceGuard, }, { provide: APP_GUARD, useClass: RoleGuard, }, ], }) export class AppModule {}
-
app.service.ts:
import { Injectable } from '@nestjs/common'; @Injectable() export class AppService { getHello(): string { return 'Hello, World!'; } getProtectedMessage(): string { return 'This is a protected route'; } getTestMessage(): string { return 'This is a test route'; } }
-
main.ts:
import { NestFactory } from '@nestjs/core'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { AppModule } from './app.module'; import { PrismaService } from './prisma.service'; async function bootstrap() { const app = await NestFactory.create(AppModule); // Swagger configuration const config = new DocumentBuilder() .setTitle('API Documentation') .setDescription('The API description') .setVersion('1.0') .addBearerAuth() .addTag('app') .addTag('hello') .build(); const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup('api', app, document); const prismaService = app.get(PrismaService); await prismaService.enableShutdownHooks(app); await app.listen(3000); } bootstrap();
Issue
When using Keycloak with my NestJS application, I get the following error in the Keycloak logs:
WARN [Keycloak] Cannot validate access token: Error: Grant validation failed. Reason: invalid token (wrong ISS)
API Calls
-
Public Endpoints:
- GET
http://localhost:3000/
(works correctly) - GET
http://localhost:3000/test
(works correctly) - GET
http://localhost:3000/hello
(works correctly) - GET
http://localhost:8081/realms/test
(works correctly)
- GET
-
Get Token from Keycloak: (works correctly)
- POST
http://localhost:8081/realms/test/protocol/openid-connect/token
- POST
-
Access Protected Route with Bearer Token: (fails with 401 Unauthorized)
- GET
http://localhost:3000/protected
- GET
What I Tried
**Valid Token Check:
**
I used the Keycloak admin interface to get a token and tried accessing the protected route.
Expected: Successful access to the protected route with the valid token.
Actual Result: Received a 401 Unauthorized response.
Different authServerUrl
Configurations:
I tested various configurations for authServerUrl in app.module.ts:
- http://127.0.0.1:8081/
- http://127.0.0.1:8081/auth
- http://localhost:8081/auth
Expected: Proper token validation and successful access to the protected route.
Actual Result: The same 401 Unauthorized response with the wrong ISS error.
Recreating the Keycloak Realm:
I attempted to recreate the Keycloak realm to ensure no configuration issues.
Expected: Correctly configured realm leading to successful token validation.
Actual Result: The issue persists with the same error.
Search on Stack Overflow:
I searched for solutions on Stack Overflow using keywords related to this error, but I did not find a satisfactory answer.
Expected: To find solutions or clues to resolve the token validation issue.
Result: No conclusive answers found.
Questions
- What can I check or adjust to resolve this error?
- Are there additional configurations I should include in my Docker or NestJS setup to ensure proper integration with Keycloak?
Thank you for your help!
2
Answers
I found this thread that fixed my problem.
To summarize: To request a token my frontend or postman (outside of container) will communicate with my container url (localhost) so the issuer will be localhost. But to verify the token, my backend (inside container) will communicate through the container network (in my case http://keycloak:8080). The issuer will then be different and the error will occured.
To fix you need to manually configure the Frontent url in keycloak admin console (in my case http://keycloak:8080)
Problem is you must config keycloak URL from FE and BE is same. In BE, it only call keycloak over docker network with host
http://keycloak:8080
but FE (web or postman) is host machine, that mean you config point tohttp://localhost:8081
. This config makeiss
from token and server do not same.In development mode, I think you shouldn’t run BE inside docker, just run on your machine and update config BE and FE point to keycloak URL:
http://localhost:8081
.So, if you want run every thing in Docker, you should config host in your machine just add below line in
/etc/hosts
and edit expose keycloak port
8080:8080
. With this config, you can accesshttp://keycloak:8080
from both inside (it point to container ipkeycloak
) and outside (it point to127.0.0.1
) docker , so should update keycloak url is ‘http://keycloak:8080’ at both BE and FE.This way don’t work if you setup
network_mode: "host"
in docker because it mount file/etc/hosts
from machine to docker and BE can’t call extract containerkeycloak
.