Endless loop authorization endpoint for the OpenID Connect

I have an application using Angular, Keycloak, and Nginx. Nginx is configured to handle reverse proxy and SSL termination. The application worked fine before enabling SSL termination. After enabling it, I encounter the following issue:

  1. I visit the application, and it redirects me to the Keycloak login page.
  2. I enter my credentials, and the application redirects me to the homepage (works fine up to this point).
  3. If I reload the page or navigate to another page, the application enters a loop. The console log shows repeated requests to:
  • https://domain/realms/realm/protocol/openid-connect/3p-cookies/step1.html
  • https://domain/realms/realm/protocol/openid-connect/3p-cookies/step2.html

Additionally:

  • If I refresh the page before logging in, the same loop occurs.
  • In the network tab, I observe that a request to https://domain/realms/realm/protocol/openid-connect/auth?client_id returns a 200 response (HTML file) instead of the expected 302 redirect. This is followed by step1 and step2 requests.
  • Cookies AUTH_SESSION_ID, KEYCLOAK_SESSION, KEYCLOAK_IDENTITY, and their legacy versions are saved in the browser.

Technical Details:

  • Angular: 18
  • Keycloak Angular: ^16.1.0
  • Keycloak JS: ^25.0.6
  • Keycloak Docker Version: quay.io/keycloak/keycloak:25.0.6

Relevant Code:

Angular Configuration (app.module.ts):

export function initializeKeycloak(keycloak: KeycloakService) {
  return () =>
    keycloak.init({
      config: {
        url: 'https://domain',
        realm: 'realm',
        clientId: 'realm-client',
      },
      initOptions: {
        onLoad: 'check-sso',
        silentCheckSsoRedirectUri: window.location.origin + '/assets/silent-check-sso.html',
        checkLoginIframe: false,
      },
      enableBearerInterceptor: true,
      bearerExcludedUrls: ['/assets', '/clients/public'],
    });
}

Authguard

import { Injectable } from '@angular/core';
import {
  ActivatedRouteSnapshot,
  Router,
  RouterStateSnapshot,
} from '@angular/router';
import { KeycloakAuthGuard, KeycloakService } from 'keycloak-angular';

@Injectable({
  providedIn: 'root',
})
export class AuthGuard extends KeycloakAuthGuard {
  constructor(
    protected override readonly router: Router,
    protected readonly keycloak: KeycloakService
  ) {
    super(router, keycloak);
  }

  public async isAccessAllowed(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ) {
    // Force the user to log in if currently unauthenticated.
    if (!this.authenticated) {
      console.log("authguard: not auth");
      await this.keycloak.login({
        redirectUri: window.location.origin + state.url
      });
    }

    // Get the roles required from the route.
    const requiredRoles = route.data['roles'];
    console.log("authguard:  auth 1");
    // Allow the user to to proceed if no additional roles are required to access the route.
    if (!(requiredRoles instanceof Array) || requiredRoles.length === 0) {
      return true;
    }

    // Allow the user to proceed if all the required roles are present.
    console.log("authguard:  auth 2");
    return requiredRoles.every((role) => this.roles.includes(role));
  }
}

Nginx Configuration:

events {
    worker_connections 1024;
}

http {

    include /etc/nginx/mime.types;
    default_type  application/octet-stream;

    server {
        listen 443 ssl;
        server_name domain;

        # SSL certificates
        ssl_certificate /etc/pki/tls/domain.crt;
        ssl_certificate_key /etc/pki/tls/domain.rsa;

        ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;
        ssl_ciphers         HIGH:!aNULL:!MD5;

        root /var/www/html/dist;
        index index.html;

        # Frontend Configuration
        location / {
            try_files $uri /index.html;
        }

        # Backend API Proxy
        location /api/ {
            proxy_pass ip;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_set_header Authorization $http_authorization;
        }

        #Keycloak Proxy
        location /realms/ {
            proxy_pass http://ip;
            proxy_set_header Host               $host:$server_port;
            proxy_set_header X-Real-IP          $remote_addr;
            proxy_set_header X-Forwarded-For    $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto  $scheme;
            proxy_set_header X-Forwarded-Port   $server_port;
            proxy_pass_header       Set-Cookie;
        }

        #Keycloak Proxy
        location /resources/ {
            proxy_pass ip;
            proxy_set_header    Host               $host;
            proxy_set_header    X-Real-IP          $remote_addr;
            proxy_set_header    X-Forwarded-For    $proxy_add_x_forwarded_for;
            proxy_set_header    X-Forwarded-Host   $host;
            proxy_set_header    X-Forwarded-Server $host;
            proxy_set_header    X-Forwarded-Port   $server_port;
            proxy_set_header    X-Forwarded-Proto  $scheme;
        }

    }
}

Keycloak Docker Configuration:

services:
  keycloak:
    image: quay.io/keycloak/keycloak:25.0.6
    container_name: keycloak
    environment:
      KEYCLOAK_IMPORT: /opt/keycloak/data/import/realm.json
      KC_HTTP_ENABLED: 'true'
      KC_HTTP_PORT: 8081
      KC_PROXY_HEADERS: xforwarded      
      PROXY_ADDRESS_FORWARDING: 'true'
      KC_HOSTNAME_STRICT: 'false'

Question: What could be causing this redirect loop after enabling SSL termination? How can I resolve it while maintaining secure SSL termination?

Looks like there is a difference between your keycloak config where you set it to expect a X-Forwarded header , but in nginx you set x-forwarded.for