This project provides an NJS (NGINX JavaScript) module for decoding and utilizing Azure Easy Auth headers in NGINX configurations.
It enables NGINX to interpret and leverage authentication information provided by Azure App Service or Azure Container Apps built-in authentication.
The azure_easy_auth.js module provides functionality to handle authentication data from Azure Easy Auth in NGINX environments. Key capabilities include:
- HTTP Header Access: Provides direct access to Azure Easy Auth HTTP headers
getHeaderClientPrincipal()
: Retrieves rawX-MS-CLIENT-PRINCIPAL
header valuegetHeaderClientPrincipalId()
: RetrievesX-MS-CLIENT-PRINCIPAL-ID
header directlygetHeaderClientPrincipalName()
: RetrievesX-MS-CLIENT-PRINCIPAL-NAME
header directlygetHeaderClientPrincipalIdp()
: RetrievesX-MS-CLIENT-PRINCIPAL-IDP
header directlydecodeHeaderClientPrincipal()
: Decodes base64 header into JSON object
- Identity Functions: Provides convenient methods to access common user identity attributes encoded in
X-MS-CLIENT-PRINCIPAL
:getClaimEmail()
: Retrieves user's email addressgetClaimName()
: Extracts display namegetClaimObjectId()
: Obtains unique object identifiergetClaimPreferredUsername()
: Gets preferred usernamegetClaimGroups()
: Lists all group membershipsgetClaimValue()
,getClaimValues()
: Accesses custom claim fields
- Authorization Utilities: Simplifies access control implementation:
hasClaimGroup()
: Verifies if user belongs to a specific groupisAuthorizedPrincipals()
: Implements RBAC by checking user's object ID and group memberships against allowed principals
Azure Easy Auth exposes authentication context through HTTP headers as documented in Microsoft Learn:
X-MS-CLIENT-PRINCIPAL
: Base64-encoded JSON containing all claimsX-MS-CLIENT-PRINCIPAL-ID
: User's unique identifierX-MS-CLIENT-PRINCIPAL-NAME
: User's name or usernameX-MS-CLIENT-PRINCIPAL-IDP
: Identity provider identifier (e.g., "aad" for Microsoft Entra ID)
This project publishes customized Docker images based on the official NGINX images:
ghcr.io/yaegashi/azure-easy-auth-njs/nginx:VERSION
Here, VERSION
corresponds to the base version, such as 1.27.4
or latest
.
These images are designed to support the following projects:
- yaegashi/azdops-nginx-aas (for Azure App Service)
- yaegashi/azdops-nginx-aca (for Azure Container Apps)
The Docker image includes the following features:
-
The content of the njs folder is available in
/etc/nginx/njs
. -
The following directives are added to
/etc/nginx/nginx.conf
:load_module modules/ngx_http_js_module.so; js_path /etc/nginx/njs; js_path /nginx/njs;
-
You can mount a persistent volume, such as an Azure Files share, at
/nginx
in the container. When/nginx
exists, the following persistent folders will be created:Persistent Folder Description /nginx/templates
Configuration template folder. Symlinked to /etc/nginx/templates
. The defaultdefault.conf.template
will be placed if empty./nginx/sites/default
Default document root folder. The default index.html
will be placed if empty./nginx/njs
NJS module folder. Included in the NJS search paths via js_path
./nginx/logs
Log output folder. Additionally, it monitors
/etc/nginx/templates
and reloads the NGINX server when content changes are detected. -
It launches an OpenSSH server (sshd) when the container runs on Azure App Service.
You can customize NGINX using /nginx/templates/default.conf.template
in the persistent folder.
${NGINX_HOST}
and ${NGINX_PORT}
in template files will be replaced with the corresponding environment variables using envsubst
.
Fluent Bit aggregates logs from NGINX (access/error) and other services (monitor) into a single container stdout stream. The output follows this format, with a timestamp, log origin, and the actual log message:
1743312054.855268 monitor 2025-03-30T05:20:54.85518 I: Change detected in /etc/nginx/templates
1743312054.860397 monitor 2025-03-30T05:20:54.86034 20-envsubst-on-templates.sh: Running envsubst on /etc/nginx/templates/default.conf.template to /etc/nginx/conf.d/default.conf
1743312054.869931 monitor 2025-03-30T05:20:54.86986 nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
1743312054.870034 monitor 2025-03-30T05:20:54.86997 nginx: configuration file /etc/nginx/nginx.conf test is successful
1743312054.870827 monitor 2025-03-30T05:20:54.87075 I: Reloading nginx service
1743312054.872446 monitor 2025-03-30T05:20:54.87156 ok: run: /docker/service/nginx: (pid 54) 3751s
1743312054.975864 error 2025/03/30 05:20:54 [notice] 2376#2376: gracefully shutting down
1743312054.975869 error 2025/03/30 05:20:54 [notice] 2375#2375: gracefully shutting down
1743312054.975870 error 2025/03/30 05:20:54 [notice] 2376#2376: exiting
1743312054.975870 error 2025/03/30 05:20:54 [notice] 2375#2375: exiting
1743312054.975871 error 2025/03/30 05:20:54 [notice] 2375#2375: exit
1743312054.975871 error 2025/03/30 05:20:54 [notice] 2376#2376: exit
1743312054.985042 error 2025/03/30 05:20:54 [notice] 54#54: signal 17 (SIGCHLD) received from 2375
1743312054.985065 error 2025/03/30 05:20:54 [notice] 54#54: worker process 2375 exited with code 0
1743312054.985148 error 2025/03/30 05:20:54 [notice] 54#54: worker process 2376 exited with code 0
1743312054.985169 error 2025/03/30 05:20:54 [notice] 54#54: signal 29 (SIGIO) received
1743312082.555455 access 172.18.0.1 - - [30/Mar/2025:05:21:22 +0000] "GET / HTTP/1.1" 304 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36" "10.240.2.23"
1743312088.008587 access 172.18.0.1 - - [30/Mar/2025:05:21:28 +0000] "GET /abc HTTP/1.1" 404 555 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36" "10.240.2.23"
1743312088.008511 error 2025/03/30 05:21:28 [error] 2408#2408: *6 open() "/nginx/sites/default/abc" failed (2: No such file or directory), client: 172.18.0.1, server: _, request: "GET /abc HTTP/1.1", host: "localhost:8080"
On container startup, it creates a log file in the persistent folder following this naming convention:
/nginx/logs/%Y/%m/%d/%Y%m%dT%H%M%S_{container_hostname}.log
The log file contains the same output that is streamed to stdout, providing both real-time monitoring and persistent logging capabilities.
On the container starting,
docker/default.conf.template are copied to /nginx/templates/default.conf.template
if not exists.
server {
listen ${NGINX_PORT} default_server;
server_name _;
root /nginx/sites/default;
}
Put the following in /nginx/template/default.conf.template
:
server {
listen ${NGINX_PORT};
server_name _;
root /nginx/sites/default;
js_import auth from azure_easy_auth.js;
js_set $is_authorized auth.isAuthorizedPrincipals;
location ~ ^/secure/ {
set $authorized_principals "98a7b6c5-d4e3-21f0-9g8h-765432109876";
if ($is_authorized = "0") {
return 403 "Forbidden";
}
}
}
This configuration secures paths beginning with /secure/
using the isAuthorizedPrincipals
function. The function takes the $authorized_principals
NGINX variable as input, which can contain multiple comma-separated GUIDs. The function returns "1"
if any of these GUIDs match the authenticated user's object ID or group claims, or "0"
if no match is found. Access is denied with a 403 response when the function returns "0"
.
server {
listen ${NGINX_PORT};
server_name _;
root /nginx/sites/default;
js_import auth from azure_easy_auth.js;
js_set $is_authorized auth.isAuthorizedPrincipals;
set $super_principal "123e4567-e89b-12d3-a456-426614174000";
location ~ ^/secure/(?<secured_principal>[^/]+) {
set $authorized_principals "$secured_principal,$super_principal";
if ($is_authorized = "0") {
return 403 "Forbidden";
}
}
}
This configuration demonstrates dynamic permission setting based on the request path. It extracts a $secured_principal
(GUID) from paths like /secure/{GUID}/...
and combines it with an always-authorized $super_principal
to create the $authorized_principals
variable.
Access is granted only if the user's object ID or group claims match either the $secured_principal
or the $super_principal
.
map $host $site_name {
default default;
~*^${NGINX_HOST}$ default;
~*^(.+)\.${NGINX_HOST}$ $1;
}
server {
listen ${NGINX_PORT};
server_name .${NGINX_HOST};
root /nginx/sites/$site_name;
js_import auth from azure_easy_auth.js;
js_set $is_authorized auth.isAuthorizedPrincipals;
if ($site_name ~* ^pr-review-) {
# Set PR reviewer's group principal
set $authorized_principals "85b93f9c-7d2e-4a80-b71c-425ae32f1cc1";
if ($is_authorized = "0") {
return 403 "Forbidden";
}
}
}
server {
listen ${NGINX_PORT} default_server;
server_name _;
return 404 "Not Found";
}
This configuration serves different content based on subdomains. When NGINX_HOST=example.com
, it behaves as follows:
- Access to the main domain (
example.com
) serves content from/nginx/sites/default
- Regular subdomains (e.g.,
blog.example.com
) serve content from their corresponding subdirectories (/nginx/sites/blog
) - Pull request review subdomains (e.g.,
pr-review-123.example.com
) are protected and only accessible by authenticated users in the group85b93f9c-7d2e-4a80-b71c-425ae32f1cc1
- Access to any undefined domain (e.g.,
example.net
) returns a 404 error
This pattern is particularly useful in team development environments where you might create dedicated preview environments for each pull request, accessible only to specific reviewers.