traP Member's Blog

署名付きクッキー / Omniauth Strategyを書く / GitLabで独自認証

kaz
このエントリーをはてなブックマークに追加

メリークリスマス!
traPの鯖管kazです。

この記事はアドベントカレンダー2016 25日目の記事……なんですが、
「曲がりなりにも東工大の技術系サークルであるtraPのアドベントカレンダー企画にしては技術系の記事少なくない!?」
みたいな声が聞こえたり聞こえなかったりしたので、技術ネタです。

シングル・サイン・オンにあこがれて

ちょっとだけ、前座にお付き合いください。

GitLab

うちのサークルでは、複数人開発プロジェクトの全てでgitを使っているんですが、
そのリモートリポジトリをホスティングするためにGitLabのCommunityEditionを使っています。

ほかにも

これはちょっと(未来の新入部員に向けた)宣伝というか、どうでもいいんですけど、
他にも部内SNSだとか、ファイル共有用にownCloudだとか、ドキュメント共有用のcrowiだとか、
このブログもそうですし、こんなかんなカンジでいろいろなアプリを運用しています。

あとDockerを使ってミニVPSみたいなことも……とか。

crowiはあんまり有名じゃないOSSですけど、Markdownでwikiを管理できるカンジのアレで、
非常に使い勝手が良いです。オススメ。

アカウント管理

で、こうやってアプリをたくさん運用してるんですが、アプリごとにアカウントを用意してると非常にメンドくないですか?
アプリごとにログインしなくちゃならないし、メールアドレスを変えたら全部変えなくちゃならないし、パスワードはどうしよう?とか。

SSO

そこでシングル・サイン・オンです!
1つのアカウントですべてのサービスにログインできたら嬉しいじゃないですか。
シングル・サイン・オンというのは、1つのアカウントで複数のサービスにログインできる仕組みのことです。

SAML

SAMLというのがありまして、コレは認証情報をXMLでやり取りするための仕様で、
シングル・サイン・オンの実装として用いられます。

SAMLは認証を担当するIdPと、認証情報を利用するSPに分かれています。
SPというのが各アプリで、この人達がIdPに認証を委任するカンジですね。

SAMLのツライところ

SAMLってけっこうニッチな技術っていうか、流行ってないというか。
企業とかそういうトコじゃないと、シングル・サイン・オン自体の需要があんまりないんですかね。

とにかく情報が少ないので色々苦労します。
アプリごとに実装されてる言語が違うわけで、その言語ごとにSAMLの実装を探さないといけないし。
SAMLライブラリがあっても保守されてなくて結局自分でなんとかしなくちゃならなかったり。。。

はじめはOpenAMを使おうかなと思ったんですけどよく分からないのでやめました。
うちのサークルで使ってるSNSは内製なんですが、コレにSAML IdPを自分で実装しました。

とにかくつらい

とにかくつらいのでもうSAML使いたくないです。

SAMLはけっこう複雑なんですけど、なんで複雑なのかって言うと、
異なるドメイン間での認証ができたりいろいろ細かい機能があるからなんですかね。

署名付きクッキー☆

そこでボクがSSOの実装として推したいのが署名付きクッキーです。
クッキーでSSOを実装している例をあまり見ないのですけど、やっぱりコレ何か問題があるのだろうか。
小一時間考えたけどそんなに問題なさそうだった。

どうやって認証するかというと、クッキーにユーザ識別子(要はIDです)をのっけとくだけ!
……これだけだと余裕でCookie書き換えられるし、クソ簡単なCTFかな???ってなってしまうので、
HMACとか非対称暗号で署名を付けます。

認証情報を利用する側で署名検証をして一致してなかったら弾くようにすれば、
鍵を持ってる認証サーバだけが認証情報を発行できるのでなりすましができない!わけです。

うれしさ

SAMLだと、

  1. SPにアクセス → IdPにリダイレクト
  2. IdPにアクセス → SPにリダイレクト
  3. SPにアクセス → ログイン完了

ってカンジで3往復目でやっとログインが完了するんですけど、
クッキーは毎回送ってるので、

  1. SPにアクセス → ログイン完了

というカンジに一発OKです。速い!!!
この要求されてないけど大事な情報を毎回送ってるってのがどうなの?ってカンジはありますが。。。。

つらさ

Cookieの仕様てきに、クロスオリジンでの認証がツライです。

それ意味ないじゃん!って思うかもしれませんが、
上位のドメインにならクッキーを書き込めるので、同じSuffixをもつドメインで運用しているなら何も問題がありません。

(いまこれを書いててdomain=*.comみたいなクッキーを発行したらどうなるんだろうとちょっと気になった。)

セキュリティ面で思いつくヤバそうなポイントとしては、

  • 秘密鍵が漏れるとすべておしまい(それはそう)
    • これはSAMLとかでもそうだから
  • 一度発行した認証情報のRevokeができない
    • つまり署名とセットでクッキーが漏れたら死ぬねという話
    • ユーザの1人でもCookieを漏らすと鍵を更新しなくちゃならない
    • secureフラグとhttpOnlyフラグを立てて於けばなんとかなる!ならない?

どうせそんなに超重要なデータなんて預かってない(と思う)のでなんとかなるよ!

前置きが長い

そういうことで、GitLabに署名付きクッキーによる独自認証機能をつける運びとなりました。

OmniAuth

GitLabはOmniauthを介して、いろんなアカウントでログインできるようになってるので、
比較的かんたんにSSOを実現できます。

Strategy

Omniauthでは、あるサービスのアカウントを使ってのログイン動作をStrategyと呼ばれるクラスに記述します。
このStrategyを自分で実装すれば、独自認証ができるわけです。

Strategyの書き方

Strategy Contribution Guideを読めば大体わかります。
Developer Strategyを改造しながら作るといいよ!って書いてあるので従いましょう。

module OmniAuth
  module Strategies
    class Mylogin
      include OmniAuth::Strategy

      option :fields, [:name, :email]
      option :uid_field, :email

      def request_phase
        form = OmniAuth::Form.new(:title => 'User Info', :url => callback_path)
        options.fields.each do |field|
          form.text_field field.to_s.capitalize.tr('_', ' '), field.to_s
        end
        form.button 'Sign In'
        form.to_response
      end

      uid do
        request.params[options.uid_field.to_s]
      end

      info do
        options.fields.inject({}) do |hash, field|
          hash[field] = request.params[field.to_s]
          hash
        end
      end
    end
  end
end

Declarative Configuration

      option :fields, [:name, :email]
      option :uid_field, :email

はじめにこんな感じで、認証に使うデータを定義しておきましょうみたいなお話らしいです。
ガチなやつを作るなら、外部サービスのAPIキーとかそういうやつでしょうか。

Defining the Request Phase

      def request_phase
        form = OmniAuth::Form.new(:title => 'User Info', :url => callback_path)
        options.fields.each do |field|
          form.text_field field.to_s.capitalize.tr('_', ' '), field.to_s
        end
        form.button 'Sign In'
        form.to_response
      end

リクエストフェーズでは、実際に外部サービスに認証を委任したりします。
このDeveloperStrategyでは、画面にフォームを入力してユーザー名をメールアドレスを入力してもらう形になってます。
本来なら、ここで外部サービスにリダイレクトを飛ばしたりします。

Defining the Callback Phase

      uid do
        request.params[options.uid_field.to_s]
      end

      info do
        options.fields.inject({}) do |hash, field|
          hash[field] = request.params[field.to_s]
          hash
        end
      end

uidでIDを返して、infoでメールとか名前とかのハッシュを返せばOKです。

RequestPhaseで得られるcallback_pathにリダイレクトを返してもらうようにすれば、
外部サービスから認証情報が飛んできて、そいつをココでパースして認証完了、という流れです。

本当はココはcallback_phaseというメソッドを定義して、そこでomniauth.authにAuthHashを組み立てるんですけど、
uidとinfoをセットすればsuperクラスでうまいこと処理してくれます。
なので、callback_phaseをオーバーライドするならば自分でAuthHashを作るか、最後にsuperのcallback_phaseを呼ばないと死にます。

おわり

簡単ですね(白目)

すいません、ぶっちゃけ既存のStrategyを読んてパクった方が手っ取り早いです。
List of Strategiesにたくさんのってるので、適当にコードを追ってみれば処理の流れがわかるかと思います。

日本語でStrategyの書き方が紹介されてる記事

GitLabに組み込もう

そしたらGitLabに組み込んでみたくなりますよね。

Using Custom Omniauth Providersっていう説明があるんですけど、

Note: The following information only applies for installations from source.

は?って言うカンジですね。
ちなみに、GitLabをソースからインストールするのはマジで闇なのでオススメしません。

普通はOmnibusパッケージっていうのでインストールするはずなんですけど、ココにOmniauthストラテジを追加するにはどうするかというお話ですが、
今回はDockerイメージを使ってインストールしたGitLabで説明します。

設置

GitLabのソースがあるディレクトリを探して、(dockerでインストールしたなら、コンテナの中の/opt/gitlab/embedded/service/gitlab-railsです)
ここから相対パスでlib/omniauth/strategies/に自分で作ったStrategyをいれます。仮にmylogin.rbとしましょう。

そしたらconfig/initializers/omniauth.rbに

@@ -29,6 +29,7 @@ end
 
 module OmniAuth
   module Strategies
+    autoload :Mylogin, Rails.root.join('lib', 'omniauth', 'strategies', 'mylogin')
     autoload :Bitbucket, Rails.root.join('lib', 'omniauth', 'strategies', 'bitbucket')
   end
end

こういうカンジの行を追加します。

設定の変更

/etc/gitlab.rbを設定します

gitlab_rails['omniauth_enabled'] = true
gitlab_rails['omniauth_allow_single_sign_on'] = ['mylogin'] # SSOを許可する
gitlab_rails['omniauth_auto_sign_in_with_provider'] = 'mylogin' # GitLab標準のログイン画面をスキップしてこのStrategyでログインする
gitlab_rails['omniauth_block_auto_created_users'] = false # Omniauthで新規ユーザができたときにBlockしない
gitlab_rails['omniauth_providers'] = [
  {
    'name' => 'mylogin',
    'args' => {
       # ここに書いた変数は、Strategyからoptions.hogeみたいなかんじでアクセスできます
    }
  }
]

コレで、gitlab-ctl reconfigureすればおしまいです。

うまくいっていればこんな感じにボタンがでてきます。

参考

うちのサークルで使ってるGitlabがあててるパッチです。
もしかしたら参考になるかもしれない。
https://github.com/kaz/docker-gitlab/blob/master/patch.diff

おしまい

だいぶ雑ですけど、おわりです。

ちなみに、アイキャッチはGitLabのコントリビューターが1000人になった記念でもらったカードです。

おまけ

こんなツイートを見た

traPの鯖管は滅茶苦茶にいそがしいです。

このエントリーをはてなブックマークに追加

コメントを残す

メールアドレスが公開されることはありません。