Let's write β

プログラミング中にできたことか、思ったこととか

SwaggerUIをRailsから配信し、AWS ALB上でGoogleのOAuth認証をかけてチーム内で共有した

API開発をするサーバサイドとAPIを利用するクライアントサイドというチーム構成でプロダクト開発をしていくにあたり、クライアントサイドがAPIの開発完了を待つことなくつなぎこみの準備をすすめておくためには、クライアントサイドとサーバサイドで事前にAPIのインタフェースを見やすい形式で共有しておく事が必要です。

また、現在開発しているAPIは外部に公開する性質のものではなく、クライアントアプリのみとの通信を想定した内部APIであるため、社内のGSuiteアカウントを持っている人のみが閲覧可能としたい物でした。

さらに、プロダクトがまだリリース前の模索の時期であるため、開発コストやサービスの利用コストをなるべく少なくしたいため、現在利用しているAWS上で簡単に設定できる物がのぞましい状況でした。

そこで、Rails上にCDN経由でSwaggerUIを表示し、GoogleのOAuth2認証をAWSのALBレイヤーでかける事という方法をとりました。

やった事

OpenAPIのドキュメントをRailsプロジェクト内のdocs/openapi.yamlに記述

OpenAPIのドキュメントをdocs/openapi.yamlに配置しました、OpenAPI自体はJSONでもYAMLでも書けますがエディターでの編集性を考えるとYAMLの方が書きやすかったため、YAML形式にしています。

また、プロジェクト内に配置する事でcommitteeなどのgemを利用して、 ドキュメントと実際の挙動が一致しているかのテストをCIで実施する事も可能になります。

json-schema-ref-parserでYAMLからJSON

YAMLで編集する中で、一つのドキュメントに全ての情報をまとめていると、 次第にドキュメントが長くなっていき編集コストや管理コストが増大してしまいます。

そこで、OpenAPIの$ref参照を用いて他のファイルの中身を参照できる機能を利用し、 API毎などの単位で適宜複数のドキュメントに分割した方がドキュメントの見通しが良くなります。

現在利用しているテストライブラリでは、このドキュメントの参照の解決がうまく動作しなかったため、 事前に参照を解決し一つのJSONファイルに変換するためにjson-schema-ref-parserというnodeのライブラリを利用しました。

github.com

そして、以下のようなスクリプトdocs/generate_openapi_json.jsとして保存しておきます。

const $RefParser = require("@apidevtools/json-schema-ref-parser");
const fs = require('fs');
const path = require('path');

const yaml_file_path = path.join(__dirname, 'openapi.yaml');
const json_file_path = path.join(__dirname, '../app/views/swagger/uis/openapi.json');

$RefParser.bundle(yaml_file_path, (err, schema) => {
  if (err) throw err;
  fs.writeFile(json_file_path, JSON.stringify(schema), (err) => {
    if (err) throw err;
  });
})

このスクリプトを適宜、package.jsonなどの"script"から呼びだせるようにしておきます。

swagger-uiをRailsから配信する

ドキュメントを閲覧しやすく表示するために、SwaggerUIを利用しています。 いくつかのGemでは、SwaggerUIをRailsに埋めこむ物を提供していましたが、 あまりメンテナンスされていなかったり、対応しているSwaggerUIのバージョンが、 プロジェクトで利用しているOAS3に対応していなかったりしたため、今回は自前でSwaggerUIを表示する事に決めました。

SwaggerUIを配信するためのコントローラーをRailsに配置

# config/routes.rb
  namespace :swagger do
    get '/ui', to: 'uis#index'
    get '/openapi', to: 'uis#openapi'
  end
# app/controllers/swagger/uis_controller.rb
# frozen_string_literal: true
module Swagger
  class UisController < ::ApplicationController
    def index
      render layout: false
    end

    def openapi
      render :openapi
    end
  end
end

このコントローラでは、/swagger/uiというpathでSwaggerUIを、 swagger/openapi.json というパスでopenapi.jsonを配信する想定となっています。 また、openapi.jsonファイルの生成時に app/views/swagger/uis/openapi.json に配置したファイルをopenapiアクションでは表示するようにしています。

ViewファイルでSwaggerUIをCDN経由で表示

SwaggerUIはunpkg越しに配信されています。

swagger-ui/installation.md at master · swagger-api/swagger-ui · GitHub

今回はそちらを利用しています:

<!-- app/views/swagger/uis/index.html.erb -->
<div id="swagger-ui"></div>

<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@3.25.0/swagger-ui.css">
<style>
html
  {
  box-sizing: border-box;
  overflow: -moz-scrollbars-vertical;
  overflow-y: scroll;
}

*,
  *:before,
  *:after
    {
    box-sizing: inherit;
  }

  body
    {
    margin:0;
    background: #fafafa;
  }
</style>
<script src="https://unpkg.com/swagger-ui-dist@3.25.0/swagger-ui-standalone-preset.js"></script>
<script src="https://unpkg.com/swagger-ui-dist@3.25.0/swagger-ui-bundle.js"></script>
<script>
  window.onload = function() {
    // Build a system
    const ui = SwaggerUIBundle({
      url: "<%= Rails.application.credentials.dig(:swagger, :document_url) %>",
      dom_id: '#swagger-ui',
      deepLinking: true,
      presets: [
        SwaggerUIBundle.presets.apis,
        SwaggerUIStandalonePreset
      ],
      plugins: [
        SwaggerUIBundle.plugins.DownloadUrl
      ],
      layout: "StandaloneLayout",
    })
    window.ui = ui
  }
</script>

このViewではunpkg越しにswagger-uiを埋めこんでいるだけでなく、 OpenAPIのドキュメントがどこから手にはいるのかを、Railsのcredentialsから取得しています。 たとえば、development環境ではhttp://localhost:3000/swagger/openapi.jsonから手にはいりますが、 ステージングサーバーではホスト名がことなったりするため、このようにしています。

Google OAuth2認証を作成する

こちらは、Google Developer Consoleの「APIとサービス」から「OAuth 同意画面」と「認証情報」のOAuthクライアントを作成する事で可能です。 この時に、OAuth 同意画面を「内部」に設定しておく事で、組織内部の人間のみが認可されるようになります。

ALBレイヤーで/swagger/*Google OAuth2認証をかける

弊社のプロジェクトでは、terraformを利用してAWSのインフラを量していたため、 OAuthクライアントのキーとシークレットをAWS KMSで暗号化し、以下のようにaws_lb_listener_ruleとして追加しました。

resource "aws_lb_listener_rule" "swagger" {
  listener_arn = data.aws_lb_listener.main.arn
  priority     = 1

  action {
    order = 1
    type  = "authenticate-oidc"

    authenticate_oidc {
      authentication_request_extra_params = {}
      authorization_endpoint              = "https://accounts.google.com/o/oauth2/v2/auth"
      client_id                           = var.client_id
      client_secret                       = "${data.aws_kms_secrets.main.plaintext["client_secret"]}"
      issuer                              = "https://accounts.google.com"
      on_unauthenticated_request          = "authenticate"
      scope                               = "openid"
      session_cookie_name                 = "AWSELBAuthSessionCookie"
      session_timeout                     = 604800
      token_endpoint                      = "https://oauth2.googleapis.com/token"
      user_info_endpoint                  = "https://openidconnect.googleapis.com/v1/userinfo"
    }
  }

  action {
    order            = 2
    target_group_arn = data.aws_lb_target_group.main.arn
    type             = "forward"
  }

  condition {
    field = "path-pattern"
    values = [
      "/swagger*",
    ]
  }
}

このようにする事で、/swagger*に一致するパスにアクセスされようとした場合に、自動的にGoogle OAuth2認証がかかるようになります。

結果

f:id:Pocket7878_dev:20200429150633p:plain

無事、SwaggerUIが表示されるようになり、アクセスしようとすると認証が求められるようになりました!

追記

SwaggerUIを配信する部分をRailsのMountableEngineのgemとして切りだしました

github.com