packages feed

ms-auth 0.3.0.0 → 0.4.0.0

raw patch · 6 files changed

+114/−116 lines, 6 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- MSAuth: applyDotEnv :: MonadIO m => Maybe FilePath -> m ()
- MSAuth: fetchUpdateToken :: MonadIO m => IdpApplication 'ClientCredentials AzureAD -> Token OAuth2Token -> Manager -> m ()
- MSAuth: newNoToken :: MonadIO m => m (Token t)
- Network.OAuth2.Session: data UserSub
- Network.OAuth2.Session: defaultAzureCredential :: MonadIO m => String -> String -> IdpApplication 'ClientCredentials AzureAD -> Token OAuth2Token -> Manager -> m ()
- Network.OAuth2.Session: expireToken :: MonadIO m => Token t -> m ()
- Network.OAuth2.Session: expireUser :: (MonadIO m, Ord uid) => Tokens uid t -> uid -> m ()
- Network.OAuth2.Session: fetchUpdateToken :: MonadIO m => IdpApplication 'ClientCredentials AzureAD -> Token OAuth2Token -> Manager -> m ()
- Network.OAuth2.Session: instance (GHC.Classes.Eq uid, GHC.Classes.Eq t) => GHC.Classes.Eq (Network.OAuth2.Session.TokensData uid t)
- Network.OAuth2.Session: instance (GHC.Show.Show uid, GHC.Show.Show t) => GHC.Show.Show (Network.OAuth2.Session.TokensData uid t)
- Network.OAuth2.Session: instance GHC.Classes.Eq Network.OAuth2.Session.OAuthSessionError
- Network.OAuth2.Session: instance GHC.Exception.Type.Exception Network.OAuth2.Session.OAuthSessionError
- Network.OAuth2.Session: instance GHC.Show.Show Network.OAuth2.Session.OAuthSessionError
- Network.OAuth2.Session: loginEndpoint :: MonadIO m => IdpApplication 'AuthorizationCode AzureAD -> RoutePattern -> Scotty m ()
- Network.OAuth2.Session: lookupUser :: (MonadIO m, Ord uid) => Tokens uid t -> uid -> m (Maybe t)
- Network.OAuth2.Session: newNoToken :: MonadIO m => m (Token t)
- Network.OAuth2.Session: newTokens :: (MonadIO m, Ord uid) => m (Tokens uid t)
- Network.OAuth2.Session: readToken :: MonadIO m => Token t -> m (Maybe t)
- Network.OAuth2.Session: replyEndpoint :: MonadIO m => IdpApplication 'AuthorizationCode AzureAD -> Tokens UserSub OAuth2Token -> Manager -> RoutePattern -> Scotty m ()
- Network.OAuth2.Session: tokensToList :: MonadIO m => Tokens k a -> m [(k, a)]
- Network.OAuth2.Session: type Action = ActionT Text
- Network.OAuth2.Session: type Scotty = ScottyT Text
- Network.OAuth2.Session: type Token t = TVar (Maybe t)
- Network.OAuth2.Session: type Tokens uid t = TVar (TokensData uid t)
- Network.OAuth2.Session: withAADUser :: MonadIO m => Tokens UserSub t -> Text -> (t -> Action m ()) -> Action m ()
+ MSAuth: tokenUpdateLoop :: MonadIO m => IdpApplication 'ClientCredentials AzureAD -> Manager -> m (Token OAuth2Token)
+ Network.OAuth2.Provider.AzureAD: azureBotFrameworkADApp :: MonadIO m => Text -> m (IdpApplication 'ClientCredentials AzureAD)

Files

CHANGELOG.md view
@@ -8,6 +8,16 @@  ## 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@@ -15,7 +25,6 @@ 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
ms-auth.cabal view
@@ -1,5 +1,5 @@ name:                ms-auth-version:             0.3.0.0+version:             0.4.0.0 synopsis:            Microsoft Authentication API description:         Bindings to the Microsoft Identity API / Active Directory (AD) for building applications that use either Authorization Code (User-facing) or (App-only) authorization flows. Helper functions are provided for building OAuth2 authentication flows and keep tokens transactionally secure and up to date. homepage:            https://github.com/unfoldml/ms-graph-api@@ -19,10 +19,9 @@   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
− src/DotEnv.hs
@@ -1,67 +0,0 @@-module DotEnv (applyDotEnv) where--import Control.Monad.IO.Class (MonadIO(..))-import Data.Foldable (traverse_)-import Data.Functor (void)-import Data.List (sortOn)-import Data.Maybe (listToMaybe, fromMaybe)-import Data.Ord (Down(..))-import System.Environment (setEnv)-import qualified Text.ParserCombinators.ReadP as P (ReadP, readP_to_S, char, munch, sepBy1)---- directory-import System.Directory (doesFileExist)---- | Load, parse and apply a @.env@ file------ NB : overwrites any preexisting env vars------ NB2 : if the @.env@ file is not found the program continues (i.e. this function is a no-op in that case)-applyDotEnv :: MonadIO m =>-               Maybe FilePath -- ^ defaults to @.env@ if Nothing-            -> m ()-applyDotEnv mfp = liftIO $ do-  let-    fpath = fromMaybe ".env" mfp-  ok <- doesFileExist fpath-  if ok-    then-    do-      mp <- parseDotEnv <$> readFile fpath-      case mp of-        Just es -> setEnvs es-        Nothing -> putStrLn $ unwords ["dotenv: cannot parse", fpath]-    else-    do-      putStrLn $ unwords ["dotenv:", fpath, "not found"]--setEnvs :: MonadIO m => [(String, String)] -> m ()-setEnvs = traverse_ insf-  where-    insf (k, v) = liftIO $ do-      setEnv k v-      putStrLn $ unwords ["dotenv: set", k] -- DEBUG--parseDotEnv :: String -- ^ contents of the @.env@ file-            -> Maybe [(String, String)]-parseDotEnv = parse1 keyValues--keyValues :: P.ReadP [(String, String)]-keyValues = P.sepBy1 keyValue (P.char '\n') <* P.char '\n'--keyValue :: P.ReadP (String, String)-keyValue = do-  k <- keyP-  void $ P.char '='-  v <- valueP-  pure (k, v)--keyP, valueP :: P.ReadP String-keyP = P.munch (/= '=')-valueP = P.munch (/= '\n')---- parse :: P.ReadP b -> String -> Maybe b--- parse p str = fst <$> (listToMaybe $ P.readP_to_S p str)--parse1 :: Foldable t => P.ReadP (t a) -> String -> Maybe (t a)-parse1 p str = fmap fst $ listToMaybe $ sortOn (Down . length . fst) (P.readP_to_S p str)
src/MSAuth.hs view
@@ -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)@@ -27,7 +25,6 @@   , withAADUser   , Scotty   , Action-              ) where+  ) where  import Network.OAuth2.Session-import DotEnv (applyDotEnv)
src/Network/OAuth2/Provider/AzureAD.hs view
@@ -7,16 +7,19 @@ {-# 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.4 module Network.OAuth2.Provider.AzureAD (     AzureAD     -- * Environment variables     , envClientId     , envClientSecret     , envTenantId-    -- * App flow+    -- * Client Credentials auth flow     , azureADApp-    -- * Delegated permissions OAuth2 flow+    , azureBotFrameworkADApp+    -- * Auth Code Grant auth flow     , OAuthCfg(..)     , AzureADUser     , azureOAuthADApp@@ -75,9 +78,9 @@   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 --@@ -113,9 +116,43 @@     , 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 AzureAD)+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+                                         } --- * Delegated permissions flow +-- data AzureBotFramework = AzureBotFramework deriving (Eq, Show)++defaultAzureBotFrameworkIdp :: Idp AzureAD+defaultAzureBotFrameworkIdp = Idp {+  idpFetchUserInfo = authGetJSON @(IdpUserInfo AzureAD)+  , 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"]+                                  }++++++++-- * Authorization Code Grant flow+ type instance IdpUserInfo AzureAD = AzureADUser  -- | Configuration object of the OAuth2 application@@ -163,7 +200,7 @@     , idpAppAuthorizeState = "CHANGE_ME" -- https://stackoverflow.com/questions/26132066/what-is-the-purpose-of-the-state-parameter-in-oauth-authorization-request     , idpAppAuthorizeExtraParams = Map.empty     , idpAppRedirectUri = [uri|http://localhost|] ---    , idpAppName = "default-azure-app" --+    , idpAppName = "" --     , idpAppTokenRequestAuthenticationMethod = ClientSecretBasic     , idp = defaultAzureADIdp     }@@ -177,6 +214,8 @@     , idpAuthorizeEndpoint = [uri|https://login.microsoftonline.com/common/oauth2/v2.0/authorize|]     , idpTokenEndpoint = [uri|https://login.microsoftonline.com/common/oauth2/v2.0/token|]     }++  -- | https://learn.microsoft.com/en-us/azure/active-directory/develop/userinfo data AzureADUser = AzureADUser
src/Network/OAuth2/Session.hs view
@@ -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@@ -145,7 +146,7 @@   --- * 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)   @@ -162,6 +163,26 @@ 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 ()@@ -207,15 +228,17 @@                        -> 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@@ -233,17 +256,10 @@           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   + -- * Managed identity  -- | With its managed identity, an app can obtain tokens for Azure resources that are protected by Azure Active Directory, such as Azure SQL Database, Azure Key Vault, and Azure Storage. These tokens represent the application accessing the resource, and not any specific user of the application.@@ -350,14 +366,16 @@ -- 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@@ -372,14 +390,17 @@           Left es -> pure $ Left (OASEJWTException es) -- id token validation failed     Left es -> pure $ Left (OASEOAuth2Errors es) + -- | 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