packages feed

chakra-0.1.0: src/Chakra/JWT.hs

{-# LANGUAGE NoImplicitPrelude   #-}
{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE ScopedTypeVariables #-}

-- |Contains JWT authentication settings
module Chakra.JWT where

import           Control.Lens         (preview)
import           Control.Monad.Except (Monad (return))
import           Crypto.JOSE          (JWK, JWKSet (..))
import           Crypto.JWT           (StringOrURI, string, uri)
import qualified Data.Aeson           as Aeson
import           Data.ByteString      (readFile)
import qualified Data.Text            as T
import           Network.URI          (parseURI)
import           RIO                  (Eq ((==)), IO, Maybe (Just, Nothing),
                                       Semigroup ((<>)), const, either, error,
                                       fromMaybe, id, maybe, ($), (++))
import           Servant.Auth.Server  (IsMatch (..), JWTSettings (..),
                                       generateKey)
import           System.Environment   (lookupEnv)

-- |Build JWT settings to be used in Servant Auth context
-- Looks for `JWK_AUDIENCES` and `JWK_PATH` in environment values
-- to load the sig file and value to verify the incoming jwt audience claim
getJWTAuthSettings :: IO JWTSettings
getJWTAuthSettings = do
  jwkSet <- acquireJwks
  signKey <- generateKey
  audienceCfg <- lookupEnv "JWK_AUDIENCES"
  let audMatches = maybe (const Matches) checkAud audienceCfg
      checkAud audConfig = \tokenAud ->
        if preview uri tokenAud == parseURI audConfig then
          Matches
        else if preview string tokenAud == Just (T.pack audConfig) then
          Matches
        else
          DoesNotMatch
  return $ buildJWTSettings signKey jwkSet audMatches

buildJWTSettings :: JWK -> JWKSet -> (StringOrURI -> IsMatch) -> JWTSettings
buildJWTSettings signKey jwkSet audMatches =
  JWTSettings
    { signingKey = signKey,
      jwtAlg = Nothing,
      validationKeys = vkeys signKey jwkSet,
      audienceMatches = audMatches
    }
  where
    vkeys k (JWKSet x) = JWKSet (x ++ [k])

acquireJwks :: IO JWKSet
acquireJwks = do
  envUrl <- lookupEnv "JWK_PATH"
  let jwkPath = fromMaybe "secrets/jwk.sig" envUrl
  fileContent <- readFile jwkPath
  let parsed = Aeson.eitherDecodeStrict fileContent
  return $ either (\e -> error $ "Invalid JWK file: " <> e) id parsed