Configuring SAML Authentication with Microsoft Entra ID for SonarQube

Must-share information (formatted with Markdown):

  • Which versions are you using (SonarQube Server / Community Build, Scanner, Plugin, and any relevant extension)
    SonarQube Server / Community Build: sonarqube:25.7.0.110598-community
  • How is SonarQube deployed: zip, Docker, Helm
    Docker
  • What are you trying to achieve
    SAML authentication with Microsoft Entra ID
  • What have you tried so far to achieve this
    Configured SAML authentication following official documentation

I want to share how I successfully implemented SAML authentication with Microsoft Entra ID, as I encountered the following error:

/sessions/unauthorized
You're not authorized to access this page. Please contact the administrator.

Although the “Test Configuration” for SAML worked successfully:

/saml/validation
SAML Authentication Test
Success

In debug mode, the web.log showed the following error:

DEBUG web[8adccbd1-ea78-4a6c-a0b1-dd00e84a322d][auth.event] login failure [cause|Cookie 'OAUTHSTATE' is missing][method|OAUTH2][provider|EXTERNAL|SAML][IP|172.18.0.4|172.18.0.1][login|]

I followed these tutorials:

I researched these errors in the SonarQube community and online but found no successful solutions. The issue appeared related to how Chromium-based browsers (e.g., Edge) handle cookies, as the error occurred in Edge but not in Firefox.

I deployed SonarQube using Docker, but to resolve the issue, I needed to deploy it behind Nginx with HTTPS. In my lab, I used a localhost URL with a certificate generated using mkcert. Below are the configurations:

Certificate Generation

# Install mkcert with winget
winget install FiloSottile.mkcert

# Create local CA
mkcert -install

# Generate certificate for localhost
mkcert localhost 127.0.0.1 ::1

# Move generated files
move localhost+2.pem ./Web/sonarqube.crt
move localhost+2-key.pem ./Web/sonarqube.key

docker-compose.yml

services:
  sonarqube:
    image: sonarqube:community
    container_name: sonarqube
    restart: unless-stopped 
    depends_on: 
      - sonarqube_db
    env_file:
      - ./WEB/.env-sonarqube
    networks:
      - sonarqube-network
    ports:
      - '9000:9000'
    volumes:
      - sonarqube_conf:/opt/sonarqube/conf
      - sonarqube_data:/opt/sonarqube/data
      - sonarqube_extensions:/opt/sonarqube/extensions
      - sonarqube_logs:/opt/sonarqube/logs
      - sonarqube_temp:/opt/sonarqube/temp
      - ./Plugins:/opt/sonarqube/extensions/plugins

  sonarqube_db:
    image: mcr.microsoft.com/mssql/server:2022-latest
    container_name: sonarqube_db-sqlserver
    restart: unless-stopped 
    networks:
      - sonarqube-network
    environment:
      - MSSQL_DATA_DIR=/var/opt/mssql/data
      - MSSQL_LOG_DIR=/var/opt/mssql/log
      - MSSQL_BACKUP_DIR=/var/opt/mssql/backup
    env_file:
      - ./MSSQLSERVER/.env-sqlserver
      - ./MSSQLSERVER/.env-sapassword
    ports:
      - '1433:1433'
    volumes:
      - sql-server-data:/var/opt/mssql/
      - ./MSSQLSERVER:/var/opt/mssql/backup
      - ./MSSQLSERVER/setup.sql:/docker-entrypoint-initdb.d/init.sql

  nginx:
    image: nginx:latest
    container_name: sonar-nginx
    restart: unless-stopped 
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./WEB/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./WEB/sonarqube.crt:/etc/nginx/ssl/sonarqube.crt:ro
      - ./WEB/sonarqube.key:/etc/nginx/ssl/sonarqube.key:ro
    depends_on:
      - sonarqube
    networks:
      - sonarqube-network

networks:
  sonarqube-network:
    driver: bridge

volumes:
  sonarqube_conf:
  sonarqube_temp:
  sonarqube_data:
  sonarqube_logs:
  sonarqube_extensions:
  sql-server-data:
    driver: local

.\WEB.env-sonarqube

SONAR_WEB_JAVAOPTS=-Xmx2g -Xms2g -Dserver.use-forward-headers=true -Dserver.forward-headers-strategy=framework -Dserver.tomcat.redirect-context-root=false -Dserver.tomcat.use-relative-redirects=false -Dserver.servlet.session.cookie.same-site=None -Dserver.servlet.session.cookie.secure=true -XX:+HeapDumpOnOutOfMemoryError
SONAR_WEB_JAVAADDITIONALOPTS=-Dserver.use-forward-headers=true -Dserver.forward-headers-strategy=framework -Dspring.session.cookie.same-site=None -javaagent:./extensions/plugins/sonarqube-community-branch-plugin-25.6.0.jar=web
SONAR_CE_JAVAOPTS=-Xmx1G -Xms1G -XX:+HeapDumpOnOutOfMemoryError
SONAR_CE_JAVAADDITIONALOPTS=-javaagent:./extensions/plugins/sonarqube-community-branch-plugin-25.6.0.jar=ce
SONAR_WEB_SSO_ENABLE=true
SONAR_WEB_SSO_LOGINHEADER=X-Forwarded-Login
SONAR_WEB_SSO_NAMEHEADER=X-Forwarded-Name
SONAR_WEB_SSO_EMAILHEADER=X-Forwarded-Email
SONAR_WEB_SSO_GROUPSHEADER=X-Forwarded-Groups
SONAR_JDBC_URL=jdbc:sqlserver://sonarqube_db-sqlserver;databaseName=sonarqube_original;encrypt=true;trustServerCertificate=true
SONAR_JDBC_USERNAME=sonarqube
SONAR_JDBC_PASSWORD=sonarqube

.\WEB\nginx.conf

events {
    worker_connections 1024;
}

http {
    # Basic configuration
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    
    # Logs
    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

    # HTTP server - Redirect to HTTPS
    server {
        listen 80;
        server_name localhost;
        
        return 301 https://$server_name$request_uri;
    }

    # HTTPS server - SonarQube
    server {
        listen 443 ssl;
        http2 on;
        server_name localhost;

        # SSL configuration
        ssl_certificate /etc/nginx/ssl/sonarqube.crt;
        ssl_certificate_key /etc/nginx/ssl/sonarqube.key;
        
        # Enhanced SSL settings
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384;
        ssl_prefer_server_ciphers off;
        ssl_session_cache shared:SSL:10m;
        ssl_session_timeout 10m;

        # Security headers (adjusted for cross-site cookies)
        add_header X-Frame-Options SAMEORIGIN;
        add_header X-Content-Type-Options nosniff;
        add_header X-XSS-Protection "1; mode=block";
        
        # Proxy configuration for SonarQube
        location / {
            proxy_pass http://sonarqube:9000;
            
            # Critical headers for HTTPS
            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 https;
            proxy_set_header X-Forwarded-Ssl on;
            proxy_set_header X-Forwarded-Host $server_name;
            proxy_set_header X-Forwarded-Port 443;
            proxy_set_header X-Scheme https;
            proxy_set_header X-Forwarded-Protocol https;
            proxy_set_header X-Forwarded-Prefix "";
            proxy_set_header X-Url-Scheme https;
            
            # Headers for SAML cookies - Critical for Edge
            proxy_set_header Cookie $http_cookie;
            proxy_pass_header Set-Cookie;
            
            # Cross-site cookie settings (Edge compatibility)
            proxy_cookie_path / /;
            proxy_cookie_domain localhost localhost;
            proxy_cookie_flags ~ secure;
            
            # Additional proxy settings
            proxy_connect_timeout 60s;
            proxy_send_timeout 60s;
            proxy_read_timeout 60s;
            proxy_buffering off;
            proxy_request_buffering off;
            proxy_redirect off;
            
            # Buffer size for SAML
            proxy_buffer_size 32k;
            proxy_buffers 8 32k;
            proxy_busy_buffers_size 64k;
        }
        location /sessions/init/saml {
            proxy_pass http://sonarqube:9000/sessions/init/saml;
            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-Host $server_name;
            proxy_set_header X-Forwarded-Port $server_port;
            proxy_set_header Cookie $http_cookie;
            proxy_pass_header Set-Cookie;
            proxy_cookie_path / /;
            proxy_cookie_domain localhost localhost;
            # Add SameSite=None to OAUTHSTATE cookie
            proxy_cookie_flags OAUTHSTATE samesite=none secure;
        }
        location /oauth2/callback/saml {
            proxy_pass http://sonarqube:9000/oauth2/callback/saml;
            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-Host $server_name;
            proxy_set_header X-Forwarded-Port $server_port;
            proxy_set_header Cookie $http_cookie;
            proxy_pass_header Set-Cookie;
            proxy_cookie_path / /;
            proxy_cookie_domain localhost localhost;
            # Add SameSite=None to any cookie in the callback
            proxy_cookie_flags ~ samesite=none secure;
        }
    }
}

Microsoft Entra ID Enterprise Application Configuration

Basic SAML Configuration
Identifier (Entity ID): sonarqube
Reply URL (Assertion Consumer Service URL): https://localhost/oauth2/callback/saml
Sign on URL: https://localhost
Relay State (Optional): Optional
Logout Url (Optional): https://localhost/sessions/logout

SonarQube UI Configuration

Administration > Configuration > General Settings > Authentication > SAML > SAML Configuration: 
* SAML user login attribute*: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name
* SAML user name attribute*: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname
* SAML user email attribute: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress
* SAML group attribute: http://schemas.microsoft.com/ws/2008/06/identity/claims/groups

Administration > Configuration > General Settings > General:

* Server base URL: https://localhost

I hope this guide helps others facing similar issues or serves as a reference for myself in the future.

2 Likes