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:
- I visit the application, and it redirects me to the Keycloak login page.
- I enter my credentials, and the application redirects me to the homepage (works fine up to this point).
- 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 bystep1
andstep2
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?