Yahoo! JapanのOpenID Connectをyesodで試す
はじめに
SNSなどで、ほかのアプリケーションに、プロフィールの閲覧などを許す機能がある。これが認可であり、OAuthという仕組みで、それが実現される。このOAuthという仕組みは認可のためのわくぐみであり、本人確認(認証)のための機能ではない。
「認可をあたえられるのは本人だけである」という理屈で、この「認可」を本人確認に使う場合がある。しかし、これをすると、大きなセキュリティホールを作ってしまう。
単なるOAuth 2.0を認証に使うと、車が通れるほどのどでかいセキュリティ・ホールができる
OAuth 2.0のわくぐみのうえで、きちんとした本人確認(認証)をおこなう仕組みがある。それがOpenID Connectだ。
何をするか
HaskellによるWebアプリケーションフレームワークであるYesodを使って、Yahoo! JapanのOpenID Connectの機能を試してみる。つぎのページの内容をYesodで実装した。
このページでは、おもにコードの紹介をする。細かい説明については上記のページを参照してほしい。
上記のページを参照したうえで、Yesodで試そうと思ったときに、この記事を読むのがいいかと思う。
前提
HaskellのStackはインストールされているものとする。
また、Yahoo IDは取得ずみとする。
Yesodによるテストサーバを作る
Yesodでテストサーバを動かす。
% stack new testOpenID yesod-simple && cd testOpenID
% stack install yesod-bin cabal-install --install-ghc
% stack build
% stack exec -- yesod devel
これでhttp://localhost:3000/ に接続する。もしかすると、うまくいかないかもしれない。そんなときには、stack ... develしたターミナルでEnterを入力すると、うまくいくかもしれない。
Yahoo! Japanにアプリケーションを登録する
https://e.developer.yahoo.co.jp/dashboard/ に接続し、「新しいアプリケーションを開発」ボタンをクリックする。
アプリケーション名を好きな名前に設定する。サイトURLをhttp://localhost:3000/ として、ガイドラインへの同意をクリックして、「確認」ボタンを押す。登録をクリックする。
アプリケーションIDとシークレットが必要になるのでメモしておこう。
ファイルy_clientId.txtとy_clientSecret.txtを、それぞれの内容で作成しておく。
ダミーのページを用意する
ログイン後のページとしてダミーのページを用意する。
% vi testOpenID.cabal
libraryのexposed-modules:のところの最後を、つぎのようにする。
Handler.Home
Handler.Comment
Handler.YLogined
Handler.YLoginedを追加した。
config/routesに追加する。
/ylogined YLoginedR GET
Handler.Homeモジュールをコピーする。
% cp Handler/Home.hs Handler/YLogined.hs
モジュール宣言をHandler.HomeからHandler.YLoginedにする。
getHomeRをgetYLoginedRに置き換える。
postHomeRは削除する。
% vi Application.hs
(import Handler.YLoginedを追加する)
これで、stack exec -- yesod develをする。
http://localhost:3000/ylogined に接続してみよう。
リダイレクトする
http://localhost:3000/ への接続を、Yahoo! Japanの認可用のページにリダイレクトする。
GETメソッドでいくつかのパラメータをわたす。
まずは、Data.Textモジュールなどを導入する。
% vi Hundler/Home.hs
import qualified Data.Text as Text
import qualified Data.Text.IO as Text
getHomeRの内容をリダイレクトに書き換える。
% vi Hundler/Home.hs
getHomeR = do
yClient <- lift $
Text.concat . Text.lines <$> Text.readFile "y_clientId.txt"
redirect $
"https://auth.login.yahoo.co.jp/yconnect/v1/authorization?" <>
"response_type=code+id_token&" <>
"scope=openid+profile&" <>
"client_id=" <> yClientId <> "&state=hogeru&" <>
"nonce=abcdefghijklmnop&" <>
"redirect_uri=http://localhost:3000/logined"
stack exec -- yesod develとしhttp://localhost:3000/ に接続すると、redirect_uri is invalidというエラーとなる。
https://e.developer.yahoo.co.jp/dashboard/ に接続し、編集ボタンをクリックする。
コールバックURLをhttp://localhost:3000/ylogined に変更する。
パラメータを確認する
Yahoo! Japanのページにリダイレクトすると、指定したページに、さらに、リダイレクトされる。そのときcodeとstateというパラメータがわたされる。これを表示してみよう。
% vi Hundler/YLogined.hs
getYLoginedR = do
Just code <- lookupGetParam "code"
Just state <- lookupGetParam "state"
print code
print state
(formWidget, formEnctype) <- generateFormPost sampleForm
...
アクセストークン、IDトークンを取得
Network.HTTP.Simpleモジュールを導入する。
% vi Hundler/YLogined.hs
import Network.HTTP.Simple
ByteStringをあつかうので、モジュールを導入する。
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BSC
Base64エンコードが必要なのでモジュールを導入する。
import qualified Data.ByteString.Base64.URL as B64
これらのモジュールを使うために.cabalファイルのlibraryセクションの依存パッケージのところにつぎのように追加する。
% vi testOpenID.cabal
, base64-bytestring
ファイルからクライアントIDとクライアントシークレットを読み込む。
% vi Hundler/YLogined.hs
...
print state
yClientId <-
lift $ BS.concat . BSC.lines <$> BS.readFile "y_clientId.txt"
yClientSecret <-
lift $ BS.concat . BSC.lines <$> BS.readFile "y_clientSecret.txt"
アクセストークン、IDトークンを要求するための、Requestを作成する。
% vi Hundler/YLogined.hs
initReq <-
parseRequest "https://auth.login.yahoo.co.jp/yconnect/v1/token"
let yClientIdSecret = B64.encode $ yClientId <> ":" <> yClientSecret
req = setRequestHeader "Content-Type"
["application/x-www-form-urlencoded"]
initReq { method = "POST" }
req' = setRequestHeader "Authorization"
["Basic " <> yClientIdSecret] req
req'' = setRequestBody (RequestBodyBS $
"grant_type=authorization_code&code=" <>
encodeUtf8 code <>
"&redirect_uri=http://localhost:3000/ylogined") req'
できあがったRequestでYahoo! Japanに接続して、応答を表示する。
rBody <- getResponseBody <$> httpLBS req''
print rBody
JSON形式のデータをハッシュに読み込む。まずは、モジュールを導入する。
import qualified Data.Aeson as Aeson
import qualified Data.HashMap.Lazy as HML
ハッシュに読み込んで、アクセストークンとIDトークンとを取り出す。
let Just resp = Aeson.decode rBody :: Maybe Aeson.Object
print $ keys resp
let Just (String at) = HML.lookup "access_token" resp
Just (String it) = HML.lookup "id_token" resp
print at
print it
IDトークンの内容を見る
必要なモジュールを導入する。
import qualified Data.Text as Text
import qualified Data.ByteString.Lazy as LBS
IDトークンをヘッダー部、ペイロード部、シグネチャ部にわけて、前2者のなかみを見てみる。
let [hd, pl, sg] = Text.splitOn "." it
[Just hdd, Just pld] = map
((Aeson.decode :: LBS.ByteString -> Maybe Aeson.Object)
. LBS.fromStrict
. either (error . show) id
. B64.decode . encodeUtf8)
[hd, pl]
print hdd
print pld
IDトークンの内容を検証する
cryptohashパッケージを追加する。
% vi testOpenID.cabal
library
...
build-depends:
...
, base64-bytestring
, cryptohash
必要なモジュールを導入する。
% vi Handler/YLogined.hs
import Crypto.MAC.HMAC
import qualified Crypto.Hash.SHA256 as SHA256
署名を確認する。
% vi Hansler/YLogined.hs
putStrLn sg
lift . BSC.putStrLn . B64.encode
. hmac SHA256.hash 64 yClientSecret
$ encodeUtf8 hd <> "." <> encodeUtf8 pl
その他のプロフィールの内容などを入手する
Yahoo! Japanからユーザの情報を取得する。
initReq2 <- parseRequest $
"https://userinfo.yahooapis.jp/yconnect/v1/attribute?schema=openid"
let req2 = setRequestHeader
"Authorization" ["Bearer " <> encodeUtf8 at] initReq2
rBody2 <- getResponseBody <$> httpLBS req2
let Just json2 = Aeson.decode rBody2 :: Maybe Aeson.Object
mapM_ print $ HML.toList json2
コード
GitHubに公開してあります。
これの/yesod/testOpenID以下です。