python/flaskでgoogleにOpenIDでログインしてみた。ライブラリ無しで。
昨日公開したwebアプリ、ハッカソン期間中にはギリギリ間に合わなかったのですが、実は本当はOpenIDに対応させる予定だったのです。 私は結局認証部分には一度も手を付けずに終わってしまったので、すこし挑戦してみようかと。 実用目的ではないので、自前で全て実装してみることにしました。 情報が少なくて苦労しましたが、案外簡単だった。
とりあえず、フレームワークにflaskを使います。ルーティングで手間取りたくないので。あとはpythonの標準ライブラリだけで頑張ります。
OpenID Connectでの認証の大雑把な流れ
詳しいことは私もよく分かっていないので、本当に大雑把に。
OpenIDを利用するサイト(RPとか呼ばれるそうです)がOpenIDを提供しているサイト(こちらはOPというそうです)に自分の情報をクエリに入れてリダイレクトします。 クライアントからアクセスを受けたOPはログイン画面を出して、ログインした(もしくはキャンセルした)ら、RPのサイトに更にリダイレクトします。 リダクレクトされてきたURLにユーザに関する情報が入っていて、こいつをOPに問い合わせたりしてユーザの情報を取得する、ということらしいです。
やっぱりよく分からないので 第1回 OpenIDサービスを利用して,OpenIDの仕組みを理解する:いますぐ使えるOpenID|gihyo.jp … 技術評論社 あたりを読んでください。 とりあえず、リダイレクトの連続で事が運ぶのだと、そういうことですね、うん。
googleでクライアントIDを取得する
googleのデベロッパーコンソールに行って、適当にプロジェクトとかいうやつを作ります。なんか本当に適当で大丈夫そうです。 プロジェクトを作ったら、左の方にある「APIと認証」の「認証情報」ってところにある「認証情報を追加」から「OAuth 2.0 クライアント ID」を作ります。 多分サービス名を設定しろ的なことを言われるので、言われた通りにします。
「ウェブアプリケーション」を選んで、「承認済みのリダイレクト URI」とやらに認証に使うURLを入れます。localhostとかも大丈夫です。
この記事では、http://localhost:5000/login/check
に設定しておきます。
完了するとクライアントIDとクライアントシークレットという二つの文字列が生成されるので、どこかに控えておいてください。(まあでも後からでも見れるのでどっかやっちゃっても平気です)
ログイン画面を出してみる
クライアントIDを取得できたら、実際にアプリを作り始めましょう。といっても最初にお話しした通り、最初にするのはただのリダイレクトです。
import urllib.parse
import flask
client_id = '-- デベロッパーコンソールで取得したクライアントID --'
client_secret = '-- こっちはクライアントシークレット --'
redirect_uri = 'http://localhost:5000/login/check'
state = 'this is test' # 本当はこれはランダム
@app.route('/login')
def login():
return flask.redirect('https://accounts.google.com/o/oauth2/auth?{}'.format(urllib.parse.urlencode({
'client_id': client_id,
'scope': 'email',
'redirect_uri': redirect_uri,
'state': state,
'openid.realm': 'http://localhost:5000',
'response_type': 'code'
})))
こんな感じにしてみました。
https://accounts.google.com/o/oauth2/auth
に諸々のクエリを付けてリダイレクトしています。client_id
,redirect_uri
なんかは設定/取得したものをそのまま渡します。openid.realm
にはオリジンを渡せば良いっぽい?response_type
には常にcode
を設定します。scope
には欲しいデータを指定すれば良いらしい。profileとかemailとか。スペース区切りで両方も行けます。state
はあとで使いますが、ワンタイムパスのようなものらしいです。乱数を設定してください。でも面倒なのでここでは定数です。
このコードを動かして/login
にアクセスすると、見慣れたグーグルの画面に転送されると思います。
アカウントの情報を取得してみる
ログイン画面を出せてログインも(一応は)出来るようになったので、アカウントの情報を取得してみましょう。
ソースコードは増えた部分だけ書きます。結合したものがこの記事の下にあるので、動作確認にはそちらのほうが良いかも。
import urllib.request
import json
import base64
@app.route('/login/check')
def check():
if flask.request.args.get('state') != state:
return 'invalid state'
dat = urllib.request.urlopen('https://www.googleapis.com/oauth2/v4/token', urllib.parse.urlencode({
'code': flask.request.args.get('code'),
'client_id': client_id,
'client_secret': client_secret,
'redirect_uri': redirect_uri,
'grant_type': 'authorization_code'
}).encode('ascii')).read()
dat = json.loads(dat.decode('ascii'))
id_token = dat['id_token'].split('.')[1] # 署名はとりあえず無視する
id_token = id_token + '=' * (4 - len(id_token)%%4) # パディングが足りなかったりするっぽいので補う
id_token = base64.b64decode(id_token, '-_')
id_token = json.loads(id_token.decode('ascii'))
return 'success!<br>hello, {}.'.format(id_token['email'])
こんな感じで。
さきほど設定したstate
を冒頭でチェックしています。今回は定数なのでチェックする意味がありませんが、本来はこれで同じセッションかどうか調べたりするようです。
今回はチェックしていませんが、ログインがキャンセルされたなどの問題があった場合はerror
にエラーメッセージが入るそうです。
んで、受け取ったクエリの内、code
ってやつをグーグルにPOSTで送ります。アドレスはhttps://www.googleapis.com/oauth2/v4/token
に。なんかバージョンによって色々あるようですが、たぶんこれが(2015年8月時点では)最新です。
client_id
, client_secret
, redirect_uri
などはデベロッパーコンソールで設定もしくは取得した内容そのままです。
grant_type
というのはよく分からないのですが、とりあえずauthorization_code
を指定してくれとのことです。
データを送ると、json形式で色々データが返ってきます。同じcodeを使って何度も取得しようとすると400 Bad Requestが返るみたい。
返ってくるjsonには色々情報が入っているのですが、とりあえず欲しいのはid_token
というパラメータの中身。
ただこのデータ、JSON Web Tokenとかいう形式でエンコードされています。 署名とJSONデータをbase64エンコードしてピリオドで繋いだものらしいのですが、面倒なので署名は無視します。 ピリオド区切りの二番目のデータが本体らしいのでそこだけ抜き出して、足りないパディングを補ってからbase64デコードします。
デコードしたデータはただのjsonになるので、パースすれば完了。emailアドレスも中に入っているので、そいつを表示させるようにしてみました。 ユーザ情報の取得はさらに別のAPIを叩くことになるそうです。面倒なのでここでは扱いません。
まとめ
こういうのってなんかやたらと面倒臭そうなイメージだったのですが、思いの外簡単に出来ました。 まあ、それでもやっぱりライブラリ使えよって感じはしますね。なんだか情報少ないし。APIもちょいちょい変わるみたいだし。
今回使ったコードのフルバージョンを掲載しておきます。
import urllib.request
import urllib.parse
import json
import base64
import flask
app = flask.Flask(__name__)
client_id = '-- デベロッパーコンソールで取得したクライアントID --'
client_secret = '-- こっちはクライアントシークレット --'
redirect_uri = 'http://localhost:5000/login/check'
state = 'this is test' # 本当はこれはランダム
@app.route('/login')
def login():
return flask.redirect('https://accounts.google.com/o/oauth2/auth?{}'.format(urllib.parse.urlencode({
'client_id': client_id,
'scope': 'profile email',
'redirect_uri': redirect_uri,
'state': state,
'openid.realm': 'http://localhost:5000',
'response_type': 'code'
})))
@app.route('/login/check')
def check():
print(flask.request.args.get('state'))
dat = urllib.request.urlopen('https://www.googleapis.com/oauth2/v4/token', urllib.parse.urlencode({
'code': flask.request.args.get('code'),
'client_id': client_id,
'client_secret': client_secret,
'redirect_uri': redirect_uri,
'grant_type': 'authorization_code'
}).encode('ascii')).read()
dat = json.loads(dat.decode('ascii'))
id_token = dat['id_token'].split('.')[1] # 署名はとりあえず無視する
id_token = id_token + '=' * (4 - len(id_token)%%4) # パディングが足りなかったりするっぽいので補う
id_token = base64.b64decode(id_token)
id_token = json.loads(id_token.decode('ascii'))
return 'success!<br>hello, {}.'.format(id_token['email'])
if __name__ == '__main__':
app.run(debug=True)
参考: