メリークリスマス!
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だと、
- SPにアクセス → IdPにリダイレクト
- IdPにアクセス → SPにリダイレクト
- SPにアクセス → ログイン完了
ってカンジで3往復目でやっとログインが完了するんですけど、
クッキーは毎回送ってるので、
- 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の書き方が紹介されてる記事
- OmniAuth OAuth2 を使って OAuth2 のストラテジーを作るときに知っていると幸せになれるかもしれないこと
- OAuthでomniauth-facebookが何をやっているかを見てみた
- OmniAuthで独自認証機構を作る
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人になった記念でもらったカードです。
おまけ
こんなツイートを見た
「サーバ管理者が暇そうにしているのは、彼がちゃんと仕事をしている証拠だ」という考え方がある。彼はちゃんと仕事をしているからサーバは安定しトラブルもなく動くので、彼は暇になっているのだ。
— 吉良理人@ねもい (@big_bros) 2016年12月20日
traPの鯖管は滅茶苦茶にいそがしいです。