skip to Main Content

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 Token from Keycloak: (works correctly)

    • POST http://localhost:8081/realms/test/protocol/openid-connect/token
  • Access Protected Route with Bearer Token: (fails with 401 Unauthorized)

    • GET http://localhost:3000/protected

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:

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

  1. What can I check or adjust to resolve this error?
  2. 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


  1. 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)

    Login or Signup to reply.
  2. 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 to http://localhost:8081. This config make iss 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

    127.0.0.1 keycloak
    

    and edit expose keycloak port 8080:8080. With this config, you can access http://keycloak:8080 from both inside (it point to container ip keycloak) and outside (it point to 127.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 container keycloak.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search