以下の内容はhttps://ponzmild.hatenablog.com/entry/2025/08/03/182407より取得しました。


JavaScriptでnginxのOAuth2認可処理を書く

nginxコミュニティ版をリバプロとして使うときに、バックエンドにトラフィックを流す前にOAuth2のアクセストークンの中身を参照して認可処理を挟みたい。 Luaスクリプトを書けば細かいカスタマイズはできそうですが、もう少し慣れたJavaScriptで実現する方法を備忘録として残します。

やりたいこと

  • クライアントはOAuth2で取得したアクセストークンを渡す。
  • アクセストークンで指定されたスコープでアクセス制御したい。
    • 特定のスコープを含む場合のみバックエンドのAPIトラフィックを流す。
    • 特定のスコープを含まない場合は、バックエンドのAPIへのトラフィックを流さずクライアントに403 Forbiddenを返す。

実現方式(この記事のモチベーション)

JavaScriptだけでなく他の選択肢も並べてみます。

  1. JWT認証認可モジュール "ngx_http_auth_jwt_module" を入れる ... 有償版のNginx Plusの機能 ref: Setting up JWT Authentication | NGINX Documentation
  2. Luaスクリプトを自分で書く
  3. JavaScritpモジュール "ngx_http_js_module" を入れてJavaScriptを自分で書く
  4. JWT認証認可機能が入った別のリバプロに切り替える

nginxはコミュニティ版を使用しているので1と4をまず除外。 そうなると自分で認可処理を書かないといけないので、慣れている3.JavaScriptで書くことにしました。

やり方

njs概要

nginx でJavaScriptを扱うモジュールは "ngx_http_js_module" (以下、njs) です。 nginxの多種多様な処理をJavaScriptで実現するためのモジュールです。 njs というモジュール略称と同名の組み込みJavaScriptエンジンでJavaScriptを処理します。 njsはECMAScript 5.1と6の一部と互換性があります。 JavaScriptエンジンは他にもモジュール組み込みのQuickJSにも差し替え可能で、ES2023まで互換性範囲を広げて扱えます。この記事ではJavaScriptエンジンにnjsを使用します。

nginx.org

前提

nginxをリバースプロキシとしてローカルのコンテナをたて、バックエンドに httpbin.org を使用しました。 また、JWTアクセストークンを発行するIDプロバイダーはローカルのKeycloakコンテナを使用します。 使用したコンテナのバージョンは次のとおりです。

ソフトウェア バージョン (コンテナイメージ) 起動ポート
nginx docker.io/library/nginx:1.28-alpine 8081
Keycloak (IdP) quay.io/keycloak/keycloak:26.3.2 8080

モジュールのインストール

nginxのモジュールのインストール作業を簡略化するためコンテナイメージを使います。 njsはnginxの公式のコンテナイメージの1.28系には最初から入っています。

https://github.com/nginx/docker-nginx/blob/8852665dbc86d516617450cf6117786a93f37bea/stable/alpine/Dockerfile#L11-L18

RUN set -x \
    && apkArch="$(cat /etc/apk/arch)" \
    && nginxPackages=" \
        nginx=${NGINX_VERSION}-r${PKG_RELEASE} \
        nginx-module-xslt=${NGINX_VERSION}-r${DYNPKG_RELEASE} \
        nginx-module-geoip=${NGINX_VERSION}-r${DYNPKG_RELEASE} \
        nginx-module-image-filter=${NGINX_VERSION}-r${DYNPKG_RELEASE} \
        nginx-module-njs=${NGINX_VERSION}.${NJS_VERSION}-r${NJS_RELEASE} \

認可処理のJavaScript部分はサンプルがGitHubリポジトリにあったので参考にして書いてみます。

github.com

認可処理の実装

プレーンなJavaScriptで記述します。 サンプルは var を使っていたのですが、njsはES5,6と互換性があるので今どきの書き方に合わせて const にしました。

// nginx/njs/authz.js

function jwt(data) {
    const parts = data.split('.').slice(0,2)
        .map(v=>Buffer.from(v, 'base64url').toString())
        .map(JSON.parse);
    return { headers:parts[0], payload: parts[1] };
}

function authorize(r) {
    // Authorizationヘッダーの "Bearer " 部分を除外してアクセストークンのみ取り出す
    const payload_scopes = jwt(r.headersIn.Authorization.slice(7)).payload.scope;

    // 読み取り権限 demo/read がなければ権限エラーとする
    if (!payload_scopes.split(' ').includes('demo/read')) {
        r.error('Forbidden: insufficient scope');
        r.return(403);
        return;
    }

    r.return(200);
}

export default {authorize}

nginx設定

認証認可用のリダイレクト処理 auth_request と、内部リダイレクト用のパス定義 internal を駆使しながら設定を書きます。 proxy_passjs_content は同一ブロックには書けなかったので、/secured-api/* にマッチしたリクエストを /auth に内部リダイレクトして認可処理を走らせて 2xx が返ってきたらバックエンドを通す方式とした。

# njs モジュールを読み込み
load_module modules/ngx_http_js_module.so;

events {  }

http {
  # 認可処理用のJavaScriptを読み込み
  js_path    "/etc/nginx/njs/";
  js_import  authz.js;

  server {
    listen 80;

    location / {
      root   /usr/share/nginx/html;
      index  index.html index.htm;
    }

    location /healthz {
      return 200;
    }

    # 内部リダイレクト先の認可処理エンドポイント
    location /auth {
      internal;
      js_content authz.authorize;
    }

    # 保護されたエンドポイント
    location /secured-api {
      auth_request      /auth;
      proxy_pass        https://httpbin.org/anything;
      proxy_redirect    off;
      proxy_set_header  Host $host;
      proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header  X-Forwarded-Proto $scheme;
      proxy_set_header  Authorization "";
    }
  }
}

コンテナ実行

Docker/Podman Composeで必要なコンテナを実行します。

compose.yml

services:
  idp:
    container_name: idp
    image: quay.io/keycloak/keycloak:26.3.2
    ports:
      - "8080:8080"
    command:
      - start-dev
    environment:
      - KC_BOOTSTRAP_ADMIN_USERNAME=admin
      - KC_BOOTSTRAP_ADMIN_PASSWORD=admin

  proxy:
    container_name: proxy
    image: public.ecr.aws/docker/library/nginx:1.28-alpine
    ports:
      - "8081:80"
    environment:
      - NGINX_ENTRYPOINT_QUIET_LOGS=1
    volumes:
      - ./nginx/config/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/njs:/etc/nginx/njs

compose.yml を入力としてコンテナを起動します。

$ podman compose up -d
 ✔ Container idp                Started
 ✔ Container proxy              Started

nginxのコンテナのログにnjsが起動していることがわかります。

2025/08/03 03:28:02 [notice] 1#1: js vm init njs: ...

リクエスト実行

Keycloakのアカウントコンソール http://localhost:8080/admin/master/console を開いて事前にレルム、クライアントID、シークレット、スコープを作成しておく。方法はこの記事の範囲を超えるので割愛。

項目名 設定値
レルム(realms) demo
クライアントID demo-client
クライアントシークレット Keycloakで自動生成
クライアントスコープ demo/read (読み取り)、demo/write (書き込み)

Keycloakのトークンエンドポイントからアクセストークンを取得する。 リクエストパラメータのscopeに読み取り権限 (demo/read) を指定することでアクセストークンにもscopeを含めるようにする。

ACCESS_TOKEN=$(curl -X POST -H 'Content-Type: application/x-www-form-urlencoded' \
    -d grant_type=client_credentials \
    -d client_id=demo-client \
    -d client_secret=***** \
    -d 'scope=demo/read' \
    http://localhost:8080/realms/demo/protocol/openid-connect/token \
    | jq -r '.access_token')

アクセストークンのペイロードをデコードすると scope に demo/read が含まれている。

$ echo $ACCESS_TOKEN | awk -F'.' '{print $2}' | base64 --decode | jq -r '.scope'
profile demo/read email

アクセストークンを含めてnginx経由でバックエンドにAPIを投げてみる。 認可処理で許可したスコープ demo/read が含まれているのでこれは問題なく200 OKでレスポンスが返ってくる。

$ curl -v --oauth2-bearer ${ACCESS_TOKEN} http://localhost:8081/secured-api/hogehoge           
...
< HTTP/1.1 200 OK
< Server: nginx
...
{
  "args": {}, 
  "data": "", 
  "files": {}, 
  "form": {}, 
  "headers": {
    "Accept": "*/*", 
    "Host": "localhost", 
    "User-Agent": "curl/8.15.0", 
    "X-Amzn-Trace-Id": "Root=...
  }, 
  "json": null, 
  "method": "GET", 
  "origin": "10.20.33.45", 
  "url": "https://localhost/anything/hogehoge"
}

scopeに demo/read を含まない別のアクセストークンを指定してみると、設定どおり 403 Forbidden で権限エラーが返されました。

$ curl -v --oauth2-bearer ${ACCESS_TOKEN} http://localhost:8081/secured-api/piyofuga
...
< HTTP/1.1 403 Forbidden
< Server: nginx
...
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx</center>
</body>
</html>

このときnginxコンテナのエラーログに error レベルでJavaScriptで定義した権限エラーのメッセージが出力されました。

2025/08/03 08:48:19 [error] 17#17: *1 js: Forbidden: insufficient scope

最終的なディレクトリ構造

$ tree .
.
├── compose.yml
└── nginx
    ├── config
    │   └── nginx.conf
    └── njs
        └── authz.js

以上。




以上の内容はhttps://ponzmild.hatenablog.com/entry/2025/08/03/182407より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14