An open API service indexing awesome lists of open source software.

https://github.com/yaegashi/azure-easy-auth-njs

Azure Easy Auth NJS Module
https://github.com/yaegashi/azure-easy-auth-njs

azure azure-app-service azure-container-apps nginx njs

Last synced: 2 months ago
JSON representation

Azure Easy Auth NJS Module

Awesome Lists containing this project

README

          

# Azure Easy Auth NJS Module

## Introduction

This project provides an [NJS (NGINX JavaScript)](https://nginx.org/en/docs/njs/) 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](https://learn.microsoft.com/en-us/azure/app-service/overview-authentication-authorization) or
[Azure Container Apps](https://learn.microsoft.com/en-us/azure/container-apps/authentication) built-in authentication.

## Module Capabilities

The [azure_easy_auth.js](njs/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 raw `X-MS-CLIENT-PRINCIPAL` header value
- `getHeaderClientPrincipalId()`: Retrieves `X-MS-CLIENT-PRINCIPAL-ID` header directly
- `getHeaderClientPrincipalName()`: Retrieves `X-MS-CLIENT-PRINCIPAL-NAME` header directly
- `getHeaderClientPrincipalIdp()`: Retrieves `X-MS-CLIENT-PRINCIPAL-IDP` header directly
- `decodeHeaderClientPrincipal()`: 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 address
- `getClaimName()`: Extracts display name
- `getClaimObjectId()`: Obtains unique object identifier
- `getClaimPreferredUsername()`: Gets preferred username
- `getClaimGroups()`: Lists all group memberships
- `getClaimValue()`, `getClaimValues()`: Accesses custom claim fields
- **Authorization Utilities**: Simplifies access control implementation:
- `hasClaimGroup()`: Verifies if user belongs to a specific group
- `isAuthorizedPrincipals()`: 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](https://learn.microsoft.com/en-us/azure/app-service/configure-authentication-user-identities):

- `X-MS-CLIENT-PRINCIPAL`: Base64-encoded JSON containing all claims
- `X-MS-CLIENT-PRINCIPAL-ID`: User's unique identifier
- `X-MS-CLIENT-PRINCIPAL-NAME`: User's name or username
- `X-MS-CLIENT-PRINCIPAL-IDP`: Identity provider identifier (e.g., "aad" for Microsoft Entra ID)

## Docker Images

### Overview

This project publishes customized Docker images based on [the official NGINX images](https://hub.docker.com/_/nginx):

```
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](https://github.com/yaegashi/azdops-nginx-aas) (for Azure App Service)
- [yaegashi/azdops-nginx-aca](https://github.com/yaegashi/azdops-nginx-aca) (for Azure Container Apps)

### Features

The Docker image includes the following features:

- The content of the [njs](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 default `default.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`.

### Logging

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.

### Default Template Configuration

On the container starting,
[docker/default.conf.template](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;
}
```

### Secure Path Configuration

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"`.

### Dynamic Secure Path Configuration

```
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/(?[^/]+) {
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`.

### Multiple Subdomain Configuration

```
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 group `85b93f9c-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.