OpenResty（Nginx）とluaでBasic認証＋認可を実装する

# OpenRestyとは

[OpenResty](https://openresty.org/en/)とは、NginxをベースにLuaJITを搭載したWebサーバーです。Luaのライブラリなどが利用できます。

# 環境
- Ubuntu 20.04
- Docker version 20.10.14
- Docker Compose version v2.4.1
- nginx version: openresty/1.21.4.1

# なぜNginxを使わなかったのか？

今回やりたいことは、Basic認証と認可を実装することです。Nginx単体でも認証は簡単に実装できますが、認可の仕組みは意外と骨が折れます。

:::note info
OpenRestyを利用することで、LuaからNginxの変数にアクセスできます。ログインしてきたユーザー名などを取得できれば、Luaスクリプト内の認可ロジックでそのユーザーを許可するかどうか判定できるようになります。
:::

# 検証ソース

## ディレクトリ構成
ソースの配置は以下を想定しています。

```bash
.
├── docker-compose.yaml
└── nginx
    ├── conf.d
    │   └── default.conf
    ├── lua
    │   └── auth.lua
    └── nginx.conf
```

## Docker Compose
```yaml:docker-compose.yaml
version: "3"
services:
  openresty:
    image: openresty/openresty:latest
    hostname: openresty
    container_name: openresty
    volumes:
      - ./nginx/nginx.conf:/usr/local/openresty/nginx/conf/nginx.conf
      - ./nginx/conf.d/default.conf:/etc/nginx/conf.d/default.conf
      - ./nginx/lua/auth.lua:/etc/nginx/lua/auth.lua
      - ./nginx/.htpasswd:/etc/nginx/.htpasswd
    ports:
      - "80:80"
    restart: always
```

## OpenRestyの設定

`nginx.conf`はデフォルトのまま変更していません。

```conf:nginx.conf
# nginx.conf  --  docker-openresty
#
# This file is installed to:
#   `/usr/local/openresty/nginx/conf/nginx.conf`
# and is the file loaded by nginx at startup,
# unless the user specifies otherwise.
#
# It tracks the upstream OpenResty's `nginx.conf`, but removes the `server`
# section and adds this directive:
#     `include /etc/nginx/conf.d/*.conf;`
#
# The `docker-openresty` file `nginx.vh.default.conf` is copied to
# `/etc/nginx/conf.d/default.conf`.  It contains the `server section
# of the upstream `nginx.conf`.
#
# See https://github.com/openresty/docker-openresty/blob/master/README.md#nginx-config-files
#

#user  nobody;
#worker_processes 1;

# Enables the use of JIT for regular expressions to speed-up their processing.
pcre_jit on;



#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    # Enables or disables the use of underscores in client request header fields.
    # When the use of underscores is disabled, request header fields whose names contain underscores are marked as invalid and become subject to the ignore_invalid_headers directive.
    # underscores_in_headers off;

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  logs/access.log  main;

        # Log in JSON Format
        # log_format nginxlog_json escape=json '{ "timestamp": "$time_iso8601", '
        # '"remote_addr": "$remote_addr", '
        #  '"body_bytes_sent": $body_bytes_sent, '
        #  '"request_time": $request_time, '
        #  '"response_status": $status, '
        #  '"request": "$request", '
        #  '"request_method": "$request_method", '
        #  '"host": "$host",'
        #  '"upstream_addr": "$upstream_addr",'
        #  '"http_x_forwarded_for": "$http_x_forwarded_for",'
        #  '"http_referrer": "$http_referer", '
        #  '"http_user_agent": "$http_user_agent", '
        #  '"http_version": "$server_protocol", '
        #  '"nginx_access": true }';
        # access_log /dev/stdout nginxlog_json;

    # See Move default writable paths to a dedicated directory (#119)
    # https://github.com/openresty/docker-openresty/issues/119
    client_body_temp_path /var/run/openresty/nginx-client-body;
    proxy_temp_path       /var/run/openresty/nginx-proxy;
    fastcgi_temp_path     /var/run/openresty/nginx-fastcgi;
    uwsgi_temp_path       /var/run/openresty/nginx-uwsgi;
    scgi_temp_path        /var/run/openresty/nginx-scgi;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;

    include /etc/nginx/conf.d/*.conf;

    # Don't reveal OpenResty version to clients.
    # server_tokens off;
}
```

基本認証の設定とluaスクリプトの設定を追記しています。

```diff_conf:default.conf
# nginx.vh.default.conf  --  docker-openresty
#
# This file is installed to:
#   `/etc/nginx/conf.d/default.conf`
#
# It tracks the `server` section of the upstream OpenResty's `nginx.conf`.
#
# This config (and any other configs in `etc/nginx/conf.d/`) is loaded by
# default by the `include` directive in `/usr/local/openresty/nginx/conf/nginx.conf`.
#
# See https://github.com/openresty/docker-openresty/blob/master/README.md#nginx-config-files
#


server {
    listen       80;
    server_name  localhost;

    #charset koi8-r;
    #access_log  /var/log/nginx/host.access.log  main;

    location / {
        root   /usr/local/openresty/nginx/html;
        index  index.html index.htm;
+       auth_basic "basic auth";
+       auth_basic_user_file /etc/nginx/.htpasswd;
+       access_by_lua_file /etc/nginx/lua/auth.lua;
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/local/openresty/nginx/html;
    }

    # proxy the PHP scripts to Apache listening on 127.0.0.1:80
    #
    #location ~ \.php$ {
    #    proxy_pass   http://127.0.0.1;
    #}

    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    #
    #location ~ \.php$ {
    #    root           /usr/local/openresty/nginx/html;
    #    fastcgi_pass   127.0.0.1:9000;
    #    fastcgi_index  index.php;
    #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
    #    include        fastcgi_params;
    #}

    # deny access to .htaccess files, if Apache's document root
    # concurs with nginx's one
    #
    #location ~ /\.ht {
    #    deny  all;
    #}
}

```
以下が基本認証の設定です。コンテナ内の`/etc/nginx/.htpasswd`にユーザーとパスワードの情報を持たせています。
```conf
        auth_basic "basic auth";
        auth_basic_user_file /etc/nginx/.htpasswd;
```
今回は`admin`と`test`の2つユーザーを用意します。パスワードはそれぞれ、`admin`、`test`としています。
```.htpasswd
admin:$apr1$83K7MhNj$OTxCQTTH8eCkfLY94DdG41
test:$apr1$0jX4sM1.$wgnHDKX3u2dyALc7.uEYF/
```
ちなみにパスワードの生成するときは以下のコマンドを実行します。
[こちら](https://qiita.com/zwirky/items/964c7c51644a84fefb00)の記事を参考にしました。

:::note info
`htpasswd`コマンドの`-c`オプションは、新規にファイルを作成するオプションであるため、２つ目以降のユーザーを保存したい場合は`-c`オプションなしで実行すると追記になるようです。
:::
```
htpasswd -c ./nginx/.htpasswd admin
htpasswd ./nginx/.htpasswd test
```

`htpasswd`コマンドが無い場合は、以下のようにインストールします。
```bash
apt-get -y install apache2-utils
```

## Luaスクリプト（認可用）

`default.conf`内の以下の設定は、認可用のLuaスクリプトを定義しています。

```conf
        access_by_lua_file /etc/nginx/lua/auth.lua;
```

```lua:auth.lua
-- remote user名を取得
local remote_user = ngx.var.remote_user
if remote_user ~= "admin" then
    -- adminユーザーでなければ403
    ngx.header.content_type = "text/plain"
    ngx.log(ngx.STDERR, remote_user.." にはアクセス権がありません")
    ngx.status = ngx.HTTP_FORBIDDEN
    return ngx.exit(403)
end
```

- **ngx.var.remote_user**
    - ngx.var.XXXでNginxの変数にアクセスできます。`remote_user`にはユーザー名が入っています。

if分以降は、`remote_user`が`admin`ユーザーでなければ403でアクセスできないようにしています。今回のケースだと、`admin`ユーザーはOpenRestyのデフォルトページにアクセスできるが、`test`ユーザーはアクセスできない状況を作っています。

# コンテナ起動

```bash
docker-compose up -d
```

# アクセス確認

- http://localhost/

## `admin`ユーザーでログインする場合

OpenRestyのデフォルトページが表示されます。

![openresty-default-page.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2518403/10f084d2-33d5-cffa-60ac-1fe292844fc8.png)

## `test`ユーザーでログインする場合

403が表示されます。

![403.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2518403/98135297-b7c6-2787-a514-8b750ab1c2d5.png)

# OpenRestyのログを確認

```bash
docker logs openresty
```

`test`ユーザーが403になっていることが確認できます。
```
2023/03/19 09:08:50 [] 7#7: *2 [lua] auth.lua:7: test にはアクセス権がありません, client: 172.24.0.1, server: localhost, request: "GET / HTTP/1.1", host: "localhost"
172.24.0.1 - test [19/Mar/2023:09:08:50 +0000] "GET / HTTP/1.1" 403 561 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36 Edg/111.0.1661.44"
```

# OpenRestyでよく使うコマンド

## 設定ファイルが有効かチェック(`-t`)

```bash
docker exec -it openresty bash -c 'openresty -t'
```
実行結果の例。
```
nginx: the configuration file /usr/local/openresty/nginx/conf/nginx.conf syntax is ok
nginx: configuration file /usr/local/openresty/nginx/conf/nginx.conf test is successful
```

## OpenRestyのリロード(`-s reload`)

```bash
docker exec -it openresty bash -c 'openresty -s reload'
```
実行結果の例。
```
2023/03/19 09:16:43 [notice] 15#15: signal process started
```

# まとめ

OpenRestyを利用することで、Basic認証に認可の仕組みを実装することができました。Luaスクリプトをもう少し工夫すれば複雑な認証・認可の仕組みを実装できそうです。

以下の記事で、Ldap認証で同様のことを試してみました。

- [OpenResty（Nginx）+Lua+OpenLDAP+ngx_auth_modでLdap認証と認可を実装する](https://qiita.com/v1tam1n/items/e9503b8a5e946d7f7951)

# 参考

- [openresty/lua-nginx-module](https://github.com/openresty/lua-nginx-module)
- [逆引きlua-nginx-module](https://gist.github.com/ykst/52205d4d4968298137ea0050c4569170#nginx%E3%81%AE%E5%A4%89%E6%95%B0%E3%81%AB%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E3%81%99%E3%82%8B)