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 $
Auth.mkGithubProvider oauthName oauthId oauthSecret emailAllowList Nothing
providers = HM.fromList [("github", ghProvider)]§ 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
enforceLoginPath authMiddleware monocleApp = app'
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
createAuthMiddleware = do
envs <- traverse lookupEnv ["PUBLIC_URL", "OAUTH_NAME", "OAUTH_ID", "OAUTH_SECRET"]
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
:<|> searchQueryAuthAnd here is an example usage for the whoami endpoint:
authWhoAmI :: Vault -> AuthPB.WhoAmIRequest -> AppM AuthPB.WhoAmIResponse
authWhoAmI vault = const $ pure response
where
response :: AuthPB.WhoAmIResponse
response = AuthPB.WhoAmIResponse $ toLazy $ show user
user = fromMaybe (error "Authentication is missing") (Auth.getAuthUserFromVault vault)We contributed a new function to enable using
wai-middleware-authwithservant: wai-middleware-auth#25.