BlankTar

about | blog | works | photo

Auth0という認証/認可サービスのワークショップに参加してきました。
Node学園祭2017の二日目のワークショップなのですが、この記事はNodeじゃなくてpythonです。ごめんなさい。

Auth0は色々な言語/環境でログイン/ログアウトの機能を簡単に実現するためのサービスです。
あらゆるソーシャルアカウント(ソーシャルじゃないやつもある)と繋がってシングルサインオンを簡単に実現出来たりとか、スマホやメールを使ったパスワードレスも出来たりとか、かなり万能な感じです。
しかも、それらの機能を全て遮蔽してくれているので、基本的な対応さえすればあとは全て管理ページをマウスでポチポチ操作するだけで色んなことが出来ます。すごい。

全部繋げたサンプルは記事の一番下にあります。

クライアントを作る

まずは、Auth0の管理ページでクライアントを作成します。

左側のメニューからClientsに移動して、「CREATE CLIENT」を選んで新しいクライアントを作ります。
名前は適当に、client typeは「Regular Web Applications」を選んでください。

クライアントが出来たら、Settingsに移動してAllowed Callback URLsAllowed Logout URLsを設定します。
Callback URLはログインの時に使うアドレスで、今回はhttp://localhost:5000/callbackを追加してください。
Logout URLはログアウトした後にリダイレクトする先のアドレスです。こちらはhttp://localhost:5000/を追加してください。

必要なライブラリを入れる

$ pip install flask flask_oauthlib python-jose

基本的にはこれだけ。

Flaskの準備

いつものappを作るやつです。

import flask


app = flask.Flask(__name__)
app.secret_key = '??????????'

??????????の部分はCookieの暗号化に使うキーです。何か適当な文字列にしてください。

Auth0と繋ぐための準備

次に、必要な変数の定義とかをします。
Auth0とはoauthでやりとりするので、主にそのあたりの設定になります。

import flask_oauthlib.client


AUTH0_CLIENT_ID = '??????????'
AUTH0_CLIENT_SECRET = '??????????'
AUTH0_DOMAIN = '??????????.auth0.com'


oauth = flask_oauthlib.client.OAuth(app)
auth0 = oauth.remote_app(
	'auth0',
	consumer_key=AUTH0_CLIENT_ID,
	consumer_secret=AUTH0_CLIENT_SECRET,
	request_token_params={
		'scope': 'openid profile',
		'audience': 'https://{}/userinfo'.format(AUTH0_DOMAIN),
	},
	base_url='https://{}'.format(AUTH0_DOMAIN),
	access_token_method='POST',
	access_token_url='/oauth/token',
	authorize_url='/authorize',
)

定数部分は先ほど作ったクライアントのSettingsに書いてある情報で置き換えてください。

ログイン部分を作る

手始めに、ログインに必要なページを作っていきます。

最初はログインページです。正確には、ログインページにリダイレクトするページです。

@app.route('/login')
def login():
	return auth0.authorize(callback=flask.url_for('auth_callback', _external=True))

もう一つ必要なのが、ログインページから戻ってくる先、コールバックです。

import urllib.request


@app.route('/callback')
def auth_callback():
	# Auth0がくれた情報を取得する。
	resp = auth0.authorized_response()
	if resp is None:
		return 'nothing data', 403

	# 署名をチェックするための情報を取得してくる。
	with urllib.request.urlopen('https://{}/.well-known/jwks.json'.format(AUTH0_DOMAIN)) as jwks:
		key = jwks.read()

	# JWT形式のデータを復号して、ユーザーについての情報を得る。
	# ついでに、署名が正しいかどうか検証している。
	try:
		payload = jwt.decode(resp['id_token'], key, audience=AUTH0_CLIENT_ID)
	except Exception as e:
		print(e)
		return 'something wrong', 403  # 署名がおかしい。

	# flaskのSessionを使ってcookieにユーザーデータを保存。
	flask.session['profile'] = {
		'id': payload['sub'],
		'name': payload['name'],
		'picture': payload['picture'],
	}

	# マイページに飛ばす。
	return flask.redirect(flask.url_for('mypage'))

これで、上手くいけばcookieにユーザーの情報を保存することが出来るはずです。
ログイン完了後にマイページに飛ばしていますが、これはあとで用意します。

デフォルトではIDとパスワードを使った認証か、あるいはGoogleのアカウントを使った認証かの二通りが表示されるかと思います。
この辺の設定は管理ページのSocial Connectionsで行なうので、プログラム上では何を使うか気にする必要がありません。

ログアウト出来るようにする。

ログインが出来たら、今度はログアウトを用意します。

ぶっちゃけflask.sessionからデータを消せばそれだけでログアウト出来るのですが、ちゃんとお行儀良くAuth0にもログアウトしたことを伝えておきましょう。
Auth0は色々ログを取ってくれているので、これをしておくと色々便利だと思います。たぶん。全く分かっていませんが。
あとは特殊なログイン方法を使うときに必要なのかもしれない? やっぱり分かっていませんが。

import urllib.parse


@app.route('/logout')
def logout():
	del flask.session['profile']  # cookieから消す

	# Auth0にも伝える
	params = {'returnTo': flask.url_for('index', _external=True), 'client_id': AUTH0_CLIENT_ID}
	return flask.redirect(auth0.base_url + '/v2/logout?' + urllib.parse.urlencode(params))

Auth0に渡すクエリの扱いが煩雑に見えますが、よく見るとわりとシンプルな感じです。

このページにアクセスすると、ログアウト処理をしてからreturnToに渡したアドレスにリダイレクトされます。

これで、ログイン/ログアウトに必要な機能を全て用意出来ました。

トップページとかマイページとか

テストしやすいように、諸々のページを用意します。

まずはプロフィールを表示するマイページ。

@app.route('/mypage')
def mypage():
	if 'profile' not in flask.session:
		return flask.redirect(flask.url_for('login'))

	return '''
		<img src="{picture}"><br>
		name: <b>{name}</b><br>
		ID: <b>{id}</b><br>
		<br>
		<a href="/">back to top</a>
	'''.format(**flask.session['profile'])

ほぼ解説要らずな感じのシンプルな内容ですね。flask.sessionから取ってきているだけです。

そしてトップページ。

@app.route('/')
def index():
	if 'profile' in flask.session:
		return '''
			welcome <a href="/mypage">{}</a>!<br>
			<br>
			<a href="/logout">logout</a>
		'''.format(flask.session['profile']['name'])
	else:
		return '''
			welcome!<br>
			<br>
			<a href="/login">login</a>
		'''.format(flask.url_for('login'))

こちらはもっと単純。

で、いつもの実行部分。

if __name__ == '__main__':
	app.run(debug=True)

まとめ

これで、ログインとログアウトの実装が出来ました。
Auth0用のライブラリが無いのでわりと手動な部分が多いですが、それでもかなり簡単な感じです。
IDとパスワード使うやつだけだったらともかく、シングルサインオンとか二段階認証とかしたいなら是非とも使うべきという感じですね。

7,000ユーザーまでは無料という太っ腹っぷりなので、ちょっと作ってみるくらいならばんばん使っても良さそうです。

完成形

最後に、全部繋いだソースコードです。内容はほぼほぼ同じです。

import urllib.request
import urllib.parse

import flask
import flask_oauthlib.client
from jose import jwt


app = flask.Flask(__name__)
app.secret_key = 'this is secret'


AUTH0_CLIENT_ID = '??????????'
AUTH0_CLIENT_SECRET = '??????????'
AUTH0_DOMAIN = '??????????.auth0.com'


oauth = flask_oauthlib.client.OAuth(app)
auth0 = oauth.remote_app(
	'auth0',
	consumer_key=AUTH0_CLIENT_ID,
	consumer_secret=AUTH0_CLIENT_SECRET,
	request_token_params={
		'scope': 'openid profile',
		'audience': 'https://{}/userinfo'.format(AUTH0_DOMAIN),
	},
	base_url='https://{}'.format(AUTH0_DOMAIN),
	access_token_method='POST',
	access_token_url='/oauth/token',
	authorize_url='/authorize',
)


@app.route('/login')
def login():
	return auth0.authorize(callback=flask.url_for('auth_callback', _external=True))


@app.route('/callback')
def auth_callback():
	resp = auth0.authorized_response()
	if resp is None:
		return 'nothing data', 403

	with urllib.request.urlopen('https://{}/.well-known/jwks.json'.format(AUTH0_DOMAIN)) as jwks:
		key = jwks.read()

	try:
		payload = jwt.decode(resp['id_token'], key, audience=AUTH0_CLIENT_ID)
	except Exception as e:
		print(e)
		return 'something wrong', 403

	flask.session['profile'] = {
		'id': payload['sub'],
		'name': payload['name'],
		'picture': payload['picture'],
	}

	return flask.redirect(flask.url_for('mypage'))


@app.route('/mypage')
def mypage():
	if 'profile' not in flask.session:
		return flask.redirect(flask.url_for('login'))

	return '''
		<img src="{picture}"><br>
		name: <b>{name}</b><br>
		ID: <b>{id}</b><br>
		<br>
		<a href="/">back to top</a>
	'''.format(**flask.session['profile'])


@app.route('/logout')
def logout():
	del flask.session['profile']

	params = {'returnTo': flask.url_for('index', _external=True), 'client_id': AUTH0_CLIENT_ID}
	return flask.redirect(auth0.base_url + '/v2/logout?' + urllib.parse.urlencode(params))


@app.route('/')
def index():
	if 'profile' in flask.session:
		return '''
			welcome <a href="/mypage">{}</a>!<br>
			<br>
			<a href="/logout">logout</a>
		'''.format(flask.session['profile']['name'])
	else:
		return '''
			welcome!<br>
			<br>
			<a href="/login">login</a>
		'''.format(flask.url_for('login'))


if __name__ == '__main__':
	app.run()

HDDが壊れました。なんだか急にファイルサーバーにアクセス出来なくなって、何かと思ったらHDDが壊れてました。
dmesgを見ていると、critical medium errorとかいうのがひたすら出続ける感じ。correct出来ていたので完全に読めないわけじゃないみたいなんですが、直ってもすぐ壊れそうなので交換しました。
自宅のファイルサーバーをbtrfsにして初のディスク交換なので、やった作業をメモ。

まずは物理的に交換

そもそもどのデバイスが壊れたのかよう分からんなんてこともあるわけですが、今回はdmesgにエラーが出てたのでわりと簡単に判明。
名前さえ分かればhdparmとか使ってどのディスクなのか特定すれば良いと思います。この例は/dev/sdcが壊れてた場合。

# hdparm -i /dev/sdc

ddとかで読み込み発生させてアクセスランプ光らせる方法もあるにはある(というか今回やった)のですが、状況によっては色々壊れそうなのであんまりアクセスしない方が良い気もします。よく分からないけど。

で、ディスクを買いに走って交換。
まともに読める場合はbtrfs replaceってコマンドで比較的速めに交換作業が出来るらしいのですが、今回はダメっぽかったので諦めて引っこ抜いちゃいました。

今度はソフト的に交換

交換はオンラインでやるので、まずは縮退モードでマウントします。気持ち的にはro付けたいけど、付けるとデバイスの追加/削除が出来ないので我慢です。

デバイス名はとりあえず/dev/sdbとしているけれど、環境に合わせてよしなに。マウント出来ればそれで良いと思います。
マウント先はここでは/mnt/btrfs。こちらもよしなに。

# mount -o degraded /dev/sdb /mnt/btrfs

マウント出来たら、新しいハードディスクを追加します。ここでは/dev/sdcが新しいやつ。
既にパーティションが切ってあるディスクだと断わられちゃいますが、-fオプションを付けてやればおっけーです。

# btrfs device add /dev/sdc /mnt/btrfs

最後に、壊れたディスクをファイルシステムから切り離します。
RAIDの場合は切り離しつつ自動で再構成するので、めっちゃ時間が掛かるのを覚悟してから始めてください。私は迂闊にやってびっくりしました。

# btrfs device remove missing /mnt/btrfs

btrfs device statsとかbtrfs filesystem usageとか見ながら祈ったり、次の日まで寝たりすると良いと思います。

これで交換作業は完了、なはずです。
この記事を書いている時点ではリバランスがまだ全然終わっていないのですが…無事終わると良いな…。

昨日のPySparkのRandomForestを使った記事に引き続き、今日はscikit-learnのRandomForestを使ってみます。

データの読み込みとプロット

>>> import matplotlib.pyplot as plt
>>> import sklearn.datasets

>>> iris = sklearn.datasets.load_iris()
>>> features = iris.data[:, [0, 2]]

>>> plt.scatter(*features.T, c=[['orange', 'green', 'blue'][x] for x in iris.target])
>>> plt.show()

irisのデータのプロット

昨日とほぼ同じです。今回も長さのデータだけで幅は使いません。

訓練用とテスト用のデータを分ける

精度を測るべく、訓練用とテスト用のデータを分けます。
昨日と同じく7:3の割合で行きます。

>>> import sklearn.model_selection

>>> train_x, test_x, train_y, test_y = sklearn.model_selection.train_test_split(features, iris.target, test_size=0.3)

x(特徴量)とy(ラベル)が別々になっているので、PySparkよりちょっと面倒臭い感じがする。

学習器を作って学習

昨日のと違って前処理をしていないので、かなりシンプル。

>>> import sklearn.ensemble

>>> rf = sklearn.ensemble.RandomForestClassifier()
>>> rf.fit(train_x, train_y)

PySparkの場合と違って、fitの戻り値を保持する必要はありません。

実行して、精度を確かめる

学習が終わったら、実行してみましょう。

>>> prediction = rf.predict(test_x)
>>> print(prediction)
[0 2 2 1 1 2 0 0 2 2 2 0 0 2 2 2 0 0 1 2 2 2 2 2 0 0 2 1 0 1 2 0 0 1 1 1 0
 0 1 1 0 2 1 1 1]

こんな感じ。predict()を呼ぶと分類した結果が入ったnumpyの配列が返ってきます。

分類器自体に精度を計算するメソッドがあるので、それを使えば簡単に精度を確認出来ます。

>>> accuracy = rf.score(test_x, test_y)
>>> print('accuracy {0:.2%}'.format(accuracy))
95.56%

実行し直すのでデータ量が多い場合は時間が掛かっちゃいそうな気がします。
分類結果も使うのであれば、自分で計算した方が良いかもしれません。

どこを間違えたのかプロットしてみる

>>> plt.scatter(*test_x.T, c=[['orange', 'green', 'blue'][answer] if answer == predict else 'red' for answer, predict in zip(test_y, prediction)])
>>> plt.show()

irisの分類結果のプロット

昨日のものとほぼ一緒。赤い点が間違えた所です。

scikit-learnもmatplotlibもnumpy.arrayを使うので、ほぼ何も変換せずに使えます。せいぜい転置してるくらい。

まとめ

繋げると大体以下のようになります。

import matplotlib.pyplot as plt
import sklearn.datasets
import sklearn.ensemble
import sklearn.model_selection


# データの用意
iris = sklearn.datasets.load_iris()
features = iris.data[:, [0, 2]]

train_x, test_x, train_y, test_y = sklearn.model_selection.train_test_split(features, iris.target, test_size=0.3)


# 学習
rf = sklearn.ensemble.RandomForestClassifier()
rf.fit(train_x, train_y)


# 評価
accuracy = rf.score(test_x, test_y)
print('accuracy {0:.2%}'.format(accuracy))


# 結果のプロット
prediction = rf.predict(test_x)

plt.scatter(*test_x.T, c=[['orange', 'green', 'blue'][answer] if answer == predict else 'red' for answer, predict in zip(test_y, prediction)])
plt.show()

いやあ、超簡単。
とりあえずこっちでやって、時間が掛かりそうならPySparkに移行するのが良いのかもしれません。
大まかなプログラムの流れは変わらないですし、移行するのはそんなに問題にならなそう。

[ << ] [ 1 ] [ 3 ] [ 5 ] [ >> ]