Too many redirects after login to Keycloak with Nginx as reverse proxy

I have secured a PHP app using Keycloak and the package provided here.

I’m running Nginx as a reverse proxy and Keycloak on the same machine with Ubuntu 18.04. Also I think it’s worth mentioning that all ports on the server are blocked for external access except 80, 443, and 1367 (for SSH).

My Ngixn config:

upstream keycloak_server {
   server 127.0.0.1:8180;
}

server {
	listen 80 default_server;
	listen [::]:80 default_server;

	server_name example.com;

	root /var/www/html;

	# Add index.php to the list if you are using PHP
	index index.html index.htm index.nginx-debian.html;

	return 301 https://$server_name$request_uri;
}

server {

	# SSL configuration

	listen 443 ssl default_server;
	listen [::]:443 ssl default_server;

	ssl_certificate /etc/nginx/ssl/chain.pem;
    ssl_certificate_key /etc/nginx/ssl/private_key.key;
        
    ssl_protocols TLSv1.2;
    ssl_prefer_server_ciphers on;
    ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
    ssl_ecdh_curve secp384r1;
    ssl_session_cache shared:SSL:10m;
    ssl_session_tickets off;
    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 8.8.8.8 8.8.4.4 valid=300s;
    resolver_timeout 5s;
    add_header Strict-Transport-Security "max-age=63072000; includeSubdomains";
    add_header X-Content-Type-Options nosniff;

    ssl_dhparam /etc/ssl/certs/dhparam.pem;

	location /auth/ {
		proxy_pass http://sso_server;

		proxy_http_version 1.1;

		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;
	}

}

My PHP snippet to connect to Keycloak:

<?php

    require '../vendor/autoload.php';

    session_start();

    $provider = new Stevenmaguire\OAuth2\Client\Provider\Keycloak([
        'authServerUrl'             => 'https://example.com/auth',
        'realm'                     => 'sso',
        'clientId'                  => 'php',
        'clientSecret'              => 'client_secret',
        'redirectUri'               => 'http://localhost/php-sso/staff/profile.php',
        'encryptionAlgorithm'       => null,
        'encryptionKey'             => null,
        'encryptionKeyPath'         => null
    ]);

When I run my PHP app on another machine using XAMPP and call the secured URL, it redirects to Keycloak login page correctly. But when I enter the username and password and the app wants to redirect to the secured page, it gets stuck in a redirect loop and raise “Too many redirect” error on both Chrome and Firefox.

Please consider it has been working correctly recently until the admin of the server blocked all ports (except those mentioned earlier) on the server. Can this problem be related to the ports which are blocked on the server or something wrong with my Nginx config?

Are there any opinions on this problem?

At least your redirect URI looks worng, it should point to the external https-URL of the php app.

Here I used this redirect URL as an example but on the real DEV env it points to a https-URL, and the problem still exists.

It doesn’t look like a Keycloak problem, but your app problem.

It looks like you have valid SSO session so any request to KEYCLOAK/…/auth are 302 redirected to the APP/…/profile.php with the token/code (it is not clear which flow are you using). But your app thinks for some unknown reason, that request is not authenticated and it also 302 redirects it back to KEYCLOAK/…/authenticate. Your KEYCLOAK and your APP are redirecting to each other until they reach some browser redirects limit and browser stops their redirects.

Your app (or used lib GitHub - stevenmaguire/oauth2-keycloak: Keycloak Provider for OAuth 2.0 Client) seems to have a problem. There can be a many problems, but you didn’t provide reproducible example, so everything is just guess. My humble guess: localhost is a problem - try to use real domain instead or cookies are problem - app usually saves (auth) states into cookies and they may be misconfigured (e.g. http only, samesite, …) so cookies are rejected - check browser console for errors.

if your application is deployed as HA, ensure that you either have sticky sessions enabled or using some kind of session replication across your application nodes.

One more thing: The redirect URI should point to an endpoint that handles the token (the code that is show as the example). To me it looks like you are redirect to the users profile without handling the authentication first…

I exactly implemented my PHP app based on the documentation in GitHub - stevenmaguire/oauth2-keycloak: Keycloak Provider for OAuth 2.0 Client. When I run Keycloak instance on my localhost and connect this PHP app to it, it works correctly without any problems. The problem raises when I want to connect PHP app to the Keycloak instance on the real server with a real URI. This means that the PHP app is working correctly, because if it has any problems it shouldn’t work as expected on localhost, too. Do you agree?
Also I haven’t noticed of any errors in the console of the browser.

This endpoint handles the authentication result. As I mentioned earlier it works with Keycloak instance on localhost correctly without any problems, but I cannot understand why it does work with Keycloak instanse on the server.

sso.php code:

<?php

    require '../vendor/autoload.php';

    session_start();

    $provider = new Stevenmaguire\OAuth2\Client\Provider\Keycloak([
        'authServerUrl'             => 'https://example.com/auth',
        'realm'                     => 'sso',
        'clientId'                  => 'php',
        'clientSecret'              => 'client_secret',
        'redirectUri'               => 'http://localhost/php-sso/staff/profile.php',
        'encryptionAlgorithm'       => null,
        'encryptionKey'             => null,
        'encryptionKeyPath'         => null
    ]);

    if (!isset($_GET['code'])) {
        // If we don't have an authorization code then get one
        $authUrl = $provider->getAuthorizationUrl();
        $_SESSION['oauth2state'] = $provider->getState();
        header('Location: '.$authUrl);
        exit;

    // Check given state against previously stored one to mitigate CSRF attack
    } elseif (empty($_GET['state']) || (isset($_SESSION['oauth2state']) && $_GET['state'] !== $_SESSION['oauth2state'])) {
        if (isset($_SESSION['oauth2state'])) {
            unset($_SESSION['oauth2state']);
        }
        exit();

    } else {
        // Try to get an access token (using the authorization code grant)
        try {
            $accessToken = $provider->getAccessToken('authorization_code', [
                'code' => $_GET['code']
            ]);
        } catch (Exception $e) {
            $authUrl = $provider->getAuthorizationUrl();
            $_SESSION['oauth2state'] = $provider->getState();
            header('Location: '.$authUrl);
            exit('Failed to get access token: '.$e->getMessage());
        }

        // Optional: Now you have a token you can look up a users profile data
        try {
            // We got an access token, let's now get the user's details
            $user = $provider->getResourceOwner($accessToken);

            // You can use these details to show user's SSO information
            $_SESSION['username'] = $user->toArray()["preferred_username"];
            $_SESSION['firstname'] = $user->toArray()["given_name"];
            $_SESSION['lastname'] = $user->toArray()["family_name"];
            $_SESSION['email'] = $user->toArray()["email"];

        } catch (Exception $e) {
            exit('Failed to get resource owner: '.$e->getMessage());
        }

        $_SESSION["logouturl"] = $provider->getLogoutUrl(array('redirect_uri'=>'http://localhost/php-sso/staff/staff.html'));
    }

And my profile.php code:

<?php
  require_once("./sso.php");
?>

<!DOCTYPE html>

<html>
  <head>
    <title>User Info</title>
    <meta charset="utf-8" />
    <link rel="stylesheet" href="./styles/bootstrap/css/bootstrap.min.css" />
    <link rel="stylesheet" href="./styles/profileStyle.css" />
  </head>

  <body>
    <header>
      <button data-toggle="collapse" data-target="#collapseMenu" class="top-header_collapse_button">
        <i class="menu-icon"></i>
      </button>
      <div class="container">
        <div class="top-header">
          <div class="top-header_main-menu">
            <div class="top-header_main-menu_profile">
              <a class="dropdown" href="<?php
                unset($_SESSION["oauth2state"]);
                unset($_SESSION["state"]);
                session_destroy();
                echo $_SESSION["logouturl"]; 
              ?>" id="top-header-profile">logout</a>
            </div>
          </div>
        </div>
      </div>
    </header>
    
    <section class="home_hero" style="background:url(images/backgroundImage.jpg) no-repeat center;height: 289px;width:100%;position: relative;background-size: cover">
      <span class="">
        <span></span>
      </span>
    </section>

    <div class="col-lg-12 col-md-12 col-xs-12 main-leftPart contactUs-page aboutUs-page">
      <div class="">
        <div class="header-innerPage text-center"></div>
      </div>
      <div class="content_inner">
        <div class="container">
          <div class="content-innerPage profile_wrapper">
            <div class="row">
              <div class="clearfix"></div>
              <div class="m-t-50 profile_information">
                <div class="profile_header">
                  <span class="user_icon_profile"> </span>
                  <span class="user_info_name">
                    <?php echo $_SESSION['username'] ?>
                  </span>
                </div>
                <div class="profile_body">
                  <div class="container-fluid">
                    <div class="value_item row">
                      <div class="col-xs-6 text-left">
                        <span class="label_item"> First name : </span>
                      </div>
                      <div class="col-xs-6 text-right">
                        <span class="label_value">
                          <?php echo $_SESSION["firstname"]?>
                        </span>
                      </div>
                    </div>
                    <div class="value_item row">
                      <div class="col-xs-6 text-left">
                        <span class="label_item"> Last name : </span>
                      </div>
                      <div class="col-xs-6 text-right">
                        <span class="label_value">
                          <?php echo $_SESSION["lastname"] ?>
                        </span>
                      </div>
                    </div>
                    <div class="value_item row">
                      <div class="col-xs-6 text-left">
                        <span class="label_item"> Email : </span>
                      </div>
                      <div class="col-xs-6 text-right">
                        <span class="label_value">
                          <?php echo $_SESSION["email"]?>
                        </span>
                      </div>
                    </div>

                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
    
  </body>
</html>

I made this simple PHP app just to test Keycloak integration with PHP app.

Please provide HAR file with your logins/redirects. It is really not clear what are you doing. But obviously on real domain is localhost redirect URL wrong. Check PHP logs (it will be better if you logs exceptions).

I set logs in my PHP code and got the following error message, when the app sends requests to Keycloak authentication endpoint:

Fatal error: Uncaught GuzzleHttp\Exception\RequestException: cURL error 60: SSL certificate problem: unable to get local issuer certificate (see http://curl.haxx.se/libcurl/c/libcurl-errors.html) in C:\xampp\htdocs\php-sso\vendor\guzzlehttp\guzzle\src\Handler\CurlFactory.php:186

Then I checked nginx config in the server using nginx -t command and got the following message:

nginx: [warn] "ssl_stapling" ignored, issuer certificate not found for certificate "/etc/nginx/ssl/example.com.pem"
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Based on the above message I found the problem. It was because of the issuer of the SSL certificate which I modified my SSL certificate chain and now PHP app can connect to Keycloak server correctly.