laraveldockernginxsimplesamlphp

The response was received at https://localhost/saml2/acs instead of https://localhost:8443/saml2/acs


I'm very new to SimpleSAMLphp and I'm diving in headfirst with the help of productivity AI answers so please bear with me. (Currently in a very tight schedule project so I had to use AI. I know it isn't really reliable, and yes I have been double-checking the info it gives me.)

I'm using 24Slides/laravel-saml2 together with Laravel, and not aacotroneo/laravel-saml2 as suggested on its README. There were some errors here and there that were solved later on, but I keep bumping into this error when accessing https://localhost:8443/login.

Before getting the error, I do see the 'Enter username and password' page, which tells me that it's working, but after I enter my details and the app processes it, it always leads me to a 500 Server Error page with The response was received at https://localhost/saml2/acs instead of https://localhost:8443/saml2/acs at the header.

config/saml2.php:

<?php

return [
    'default' => 'default',
    'tenantModel' => \Slides\Saml2\Models\Tenant::class,
    'useRoutes' => true,
    'routesPrefix' => 'saml2',
    'routesMiddleware' => ['web'],
    'retrieveParametersFromServer' => false,
    'loginRoute' => 'login',
    'logoutRoute' => 'logout',
    'errorRoute' => env('SAML2_ERROR_URL'),
    'strict' => true,
    'debug' => env('SAML2_DEBUG', env('APP_DEBUG', false)),
    'proxyVars' => true,
    'sp' => [
        'NameIDFormat' => 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
        'entityId' => 'https://localhost:8443/saml2/metadata',
        'assertionConsumerService' => [
            'url' => 'https://localhost:8443/saml2/acs',
        ],
        'singleLogoutService' => [
            'url' => 'https://localhost:8443/saml2/sls'
        ],

        'x509cert' => file_get_contents(base_path('storage/saml/sp-cert.crt')),
        'privateKey' => file_get_contents(base_path('storage/saml/sp-key.pem')),
    ],

    'load_migrations' => true,

    'idp' => [
        'entityId' => 'https://localhost:8443/simplesaml/saml2/idp/metadata.php',
        'singleSignOnService' => [
            'url' => 'https://localhost:8443/simplesaml/saml2/idp/SSOService.php',
        ],
        'singleLogoutService' => [
            'url' => 'https://localhost:8443/simplesaml/saml2/idp/SingleLogoutService.php',
        ],
        'x509cert' => env('SAML2_IDP_X509', ''),
    ],
];

simplesaml/metadata/saml20-sp-remote.php:

$metadata['https://localhost:8443/saml2/metadata'] = [
    'AssertionConsumerService' => [
        [
            'Location' => 'https://localhost:8443/saml2/acs',
            'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
        ],
    ],
    'SingleLogoutService' => [
        [
            'Location' => 'https://localhost:8443/module.php/saml2/logout',
            'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
        ],
    ],
];

Additional edit: .env:

APP_NAME=Laravel
APP_ENV=local
APP_FORCE_HTTPS=true
APP_KEY=
APP_DEBUG=true
APP_TIMEZONE=
APP_URL=https://localhost:8443

APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US

APP_MAINTENANCE_DRIVER=file

SAML2_DEBUG=true
SAML2_SP_ENTITYID=https://localhost:8443/saml2/metadata
SAML2_SP_ACS=https://localhost:8443/saml2/acs
SAML2_IDP_ENTITYID=https://localhost:8443/metadata
SAML2_IDP_SSO=https://localhost:8443/sso
SAML2_IDP_SLO=https://localhost:8443/slo
SAML2_IDP_X509=

PHP_CLI_SERVER_WORKERS=4

BCRYPT_ROUNDS=12

LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug

DB_CONNECTION=pgsql
DB_HOST=postgres
DB_PORT=5432
DB_DATABASE=
DB_USERNAME=
DB_PASSWORD=

SESSION_DRIVER=database
SESSION_COOKIE=laravel_session
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null

BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database

MEMCACHED_HOST=127.0.0.1

VITE_APP_NAME="${APP_NAME}"

I did try searching for similar issues like mine but the closest I got was this issue.

Using Docker, NGINX, Laravel, and SimpleSAML.

Please let me know which files I need to place here or anything else. Thank you so much.


Solution

  • I think I got the answer. (I asked for help from a co-worker in the office.)

    SOLUTION: on app/Http/Middleware, create a file EnableSamlProxyVars.php and add this to the file:

    namespace App\Http\Middleware; 
    use Closure; 
    use OneLogin\Saml2\Utils; 
    
    class EnableSamlProxyVars { 
            public function handle($request, Closure $next) { 
                Utils::setProxyVars(true); 
                return $next($request); 
            } 
    }
    

    then adding it to bootstrap/app.php:

    $middleware->use([
        App\Http\Middleware\TrustProxies::class,
        \App\Http\Middleware\EnableSamlProxyVars::class,
        \Illuminate\Http\Middleware\HandleCors::class
    ]);
    

    additional info: TrustProxies.php includes:

    namespace App\Http\Middleware;
    
    use Illuminate\Http\Request;
    use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
    use Illuminate\Http\Middleware\TrustProxies as Middleware;
    use Closure;
    
    class TrustProxies extends Middleware {
        protected $proxies = '*';
    
        protected $headers =
            Request::HEADER_X_FORWARDED_FOR |
            Request::HEADER_X_FORWARDED_HOST |
            Request::HEADER_X_FORWARDED_PORT |
            Request::HEADER_X_FORWARDED_PROTO;
    }
    

    ============================================

    (Apologies if this part is too detailed, as I want to potentially help those who may come across this error in the future, but please do let me know how to simplify this.)

    Explanation:

    TL;DR: OneLogin (based on by 24slides/laravel-saml2) sets proxyVars to false on its Utils.php file (vendor/onelogin/php-saml/src/Saml2/Utils.php).

    On my routes/web.php,

    Route::prefix('saml2')->group(function () {
        Route::get('login', [Saml2Controller::class, 'login']);
        Route::post('acs', [Saml2Controller::class, 'acs'])->withoutMiddleware([\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class]);
        Route::get('logout', [Saml2Controller::class, 'logout']);
        Route::get('sls', [Saml2Controller::class, 'sls']);
        Route::get('metadata', [Saml2Controller::class, 'metadata']);
    });
    

    It took a lot of time checking the functions and seeing where the 443 port was coming from, but it led us to vendor/onelogin/php-saml/src/Saml2/Response.php: (particularly on this section)

    $urlComparisonLength = $security['destinationStrictlyMatches'] ? strlen($destination) : strlen($currentURL);
        if (strncmp($destination, $currentURL, $urlComparisonLength) !== 0) {
            $currentURLNoRouted = Utils::getSelfURLNoQuery();
            $urlComparisonLength = $security['destinationStrictlyMatches'] ? strlen($destination) : strlen($currentURLNoRouted);
            if (strncmp($destination, $currentURLNoRouted, $urlComparisonLength) !== 0) {
                throw new ValidationError(
                    "The response was received at $currentURL instead of $destination",
                    ValidationError::WRONG_DESTINATION
                );
            }
        }
    

    Utils::getSelfURLNoQuery led us to getSelfURLhost(), which then led us to getSelfPort().

    if (self::$_port) {
        $portnumber = self::$_port;
        //var_dump($_port . 'one');
    } else if (self::getProxyVars() && isset($_SERVER["HTTP_X_FORWARDED_PORT"])) {
        $portnumber = $_SERVER["HTTP_X_FORWARDED_PORT"];
        //var_dump($portnumber . 'two');
    } else if (isset($_SERVER["SERVER_PORT"])) {
        $portnumber = $_SERVER["SERVER_PORT"];
        //var_dump($portnumber . 'three');
    } 
    

    (var_dump helped locating which part was the system going through.)

    Adding this to any of our Laravel files (added this to routes/web.php):

    dd([
        'server' => $_SERVER,
        'headers' => getallheaders(),
        'scheme' => request()->getScheme(),
        'port' => request()->getPort(),
        'url' => url('/'),
    ]);
    

    helped confirm that OneLogin was getting $_SERVER["SERVER_PORT"] which was still equal to 443 because of this variable in Utils.php:

    class Utils
    {
        const RESPONSE_SIGNATURE_XPATH = "/samlp:Response/ds:Signature";
        const ASSERTION_SIGNATURE_XPATH = "/samlp:Response/saml:Assertion/ds:Signature";
    
        /**
         * @var bool Control if the `Forwarded-For-*` headers are used
         */
        private static $_proxyVars = false;
    

    The middleware solution was recommended by AI, as it looked like OneLogin didn't get the 'proxyVars' => true, in my config/saml2.php.

    The solution mostly focused on changing proxyVars to true so that the system would go through the else if (self::getProxyVars() && isset($_SERVER["HTTP_X_FORWARDED_PORT"])) part.

    Refactored docker/nginx/default.conf:

    server {
        listen 443 ssl;
        server_name localhost;
    
        ssl_certificate /etc/nginx/ssl/localhost.pem;
        ssl_certificate_key /etc/nginx/ssl/localhost-key.pem;
    
        root /app/public;
        index index.php index.html;
    
        access_log /var/log/nginx/access.log;
        error_log /var/log/nginx/error.log;
    
        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 X-Forwarded-Port 8443;
    
        # Laravel app routes
        location / {
            try_files $uri $uri/ /index.php?$query_string;
    
            proxy_set_header Host $host;
            proxy_pass http://app:80;
            proxy_set_header X-Forwarded-Port 8443;
        }
    
        # PHP handling
        location ~ \.php$ {
            include fastcgi_params;
            fastcgi_pass app:9000;
            fastcgi_index index.php;
            fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
            fastcgi_param PATH_INFO $fastcgi_path_info;
            fastcgi_param HTTPS on;
    
            fastcgi_param HTTP_X_FORWARDED_PROTO $scheme;
            fastcgi_param HTTP_X_FORWARDED_PORT 8443;
        }
    
        # SimpleSAMLphp frontend
        location ^~ /simplesaml/ {
            alias /app/simplesaml/public/;
            index index.php;
    
            location ~ ^/simplesaml/(.*\.php)(/.*)?$ {
                alias /app/simplesaml/public/;
                include fastcgi_params;
                fastcgi_pass app:9000;
                fastcgi_index index.php;
                fastcgi_param SCRIPT_FILENAME /app/simplesaml/public/$1;
                fastcgi_param PATH_INFO $2;
                fastcgi_param HTTP_X_FORWARDED_PORT 8443;
            }
        }
    }