NginxをOpenID ConnectのRelying Partyとして実装する

この記事は[認証認可アドベントカレンダー](https://qiita.com/advent-calendar/2019/identity)の24日目の記事です。

# はじめに
本記事ではNginxをOpenID ConnectのRelying Partyとして実装する方法を紹介します。  
NginxをOpenID ConnectのRelying Partyとして実装することによって、既存の実装に依存せずOpenID Connectによる認証の導入が期待できます。  

今回、NginxのOpenIDConnectのRelying Partyとしての実装に[lua-resty-openidc](https://github.com/zmartzone/lua-resty-openidc)を利用します。
[Nginx+](https://www.nginx.com/products/nginx/)では[nginx-openid-connect](https://github.com/nginxinc/nginx-openid-connect)が利用できます。まず、こちらを試してみるのが良いかもしれません。

本記事ではサンプル実装も用意しています。最後に動作確認方法も記載しているので、合わせて確認していただければ幸いです。
https://github.com/kg0r0/lua-resty-openidc-example


サンプル実装はAuthorization Code Flowをサポートしており、下記のシーケンスのように動作しているはずです。
![lua_resty_openidc_diagram.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213529/e2513771-035a-4ac6-218a-cd9544a91da6.png)

# Nginxモジュールについて
今回、Nginxで動作するモジュールの開発に[OpenResty](https://openresty.org/en/)を利用します。
OpenRestyを利用することにより、NginxでLuaJITや便利なライブラリが利用できるようになります。
また、Luaには[Luarocks](https://luarocks.org/)というパッケージ管理システムが存在し、こちらも合わせて利用します。
OpenResty、Luarocksの詳しい利用方法については、どちらもOpenRestyの公式ページに記載されているのでこちらを確認してください。

- OpenRestyのインストール: https://openresty.org/en/installation.html
- Luarocksのインストール: https://openresty.org/en/using-luarocks.html

Nginxで利用するLua関連のディレクティブには実行順序が決まっています。
具体的には、Initialization Phase、Rewrite/Access Phase、Content Phase、Log Phaseが存在します。
認証処理はコンテンツの生成・取得よりも前に行う処理なので、Rewrite/Access Phaseで行うのが良さそうです。
よって、``access_by_lua_file``ディレクティブで認証処理を記述しているLuaのファイルを呼び出します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213529/048c727c-4f75-f258-ebd4-8c7fc7482b73.png)
(引用元: https://github.com/openresty/lua-nginx-module)


# OpenID Connectによる認証の導入

lua-resty-openidcを利用してNginxにOpenID Connectによる認証を導入していきます。
まず、Luarocksでlua-resty-openidcをインストールします。

```bash

$ /usr/local/openresty/luajit/bin/luarocks install lua-resty-openidc
```

一連の認証処理は、lua-resty-openidcの``authenticate()``を呼び出すことで行います。
よって、まずはlua-resty-openidcや``authenticate()``に渡すパラメーターを用意する必要があります。
具体的には、Redirect URI、[OpenID Provider Configuration Request](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig)のURL、Client ID、Client Secretといった値を事前にOpenID provider(OP)に登録・取得しておきます。
用意した値は下記のように``authenticate()``に渡して利用します。

```lua:main.lua
local opts = {
    ssl_verify = "no",
    redirect_uri = "https://<hostname>/secure/redirect_uri",
    discovery = "<issuer>/.well-known/openid-configuration",
    client_id = "<client_id>",
    client_secret = "<client_secret>"
}

local res, err = require("resty.openidc").authenticate(opts)
if err then
    ngx.log(ngx.ERR, err)
    ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
end
ngx.req.set_header("X-USER", res.id_token.sub)

ngx.var.pass="http://webapp:3000/

```
その他設定値についての詳細やGoogle Signinで利用する設定のサンプルなどはlua-resty-openidcの[README](https://github.com/zmartzone/lua-resty-openidc/blob/master/README.md)や[Wiki](https://github.com/zmartzone/lua-resty-openidc/wiki)に記載されているので合わせて参照してください。
``authenticate()``を呼び出し、エラーがなかった場合、戻り値として下記の通り、ID Token、Access Tokenやユーザーに関する情報が返ってきます。

>    -- at this point res is a Lua table with 3 keys:
          --   id_token    : a Lua table with the claims from the id_token (required)
          --   access_token: the access token (optional)
          --   user        : a Lua table with the claims returned from the user info endpoint (optional)
(引用元: https://github.com/zmartzone/lua-resty-openidc/blob/master/README.md#sample-configuration-for-google-signin)

次に、上記の認証処理を行うスクリプトを呼び出す設定をnginx.confに記述します。
スクリプトの配置について、OpenRestyを利用する場合、OpenRestyに含まれるパッケージはデフォルトで``/usr/local/openresty/``以下に展開されるため、今回は認証処理を記述したLuaスクリプトも``/usr/local/openresty/``以下に配置します。
スクリプトの配置場所が決まったら、前述したとおりnginx.confの``access_by_lua_file``ディレクティブでスクリプトのパスを指定します。
ここまでのlua-resty-openidcを利用する設定を記述した場合、nginx.confは下記のようになります。

```nginx:nginx.conf
#user  nobody;
worker_processes  1;

#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;

    #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;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

        server {
        listen       80;
        server_name  localhost;
        resolver local=on ipv6=off;

        location / {
                set $pass '';
                default_type 'text/html';
                access_by_lua_file /usr/local/openresty/main.lua;
                proxy_pass $pass;
        }

    }

    #gzip  on;

    include /etc/nginx/conf.d/*.conf;
}
```
以上で[lua-resty-openidc](https://github.com/zmartzone/lua-resty-openidc)の導入は終了です。問題なく設定ができていれば、ここまでで動作確認を行うことができるはずです。

# Redisによるセッション管理の導入
実際の運用を考えた際、セッション情報をNginxのメモリ上ではなく、Redisなどに格納する必要がでてきます。
lua-resty-openidcは下記の通り、内部で[lua-resty-session](https://github.com/bungle/lua-resty-session)を利用しており、セッション情報の保管先の指定が可能とのことです。
> It maintains sessions for authenticated users by leveraging lua-resty-session thus offering a configurable choice between storing the session state in a client-side browser cookie or use in of the server-side storage mechanisms shared-memory|memcache|redis.
(引用元: https://github.com/zmartzone/lua-resty-openidc/blob/master/README.md#lua-resty-openidc)

しかし、本記事の執筆時点(2019/12/21)ではlua-resty-openidcには設定方法が記載されていなかったため、[lua-resty-session](https://github.com/bungle/lua-resty-session)のドキュメントを参照して設定を行います。
lua-resty-sessionのREADMEの[Pluggable Storage Adapters](https://github.com/bungle/lua-resty-session#pluggable-storage-adapters)の章に設定方法は記載されています。
セッション情報の保管先は、nginx.confで下記のように記述することで指定できるようです。

```nginx:nginx.conf
set $session_storage redis;
```
Redisの他にセッション情報の保管先として下記の指定が可能であると記載されています。


- cookie aka Client Side Cookie (this is the default adapter)
- shm aka Lua Shared Dictionary
- memcache aka Memcached Storage Backend
- redis aka Redis Backend

今回はセッション情報の保管先としてRedisを指定するので下記のパラメーターを利用して接続先などを指定します。
Redis以外のストレージを保管先として指定する際は適宜読み替えてください。

```nginx

set $session_redis_prefix        sessions;
set $session_redis_socket        unix:///var/run/redis/redis.sock;
set $session_redis_host          127.0.0.1;
set $session_redis_port          6379;
set $session_redis_auth          password;
set $session_redis_uselocking    on;
set $session_redis_spinlockwait  10000;
set $session_redis_maxlockwait   30;
set $session_redis_pool_timeout  45;
set $session_redis_pool_size     10;

```

実際に接続先の指定を行ったnginx.confのサンプルは下記のようになります。

```nginx:nginx.conf
#user  nobody;
worker_processes  1;

#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;

    #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;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

        server {
        listen       80;
        server_name  localhost;
        resolver local=on ipv6=off;

        location / {
                set $pass '';
                set $session_storage redis;
                set $session_redis_prefix        sessions;
                set $session_redis_host          redis;
                set $session_redis_port          6379;
                default_type 'text/html';
                access_by_lua_file /usr/local/openresty/main.lua;
                proxy_pass $pass;
        }

    }

    #gzip  on;

    include /etc/nginx/conf.d/*.conf;
}
```
以上でセッション管理の設定も終了になります。

# 動作確認
冒頭でも述べた通り、実装のサンプルを用意しているのでこちらの動作確認方法について触れていきます。
https://github.com/kg0r0/lua-resty-openidc-example


動作確認時、ブラウザに残っている情報により意図した動作をしないことがあります。
なるべくシークレットブラウザを利用して動作確認を行ってください。
もし意図した動作をしない場合などは[Issue](https://github.com/kg0r0/lua-resty-openidc-example/issues)を利用していただけると幸いです。

## サンプル実装について
サンプル実装は[OpenResty Official Docker Images](https://hub.docker.com/r/openresty/openresty)を利用しています。
サンプル実装で利用しているDocker Imageのflavorsは``alpine-fat``を選択しています。
これは、Luaのパッケージ管理であるLuaRocksが含まれているのが``alpine-fat``、``centos``、``xenial``であるからです。
> LuaRocks is included in the alpine-fat, centos, and xenial variants. It is excluded from alpine because it generally requires a build system and we want to keep that variant lean.
(引用元: https://hub.docker.com/r/openresty/openresty#luarocks)

また、サンプル実装では、OpenID provider(OP)として、Keycloakを利用しています。
今回利用するKeycloakは事前に動作確認に必要な情報が登録された状態で起動します。

## 手順
まず、下記の通りコマンドを実行して各コンポーネントを起動します。

```bash

$ git clone https://github.com/kg0r0/lua-resty-openidc-example.git
$ cd lua-resty-openidc-example
$ docker-compose build
$ docker-compose up -d
```

ここで、設定しているリダイレクトURLの関係上、http://keycloak でアクセスした際に、http://127.0.0.1 にアクセスされるように``/etc/hosts``を編集して下記の記述を追加します。

```:/etc/hosts
127.0.0.1	localhost keycloak
```
ここまでできたら実際にアクセスして動作確認を行います。
まず、http://localhost にアクセスします。すると、ログインページが表示されます。
ログインページが表示されたら、Username or emailに``user``、Passwordに``password``をそれぞれ入力して``[Log in]``を押下してください。

<img width="1229" alt="Screen Shot 2019-12-22 at 19.16.16.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213529/71bd172f-5922-c8de-069b-bd87c04b36c7.png">

正しく動作していれば最終的にコンテンツが表示されます。
また、開発者ツールからCookieがセットされていることも確認できます。
<img width="1071" alt="Screen Shot 2019-12-22 at 19.19.34.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213529/55229b71-facd-3038-380d-7f591c298d3e.png">

最後にセッション情報がRedisに登録されているか確認します。
まず、redisの``CONTEINER ID``を確認します。

```bash

$ docker ps -a
CONTAINER ID        IMAGE                                COMMAND                  CREATED             STATUS              PORTS                    NAMES
521f1756ccfb        redis:alpine                         "docker-entrypoint.s…"   2 weeks ago         Up 2 weeks          6379/tcp                 lua-resty-openidc-example_redis_1
```
Redisの``CONTEINER ID``を確認できたら``redis-cli``で登録されているkeyを確認してみます。

```bash

$ docker exec -it 521f1756ccfb redis-cli
127.0.0.1:6379> keys *
1) "sessions:lfgDATCFRlSKF1ML5T7usw.."
```
Cookieとしてセットされていた値が登録されていました。
Redisへのセッション情報の登録も問題なく動作しているようです。

# おわりに
本記事ではlua-resty-openidcを利用し、NginxをOpenID ConnectのRelying Partyとして実装する方法を紹介しました。
また、lua-resty-openidcでセッション管理をカスタマイズする方法も合わせて紹介しました。
OpenID Connectによる認証を導入する際の参考になったら幸いです。

# 参考
- https://github.com/zmartzone/lua-resty-openidc
- https://github.com/openresty/lua-resty-redis
- https://github.com/openresty/lua-nginx-module
- https://github.com/bungle/lua-resty-session
- https://openid.net/specs/openid-connect-discovery-1_0.html