LoginSignup
8
8

More than 5 years have passed since last update.

yesodでWebAPIを用意する際の工夫(認可と認証)

Last updated at Posted at 2015-07-05

記事投稿の動機

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

パターンマッチを使って、この定義よりも前に個別のリソースに対して制御を記述していくことになる。ここでの工夫点は、認証に失敗した場合の制御である。認証に失敗した場合に必ずログインページにリダイレクトさせるような制御だと、プログラムから利用できない。

isAuthorizedの例
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

そこで、次のサンプルコードのように、処理を分岐させる。

isAuthorizedの例'
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を取得できなくてもリダイレクトしない

...
8
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
8