Skip to content

Commit

Permalink
adding stuff
Browse files Browse the repository at this point in the history
  • Loading branch information
Marco Zocca committed Aug 11, 2023
1 parent d620c3b commit 9774e2b
Show file tree
Hide file tree
Showing 18 changed files with 369 additions and 118 deletions.
11 changes: 10 additions & 1 deletion ms-auth/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,23 @@ and this project adheres to the

## Unreleased


## 0.4.0.0

Add Bot Framework support

Breaking changes:
- MSAuth is the only public interface module
- 'newNoToken' and 'fetchUpdateToken' are not expored anymore from Session and MSAuth, in favor of a single function 'tokenUpdateLoop' which does both the initialization and the refresh loop
- 'applyDotEnv' and the 'DotEnv' module are gone. Please use the equivalent package 'dotenv-micro'

## 0.3.0.0

defaultAzureCredential - simplified version of the Microsoft Identity SDK

introduced MSAuth module that re-exports internal functions

Breaking changes:

- module Network.OAuth2.JWT is not exposed anymore
- OAuthCfg does not contain fields for client ID and secret anymore
- client ID and client secret can only be loaded from environment variables
Expand Down
3 changes: 1 addition & 2 deletions ms-auth/ms-auth.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,9 @@ library
default-language: Haskell2010
hs-source-dirs: src
exposed-modules: MSAuth
Network.OAuth2.Session
Network.OAuth2.Provider.AzureAD
other-modules: Network.OAuth2.JWT
DotEnv
Network.OAuth2.Session
build-depends: aeson
, base >= 4.7 && < 5
, bytestring
Expand Down
13 changes: 5 additions & 8 deletions ms-auth/src/MSAuth.hs
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
-- | Functions for implementing Azure AD-based authentication
--
-- Both @Auth Code Grant@ (i.e. with browser client interaction) and @App-only@ (i.e. Client Credentials) authentication flows are supported. The former is useful when a user needs to login and delegate some permissions to the application (i.e. accessing personal data), whereas the second is for server processes and automation accounts.
-- Both @Auth Code Grant@ (i.e. with a user involved in the autorization loop) and @Client Credentials Grant@ (i.e. app only) authentication flows are supported. The former is useful when a user needs to login and delegate some permissions to the application (i.e. accessing personal data), whereas the second is for server processes and automation accounts.
module MSAuth (
applyDotEnv
-- * A App-only flow (server-to-server)
, Token
, newNoToken
-- * A Client Credentials flow (server-to-server)
Token
, tokenUpdateLoop
, expireToken
, readToken
, fetchUpdateToken
-- ** Default Azure Credential
, defaultAzureCredential
-- * B Auth code grant flow (interactive)
Expand All @@ -27,7 +25,6 @@ module MSAuth (
, withAADUser
, Scotty
, Action
) where
) where

import Network.OAuth2.Session
import DotEnv (applyDotEnv)
73 changes: 42 additions & 31 deletions ms-auth/src/Network/OAuth2/Provider/AzureAD.hs
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,20 @@
{-# options_ghc -Wno-ambiguous-fields #-}
-- | Settings for using Azure Active Directory as OAuth identity provider
--
-- Both @Auth Code Grant@ (i.e. with browser client interaction) and @App-only@ (i.e. Client Credentials) authentication flows are supported. The former is useful when a user needs to login and delegate some permissions to the application (i.e. accessing personal data), whereas the second is for server processes and automation accounts.
-- Both @Auth Code Grant@ (i.e. with browser client interaction) and @Client Credentials Grant@ authentication flows are supported. The former is useful when a user needs to login and delegate some permissions to the application (i.e. accessing personal data), whereas the second is for server processes and automation accounts.
--
-- Azure Bot Framework is supported since v 0.
-- Azure Bot Framework is supported since v 0.4
module Network.OAuth2.Provider.AzureAD (
AzureAD
, AzureBotFramework
-- * Environment variables
, envClientId
, envClientSecret
, envTenantId
-- * App flow
-- * Client Credentials auth flow
, azureADApp
, azureBotFrameworkOAuthADApp
-- * Delegated permissions OAuth2 flow
, azureBotFrameworkADApp
-- * Auth Code Grant auth flow
, OAuthCfg(..)
, AzureADUser
, azureOAuthADApp
Expand Down Expand Up @@ -79,9 +79,9 @@ instance Show AzureADException where
show = \case
AADNoEnvVar e -> unwords ["Env var", e, "not found"]

-- * App-only flow
-- * Client Credentials Grant flow

-- | Azure OAuth application (i.e. with user consent screen)
-- | Azure OAuth application
--
-- NB : scope @offline_access@ is ALWAYS requested
--
Expand Down Expand Up @@ -117,8 +117,42 @@ defaultAzureADApp =
, idp = defaultAzureADIdp
}

-- | Initialize an Client Credentials token exchange application for the Bot Framework
--
--
-- Throws 'AzureADException' if @AZURE_CLIENT_ID@ and/or @AZURE_CLIENT_SECRET@ credentials are not found in the environment
azureBotFrameworkADApp :: MonadIO m =>
TL.Text -- ^ app name
-> m (IdpApplication 'ClientCredentials AzureBotFramework)
azureBotFrameworkADApp appname = do
clid <- envClientId
sec <- envClientSecret
pure $ ClientCredentialsIDPAppConfig {idpAppClientId = clid,
idpAppClientSecret = sec,
idpAppName = appname,
idpAppScope = Set.fromList ["https://api.botframework.com/.default"],
idpAppTokenRequestExtraParams = mempty,
idp = defaultAzureBotFrameworkIdp
}


data AzureBotFramework = AzureBotFramework deriving (Eq, Show)

defaultAzureBotFrameworkIdp :: Idp AzureBotFramework
defaultAzureBotFrameworkIdp = Idp {
idpFetchUserInfo = authGetJSON @(IdpUserInfo AzureBotFramework)
, idpTokenEndpoint = [uri|https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token|]
, idpUserInfoEndpoint = error $ unwords ["Azure Bot Framework Idp:", "OAuth user info endpoint is not defined"]
, idpAuthorizeEndpoint = error $ unwords ["Azure Bot Framework Idp:", "OAuth authorize endpoint is not defined"]
}



-- * Delegated permissions flow




-- * Authorization Code Grant flow

type instance IdpUserInfo AzureAD = AzureADUser

Expand Down Expand Up @@ -182,31 +216,8 @@ defaultAzureADIdp =
, idpTokenEndpoint = [uri|https://login.microsoftonline.com/common/oauth2/v2.0/token|]
}

-- |
azureBotFrameworkOAuthADApp :: ClientId
-> ClientSecret
-> TL.Text -- ^ app name
-> IdpApplication 'ClientCredentials AzureBotFramework
azureBotFrameworkOAuthADApp clid sec appname = ClientCredentialsIDPAppConfig {
idpAppClientId = clid,
idpAppClientSecret = sec,
idpAppName = appname,
idpAppScope = Set.fromList ["https://api.botframework.com/.default"],
idpAppTokenRequestExtraParams = mempty,
idp = defaultAzureBotFrameworkIdp
}


data AzureBotFramework = AzureBotFramework

defaultAzureBotFrameworkIdp :: Idp AzureBotFramework
defaultAzureBotFrameworkIdp = Idp {
idpFetchUserInfo = authGetJSON @(IdpUserInfo AzureBotFramework)
, idpTokenEndpoint = [uri|https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token|]
, idpUserInfoEndpoint = error $ unwords ["Azure Bot Framework Idp:", "OAuth user info endpoint is not defined"]
, idpAuthorizeEndpoint = error $ unwords ["Azure Bot Framework Idp:", "OAuth authorize endpoint is not defined"]
}

-- | https://learn.microsoft.com/en-us/azure/active-directory/develop/userinfo
data AzureADUser = AzureADUser
{ sub :: T.Text
Expand Down
100 changes: 50 additions & 50 deletions ms-auth/src/Network/OAuth2/Session.hs
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,22 @@
--
-- The library supports the following authentication scenarios :
--
-- * [Client Credentials](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow) (server/server or automation accounts)
-- * [Client Credentials](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow) (server/server or automation accounts), see also https://oauth.net/2/grant-types/client-credentials/
--
-- * [Authorization Code](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow) (with human users being prompted to delegate some access rights to the app)
-- * [Authorization Code](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow) (with human users being prompted to delegate some access rights to the app), see also https://oauth.net/2/grant-types/authorization-code/
--
-- and provides functions to keep tokens up to date in the background.
module Network.OAuth2.Session (
-- * A App-only flow (server-to-server)
-- * A Client Credentials Grant (i.e. server-to-server)
Token
, newNoToken
-- , newNoToken
, tokenUpdateLoop
, expireToken
, readToken
, fetchUpdateToken
-- , fetchUpdateToken
-- ** Default Azure Credential
, defaultAzureCredential
-- * B Auth code grant flow (interactive)
-- * B Auth Code Grant (i.e. with user auth in the loop)
-- ** OAuth endpoints
, loginEndpoint
, replyEndpoint
Expand Down Expand Up @@ -145,7 +146,7 @@ withAADUser ts loginURI act = aadHeaderIdToken $ \usub -> do



-- * App-only authorization scenarios (i.e via automation accounts. Human users not involved)
-- * App-only authorization scenarios, called "CLient credentials grant" https://oauth.net/2/grant-types/client-credentials/ (i.e via automation accounts. Human users not involved)



Expand All @@ -162,6 +163,26 @@ expireToken ts = atomically $ modifyTVar ts (const Nothing)
readToken :: MonadIO m => Token t -> m (Maybe t)
readToken ts = atomically $ readTVar ts

updateToken :: (MonadIO m) =>
Token OAuth2Token -> OAuth2Token -> m NominalDiffTime
updateToken ts oat = do
let
ein = fromIntegral $ fromMaybe 3600 (expiresIn oat) -- expires in [sec]
atomically $ do
writeTVar ts (Just oat)
pure ein

-- | Forks a thread and keeps the OAuth token up to date inside a TVar
tokenUpdateLoop :: MonadIO m =>
IdpApplication 'ClientCredentials AzureAD -- ^ client credentials grant only
-> Manager
-> m (Token OAuth2Token)
tokenUpdateLoop idp mgr = do
t <- newNoToken
fetchUpdateToken idp t mgr
pure t


fetchUpdateTokenWith :: MonadIO m =>
(t1 -> t2 -> ExceptT [String] IO OAuth2Token)
-> t1 -> Token OAuth2Token -> t2 -> m ()
Expand Down Expand Up @@ -207,15 +228,17 @@ tokenRequestNoExchange :: (MonadIO m) =>
-> ExceptT [String] m OAuth2Token
tokenRequestNoExchange ip mgr = withExceptT (pure . show) (conduitTokenRequest ip mgr)

-- | Fetch an OAuth token and keep it updated. Should be called as a first thing in the app
-- | Token refresh loop for Client Credentials Grant scenarios (Bot Framework auth etc)
--
-- Fetch an OAuth token and keep it updated. Should be called as a first thing in the app
--
-- NB : forks a thread in the background
--
-- https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow
fetchUpdateToken :: MonadIO m =>
IdpApplication 'ClientCredentials AzureAD
-> Token OAuth2Token -- ^ token TVar
-> Manager
-> Token OAuth2Token -- ^ the app manages a single token at a time
-> Manager -- ^ HTTP connection manager
-> m ()
fetchUpdateToken idpApp ts mgr = liftIO $ void $ forkFinally loop cleanup
where
Expand All @@ -233,14 +256,7 @@ fetchUpdateToken idpApp ts mgr = liftIO $ void $ forkFinally loop cleanup
threadDelay (dtSecs * 1000000) -- pause thread
loop

updateToken :: (MonadIO m) =>
Token OAuth2Token -> OAuth2Token -> m NominalDiffTime
updateToken ts oat = do
let
ein = fromIntegral $ fromMaybe 3600 (expiresIn oat) -- expires in [sec]
atomically $ do
writeTVar ts (Just oat)
pure ein




Expand Down Expand Up @@ -350,14 +366,16 @@ replyH idpApp ts mgr = do
-- bslToText = T.pack . BSL.unpack


-- | 1) the ExchangeToken arrives with the redirect once the user has approved the scopes in the browser
-- | Token refresh loop for Auth Code Grant scenarios
--
-- 1) the ExchangeToken arrives with the redirect once the user has approved the scopes in the browser
-- https://learn.microsoft.com/en-us/graph/auth-v2-user?view=graph-rest-1.0&tabs=http#authorization-response
fetchUpdateTokenACG :: MonadIO m =>
Tokens UserSub OAuth2Token
-> IdpApplication 'AuthorizationCode AzureAD
-> Manager
-> ExchangeToken -- ^ also called 'code'. Expires in 10 minutes
-> ExceptT OAuthSessionError m OAuth2Token
Tokens UserSub OAuth2Token -- ^ the app manages one token per user
-> IdpApplication 'AuthorizationCode AzureAD
-> Manager -- ^ HTTP connection manager
-> ExchangeToken -- ^ also called 'code'. Expires in 10 minutes
-> ExceptT OAuthSessionError m OAuth2Token
fetchUpdateTokenACG ts idpApp mgr etoken = ExceptT $ do
tokenResp <- runExceptT $ conduitTokenRequest idpApp mgr etoken -- OAuth2 token
case tokenResp of
Expand All @@ -373,34 +391,16 @@ fetchUpdateTokenACG ts idpApp mgr etoken = ExceptT $ do
Left es -> pure $ Left (OASEOAuth2Errors es)


-- -- -- for Bot Framework auth etc
-- fetchUpdateToken' :: MonadIO m =>
-- Tokens UserSub OAuth2Token
-- -> IdpApplication 'ClientCredentials i
-- -> Manager
-- -> ExceptT OAuthSessionError m OAuth2Token
-- fetchUpdateToken' ts idpApp mgr = ExceptT $ do
-- tokenResp <- runExceptT $ conduitTokenRequest idpApp mgr -- OAuth2 token
-- case tokenResp of
-- Right oat -> case idToken oat of
-- Nothing -> pure $ Left OASENoOpenID
-- Just idt -> do
-- idtClaimsE <- decValidIdToken idt -- decode and validate ID token
-- case idtClaimsE of
-- Right uid -> do
-- _ <- refreshLoopACG ts idpApp mgr uid oat -- fork a thread and start refresh loop for this user
-- pure $ Right oat



-- | 2) fork a thread and start token refresh loop for user @uid@
--
-- ACG stands for "authorization code grant" flow, i.e. the user consent is in the auth loop.
refreshLoopACG :: (MonadIO m, Ord uid, HasRefreshTokenRequest a) =>
Tokens uid OAuth2Token
-> IdpApplication a i
-> Manager
-> uid -- ^ user ID
-> OAuth2Token
-> m ThreadId
Tokens uid OAuth2Token
-> IdpApplication a i
-> Manager
-> uid -- ^ user ID
-> OAuth2Token
-> m ThreadId
refreshLoopACG ts idpApp mgr uid oaToken = liftIO $ forkFinally (act oaToken) cleanup
where
cleanup = \case
Expand Down
2 changes: 2 additions & 0 deletions ms-azure-api-test/Setup.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import Distribution.Simple
main = defaultMain
28 changes: 28 additions & 0 deletions ms-azure-api-test/app/Main.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{-# LANGUAGE DataKinds #-}
{-# language OverloadedStrings #-}
{-# language QuasiQuotes #-}
module Main (main) where

import Control.Monad.IO.Class (MonadIO(..))
-- hoauth2
import Network.OAuth.OAuth2 (OAuth2Token(..))
import Network.OAuth2.Experiment (IdpApplication, GrantTypeFlow(..))
-- unliftio
import UnliftIO (MonadUnliftIO(..))
import UnliftIO.Exception (throwIO)
import UnliftIO.STM (STM, TVar, atomically, newTVarIO, readTVar, writeTVar, modifyTVar)
-- uri-bytestring
import URI.ByteString.QQ (uri)

import Network.OAuth2.Provider.AzureAD (azureBotFrameworkADApp, AzureBotFramework)
import MSAuth (Token, tokenUpdateLoop, readToken, UserSub, Scotty, Action)
import MSAzureAPI (tryReq)

main :: IO ()
main = pure ()


-- also double check https://stackoverflow.com/a/63929994/2890063 in the AAD app manifest
idpApp :: MonadIO m => m (IdpApplication 'ClientCredentials AzureBotFramework)
idpApp = azureBotFrameworkADApp "ms-azure-bot-framework-api-test"

Loading

0 comments on commit 9774e2b

Please sign in to comment.