packages feed

github-rest-1.2.0: src/GitHub/REST/Auth.hs

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}

{-|
Module      :  GitHub.REST.Auth
Maintainer  :  Brandon Chinn <brandonchinn178@gmail.com>
Stability   :  experimental
Portability :  portable

Definitions for handling authentication with the GitHub REST API.
-}
module GitHub.REST.Auth (
  Token (..),
  fromToken,

  -- * Helpers for using JWT tokens with the GitHub API
  getJWTToken,
) where

import qualified Crypto.PubKey.RSA as Crypto
import Data.Aeson ((.=))
import qualified Data.Aeson as Aeson
import qualified Data.Aeson.Text as Aeson
import Data.ByteString (ByteString)
import qualified Data.Text.Encoding as Text
import qualified Data.Text.Lazy as TextL
import Data.Time (getCurrentTime)
import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)
import qualified Jose.Jwa as Jose
import qualified Jose.Jws as Jose
import qualified Jose.Jwt as Jose
import UnliftIO.Exception (Exception, throwIO)

-- | The token to use to authenticate with GitHub.
data Token
  = -- | https://developer.github.com/v3/#authentication
    AccessToken ByteString
  | -- | https://developer.github.com/apps/building-github-apps/authenticating-with-github-apps/#authenticating-as-a-github-app
    BearerToken ByteString
  deriving (Show)

fromToken :: Token -> ByteString
fromToken = \case
  AccessToken t -> "token " <> t
  BearerToken t -> "bearer " <> t

-- | The ID of your GitHub application
type AppId = Int

-- | Create a JWT token that expires in 10 minutes.
getJWTToken :: Crypto.PrivateKey -> AppId -> IO Token
getJWTToken privKey appId = do
  -- use floor to ensure expiration doesn't go past 10 minutes
  -- https://github.com/orgs/community/discussions/24635#discussioncomment-3244803
  now <- floor . utcTimeToPOSIXSeconds <$> getCurrentTime

  BearerToken . Jose.unJwt <$> signToken (mkClaims now)
  where
    mkClaims now =
      Text.encodeUtf8 . TextL.toStrict . Aeson.encodeToLazyText $
        Aeson.object
          [ "iat" .= (now :: Integer)
          , "exp" .= (now + 10 * 60)
          , "iss" .= show appId
          ]
    signToken claims =
      Jose.rsaEncode Jose.RS256 privKey claims >>= \case
        Right jwt -> pure jwt
        Left e -> throwIO $ JwtError e

-- https://github.com/tekul/jose-jwt/issues/30
data JwtError = JwtError Jose.JwtError
  deriving (Show, Exception)