Adding OAuth middleware to Servant application
With monocle#412 we are
adding OAuth support to our new servant application.
The goal is to enable user authentication so that the interface can be
personalized, for example by adding support for the self
value in search queries.
The challenge is to perform the OAuth handshake to resolve the user identity.
§ Workflow
Here is the sequence diagram:
§ OAuth configuration
Currently there is no native solution for OAuth in servant, so we are going to
use the wai-middleware-auth
. Its configuration looks like this:
-- | 'authSettings' returns the @wai-middleware-auth@ configuration
authSettings :: Text -> Text -> Text -> Text -> Auth.AuthSettings
=
authSettings publicUrl oauthName oauthId oauthSecret
Auth.setAuthAppRootStatic publicUrl. Auth.setAuthPrefix "auth"
. Auth.setAuthProviders providers
. Auth.setAuthSessionAge (3600 * 24 * 7)
$ Auth.defaultAuthSettings
where
= [".*"]
emailAllowList =
ghProvider Auth.Provider $
Nothing
Auth.mkGithubProvider oauthName oauthId oauthSecret emailAllowList = HM.fromList [("github", ghProvider)] providers
§ Dispatching the requests
In monocle, we want to support both annonymous and authenticated users, and the
wai-middleware-auth
enforces authentication for every request. So we create an
extra middleware to dispatch the authentication only when necessary:
-- | Apply the @wai-middleware-auth@ only on the paths starting with a /a/
--
-- >>> type Application = Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived
-- >>> type Middleware = Application -> Application
enforceLoginPath :: Wai.Middleware -> Wai.Middleware
= app'
enforceLoginPath authMiddleware monocleApp where
app' request| matchAuth (Wai.rawPathInfo request) = (authMiddleware monocleApp) request
| otherwise = monocleApp request
matchAuth path| "/a/" `BS.isPrefixOf` path || "/auth" `BS.isPrefixOf` path = True
| otherwise = False
§ Creating the middleware
We only enable the middleware when there is an OAuth application environment:
-- | Create the middleware with the custom login path dispatch
createAuthMiddleware :: IO Wai.Middleware
= do
createAuthMiddleware <- traverse lookupEnv ["PUBLIC_URL", "OAUTH_NAME", "OAUTH_ID", "OAUTH_SECRET"]
envs case toText <$> catMaybes envs of
->
[publicUrl, oauthName, oauthId, oauthSecret]
enforceLoginPath<$> Auth.mkAuthMiddleware (authSettings publicUrl oauthName oauthId oauthSecret)
-> pure id _
§ Updating the route
Finaly we add the Vault to the authenticated route to access the user information:
type MonocleAPI =
"a" :> "whoami" :> Vault :> ReqBody '[JSON] WhoAmIRequest :> Post '[PBJSON, JSON] WhoAmIResponse
:<|> "search" :> "fields" :> ReqBody '[JSON] FieldsRequest :> Post '[PBJSON, JSON] FieldsResponse
:<|> "search" :> "query" :> ReqBody '[JSON] QueryRequest :> Post '[PBJSON, JSON] QueryResponse
:<|> "a" :> "search" :> "query" :> Vault :> ReqBody '[JSON] QueryRequest :> Post '[PBJSON, JSON] QueryResponse
server :: ServerT MonocleAPI AppM
=
server
authWhoAmI:<|> searchFields
:<|> searchQuery
:<|> searchQueryAuth
And here is an example usage for the whoami
endpoint:
authWhoAmI :: Vault -> AuthPB.WhoAmIRequest -> AppM AuthPB.WhoAmIResponse
= const $ pure response
authWhoAmI vault where
response :: AuthPB.WhoAmIResponse
= AuthPB.WhoAmIResponse $ toLazy $ show user
response = fromMaybe (error "Authentication is missing") (Auth.getAuthUserFromVault vault) user
We contributed a new function to enable using
wai-middleware-auth
withservant
: wai-middleware-auth#25.