ihp

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?


Solution

  • 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>