module Snap.AtlassianConnect.OAuth
( requestAccessToken
, AC.AccessTokenResponse(..)
, AC.AccessToken
, AC.AccessTokenType(..)
) where
import Data.Aeson
import qualified Data.ByteString.Char8 as BSC
import qualified Data.ByteString.Lazy as BL
import qualified Data.Connect.Descriptor as D
import Data.List (isPrefixOf)
import qualified Data.Map as M
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Data.Time.Clock.POSIX as P
import Data.Time.Units (Minute)
import Data.TimeUnitUTC
import Network.Api.Support
import Network.HTTP.Client
import Network.HTTP.Client.TLS (tlsManagerSettings)
import Network.HTTP.Types
import Network.URI
import Snap.AtlassianConnect.AtlassianTypes
import qualified Snap.AtlassianConnect.Data as AC
import qualified Snap.AtlassianConnect.Instances as AC
import qualified Snap.AtlassianConnect.NetworkCommon as AC
import qualified Snap.AtlassianConnect.Tenant as AC
import qualified Web.JWT as JWT
requestAccessToken
:: AC.Tenant
-> UserKey
-> Maybe [D.ProductScope]
-> IO (Either AC.ProductErrorResponse (Maybe AC.AccessTokenResponse))
requestAccessToken tenant userKey possibleScopes = do
potentialAssertion <- generateJWTAssertion tenant userKey
case potentialAssertion of
Nothing -> return . Right $ Nothing
Just assertion ->
runRequest tlsManagerSettings POST url
( addHeader ("Host", "auth.atlassian.io")
<> addHeader ("Accept", "application/json")
<> addHeader ("Content-Type", "application/x-www-form-urlencoded")
<> setBody (renderSimpleQuery False (formParams assertion))
)
(basicResponder AC.responder)
where
url = authBaseUrl `T.append` "/oauth2/token"
formParams :: T.Text -> [(BSC.ByteString, BSC.ByteString)]
formParams assertion =
[ ("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer")
, ("assertion", T.encodeUtf8 assertion)
] ++ scopeParam
scopeParam = case possibleScopes of
Nothing -> []
Just scopes -> (: []) . (,) "scope" . BSC.intercalate " " . fmap printScope . filter ((/=) D.ActAsUser) $ scopes
printScope :: D.ProductScope -> BSC.ByteString
printScope D.Read = "READ"
printScope D.Write = "WRITE"
printScope D.Delete = "DELETE"
printScope D.ProjectAdmin = "PROJECT_ADMIN"
printScope D.SpaceAdmin = "SPACE_ADMIN"
printScope D.Admin = "ADMIN"
printScope D.ActAsUser = "ACT_AS_USER"
generateJWTAssertion :: AC.Tenant -> UserKey -> IO (Maybe T.Text)
generateJWTAssertion tenant userKey = do
currentTime <- P.getPOSIXTime
case generateAssertionClaims currentTime tenant userKey of
Nothing -> return Nothing
Just claims -> return . Just $ JWT.encodeSigned JWT.HS256 jwtSecret claims
where
jwtSecret = JWT.secret . AC.sharedSecret $ tenant
generateAssertionClaims :: P.POSIXTime -> AC.Tenant -> UserKey -> Maybe JWT.JWTClaimsSet
generateAssertionClaims fromTime tenant userKey = do
oid <- AC.oauthClientId tenant
return JWT.JWTClaimsSet
{ JWT.iss = JWT.stringOrURI $ "urn:atlassian:connect:clientid:" `T.append` oid
, JWT.sub = JWT.stringOrURI $ "urn:atlassian:connect:userkey:" `T.append` userKey
, JWT.aud = Left <$> JWT.stringOrURI authBaseUrl
, JWT.iat = JWT.numericDate fromTime
, JWT.exp = JWT.numericDate expiryTime
, JWT.nbf = Nothing
, JWT.jti = Nothing
, JWT.unregisteredClaims = M.fromList [("tnt", String . T.pack $ tenantBaseUrlString)]
}
where
tenantBaseUrlString = show . AC.getURI . AC.baseUrl $ tenant
expiryTime :: P.POSIXTime
expiryTime = fromTime + timeUnitToDiffTime expiryPeriod
expiryPeriod :: Minute
expiryPeriod = 1
authBaseUrl :: T.Text
authBaseUrl = "https://auth.atlassian.io"