hoauth2-2.9.0: src/Network/OAuth2/Experiment/Grants/DeviceAuthorization.hs
{-# LANGUAGE FlexibleInstances #-}
module Network.OAuth2.Experiment.Grants.DeviceAuthorization (
DeviceAuthorizationApplication (..),
pollDeviceTokenRequest,
) where
import Control.Concurrent
import Control.Monad.IO.Class (MonadIO (..))
import Control.Monad.Trans.Except
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Maybe
import Data.Set (Set)
import Data.Text.Lazy (Text)
import Network.HTTP.Conduit
import Network.OAuth.OAuth2
import Network.OAuth2.Experiment.Flows.DeviceAuthorizationRequest
import Network.OAuth2.Experiment.Flows.TokenRequest
import Network.OAuth2.Experiment.Flows.UserInfoRequest
import Network.OAuth2.Experiment.Types
import Prelude hiding (error)
-- | An Application that supports "Device Authorization Grant"
--
-- https://www.rfc-editor.org/rfc/rfc8628#section-3.1
data DeviceAuthorizationApplication = DeviceAuthorizationApplication
{ daName :: Text
, daClientId :: ClientId
, daClientSecret :: ClientSecret
, daScope :: Set Scope
, daAuthorizationRequestExtraParam :: Map Text Text
-- ^ Additional parameters to the device authorization request.
-- Most of identity providers follow the spec strictly but
-- AzureAD requires "tenant" parameter.
, daAuthorizationRequestAuthenticationMethod :: Maybe ClientAuthenticationMethod
-- ^ The spec requires similar authentication method as /token request.
-- Most of identity providers doesn't required it but some does like Okta.
}
pollDeviceTokenRequest ::
MonadIO m =>
IdpApplication i DeviceAuthorizationApplication ->
Manager ->
DeviceAuthorizationResponse ->
ExceptT TokenResponseError m OAuth2Token
pollDeviceTokenRequest idpApp mgr deviceAuthResp = do
pollDeviceTokenRequestInternal
idpApp
mgr
(deviceCode deviceAuthResp)
(fromMaybe 5 $ interval deviceAuthResp)
pollDeviceTokenRequestInternal ::
MonadIO m =>
IdpApplication i DeviceAuthorizationApplication ->
Manager ->
DeviceCode ->
Int ->
-- | Polling Interval
ExceptT TokenResponseError m OAuth2Token
pollDeviceTokenRequestInternal idpApp mgr deviceCode intervalSeconds = do
resp <- runExceptT (conduitTokenRequest idpApp mgr deviceCode)
case resp of
Left trRespError -> do
case tokenResponseError trRespError of
-- TODO: Didn't have a good idea to expand the error code
-- specifically for device token request flow
-- Device Token Response additional error code: https://www.rfc-editor.org/rfc/rfc8628#section-3.5
UnknownErrorCode "authorization_pending" -> do
liftIO $ threadDelay $ intervalSeconds * 1000000
pollDeviceTokenRequestInternal idpApp mgr deviceCode intervalSeconds
UnknownErrorCode "slow_down" -> do
let newIntervalSeconds = intervalSeconds + 5
liftIO $ threadDelay $ newIntervalSeconds * 1000000
pollDeviceTokenRequestInternal idpApp mgr deviceCode newIntervalSeconds
_ -> throwE trRespError
Right v -> pure v
instance HasOAuth2Key DeviceAuthorizationApplication where
mkOAuth2Key :: DeviceAuthorizationApplication -> OAuth2
mkOAuth2Key DeviceAuthorizationApplication {..} = toOAuth2Key daClientId daClientSecret
instance HasTokenRequestClientAuthenticationMethod DeviceAuthorizationApplication where
getClientAuthenticationMethod :: DeviceAuthorizationApplication -> ClientAuthenticationMethod
getClientAuthenticationMethod _ = ClientSecretBasic
instance HasDeviceAuthorizationRequest DeviceAuthorizationApplication where
mkDeviceAuthorizationRequestParam :: DeviceAuthorizationApplication -> DeviceAuthorizationRequestParam
mkDeviceAuthorizationRequestParam DeviceAuthorizationApplication {..} =
DeviceAuthorizationRequestParam
{ arScope = daScope
, arClientId =
if daAuthorizationRequestAuthenticationMethod == Just ClientSecretBasic
then Nothing
else Just daClientId
, arExtraParams = daAuthorizationRequestExtraParam
}
-- | https://www.rfc-editor.org/rfc/rfc8628#section-3.4
instance HasTokenRequest DeviceAuthorizationApplication where
type ExchangeTokenInfo DeviceAuthorizationApplication = DeviceCode
data TokenRequest DeviceAuthorizationApplication = AuthorizationCodeTokenRequest
{ trCode :: DeviceCode
, trGrantType :: GrantTypeValue
, trClientId :: Maybe ClientId
}
mkTokenRequestParam ::
DeviceAuthorizationApplication ->
DeviceCode ->
TokenRequest DeviceAuthorizationApplication
mkTokenRequestParam DeviceAuthorizationApplication {..} deviceCode =
--
-- This is a bit hacky!
-- The token request use `ClientSecretBasic` by default. (has to pick up one Client Authn Method)
-- ClientId shall be also be in request body per spec.
-- However, for some IdPs, e.g. Okta, when using `ClientSecretBasic` to authn Client,
-- it doesn't allow @client_id@ in the request body
-- 'daAuthorizationRequestAuthenticationMethod' set the tone for Authorization Request,
-- hence just follow it in the token request
AuthorizationCodeTokenRequest
{ trCode = deviceCode
, trGrantType = GTDeviceCode
, trClientId =
if daAuthorizationRequestAuthenticationMethod == Just ClientSecretBasic
then Nothing
else Just daClientId
}
instance ToQueryParam (TokenRequest DeviceAuthorizationApplication) where
toQueryParam :: TokenRequest DeviceAuthorizationApplication -> Map Text Text
toQueryParam AuthorizationCodeTokenRequest {..} =
Map.unions
[ toQueryParam trCode
, toQueryParam trGrantType
, toQueryParam trClientId
]
instance HasUserInfoRequest DeviceAuthorizationApplication