有象無象の社内ツールの認証と権限管理をいい感じにする

[mixiグループ Advent Calendar 2017](https://qiita.com/advent-calendar/2017/mixi)の１０日目の記事です。

XFLAGスタジオでデータ分析基盤の運用を担当している ojima です。

私たちのチームでは分析基盤の一部として、[Zeppelin](https://zeppelin.apache.org/) や [re:dash](https://redash.io/)、内製のダッシュボードなど、データ可視化ツール・分析ツールをいくつか社内向けに提供しています。こういったツールには必ず認証と権限管理の問題がついて回ってきます。

認証や権限管理などを集中管理する場合は LDAP などを使うのが一般的かと思いますが、 LDAP の運用はなかなか辛いものがあります。そこで、私たちのチームでは Google OAuth 2.0 と Google Group を用いて認証・権限管理を行うことにしました。

この記事では、 Zeppelin を例にして認証の流れや設定例を紹介したいと思います。

（Zeppelinは、ブラウザ上でデータの可視化や分析ができるツールです。Jupyter Notebook によく似ていますが、マルチユーザー向けの機能やプラグインの管理画面などが充実しています。）

### 認証の流れ

nginx と [oauth2_proxy](https://github.com/bitly/oauth2_proxy) を組み合わせて認証部分を組み立てます。

![diagram.png](https://qiita-image-store.s3.amazonaws.com/0/961/eb5da14a-c1a2-bbe2-5dd4-feae4a01ff66.png)

1. ユーザーが nginx にアクセス
2. nginx は oauth2_proxy に認証状態を問い合わせる。
    1. 認証済みであれば oauth2_proxy は 202 を返す。
    2. 認証済みでなければ Google OAuth の画面にリダイレクト
    3. 認証に成功すれば 202、そうでばなければ 401 を返す。ここで特定のグループに所属するユーザーだけを許可することも可能。
    4. 認証に成功した場合、ユーザー名をHTTPヘッダにセットして nginx に返す。

    参考： http://lamanotrama.hateblo.jp/entry/2016/01/18/142116
3. 認証が成功していれば nginx は zeppelin にアクセスする。その際に X-Remote-User ヘッダにユーザー情報を付加する。
4. Zeppelin は X-Remote-User ヘッダにセットされているユーザーとしてログインした状態でリクエストを処理する。

**注意**

* 上の構成では、Zeppelin は X-Remote-User がセットされていれば、無条件にログインしているものとして扱います。nginx 以外からはアクセスできないようにしておかないといけません。
* nginx 無しで oauth2_proxy 単体でもプロキシとして使うことができますが、oauth2_proxy は WebSocket に対応していません。Zeppelin では WebSocket が必要なため上のような構成にしています。


### oauth2_proxy をちょっと拡張

基本的には以上の流れで認証を行うのですが、本家の [bitly/oauth2_proxy](https://github.com/bitly/oauth2_proxy) では少し機能が足りなかったので、 [ojima-h/oauth2_proxy](https://github.com/ojima-h/oauth2_proxy) にフォークしてちょっとだけ機能を追加しました。

変更したのは以下の点です：

* 認証に成功した際に、グループ情報もHTTPヘッダに含めるようにしました。これにより、Zeppelin にグループ情報を渡すことができ、Zepplin 側でグループに応じてより細かな権限の制御ができるようになります。
* bitly/oauth2_proxy では、特定のグループにアクセスを制限するために、 Google Directory API を使っており、ドメインの管理者権限が必要でした。そこで、Google Apps Script を用いてグループ情報を取得できるようにし、管理者権限がなくても使えるようにしました。

グループ情報がHTTPヘッダを通して Zeppelin に渡ることで、Google Group ごとに Zeppelin の権限を割り振ることができるわけです。

このあたりの機能が必要であれば ojima-h/oauth2_proxy を試してもらえると良いかもしれません。

### 設定例

フォーク版の ojima-h/oauth2_proxy を使う場合の設定例を以下で紹介します。

#### Google Apps Script 作成

```js
function listMyGroups() {
  var groups = GroupsApp.getGroups();

  return groups.map(function (group) {
    var group_name = group.getEmail();
    Logger.log(group_name);
    return group_name;
  });
}
```

このようなスクリプトを作成し、Execution API で外部から呼び出せるようにします。
Execution API の設定は以下を参考にしてください。

* [Using the Execution API  |  Apps Script  |  Google Developers](https://developers.google.com/apps-script/guides/rest/api)
* [【GAS】Execution APIを使ってJavaScriptからGASにアクセスする - Qiita](https://qiita.com/ShishidoToru/items/67a7cad6b997e316cb20)

#### oauth2_proxy の設定

```sh
./oauth2_proxy \
   --email-domain="example.com"  \
   --upstream=http://127.0.0.1:8080/ \
   --cookie-secret="secret"\
   --client-id="CLIENT_ID"\
   --client-secret="CLIENT_SECRET"\
   --redirect-url=http://your-host/oauth2/callback \
   --scope="profile email https://www.googleapis.com/auth/groups" \
   --google-group="your-group@example.com,admin-group@example.com" \
   --google-script-id="SCRIPT_ID" \
   --google-script-function-name="FUNCTION_NAME" \
   --set-xauthrequest
```

CLIENT_ID や SCRIPT_ID は、 Execution API の設定時に取得したものを使います。

`--google-group` に２つのグループを指定しています。ユーザーがこれら２つのグループのいずれかに所属していれば、認証は成功し、これら２つのグループのうち実際にユーザーが所属しているグループの名前がHTTPヘッダにセットされます。

`--set-xauthrequest` を指定することで、nginx とユーザー情報などをやりとりできるようになります。

#### nginx の設定

```nginx
server {
    listen       80;
    server_name  localhost;

    location /oauth2/ {
        proxy_pass       OAUTH2_PROXY_URL;
        proxy_set_header Host                    $host;
        proxy_set_header X-Real-IP               $remote_addr;
        proxy_set_header X-Scheme                $scheme;
        proxy_set_header X-Auth-Request-Redirect $request_uri;
    }

    location / {
        auth_request /oauth2/auth;
        error_page 401 = /oauth2/sign_in;

        # pass information via X-Remote-User and X-Remote-Email headers to backend,
        # requires running with --set-xauthrequest flag
        auth_request_set $user           $upstream_http_x_auth_request_user;
        auth_request_set $groups         $upstream_http_x_auth_request_groups; # フォーク版のみ
        proxy_set_header X-Remote-User   $user;
        proxy_set_header X-Remote-Groups $groups; # フォーク版のみ

        # if you enabled --cookie-refresh, this is needed for it to work with auth_request
        auth_request_set $auth_cookie $upstream_http_set_cookie;
        add_header Set-Cookie $auth_cookie;

        proxy_pass ZEPPELIN_URL;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header X-NginX-Proxy true;
        proxy_redirect off;
    }

    location /ws {  # For websocket support
        proxy_pass ZEPPELIN_URL;
        proxy_http_version 1.1;
        proxy_set_header Upgrade websocket;
        proxy_set_header Connection upgrade;
        proxy_read_timeout 86400;
    }

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

参考：

* https://github.com/bitly/oauth2_proxy#configuring-for-use-with-the-nginx-auth_request-directive
* https://zeppelin.apache.org/docs/0.6.2/security/authentication.html

#### Zeppelin の設定 ~ HTTP ヘッダを用いたログインをサポートする

Zeppelin はデフォルトでは X-Remote-User などの HTTP ヘッダを用いたログインをサポートしていません。
しかし、Zeppelin の認証部分には [Apache Shiro](https://shiro.apache.org/) という認証フレームワークが用いられており、いくつかプラグインが提供されています。

今回は [shiro-remote-user](https://github.com/leighklotz/shiro-remote-user) というプラグインを使います。

1. [shiro-remote-user](https://github.com/leighklotz/shiro-remote-user) を clone
2. ビルド: `mvn package`
3. target/shiro-remote-user-0.0.1-SNAPSHOT.jar を、Zeppelin の lib/ 以下にコピー

#### Zeppelin の設定 ~ 認証方法とグループごとの権限を設定

conf/shiro.conf

```ini
[main]
sessionManager = org.apache.shiro.web.session.mgt.DefaultWebSessionManager

securityManager.sessionManager = $sessionManager
# 86,400,000 milliseconds = 24 hour
securityManager.sessionManager.globalSessionTimeout = 86400000
shiro.loginUrl = /api/login

remoteUserFilter = com.yahoo.shiro.remoteuser.filter.RemoteUserAuthenticationFilter
remoteUserFilter.remoteUserHeaderName = X-Remote-User
remoteUserFilter.remoteRolesHeaderName = X-Remote-Groups
remoteUserFilter.remoteRolesSeparator = ","
remoteUserRealm = com.yahoo.shiro.remoteuser.realm.RemoteUserRealm
securityManager.realms = $remoteUserRealm


[urls]
# This section is used for url-based security.
# You can secure interpreter, configuration and credential information by urls. Comment or uncomment the below urls that you want to hide.
# anon means the access is anonymous.
# authc means Form based Auth Security
# To enfore security, comment the line below and uncomment the next one
/api/version = anon
/api/interpreter/** = authc, roles[admin-group@example.com]
/api/configurations/** = authc, roles[admin-group@example.com]
/api/credential/** = authc, roles[admin-group@example.com]
/** = remoteUserFilter
```

ここでは

* ユーザー情報を X-Remote-User から取得
* グループ情報を X-Remote-Groups から取得
* interpreter や credential などの API には admin-group のみアクセスを許可

などを設定しています。

### まとめ

以上 outh2_proxy を用いて認証と権限管理を行う方法を紹介してみました。
かなり Zeppelin 寄りの説明になりましたが、もちろん幅広いWebアプリケーションに適用できます。
また、 oauth2_proxy は Google 以外の認証もサポートしており、Github などを利用することも可能です。

以上の方法では、アプリケーション側で認証機能を持つ必要がなく、ユーザー情報やグループ情報はHTTPヘッダから取得すれば良いだけなので、認証が必要なツールを自前で作る際もだいぶ手間が省けるのではないでしょうか。
nginx や oauth2_proxy は docker 上で実行してしまうのもおすすめです。

それでは良いお年を:christmas_tree: