@@ -7,48 +7,47 @@
7{-# LANGUAGE DataKinds #-}
7{-# LANGUAGE DataKinds #-}
9module GitHub.App.Auth
9module GitHub.App.Auth
12− , InstallationAuth
10+ ( InstallationAuth
13 , mkInstallationAuth
11 , mkInstallationAuth
15 , obtainAccessToken
13 , obtainAccessToken
18import Prelude hiding (exp)
19import Prelude hiding (exp)
20import Control.Concurrent (MVar, newMVar, putMVar, readMVar, takeMVar)
21import Control.Concurrent (MVar, newMVar, putMVar, readMVar, takeMVar)
21import Control.Exception.Safe (bracketOnError, catch)
22import Control.Exception.Safe (bracketOnError, catch)
22−import Control.Monad.Except (ExceptT, MonadError (throwError), runExceptT)
23+import Control.Monad.Except (MonadError (throwError), ExceptT, runExceptT)
23−import Control.Monad.Trans (lift)
24−import Crypto.PubKey.RSA (PrivateKey)
25−import Data.Aeson (FromJSON (..), withObject, (.:))
26−import qualified Data.ByteString.Lazy as LBS
27import Data.Functor (($>))
24import Data.Functor (($>))
25+import Crypto.Types.PubKey.RSA (PrivateKey)
26+import Data.Aeson (FromJSON (..), withObject, (.:))
28import Data.Maybe (fromMaybe)
27import Data.Maybe (fromMaybe)
29import Data.Semigroup ((<>))
28import Data.Semigroup ((<>))
31import Data.Text (Text)
29import Data.Text (Text)
32import qualified Data.Text as T
30import qualified Data.Text as T
33import Data.Text.Encoding (encodeUtf8)
31import Data.Text.Encoding (encodeUtf8)
34import Data.Time (NominalDiffTime, UTCTime, diffUTCTime, getCurrentTime)
32import Data.Time (NominalDiffTime, UTCTime, diffUTCTime, getCurrentTime)
35import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)
33import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)
34+import Control.Monad.Trans (lift)
35+import Data.Word (Word64)
36import GitHub.Auth (Auth (OAuth))
36import GitHub.Auth (Auth (OAuth))
37−import GitHub.Data.Apps (App)
38import GitHub.Data.Definitions (Error (HTTPError))
37import GitHub.Data.Definitions (Error (HTTPError))
39−import GitHub.Data.Id (Id, untagId)
38+import GitHub.Data.Request (StatusMap)
40−import GitHub.Data.Installations (Installation)
39+import GitHub.Request (parseResponse)
41−import GitHub.Request (StatusMap, parseResponseJSON)
40+import Web.JWT (JWTClaimsSet (exp, iat, iss), Signer (RSAPrivateKey), encodeSigned,
41+ numericDate, stringOrURI)
42import Network.HTTP.Client as HTTP
42import Network.HTTP.Client as HTTP
43import qualified Network.HTTP.Client.Internal as HTTP
43import qualified Network.HTTP.Client.Internal as HTTP
44import qualified Network.HTTP.Types as HTTP
44import qualified Network.HTTP.Types as HTTP
45−import Web.JWT (JWTClaimsSet (exp, iat, iss), Signer (RSAPrivateKey), encodeSigned, numericDate,
45+import qualified Data.ByteString.Lazy as LBS
49-- | JWT expiration time. Maximum accepted by GitHub is 10 minutes
48-- | JWT expiration time. Maximum accepted by GitHub is 10 minutes
50jwtExpTime :: NominalDiffTime
49jwtExpTime :: NominalDiffTime
51−jwtExpTime = 300
50+jwtExpTime = 600
53-- | Installation access token expiration time. It is fixed by GitHub and is equal to 1 hour
52-- | Installation access token expiration time. It is fixed by GitHub and is equal to 1 hour
54instKeyExpTime :: NominalDiffTime
53instKeyExpTime :: NominalDiffTime
@@ -65,16 +64,10 @@ baseUrl :: Text
65baseUrl = "https://api.github.com"
64baseUrl = "https://api.github.com"
68−-- | Credentials of a GitHub App
69−data AppAuth = AppAuth
70− { aaAppId :: !(Id App)
71− , aaPrivateKey :: !PrivateKey
74-- | Cached GitHub installation access token
67-- | Cached GitHub installation access token
75data InstallationToken = InstallationToken
68data InstallationToken = InstallationToken
76− { itToken :: !Auth
69+ { itToken :: Auth
77− , itExpirationTime :: !UTCTime
70+ , itExpirationTime :: UTCTime
78 } deriving (Show)
71 } deriving (Show)
80instance FromJSON InstallationToken where
73instance FromJSON InstallationToken where
@@ -84,7 +77,7 @@ instance FromJSON InstallationToken where
84 <*> o .: "expires_at"
77 <*> o .: "expires_at"
87−-- | Credentials required for an App to authenticate as an installation
80+-- | Credentials required for an app to authenticate as an installation
88data InstallationAuth = InstallationAuth
81data InstallationAuth = InstallationAuth
89 { iaClaimsSet :: !JWTClaimsSet -- ^ Prefilled claims set
82 { iaClaimsSet :: !JWTClaimsSet -- ^ Prefilled claims set
90 , iaAppPrivateKey :: !PrivateKey -- ^ Private key to sign token requests
83 , iaAppPrivateKey :: !PrivateKey -- ^ Private key to sign token requests
@@ -93,16 +86,20 @@ data InstallationAuth = InstallationAuth
90+type InstallationId = Word64
96-- | Smart constructor for 'InstallationAuth'
92-- | Smart constructor for 'InstallationAuth'
97mkInstallationAuth
93mkInstallationAuth
98− :: AppAuth
94+ :: AppId -- ^ GitHub App ID
95+ -> PrivateKey -- ^ GitHub App Private Key
99− -> Id Installation
96+ -> InstallationId -- ^ Installation ID
100 -> IO InstallationAuth
97 -> IO InstallationAuth
101−mkInstallationAuth AppAuth{aaAppId, aaPrivateKey} instId = do
98+mkInstallationAuth appId pk instId = do
102 varToken <- newMVar Nothing
99 varToken <- newMVar Nothing
103− let issuer = fromMaybe (error "impossible") . stringOrURI . T.pack . show . untagId $ aaAppId
100+ let issuer = fromMaybe (error "impossible") . stringOrURI . T.pack . show $ appId
104 claimsSet = mempty { iss = Just issuer }
101 claimsSet = mempty { iss = Just issuer }
105− pure $ InstallationAuth claimsSet aaPrivateKey (T.pack . show . untagId $ instId) varToken
102+ pure $ InstallationAuth claimsSet pk (T.pack . show $ instId) varToken
107-- | Create a request which, when executed, will obtain a new access token
104-- | Create a request which, when executed, will obtain a new access token
108createAccessTokenR :: InstallationAuth -> IO HTTP.Request
105createAccessTokenR :: InstallationAuth -> IO HTTP.Request
@@ -113,15 +110,11 @@ createAccessTokenR InstallationAuth{..} = do
113 { iat = Just $ toJsonTime currentTime
110 { iat = Just $ toJsonTime currentTime
114 , exp = Just $ toJsonTime expiryTime
111 , exp = Just $ toJsonTime expiryTime
116− jwt = encodeSigned (RSAPrivateKey iaAppPrivateKey) mempty claims
113+ jwt = encodeSigned (RSAPrivateKey iaAppPrivateKey) claims
118 req <- HTTP.parseRequest . T.unpack $ url
115 req <- HTTP.parseRequest . T.unpack $ url
120− { HTTP.requestHeaders =
121− [ ("Authorization", "Bearer " <> encodeUtf8 jwt)
117+ { HTTP.requestHeaders = [("Authorization", "Bearer " <> encodeUtf8 jwt)]
122− , ("Accept", "application/vnd.github.machine-man-preview+json")
123− , ("User-Agent", "github-app/Haskell")
125 , HTTP.checkResponse = successOrMissing Nothing
118 , HTTP.checkResponse = successOrMissing Nothing
126 , HTTP.method = "POST"
119 , HTTP.method = "POST"
@@ -148,7 +141,7 @@ obtainAccessToken mgr ia@InstallationAuth{..} = readMVar iaToken >>= \case
148 renew :: IO (Either Error Auth)
141 renew :: IO (Either Error Auth)
149 renew = bracketOnError (takeMVar iaToken) (putMVar iaToken) $ \_ -> do
142 renew = bracketOnError (takeMVar iaToken) (putMVar iaToken) $ \_ -> do
150 req <- createAccessTokenR ia
143 req <- createAccessTokenR ia
151− result <- runExceptT $ httpLbs' req >>= parseResponseJSON
144+ result <- runExceptT $ httpLbs' req >>= parseResponse
152 case result of
145 case result of
153 Right newToken -> putMVar iaToken (Just newToken) $> Right (itToken newToken)
146 Right newToken -> putMVar iaToken (Just newToken) $> Right (itToken newToken)
154 Left err -> putMVar iaToken Nothing $> Left err
147 Left err -> putMVar iaToken Nothing $> Left err