記事投稿の動機
Webサービス(人間向けのインタフェースを用意)を作るとき、本人を特定するための認証が必要になる。yesodでこれを実現するのはとても簡単で、公式ガイドのAuthentication and Authorizationを読み進めれば、第三者認証(browzeridなど)の仕組みを作ることができる。
一方、認証後のユーザが利用することになる機能をプログラムに利用させたくなるケースがある。このインタフェースがWebAPIなのだが、ガイドに書いてある内容のままでは使えない。プログラムからは第三者認証を行うことが難しい(できない?)からだ。
そこで、ある機能を人間、プログラムのどちらからも利用できるようにするために工夫したことを書いておく。
※記事の内容は、ガイドの内容およびyesodの認証、認可の仕組みを少々知っていることを前提としています。
※記事を書いている最中に、より良い方法を見つけてしまいました。「工夫したこと・新」という文章がそれにあたります。
工夫したこと・新
haddocksを確認すると、maybeAuthId
について次のように記載されている。
Retrieves user credentials, if user is authenticated.
By default, this calls defaultMaybeAuthId to get the user ID from the session. This can be overridden to allow authentication via other means, such as checking for a special token in a request header. This is especially useful for creating an API to be accessed via some means other than a browser.
通常この関数は(第三者認証などで)保持しているセッションからIDを取得するが、オーバーライドすることでリクエストヘッダに含まれるトークンをチェックするなどして、WebAPIとしての利用もできるようになる、ということだ。
ということで、工夫点は**maybeAuthId
をオーバライドすること**であり、このように定義した。今回はトークンは、クエリパラメータから受け取るようにしている。なお、トークンの生成方式については今回は割愛している。
instance YesodAuth 内
instance YesodAuth App where
type AuthId App = UserId
-- Where to send a user after successful login
loginDest _ = RootR
-- Where to send a user after logout
logoutDest _ = RootR
-- Override the above two destinations when a Referer: header is present
redirectToReferer _ = True
ここを追加
maybeAuthId = do
muid <- userIdFromToken
case muid of
Nothing -> defaultMaybeAuthId
_ -> return muid
ここまで
getAuthId creds = do
Entity uid _ <- runDB $ getBy404 $ UniqueUser $ credsIdent creds
return $ Just uid
-- You can add other plugins like BrowserID, email or OAuth here
authPlugins _ = [authBrowserId def]
authHttpManager = getHttpManager
トップレベルで定義
-- クエリパラメータからトークンを受け取り、対応するユーザIDを返す。失敗することもある。
userIdFromToken :: Handler (Maybe (Key User))
userIdFromToken = do
mtoken <- lookupGetParam "token"
case mtoken of
Nothing -> return Nothing
_ -> do
mu <- runDB $ selectFirst [UserPassword ==. mtoken] []
case mu of
Just (Entity uid _) -> return $ Just uid
_ -> return Nothing
あとは、個別のリソースに対する認証・認可の定義をすることになる。
instance Yesod 内
Yesod
インスタンス宣言内で、認証・認可の制御を行う。初期状態は全てのアクセスを認可するようになっている。
-- Default to Authorized for now.
isAuthorized _ _ = return Authorized
パターンマッチを使って、この定義よりも前に個別のリソースに対して制御を記述していくことになる。ここで特にWebサービスなのか、WebAPIなのかを意識することはない。
個別のリソースに対する認証・認可定義を終えたら、続けて次のように書いておくと良いだろう。
-- Write メソッドは、基本的に認証が必要
isAuthorized _ True = do
mauth <- maybeAuth
case mauth of
Nothing -> return AuthenticationRequired
Just _ -> return Authorized
-- 個別に定義しないリソースはアクセス可能
isAuthorized _ _ = return Authorized --初期状態
工夫したこと・旧(読まなくてOK)
※こちらの内容は、「工夫したこと・新」の内容ですべて置き換えます。私が最終的な方針を見つけ出すまでの軌跡としてメモしています。
Foundation.hs
普通に、site
型に、YesodAuth
インスタンスを与える。
nstance YesodAuth App where
type AuthId App = UserId --use ID of USER table as AuthID
-- Where to send a user after successful login
loginDest _ = RootR
-- Where to send a user after logout
logoutDest _ = RootR
-- Override the above two destinations when a Referer: header is present
redirectToReferer _ = True
-- retrieve user id for AuthID by taking credential
getAuthId creds = do
Entity uid _ <- runDB $ getBy404 $ UniqueUser $ credsIdent creds
return $ Just uid
-- use BrowserId plugin
authPlugins _ = [authBrowserId def]
Yesod
インスタンス宣言内で、認証・認可の制御を行う。初期状態は全てのアクセスを認可するようになっている。
-- Default to Authorized for now.
isAuthorized _ _ = return Authorized
パターンマッチを使って、この定義よりも前に個別のリソースに対して制御を記述していくことになる。ここでの工夫点は、認証に失敗した場合の制御である。認証に失敗した場合に必ずログインページにリダイレクトさせるような制御だと、プログラムから利用できない。
instance Yesod App where
isAuthorized RequestsR _ = do
mauth <- maybeAuth
case mauth of
Nothing ->return AuthenticationRequired
Just (Entity uid u)
| isAdmin u -> return Authorized -- 管理者権限があれば認可
| otherwise -> return $ Unauthorized ("このページは表示できません" :: Text)
isAdmin :: User -> Bool
isAdmin = userAdmin
そこで、次のサンプルコードのように、処理を分岐させる。
instance Yesod App where
isAuthorized RequestsR _ = do
mauth <- maybeAuth
case mauth of
Nothing -> do
-- 変更点 from
-- WebAPIからのアクセスは、token を受け取る
muid <- userIdFromToken
case muid of
Just x -> return Authorized
_ -> return AuthenticationRequired
-- 変更点 to
Just (Entity uid u)
| isAdmin u -> return Authorized
| otherwise -> return $ Unauthorized ("このページは表示できません" :: Text)
isAdmin :: User -> Bool
isAdmin = userAdmin
-- クエリパラメータからトークンを受け取り、対応するユーザIDを返す。失敗することもある。
userIdFromToken :: Handler (Maybe (Key User))
userIdFromToken = do
mtoken <- lookupGetParam "token"
case mtoken of
Nothing -> return Nothing
_ -> do
mu <- runDB $ selectFirst [UserPassword ==. mtoken] []
case mu of
Just (Entity uid _) -> return $ Just uid
_ -> return Nothing
こうすることで、認証を回避し、かつ制限されたユーザのみにリソースにアクセスすることを許可できる。
また、個別のリソースに対する認証・認可定義を終えたら、続けて次のように書いておくと良いだろう。
-- Write メソッドは、基本的に認証が必要
isAuthorized _ True = do
mauth <- maybeAuth
case mauth of
Nothing -> return AuthenticationRequired
Just _ -> return Authorized
-- 個別に定義しないリソースはアクセス可能
isAuthorized _ _ = return Authorized
ハンドラ
ハンドラ内では、必要な時に認証IDを取り出すことができる。取得方法は大きく2つある(実際は4つだが、簡単のため)。
maybeAuthId :: HandlerT master IO (Maybe (AuthId master))
requireAuthId :: YesodAuth master => HandlerT master IO (AuthId master)
取得する情報が、Maybe
型で包まれているかいないかという点と、requireAuth
はもし認証IDを取り出せなければログイン画面にリダイレクトさせる、という点が、2つの処理の違いである。
私はコードを簡潔にすることを主目的として、認証IDを生で取り出せるように、次のように書いていた。
getRootR :: Handler Html
getRootR = do
uid <- requireAuth
...
しかし、 **これではやはり、認証に失敗するとログインページにリダイレクトされてしまうため、プログラムから利用することができない。**したがって、ハンドラ内でrequireAuth
を使うのは、WebAPIの観点からはよろしくないだろう。これが工夫点となる。
getRootR :: Handler Html
getRootR = do
Just (Entity uid _) <- maybeAuth -- 認証IDを取得できなくてもリダイレクトしない
...