Tyler Jones
Tyler Jones

Reputation: 1343

Can IHP be used for an app backend with JWT authentication?

Has anyone used IHP for an app backend and if so, what changes need to be made for that to be doable? Is there a jwt package or something like it that allows IHP to have JWT authentication? Does Digitally Induced support using IHP as an app backend?

Upvotes: 0

Views: 78

Answers (1)

Marc Scholten
Marc Scholten

Reputation: 1396

IHP itself has JWT only as as part of DataSync right now. But here's a custom JWT implementation used at digitally induced:

{-# LANGUAGE AllowAmbiguousTypes #-}

module Application.Auth (initJWTAuthentication) where

import IHP.Prelude
import IHP.LoginSupport.Helper.Controller
import IHP.Controller.Session
import IHP.QueryBuilder
import IHP.Fetch
import IHP.ControllerSupport
import IHP.ModelSupport
import IHP.Controller.Context
import IHP.Controller.Param
import qualified Web.JWT as JWT
import qualified Data.ByteString as BS
import qualified Data.Maybe as Maybe
import qualified Data.Text as Text

{-# INLINE initJWTAuthentication #-}
initJWTAuthentication :: forall user normalizedModel.
        ( ?context :: ControllerContext
        , ?modelContext :: ModelContext
        , normalizedModel ~ NormalizeModel user
        , Typeable normalizedModel
        , Table normalizedModel
        , FromRow normalizedModel
        , PrimaryKey (GetTableName normalizedModel) ~ UUID
        , GetTableName normalizedModel ~ GetTableName user
        , FilterPrimaryKey (GetTableName normalizedModel)
        , KnownSymbol (GetModelName user)
    ) => IO ()
initJWTAuthentication = do
    let accessTokenQueryParam = (paramOrNothing  "access_token")
    let accessToken :: Maybe Text = accessTokenQueryParam <|> jwtFromAuthorizationHeader
    case accessToken of
        Just accessToken -> do
            signer <- fromContext @JWT.Signer
            let signature = JWT.decodeAndVerifySignature signer accessToken

            case signature of
                Just jwt -> do
                    let userId :: Id user = jwt
                            |> JWT.claims
                            |> JWT.sub
                            |> Maybe.fromMaybe (error "JWT missing sub")
                            |> JWT.stringOrURIToText
                            |> textToId

                    user <- fetchOneOrNothing userId
                    putContext user
                Nothing -> error "Invalid signature"
        Nothing -> pure ()

jwtFromAuthorizationHeader :: (?context :: ControllerContext) => Maybe Text
jwtFromAuthorizationHeader = do
    case getHeader "Authorization" of
        Just authHeader -> authHeader
                |> cs
                |> Text.stripPrefix "Bearer "
                |> Maybe.fromMaybe (error "Invalid format of Authorization header, expected 'Bearer <jwt>'")
                |> Just
        Nothing -> Nothing

Drop that into Application/Auth.hs. Then change Web/FrontController.hs to call the initJWTAuthentication function like this:

instance InitControllerContext WebApplication where
    initContext = do
        setLayout defaultLayout

        initAuthentication @User
        initJWTAuthentication @User
        

After that you can make API requests to your IHP app like http://myapp/SomeAction?access_token=<JWT here> or by setting the Authorization HTTP header like Authorization: Bearer <JWT here>

Upvotes: 1

Related Questions