SurePassID Identity Provider

SurePassID Identity Provider for NGINX MFA Guide

Applies to: SurePassID Identity Provider 2026.4 Audience: Administrators protecting a web application with NGINX Plus and SurePassID MFA Protocol: OpenID Connect (Authorization Code flow with PKCE)


Overview

This guide configures NGINX Plus as an OpenID Connect relying party (reverse proxy) in front of a backend application. Unauthenticated users are redirected to the SurePassID Identity Provider, complete primary authentication and MFA, and are then proxied to the backend with their identity passed as an HTTP header.

Browser ──► NGINX Plus (OIDC relying party) ──► Backend application
                 │
                 └──► SurePassID Identity Provider (/oidc/{domain}/...)
                          authorize → MFA → callback → token → userinfo

Important change from earlier releases: The Identity Provider uses tenant-scoped endpoints under /oidc/{domain}/, not the IdentityServer4-style /connect/* paths. PKCE (S256) is required by default. OIDC clients are registered in the Admin Portal, not as a JSON client object.


Requirements

  • NGINX Plus (the auth_jwt and keyval features are Plus-only)
  • The nginx-openid-connect reference implementation
  • SurePassID Identity Provider 2026.4 with OIDC enabled for the tenant
  • A registered OIDC client for NGINX (see below)

1. Install

Follow the nginx-openid-connect installation instructions, then install the jq command-line JSON processor. It is a dependency but is not installed automatically.

sudo yum install jq

SELinux workaround

NGINX may lack permission to write to /etc/nginx/conf.d. The simplest workaround is to change the owner and group of that directory to nginx:

sudo chown nginx:nginx /etc/nginx/conf.d

A better solution is to fix the SELinux httpd_t context so the NGINX process can create files in that directory. See Using NGINX Plus with SELinux.


2. Register the OIDC Client in the Admin Portal

Client registration is performed in the Admin Portal and stored in the tenant database. There is no JSON client file to deploy.

Create a new OIDC client with these settings:

Setting Value for NGINX
Client ID nginx-client-id (your choice; unique per tenant)
Client Name Display name shown on the consent screen
Client Secret Generate a strong secret; stored salted-hashed and shown only once
Redirect URIs https://app-proxy.example.com/_codexch
Post Logout Redirect URIs https://app-proxy.example.com/_logout
Allowed Grant Types authorization_code refresh_token
Allowed Scopes openid profile email offline_access
Require PKCE Enabled (default)
Require Consent Optional; disable for a seamless proxy experience

Notes:

  • Redirect URIs are matched exactly. No wildcards. The URI must match what NGINX sends, including scheme, host, port, and path.
  • Include offline_access in Allowed Scopes only if you want refresh tokens, which NGINX uses to extend sessions without re-prompting.
  • Drop refresh_token from the grant types if you do not want refresh behavior.

Optional hardening

Setting Purpose
Absolute Session Lifetime Caps the refresh chain, forcing periodic re-authentication with MFA
IP Whitelist Restricts token endpoint calls to your NGINX egress addresses (single IPs or CIDR)
FAPI Mode Enforces S256 PKCE and a mandatory nonce
Backchannel Logout URI Server-to-server signed logout token delivery
Frontchannel Logout URI Browser iframe session clearing
ACR values / Override Request ACR Controls the acr claim emitted for single- vs multi-factor logins

MFA behavior

MFA methods presented to the user are governed by the tenant's MFA policy in the Admin Portal, not by client properties. Earlier versions of this guide used an IdentityServer4 Properties bag (MfaButtons.ALL, TenantDomain.N, TenantId.N, TenantKey.N) — those keys are obsolete and are ignored.


3. Identity Provider Endpoints

All endpoints are tenant-scoped. Replace {domain} with your tenant domain and oidc.surepassid.com with your IdP host.

Endpoint Path
Discovery https://oidc.surepassid.com/oidc/{domain}/.well-known/openid-configuration
Authorization https://oidc.surepassid.com/oidc/{domain}/authorize
Token https://oidc.surepassid.com/oidc/{domain}/token
UserInfo https://oidc.surepassid.com/oidc/{domain}/userinfo
End session (logout) https://oidc.surepassid.com/oidc/{domain}/logout
JWKS https://oidc.surepassid.com/oidc/{domain}/jwks

Advertised capabilities:

  • response_types_supported: code
  • grant_types_supported: authorization_code, refresh_token
  • scopes_supported: openid, profile, email, phone, offline_access
  • id_token_signing_alg_values_supported: RS256
  • code_challenge_methods_supported: S256
  • token_endpoint_auth_methods_supported: client_secret_post, client_secret_basic, none
  • backchannel_logout_supported / frontchannel_logout_supported: true

4. Configure NGINX Plus

Run the configure.sh script using the tenant discovery URL:

./configure.sh https://oidc.surepassid.com/oidc/{domain}/.well-known/openid-configuration

openid_connect_configuration.conf

Make the following changes.

Enable PKCE. This is required by the IdP:

map $host $oidc_pkce_enable {
    default 1;
}

Point the end-session mapping at the tenant logout endpoint:

map $host $oidc_endsession_endpoint {
    default https://oidc.surepassid.com/oidc/{domain}/logout;
}

Use the JWKS URL so signing keys stay up to date automatically:

map $host $oidc_jwks_uri {
    default https://oidc.surepassid.com/oidc/{domain}/jwks;
}

Set the client ID and secret:

map $host $oidc_client {
    default "<OIDC_CLIENT_ID>";
}

map $host $oidc_client_secret {
    default "<OIDC_CLIENT_SECRET>";
}

Set the logout redirect to /oidc_logout, configured in the next section:

map $host $oidc_logout_redirect {
    # Where to send browser after requesting /logout location. This can be
    # replaced with a custom logout page, or complete URL.
    default "/oidc_logout";
}
Complete openid_connect_configuration.conf example (click to expand)
# OpenID Connect configuration
#
# Each map block allows multiple values so that multiple IdPs can be supported,
# the $host variable is used as the default input parameter but can be changed.
#
map $host $oidc_authz_endpoint {
    default https://oidc.surepassid.com/oidc/{domain}/authorize;
}

map $host $oidc_token_endpoint {
    default https://oidc.surepassid.com/oidc/{domain}/token;
}

map $host $oidc_endsession_endpoint {
    default https://oidc.surepassid.com/oidc/{domain}/logout;
}

map $host $oidc_jwks_uri {
    default https://oidc.surepassid.com/oidc/{domain}/jwks;
}

map $host $oidc_jwt_keyfile {
    default conf.d/idp_jwk.json;
}

map $host $oidc_client {
    default "<OIDC_CLIENT_ID>";
}

map $host $oidc_pkce_enable {
    default 1;                      # PKCE (S256) is required by the IdP
}

map $host $oidc_client_secret {
    default "<OIDC_CLIENT_SECRET>";
}

map $host $oidc_scopes {
    default "openid+profile+email+offline_access";
}

map $host $oidc_logout_redirect {
    # Where to send browser after requesting /logout location. This can be
    # replaced with a custom logout page, or complete URL.
    default "/oidc_logout";
}

map $host $oidc_hmac_key {
    # This should be unique for every NGINX instance/cluster
    default <UNIQUE_GENERATED_OIDC_HMAC_KEY>;
}

map $proto $oidc_cookie_flags {
    http  "Path=/; SameSite=lax;"; # For HTTP/plaintext testing
    https "Path=/; SameSite=lax; HttpOnly; Secure;"; # Production recommendation
}

map $http_x_forwarded_port $redirect_base {
    ""      $proto://$host:$server_port;
    default $proto://$host:$http_x_forwarded_port;
}

map $http_x_forwarded_proto $proto {
    ""      $scheme;
    default $http_x_forwarded_proto;
}

# ADVANCED CONFIGURATION BELOW THIS LINE
# Additional advanced configuration (server context) in openid_connect.server_conf

# JWK Set will be fetched from $oidc_jwks_uri and cached here - ensure writable by nginx user
proxy_cache_path /var/cache/nginx/jwk levels=1 keys_zone=jwk:64k max_size=1m;

# Change timeout values to at least the validity period of each token type
keyval_zone zone=oidc_id_tokens:1M state=conf.d/oidc_id_tokens.json timeout=1h;
keyval_zone zone=refresh_tokens:1M state=conf.d/refresh_tokens.json timeout=8h;
keyval_zone zone=oidc_pkce:128K timeout=90s; # Temporary storage for PKCE code verifier.

keyval $cookie_auth_token $session_jwt   zone=oidc_id_tokens;   # Exchange cookie for JWT
keyval $cookie_auth_token $refresh_token zone=refresh_tokens;   # Exchange cookie for refresh token
keyval $request_id $new_session zone=oidc_id_tokens;            # For initial session creation
keyval $request_id $new_refresh zone=refresh_tokens;            # ''
keyval $pkce_id $pkce_code_verifier zone=oidc_pkce;

auth_jwt_claim_set $jwt_audience aud; # In case aud is an array

js_import oidc from conf.d/openid_connect.js;

# vim: syntax=nginx

5. Configure the Backend Proxy (frontend.conf)

Upstream servers

upstream backend_app_server {
    zone backend_app_server 64k;

    # Server Private IP Address
    server 10.1.2.3:443;

    # DNS
    #resolver 8.8.8.8;
    #server app-proxy.example.com:443;
}

SSL

See the ngx_http_ssl_module documentation.

server {
    #####################
    # SSL Configuration #
    #####################
    server_name  app-proxy.example.com;
    listen       443 ssl;

    ssl_certificate     ssl/example.com.crt;
    ssl_certificate_key ssl/example.com.key;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         HIGH:!aNULL:!MD5;
}

Security note: TLS 1.0 and 1.1 are deprecated and should not be enabled. Use TLS 1.2 or later.

Logout location

Add the /oidc_logout location in the server block, before the / location. The post_logout_redirect_uri value must be URL-encoded and must exactly match a Post Logout Redirect URI registered on the client.

server {
    location = /oidc_logout {
        proxy_ssl_server_name on; # For SNI to the IdP
        proxy_pass $oidc_endsession_endpoint?id_token_hint=$arg_token&post_logout_redirect_uri=https%3a%2f%2fapp-proxy.example.com%2f_logout;
    }

    location / {
        # ...
    }
}

Reverse proxy to the backend

server {
    location / {
        # This site is protected with OpenID Connect
        auth_jwt "" token=$session_jwt;
        error_page 401 = @do_oidc_flow;

        #auth_jwt_key_file $oidc_jwt_keyfile;  # Enable when using filename
        auth_jwt_key_request /_jwks_uri;       # Enable when using URL

        # Successfully authenticated users are proxied to the backend,
        # with 'sub' claim passed as HTTP header
        proxy_set_header username $jwt_claim_sub;

        proxy_pass https://backend_app_server; # The backend site/app
        proxy_set_header Host app-proxy.example.com;
        proxy_cookie_domain app-proxy.example.com $host;
    }
}
Complete frontend.conf example (click to expand)
# This is the backend application we are protecting with OpenID Connect
upstream backend_app_server {
    zone backend_app_server 64k;

    # Private IP
    server 10.1.2.3:443;

    # DNS
    #resolver 8.8.8.8;
    #server app-proxy.example.com:443;
}

# Custom log format to include the 'sub' claim in the REMOTE_USER field
log_format main_jwt '$remote_addr - $jwt_claim_sub [$time_local] "$request" $status '
                    '$body_bytes_sent "$http_referer" "$http_user_agent" "$http_x_forwarded_for"';

#
# The frontend server - reverse proxy with OpenID Connect authentication
#
server {
    include conf.d/openid_connect.server_conf; # Authorization code flow and Relying Party processing
    error_log /var/log/nginx/error.log debug;  # Reduce severity level as required

    #####################
    # SSL Configuration #
    #####################
    server_name  app-proxy.example.com;
    listen       443 ssl;

    ssl_certificate     ssl/example.com.crt;
    ssl_certificate_key ssl/example.com.key;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         HIGH:!aNULL:!MD5;

    location = /oidc_logout {
        proxy_ssl_server_name on; # For SNI to the IdP
        proxy_pass $oidc_endsession_endpoint?id_token_hint=$arg_token&post_logout_redirect_uri=https%3a%2f%2fapp-proxy.example.com%2f_logout;
    }

    location / {
        # This site is protected with OpenID Connect
        auth_jwt "" token=$session_jwt;
        error_page 401 = @do_oidc_flow;

        #auth_jwt_key_file $oidc_jwt_keyfile;  # Enable when using filename
        auth_jwt_key_request /_jwks_uri;       # Enable when using URL

        # Successfully authenticated users are proxied to the backend,
        # with 'sub' claim passed as HTTP header
        proxy_set_header username $jwt_claim_sub;

        proxy_pass https://backend_app_server; # The backend site/app
        proxy_set_header Host app-proxy.example.com;
        proxy_cookie_domain app-proxy.example.com $host;
    }
}

# vim: syntax=nginx

6. Verify

  1. Confirm the discovery document loads: curl https://oidc.surepassid.com/oidc/{domain}/.well-known/openid-configuration
  2. Browse to https://app-proxy.example.com/. You should be redirected to the IdP.
  3. Complete primary authentication and the MFA challenge.
  4. Confirm you land on the backend application and that it receives the username header.
  5. Browse to /oidc_logout and confirm the session ends and the browser returns to the registered post-logout URI.

Troubleshooting

Symptom Likely cause
invalid_redirect_uri The Redirect URI is not registered exactly; check scheme, host, port, path
invalid_request mentioning PKCE $oidc_pkce_enable is 0; the IdP requires S256 PKCE
unauthorized_client Grant type not allowed on the client, or wrong tenant domain in the URL
invalid_client at token endpoint Wrong client secret, or caller IP blocked by the client IP whitelist
401 loop at the proxy JWKS not reachable, or /var/cache/nginx/jwk not writable by nginx
Logout does not return to the app post_logout_redirect_uri not URL-encoded or not registered
User re-prompted for MFA frequently Absolute Session Lifetime is short, or offline_access not granted
404 on all OIDC endpoints Using legacy /connect/* paths instead of /oidc/{domain}/*


Document History

Version Date Change
2026.4 September 15, 2026 Converted from Confluence export to Markdown; updated to the tenant-scoped OIDC implementation, mandatory PKCE, and Admin Portal client registration
SurePassID 360 Central Avenue #800 St. Petersburg, FL 33701 USA +1 (888) 200-8144 surepassid.com