Implementing SAML 2.0 Authentication for Static Content in Nginx: A Practical Guide


3 views

When working with nginx to serve static content, implementing SAML 2.0 authentication presents unique challenges compared to Apache's straightforward module-based approach. Unlike Apache's mod_mellon or mod_auth_saml, nginx lacks native SAML support, requiring a more architectural solution.

The most effective approach combines nginx's auth_request module with a separate authentication service. Here's the basic flow:

Client → Nginx → Auth Proxy → IdP → [Auth Success] → Content
                     ↑
                 SAML Handshake

1. Nginx Configuration:

server {
    listen 443 ssl;
    server_name protected.domain.com;

    location / {
        auth_request /saml_auth;
        auth_request_set $saml_user $upstream_http_x_user;
        proxy_set_header X-User $saml_user;
        
        # Your static content configuration
        root /var/www/secure_content;
    }

    location = /saml_auth {
        internal;
        proxy_pass http://localhost:8000/verify;
        proxy_pass_request_body off;
        proxy_set_header Content-Length "";
        proxy_set_header X-Original-URI $request_uri;
    }
}

2. Authentication Service (Python Flask Example):

from flask import Flask, request
from flask_saml2.sp import IdentityProvider, ServiceProvider

app = Flask(__name__)

class ExampleServiceProvider(ServiceProvider):
    def get_logout_return_url(self):
        return request.url_root

    def get_default_login_return_url(self):
        return request.url_root

app.config['SAML2_SP'] = {
    'acs_url': 'https://your.domain.com/saml/acs',
    'entity_id': 'https://your.domain.com/saml/metadata.xml',
}

app.register_blueprint(ExampleServiceProvider().create_blueprint())

@app.route('/verify')
def verify_auth():
    if not is_authenticated(request):
        return '', 401
    return '', 200

if __name__ == '__main__':
    app.run(port=8000)

Option 1: Lua Scripting

For more advanced use cases, consider using nginx's lua module with libraries like lua-resty-saml:

location /secure {
    access_by_lua_block {
        local saml = require "resty.saml"
        local ok, err = saml.authenticate()
        if not ok then
            ngx.exit(ngx.HTTP_UNAUTHORIZED)
        end
    }
    alias /path/to/static/files;
}

Option 2: OAuth2 Proxy as Middleware

While not pure SAML, many organizations use OAuth2 Proxy configured for SAML compatibility:

# In nginx.conf
location / {
    auth_request /oauth2/auth;
    error_page 401 = /oauth2/sign_in;
    proxy_pass http://oauth2_proxy:4180;
}
  • Always implement proper session timeout handling
  • Configure SP-initiated logout properly
  • Set appropriate cache headers for authenticated content
  • Monitor both nginx and auth service metrics

Common issues and their solutions:

Issue Solution
ACS URL mismatch Verify metadata configuration
Clock skew Synchronize server times
Certificate problems Check SP and IdP cert chains

Unlike Apache which has modules like mod_mellon, Nginx doesn't natively support SAML authentication. The core issue revolves around:

  • Nginx's lack of built-in SAML SP (Service Provider) capabilities
  • The stateless nature of static content delivery
  • Need for proper session management during SSO flow

The most robust approach involves using Nginx as a reverse proxy with a dedicated authentication service. Here's a typical workflow:

Client → Nginx (protected path) → Auth Service (SAML SP) → IdP → Back to Nginx with session cookie

One effective method is using OpenResty (Nginx + Lua) with a Lua SAML library:

location /protected/ {
    access_by_lua_block {
        local saml = require "resty.saml"
        local sess = ngx.var.cookie_SESSION
        
        if not sess then
            local sp = saml.new({
                entity_id = "https://your.service.provider",
                idp_sso_url = "https://idp.example.com/saml2/sso",
                idp_cert = [[-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----]],
                assertion_consumer_service_url = "https://your.service.provider/acs"
            })
            
            ngx.redirect(sp:create_authn_request())
        end
        
        -- Validate session and proceed
    }
    
    alias /path/to/static/files;
}

For those not using OpenResty, a separate authentication service can handle SAML:

server {
    listen 443 ssl;
    server_name files.example.com;
    
    location / {
        proxy_pass http://auth-service:8080/validate;
        proxy_pass_request_body off;
        proxy_set_header Content-Length "";
        proxy_set_header X-Original-URI $request_uri;
    }
    
    location /internal_static/ {
        internal;
        alias /var/www/static/;
    }
}

When using an external auth service, X-Accel can be effective:

# In your auth service (Python example)
@app.route('/auth')
def auth_check():
    if not validate_saml_session(request.cookies.get('session')):
        return redirect_to_idp()
    
    response = make_response()
    response.headers['X-Accel-Redirect'] = '/internal_static' + request.args.get('path')
    return response

Consider these mature solutions:

  • SAML Service Providers: saml2-js, python3-saml
  • Pre-built proxies: oauth2-proxy (with SAML plugin)
  • Commercial options: Pomerium, Keycloak Gatekeeper

When implementing SAML with static content:

  • Cache authenticated sessions aggressively
  • Use short-lived SAML assertions (5-10 minutes)
  • Consider implementing a secondary lightweight auth mechanism (JWT) post-SAML auth