『入る学科間違えた高専生』の日記

プログラミングのコードを書いたりする予定です。あとは日記等。あといつまで高専生やねん

認証機能のない特定のWebページにOAuth2を設定した

はじめに

今回は、 Swagger などの開発運営側にしか見られたくないけど、 Basic 認証とかちょっと運用が面倒*1になってきたので
GitHub Organization に所属している人ならアクセスができるようにしてあげようって思ってやってみたので備忘録です。

本記事では個人的に設定しておいているものだけなので、ユーザ指定でやるやつを設定しています。

調べていたら、 oauth2-proxy という便利そうなものが存在していたのでこちらを使用していこうと思います。

oauth2-proxy.github.io

前提

  • macOS 10.15.7
  • Docker engine v20.10.2
  • docker-compose 1.27.4

ざっとこんな感じ。

f:id:nanashinodonbee:20210131070724p:plain

最終的にはこんな感じの docker-compose が立ち上がります。

導入

これを実装してみようと思ったのは、前述の通り、(副業で)今までは Swagger の URI などに対して Basic 認証をかけていたのですが、アプリも同じサーバを向いているので Authorization ヘッダーが Basic 認証と取り合うから別の X-App-Authorization のようなカスタムヘッダーを使っていたり、
人が増えてきてアクセスキーを共有するのが面倒になってきたり、そもそも自分も Basic 認証のパスワードとか覚えてないから毎回パスワードをコピーしてくるのに面倒になっていた。
とかもろもろありました。

あと、私は Web フロントエンドの開発に一切興味がない(やりたくない)ので Web フロントエンドエンジニアに対してこれはここで Basic 認証かかってるからちゃんとやってねっとかの共有ができてない(コミュニケーションがなかった)事案が発生してたりして、
Staging のアプリで通信ができていない?よくわからない?どういうことだ?(パケット見たら Basic 認証通っていない)とかあったし、もろもろ面倒になったのでだったらということで今回のやつを導入することにした。

普段は Django REST Framework で開発を行っていますが、今回はわざわざ Django を使うほどではないので FastAPI を使ってデフォルト機能の Swagger UI の提供をデモに使っています。

ソースコード

bitbucket.org

バックエンドの提供

前章で書いたように今回は FastAPI を使用しています。

基本の開発環境は Docker という前提で書くので今回はバージョン情報はソースコード./backend/Pipfile.lock を参照ください。

使ったソースコードは FastAPI のドキュメントで紹介されてあるコードをほぼそのままお借りしました。

from typing import Any, Dict, Optional

import uvicorn
from fastapi import FastAPI

app = FastAPI(redoc_url=None)


@app.get("/")
async def read_root() -> Dict[str, str]:
    return {"Hello": "World"}


@app.get("/items/{item_id}")
async def read_item(item_id: int, q: Optional[str] = None) -> Dict[str, Any]:
    return {"item_id": item_id, "q": q}


if __name__ == "__main__":
    uvicorn.run("app:app", port=8000, reload=True)

ちなみにローカルで実行したい場合は

$ uvicorn app:app

もしくは

$ python app.py

で起動することはできます。

実際に叩いてみるとちゃんと返ってきますね。

$ curl -s http://127.0.0.1:8000 | jq
{
  "Hello": "World"
}

ちなみに http://127.0.0.1:8000/docs にアクセスすると Swagger UI が表示されました。

f:id:nanashinodonbee:20210131073519p:plain

FastAPI はこの辺にしておきましょう。

oauth2-proxy について

もともとは bitly/oauth2_proxy を Fork したものとのこと。
自分もはじめに出会ったのはやはり先祖のリポジトリでした。

https://cloud.githubusercontent.com/assets/45028/8027702/bd040b7a-0d6a-11e5-85b9-f8d953d04f39.png 引用: https://github.com/oauth2-proxy/oauth2-proxy

どういうことかはよくわからんけど、認証基盤をいい感じにやってくれるらしい。

公式で提供してくださっている Docker イメージを使用しています。

今回行った GitHub 以外の他に GoogleFacebook などたくさん対応している*2らしい。
例えば Google Workspace を使っているのであれば会社のドメインを使用しているメールアドレスのみアクセスを許可するなど簡単にできるとのこと。

今回は、コマンドラインと一緒に configuration file を書いてやっているのでご紹介

http_address = "0.0.0.0:4180"
upstreams = [
    "http://127.0.0.1:8000"
]
email_domains = [
    "*"
]
provider = "github"
github_users = [
    "nnsnodnb"
]
cookie_expire = "24h"

GitHub での認可なので email_domainsワイルドカードを指定しています。
GitHub Organization に所属している人のみアクセスを許可したい場合は、 github_org = "organization_name" を指定すると動きます。
github_org を指定していて github_users も指定している場合は、指定した Organization にユーザが所属していなくても指定されているユーザであればアクセスが通るとのことです。

GitHub OAuth Apps を設定

GitHub にログインをしている状態で個人であれば https://github.com/settings/applications/new を開き、
Organization であれば https://github.com/organizations/{{ organization_name }}/settings/applications/new を開きましょう。

f:id:nanashinodonbee:20210131080204p:plain

開くとこんな感じの画面を表示されます。
Application NameHomepage URL はわかるように適当に設定しましょう。

Authorization callback URL は、実際に SSL でアクセスができるサイトを用意する必要があります。 HTTP なローカルホストにコールバックさせたら oauth2-proxy がエラーを吐いたのですが、 HTTPS なローカルホストでの確認はしていません。
今回もいつもお世話になっている ngrok を使用しました。

$ ngrok http 80

このコマンドで起動したら、いつものエンドポイントが表示されると思うので HTTPS の方のドメインをコピーして Authorization callback URLhttps://hoge.jp.ngrok.io/oauth2/callback のような感じになるように /oauth2/callback を最後につけて設定を完了しましょう。

OAuth App が作成できたら自動で遷移するので Generate a new client secret ボタンを押して Client secret を生成しましょう。このコードは1度しか表示されないので気をつけましょう。

Nginx の設定

ポート通信を使うのがちょっとアレなので(別にそっち側の宗教の人じゃないけど)、今回は FastAPI をソケットを通して Nginx にプロキシしてもらうようにします。
gunicorn を使っているのでとても簡単です。

ソースコード./backend/gunicorn_conf.py を見てください。

bind = "unix://tmp/gunicorn.sock"

こんなコードがあります。現在のディレクトリの以下に /tmp があるのでその下に gunicorn.sock を作るようにしています。
gunicorn の起動コマンドで -c gunicorn_conf.py を指定しているので結構いい感じに動いてくれます。(?????) 詳しくは Dockefile

Nginx のコンテナの /etc/nginx/conf.d/default.conf を上書きするように docker-compose.yml で定義しています。

upstream gunicorn_server {
    server unix:/app/tmp/gunicorn.sock;
}

upstream github_oauth_server {
    server github_oauth:4180;
}

server {
    listen      80;
    server_name default_server;

    location / {
        try_files $uri @proxy_to_app;
    }

    location /oauth2/ {
        proxy_pass       http://github_oauth_server;
        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 = /oauth2/auth {
        proxy_pass       http://github_oauth_server;
        proxy_set_header Host             $host;
        proxy_set_header X-Real-IP        $remote_addr;
        proxy_set_header X-Scheme         $scheme;
        proxy_set_header Content-Length   "";
        proxy_pass_request_body           off;
    }

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

        auth_request_set $user   $upstream_http_x_auth_request_user;
        auth_request_set $email  $upstream_http_x_auth_request_email;
        proxy_set_header X-User  $user;
        proxy_set_header X-Email $email;
        auth_request_set $token  $upstream_http_x_auth_request_access_token;
        proxy_set_header X-Access-Token $token;

        auth_request_set $auth_cookie $upstream_http_set_cookie;
        add_header Set-Cookie $auth_cookie;

        auth_request_set $auth_cookie_name_upstream_1 $upstream_cookie_auth_cookie_name_1;

        if ($auth_cookie ~* "(; .*)") {
            set $auth_cookie_name_0 $auth_cookie;
            set $auth_cookie_name_1 "auth_cookie_name_1=$auth_cookie_name_upstream_1$1";
        }

        if ($auth_cookie_name_upstream_1) {
            add_header Set-Cookie $auth_cookie_name_0;
            add_header Set-Cookie $auth_cookie_name_1;
        }

        try_files $uri @proxy_to_app;
    }

    location @proxy_to_app {
        proxy_redirect     off;
        proxy_http_version 1.1;
        proxy_redirect     off;
        proxy_set_header   Upgrade $http_upgrade;
        proxy_set_header   Connection "upgrade";
        proxy_set_header   Host $host;
        proxy_set_header   X-Real-IP $remote_addr;
        proxy_set_header   X-Forwarded-Host $host;
        proxy_set_header   X-Forwarded-Server $host;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto $http_x_forwarded_proto;
        proxy_pass         http://gunicorn_server;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root /usr/share/nginx/html;
    }
}

gunicorn_servergithub_oauth_server の upstream を定義しています。

oauth2-proxy.github.io

上記の Nginx の設定を参考に設定しました。 Nginx 詳しくないので間違っているところあればコメントとかお願いします...
参考の設定ファイルだと、ルートディレクトリにアクセスしたら OAuth に入るというような感じなのでそれだとちょっとつらい...ということなので location /docs の前方一致で反応するように設定しました。
認可が通った場合、 try_files $uri @proxy_to_app; としているところがちょっとよくわからんけどこうやったら通ったのでとりあえず設定してまする。

docker-compose.yml

サービスとして

を定義しました。

起動前に .env.sample をコピーして .env を作成しパラメータを設定してください。

参考までにそのまま貼りますが

version: "3.8"

services:
  backend:
    build: ./backend
    image: oauth2-proxy-backend
    container_name: oauth2_proxy_backend
    restart: always
    volumes:
      - type: volume
        source: socket
        target: /app/tmp
    tty: true

  proxy:
    image: nginx:1.18.0
    container_name: oauth2_proxy_nginx
    restart: always
    volumes:
      - type: bind
        source: ./nginx/default.conf
        target: /etc/nginx/conf.d/default.conf
      - type: volume
        source: socket
        target: /app/tmp
    ports:
      - "80:80"
    depends_on:
      - backend
      - github_oauth

  github_oauth:
    image: quay.io/oauth2-proxy/oauth2-proxy:v6.1.1
    container_name: oauth2_proxy_github_oauth
    restart: always
    volumes:
      - type: bind
        source: ./oauth2/oauth2-proxy.cfg
        target: /etc/oauth2-proxy.cfg
    expose:
      - "4180"
    command: --config=/etc/oauth2-proxy.cfg --client-id="${GITHUB_CLIENT_ID}" --client-secret="${GITHUB_CLIENT_SECRET}" --cookie-secret="${COOKIE_SECRET}"

volumes:
  socket:
$ docker-compose up --build -d

で起動します。

では試してみます。

最後に

今回は docker-compose を使って oauth2-proxy を使って Swagger UI に対して OAuth2 認可を設定してみました。
実際に組み込むときにはわりと時間を要しました(既存の Nginx の設定と oauth2-proxy の設定がちょっとわからんかった)がなんとか数時間程度で実装できました。実際に自動化で実装するところまでは持って行けていないのですがそれはまた追々...

全く関係ないけど、早く旅行に行けるようにならないかな

参考

tech.misoca.jp

fastapi.tiangolo.com

*1: Authorization ヘッダーが重複して取り合ったり、人が増えてきてアクセスキーを共有するのが面倒

*2:https://oauth2-proxy.github.io/oauth2-proxy/docs/configuration/oauth_provider