Yahoo! JapanのOpenID Connectをyesodで試す

Yahoo! JapanのOpenID Connectをyesodで試す
========================================

はじめに
-------

SNSなどで、ほかのアプリケーションに、プロフィールの閲覧などを許す機能がある。これが認可であり、OAuthという仕組みで、それが実現される。このOAuthという仕組みは認可のためのわくぐみであり、本人確認(認証)のための機能ではない。

「認可をあたえられるのは本人だけである」という理屈で、この「認可」を本人確認に使う場合がある。しかし、これをすると、大きなセキュリティホールを作ってしまう。

[単なるOAuth 2.0を認証に使うと、車が通れるほどのどでかいセキュリティ・ホールができる](http://www.sakimura.org/2012/02/1487/)

OAuth 2.0のわくぐみのうえで、きちんとした本人確認(認証)をおこなう仕組みがある。それがOpenID Connectだ。

何をするか
---------

HaskellによるWebアプリケーションフレームワークであるYesodを使って、Yahoo! JapanのOpenID Connectの機能を試してみる。つぎのページの内容をYesodで実装した。

[OpenID Connect体験](http://qiita.com/sonedazaurus/items/753a65186f1be7185b39)

このページでは、おもにコードの紹介をする。細かい説明については上記のページを参照してほしい。
上記のページを参照したうえで、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に公開してあります。

https://github.com/YoshikuniJujo/testOAuth/releases/tag/qiita_20170220

これの/yesod/testOpenID以下です。