Set up OIDC authentication with Microsoft Entra
Overview
Learn how to configure F5 NGINX as a Service for Azure (NGINXaaS) OpenID Connect (OIDC) authentication with Microsoft Entra as the identity provider.
Prerequisites
-
Configure an NGINXaaS deployment with SSL/TLS certificates.
-
Enable Runtime State Sharing on the NGINXaaS deployment.
-
Register a Microsoft Entra Web application with configuring Web Redirect URIs to
https://<nginxaas_deployment_fqdn>:443/_codexch
. -
The saved information from the prerequistie 3:
<Application (client) ID>
<Directory (tenant) ID>
<Client Secret value>
Fetch the OpenID Connect metadata URL from Microsoft Entra application
Under the registered Microsoft Entra app, select Overview, on the right panel, select Endpoints, save the value of OpenID Connect metadata document
as <OpenID Connect metadata URL>
. The URL will be different depending on the account type of the Entra application, for example:
https://login.microsoftonline.com/<Directory (tenant) ID>/v2.0/.well-known/openid-configuration
(Single-tenant - Accounts in this organizational directory only)https://login.microsoftonline.com/organizations/v2.0/.well-known/openid-configuration
(Multi-tenant - Accounts in any organizational directory)https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration
(Multi-tenant - Accounts in any organizational directory and personal Microsoft accounts).https://login.microsoftonline.com/consumers/v2.0/.well-known/openid-configuration
(Personal Microsoft accounts only)
Configure NGINX as a Service for Azure to interact with the Entra ID application
Configure NGINXaaS for Azure to use Microsoft Entra as the identity provider. The following steps are based on a single-tenant Entra application. Make sure use the correct <OpenID Connect metadata URL>
for different account type of Entra application.
-
Create a clone of the nginx-openid-connect GitHub repository.
git clone https://github.com/nginxinc/nginx-openid-connect
-
Run the
configure.sh
script, and then continue Step 4../configure.sh \ --host <NGINXaaS Deployment FQDN> \ --client_id <Application (client) ID> \ --client_secret <Client Secret value> \ --auth_jwt_key request <OpenID Connect metadata URL>
Note:
It is recommended to check whether the generated configuration is correct in Step 4. -
Run the following command in the terminal to get the OpenID Connect metadata data: authorization endpoint, token endpoint, and JSON Web Key (JWT) file from the
<OpenID Connect metadata URL>
of Entra application. For example:curl https://login.microsoftonline.com/<Directory (tenant) ID>/v2.0/.well-known/openid-configuration | jq
An example of the output is as below:
{ "token_endpoint": "https://login.microsoftonline.com/<Directory (tenant) ID>/oauth2/v2.0/token", ... "jwks_uri": "https://login.microsoftonline.com/<Directory (tenant) ID>/discovery/v2.0/keys", ... "authorization_endpoint": "https://login.microsoftonline.com/<Directory (tenant) ID>/oauth2/v2.0/authorize", ... }
-
Modify
openid_connect_configuration.conf
:-
Change the
default
parameter value of each of the following map directives to the specified value. This is also the time to check the generated configuration from Step 2.map $host $oidc_authz_endpoint - Value of authorization_endpoint from Step 3 map $host $oidc_token_endpoint - Value of token_endpoint from Step 3 map $host $oidc_jwt_keyfile - Value of jwks_uri from Step 3 map $host $oidc_client – Value of the Application (client) ID from Entra map $host $oidc_client_secret – Value of the Client Secret from Entra map $host $oidc_hmac_key – A unique, long, and secure phrase map $host $zone_sync_leeway - A proper time value of the maximum timeout for zone sync
-
If the configuration already has a
proxy_cache_path
directive, ensure the cache path is different from the path used in this example. For more information on caching, see Enable content caching.proxy_cache_path /var/cache/nginx/jwt levels=1 keys_zone=jwk:64k max_size=1m;
-
Enable
sync
for the keyval memory zones and specify the state files to persist the current state across NGINX restarts. The state file paths are subject to NGINXaaS filesystem restrictions and must be placed in a directory accessible to the NGINX worker processes.keyval_zone zone=oidc_id_tokens:1M state=/opt/oidc_id_tokens.json timeout=1h sync; keyval_zone zone=oidc_access_tokens:1M state=/opt/oidc_access_tokens.json timeout=1h sync; keyval_zone zone=refresh_tokens:1M state=/opt/refresh_tokens.json timeout=8h sync; keyval_zone zone=oidc_pkce:128K timeout=90s sync; # Temporary storage for PKCE code verifier.
Note:
The state parameter for keyval_zone is optional. -
You may modify the URI defined in
map $host $oidc_logout_redirect
to specify an unprotected resource to be displayed after requesting the/logout
location.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 "/_logout"; # Built-in, simple logout page }
-
Change the
js_import oidc from conf.d/openid_connect.js;
to match the njs script’s file path. For examplenginx/conf.d/openid_connect.js
. See details in Step 7 to set the correct value.js_import oidc from nginx/conf.d/openid_connect.js;
Example openid_connect_configuration.conf
# 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 { <nginxaas_deployment_fqdn> https://login.microsoftonline.com/<Directory (tenant) ID>/oauth2/v2.0/authorize; default https://login.microsoftonline.com/<Directory (tenant) ID>/oauth2/v2.0/authorize; #www.example.com "https://my-idp/oauth2/v1/authorize"; } map $host $oidc_authz_extra_args { # Extra arguments to include in the request to the IdP's authorization # endpoint. # Some IdPs provide extended capabilities controlled by extra arguments, # for example Keycloak can select an IdP to delegate to via the # "kc_idp_hint" argument. # Arguments must be expressed as query string parameters and URL-encoded # if required. default ""; #www.example.com "kc_idp_hint=another_provider" } map $host $oidc_token_endpoint { <nginxaas_deployment_fqdn> https://login.microsoftonline.com/<Directory (tenant) ID>/oauth2/v2.0/token; default https://login.microsoftonline.com/<Directory (tenant) ID>/oauth2/v2.0/token; } map $host $oidc_jwt_keyfile { <nginxaas_deployment_fqdn> https://login.microsoftonline.com/<Directory (tenant) ID>/discovery/v2.0/keys; default https://login.microsoftonline.com/<Directory (tenant) ID>/discovery/v2.0/keys; } map $host $oidc_client { <nginxaas_deployment_fqdn> <Application (client) ID>; default <Application (client) ID>; } map $host $oidc_pkce_enable { default 0; } map $host $oidc_client_secret { <nginxaas_deployment_fqdn> <Client Secret value>; default <Client Secret value> ; } 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 "/_logout"; # Built-in, simple logout page } map $host $oidc_hmac_key { <nginxaas_deployment_fqdn> znKq3IB5YeP1t+RDCfgHaJ2O; # This should be unique for every NGINX instance/cluster default znKq3IB5YeP1t+RDCfgHaJ2O; } map $host $zone_sync_leeway { # Specifies the maximum timeout for synchronizing ID tokens between cluster # nodes when you use shared memory zone content sync. This option is only # recommended for scenarios where cluster nodes can randomly process # requests from user agents and there may be a situation where node "A" # successfully received a token, and node "B" receives the next request in # less than zone_sync_interval. default 2000; # Time in milliseconds, e.g. (zone_sync_interval * 2 * 1000) } 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=/opt/oidc_id_tokens.json timeout=1h sync; keyval_zone zone=oidc_access_tokens:1M state=/opt/oidc_access_tokens.json timeout=1h sync; keyval_zone zone=refresh_tokens:1M state=/opt/refresh_tokens.json timeout=8h sync; keyval_zone zone=oidc_pkce:128K timeout=90s sync; # Temporary storage for PKCE code verifier. keyval $cookie_auth_token $session_jwt zone=oidc_id_tokens; # Exchange cookie for JWT keyval $cookie_auth_token $access_token zone=oidc_access_tokens; # Exchange cookie for access token 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_access_token zone=oidc_access_tokens; 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 nginx/conf.d/openid_connect.js;
-
-
Modify
openid_connect.server_conf
file:-
Remove the
location /api/
block, since NGINXaaS for Azure currently restricts access to theapi
directive.location /api/ { api write=on; allow 127.0.0.1; # Only the NGINX host may call the NGINX Plus API deny all; access_log off; }
Example openid_connect.server_conf
# Advanced configuration START set $internal_error_message "NGINX / OpenID Connect login failure\n"; set $pkce_id ""; resolver 8.8.8.8; # For DNS lookup of IdP endpoints; subrequest_output_buffer_size 32k; # To fit a complete tokenset response gunzip on; # Decompress IdP responses if necessary # Advanced configuration END location = /_jwks_uri { internal; proxy_cache jwk; # Cache the JWK Set recieved from IdP proxy_cache_valid 200 12h; # How long to consider keys "fresh" proxy_cache_use_stale error timeout updating; # Use old JWK Set if cannot reach IdP proxy_ssl_server_name on; # For SNI to the IdP proxy_method GET; # In case client request was non-GET proxy_set_header Content-Length ""; # '' proxy_pass $oidc_jwt_keyfile; # Expecting to find a URI here proxy_ignore_headers Cache-Control Expires Set-Cookie; # Does not influence caching } location @do_oidc_flow { status_zone "OIDC start"; js_content oidc.auth; default_type text/plain; # In case we throw an error } set $redir_location "/_codexch"; location = /_codexch { # This location is called by the IdP after successful authentication status_zone "OIDC code exchange"; js_content oidc.codeExchange; error_page 500 502 504 @oidc_error; } location = /_token { # This location is called by oidcCodeExchange(). We use the proxy_ directives # to construct the OpenID Connect token request, as per: # http://openid.net/specs/openid-connect-core-1_0.html#TokenRequest internal; proxy_ssl_server_name on; # For SNI to the IdP proxy_set_header Content-Type "application/x-www-form-urlencoded"; proxy_set_body "grant_type=authorization_code&client_id=$oidc_client&$args&redirect_uri=$redirect_base$redir_location"; proxy_method POST; proxy_pass $oidc_token_endpoint; } location = /_refresh { # This location is called by oidcAuth() when performing a token refresh. We # use the proxy_ directives to construct the OpenID Connect token request, as per: # https://openid.net/specs/openid-connect-core-1_0.html#RefreshingAccessToken internal; proxy_ssl_server_name on; # For SNI to the IdP proxy_set_header Content-Type "application/x-www-form-urlencoded"; proxy_set_body "grant_type=refresh_token&refresh_token=$arg_token&client_id=$oidc_client&client_secret=$oidc_client_secret"; proxy_method POST; proxy_pass $oidc_token_endpoint; } location = /_id_token_validation { # This location is called by oidcCodeExchange() and oidcRefreshRequest(). We use # the auth_jwt_module to validate the OpenID Connect token response, as per: # https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation internal; auth_jwt "" token=$arg_token; js_content oidc.validateIdToken; error_page 500 502 504 @oidc_error; } location = /logout { status_zone "OIDC logout"; add_header Set-Cookie "auth_token=; $oidc_cookie_flags"; # Send empty cookie add_header Set-Cookie "auth_redir=; $oidc_cookie_flags"; # Erase original cookie js_content oidc.logout; } location = /_logout { # This location is the default value of $oidc_logout_redirect (in case it wasn't configured) default_type text/plain; return 200 "Logged out\n"; } location @oidc_error { # This location is called when oidcAuth() or oidcCodeExchange() returns an error status_zone "OIDC error"; default_type text/plain; return 500 $internal_error_message; }
-
-
Modify the
frontend.conf
file to create a valid NGINX root config file. For an existingnginx.conf
, simply copy thelocation / { ... }
block fromfrontend.conf
to the desired server block then follow the steps as below:- Wrap all lines in an http block
http {}
infrontend.conf
. Skip for an existingnginx.conf
. - Change the upstream server address to match your backend applications. Skip for an existing
nginx.conf
. - Add
load_module modules/ngx_http_js_module.so;
to the top of the root config file, if it doesn’t exist. - Add
include conf.d/openid_connect_configuration.conf;
in the http block before the server block and change to the matched file path . See details in Step 7 to set the correct file path. - Add
include conf.d/openid_connect.server_conf;
in the server block if it doesn’t exist, then change the value to the matched file path. See details in Step 7 to set the correct file path. - Replace
listen
to use443 ssl;
and specify thessl_certificate
andssl_certificate_key
directives to match the NGINXaaS deployment’s certificate and key path infrontend.conf
. For an existingnginx.conf
, make sure the desired server block has SSL enabled with correct directives. - Make sure
auth_jwt_key_file $oidc_jwt_keyfile;
is commented andauth_jwt_key_request /_jwks_uri;
is uncommented to use JWKS URI. - Change the
location
URI,proxy_pass
URL andaccess_log
to your own config. - Add a stream block with
zone_sync
enabled if it doesn’t exist; see Runtime State Sharing.
Example frontend.conf using the localhost as a upstream server
load_module modules/ngx_http_js_module.so; http { # This is the backend application we are protecting with OpenID Connect upstream my_backend { zone my_backend 64k; # Reuse the localhost as a upstream server # Modify to the real upstream server address if you have server 127.0.0.1; } # A local server block representing the upstream server for testing only # Remove if you have the real upstream servers server { listen 80; default_type text/html; return 200 '<!DOCTYPE html><h2>This is a site protected by OIDC!</h2>\n'; } # 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 # include conf.d/openid_connect_configuration.conf; 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 listen 443 ssl; # Use SSL/TLS in production ssl_certificate /etc/nginx/ssl/my-cert.crt; ssl_certificate_key /etc/nginx/ssl/my-cert.key; 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; # Bearer token is uses to authorize NGINX to access protected backend #proxy_set_header Authorization "Bearer $access_token"; # Intercept and redirect "401 Unauthorized" proxied responses to nginx # for processing with the error_page directive. Necessary if Access Token # can expire before ID Token. #proxy_intercept_errors on; proxy_pass http://my_backend; # The backend site/app access_log /var/log/nginx/access.log main_jwt; } } } stream { # Add localhost resolver for internal clustering hostname with resolver metrics collection resolver 127.0.0.1:49153 valid=20s status_zone=stream_resolver_zone1; server { listen 9000; zone_sync; zone_sync_server internal.nginxaas.nginx.com:9000 resolve; } }
- Wrap all lines in an http block
-
Make sure all the file paths in the config are set properly, and upload the NGINX configuration files.
-
When you select + New File to add a file path via the portal, the file path in the configuration should match the path where you add the file. For example, if your existing file structure in the portal is
/etc/nginx/
, and you add/etc/nginx/conf.d/openid_connect_configuration.conf
,/etc/nginx/conf.d/locations/openid_connect.server_conf
, and/etc/nginx/conf.d/openid_connect.js
, you should set the File path field to these exact paths after each + New File selection. The configuration file paths should look like this:/etc/nginx/conf.d/openid_connect_configuration.conf
/etc/nginx/conf.d/locations/openid_connect.server_conf
/etc/nginx/conf.d/openid_connect.js
In the http block before the server block:
include /etc/nginx/conf.d/openid_connect_configuration.conf;
In the server block:
include /etc/nginx/conf.d/locations/openid_connect.server_conf;
In
openid_connect_configuration.conf
:js_import oidc from /etc/nginx/conf.d/openid_connect.js;
The expected output file structure example is as below:
-
When you upload a GZIP NGINX configuration, the file path in the configuration should match the file path in the archive. Given
nginx
is a directory with the following structure whennginx.tar.gz
is created viatar -czf nginx.tar.gz nginx
:$ tree nginx nginx ├── conf.d │ ├── openid_connect.js │ ├── openid_connect.server_conf │ └── openid_connect_configuration.conf └── nginx.conf
In the http block before the server block:
include conf.d/openid_connect_configuration.conf;
In the server block:
include conf.d/openid_connect.server_conf;
In
openid_connect_configuration.conf
:js_import oidc from nginx/conf.d/openid_connect.js;
The expected output example is as below:
See Upload an NGINX configuration for more details.
-
-
In a web browser, enter
https://<nginxaas_deployment_fqdn>/<protected_uri>
, it will be redirected to the Microsoft IDP server. And after a successful login using the credentials of a user who has the authorization, the protected URI can be accessed. For example, with thefrontend.conf
in this guide, enterhttps://<nginxaas_deployment_fqdn>/
and pass the authentication, it will show:This is a site protected by OIDC!