Requirements

Install

Follow the nginx-openid-connect installation instructions.
NGINX (nginx-openid-connect) - Installation

Install the jq command-line JSON processor. There is a dependency for this but it is not automatically installed.

SELinux Issue Workaround

There is an issue with NGINIX’s permissions for the directory “/etc/nginx/conf.d". The simplest workaround for this is to change the owner and group of that directory to "nginix".

A better solution would be to fix SELinux httpd_t context to allow the nginix process to create files in that directory.
https://www.nginx.com/blog/using-nginx-plus-with-selinux/

Configuring the SurePassID Identity Provider

NGINX (nginx-openid-connect) - Configuring your IdP

NGINX OpenID Connect Client Configuration

Use the following SurePassID Identity Provider OpenID Connect client configuration for the NGNIX replying party.

Update the following properties as required.

See Client Object Reference for more details.

{
    "Enabled": true,
    "ProtocolType": "oidc",
    "ClientId": "nginx-client-id",
    "ClientName": "NGINX Client Name",
    "ClientSecrets": [
        {
            "Value": "bmdpbngtY2xpbmV0LXNlY3JldA==",
            "Type": "SharedSecret"
        }
    ],
    "AllowedGrantTypes": [
        "authorization_code"
    ],
    "RedirectUris": [
        "https://app-proxy.example.com/_codexch"
    ],
    "PostLogoutRedirectUris": [
        "https://app-proxy.example.com/_logout"
    ],
    "RequireClientSecret": true,
    "RequireConsent": true,
    "AllowRememberConsent": true,
    "RequirePkce": false,
    "AllowPlainTextPkce": false,
    "AllowAccessTokensViaBrowser": false,
    "FrontChannelLogoutSessionRequired": true,
    "BackChannelLogoutSessionRequired": true,
    "AllowOfflineAccess": false,
    "AllowedScopes": [
        "profile",
        "openid"
    ],
    "AlwaysIncludeUserClaimsInIdToken": true,
    "IdentityTokenLifetime": 300,
    "AccessTokenLifetime": 3600,
    "AuthorizationCodeLifetime": 300,
    "AbsoluteRefreshTokenLifetime": 2592000,
    "SlidingRefreshTokenLifetime": 1296000,
    "RefreshTokenUsage": 1,
    "UpdateAccessTokenClaimsOnRefresh": false,
    "RefreshTokenExpiration": 1,
    "AccessTokenType": 0,
    "EnableLocalLogin": true,
    "IdentityProviderRestrictions": [],
    "IncludeJwtId": false,
    "Claims": [],
    "AlwaysSendClientClaims": false,
    "ClientClaimsPrefix": "client_",
    "DeviceCodeLifetime": 300,
    "AllowedCorsOrigins": [],
    "Properties": {
        "MfaButtons.ALL": "PushApp,IvrQuestion,SmsQuestion,SmsOtp,EmailOtp,CallWithOtp",
        "MfaButtonsDefault": "SmsOtp,EmailOtp,CallWithOtp",
        "AllowOtpDefault": "true",

        "TenantDomain.0": "tenant0.com",
        "TenantId.0": "<TENANT_0_API_ID>",
        "TenantKey.0": "<TENANT_0_API_KEY>",
        "TenantAllowOtp.0": "true",
        "TenantMfaButtons.0": "PushApp,IvrQuestion,SmsQuestion",

        "TenantDomain.1": "tenant1.com",
        "TenantId.1": "<TENANT_1_API_ID>",
        "TenantKey.1": "<TENANT_1_API_KEY>",
        "TenantAllowOtp.1": "true",
        "TenantMfaButtons.1": "PushApp,IvrQuestion,SmsQuestion,SmsOtp,EmailOtp,CallWithOtp"
    }
}

Configuring NGINIX Plus

NGINX (nginx-openid-connect) - Configuring NGINX Plus

Run the configure.sh script using the SurePassID OpenID Connect Discovery URL.

Make the following changes to the openid_connect_configuration.conf file.

This is a complete example of the updated openid_connect_configuration.conf file.

# 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/connect/authorize;
}

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

map $host $oidc_endsession_endpoint {
    default https://oidc.surepassid.com/connect/endsession;
}
map $host $oidc_jwks_uri {
    default https://oidc.surepassid.com/.well-known/openid-configuration/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 0;
}

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_HMCA_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

Make the following changes to the frontend.conf file. This is where the backend application server is configured.

This is a complete example of the updated frontend.conf file.

# This is the backend application we are protecting with OpenID Connect
upstream backend_app_server {
    zone backend_app_server 64k;

    # Private Azure 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/surepassid.com_2022-05-14.crt;
    ssl_certificate_key ssl/surepassid.com_2022-05-14.key;
    ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers         HIGH:!aNULL:!MD5;

    location = /oidc_logout {
        # https://identityserver4.readthedocs.io/en/latest/endpoints/endsession.html
        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%3a433%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