Skip to content

Commit

Permalink
docs
Browse files Browse the repository at this point in the history
  • Loading branch information
Marco Zocca committed Jun 19, 2023
1 parent 5b260b0 commit 11fce44
Show file tree
Hide file tree
Showing 4 changed files with 46 additions and 25 deletions.
2 changes: 1 addition & 1 deletion ms-auth/ms-auth.cabal
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: ms-auth
version: 0.1.0.0
synopsis: Microsoft Authentication API
description: Bindings to the Microsoft Authentication API / Azure ActiveDirectory (AAD) for building applications that use either Delegated or App-only permissions. Helper functions are provided for building OAuth2 authentication flows and keep tokens transactionally secure and up to date.
description: Bindings to the Microsoft Identity API / Active Directory (AD) for building applications that use either Delegated or App-only permissions. Helper functions are provided for building OAuth2 authentication flows and keep tokens transactionally secure and up to date.
homepage: https://github.com/unfoldml/ms-auth
license: BSD3
license-file: LICENSE
Expand Down
32 changes: 22 additions & 10 deletions ms-auth/src/Network/OAuth2/JWT.hs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,18 @@
{-# language GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# language OverloadedStrings #-}
module Network.OAuth2.JWT where
{-# options_ghc -Wno-unused-top-binds #-}
-- | Decode and validate a JWT token
--
-- provides 'Validation' function for the individual fields as well
module Network.OAuth2.JWT (
-- * 1) Decode a string into claims
jwtClaims
-- * 2) Extract and validate the individual claims
, decValidSub, decValidExp, decValidNbf, decValidEmail, decValidAud
, UserSub, userSub, UserEmail, userEmail, ApiAudience, apiAudience
, JWTException(..)
) where

import Control.Monad.IO.Class (MonadIO(..))
import qualified Data.List.NonEmpty as NE (NonEmpty(..))
Expand All @@ -27,7 +38,7 @@ import qualified Data.Text as T (Text, unpack)
-- time
import Data.Time (UTCTime(..), NominalDiffTime, getCurrentTime, fromGregorian, addUTCTime, diffUTCTime)
-- validation-micro
import Validation.Micro (Validation(..), failure, validationToEither, maybeToSuccess)
import Validation.Micro (Validation(..), bindValidation, failure, validationToEither, maybeToSuccess)


-- | 'sub' field
Expand All @@ -43,6 +54,7 @@ newtype UserEmail = UserEmail { userEmail :: T.Text }
newtype ApiAudience = ApiAudience { apiAudience :: T.Text } deriving (Eq, Ord, Show, Generic, Typeable, IsString)
instance A.ToJSON ApiAudience

-- | Decode a string into a 'J.JWTClaimsSet'
jwtClaims :: T.Text -> Maybe J.JWTClaimsSet
jwtClaims t = J.claims <$> J.decode t

Expand All @@ -58,30 +70,30 @@ data JWTClaims =
, jcEmail :: UserEmail
} deriving (Eq, Show)

-- | @sub@
decValidSub :: J.JWTClaimsSet -> Validation (NE.NonEmpty JWTException) UserSub
decValidSub jc = decSub (J.sub jc)

-- | @exp@
decValidExp :: Maybe NominalDiffTime
-> UTCTime
-> UTCTime -- ^ current time
-> J.JWTClaimsSet
-> Validation (NE.NonEmpty JWTException) UTCTime
decValidExp nsecs t jc = decExp (J.exp jc) `bindValidation` validateExp nsecs t

decValidNbf :: UTCTime -> J.JWTClaimsSet -> Validation (NE.NonEmpty JWTException) UTCTime
-- | @nbf@
decValidNbf :: UTCTime -- ^ current time
-> J.JWTClaimsSet -> Validation (NE.NonEmpty JWTException) UTCTime
decValidNbf t jc = decNbf (J.nbf jc) `bindValidation` validateNbf t

-- | @email@
decValidEmail :: J.JWTClaimsSet -> Validation (NE.NonEmpty JWTException) UserEmail
decValidEmail jc = decEmail (J.unClaimsMap $ J.unregisteredClaims jc)

-- | @aud@
decValidAud :: ApiAudience -> J.JWTClaimsSet -> Validation (NE.NonEmpty JWTException) T.Text
decValidAud a jc = decAud (J.aud jc) `bindValidation` validateAud a

-- | NB Validation is not a monad though
bindValidation :: Validation e a -> (a -> Validation e b) -> Validation e b
bindValidation v f = case v of
Failure e -> Failure e
Success a -> f a


-- | Decode and validate the 'aud', 'exp' and 'nbf' fields of the JWT
decodeValidateJWT :: MonadIO f =>
Expand Down
31 changes: 20 additions & 11 deletions ms-auth/src/Network/OAuth2/Provider/AzureAD.hs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@
{-# language DataKinds, TypeFamilies, TypeApplications #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# options_ghc -Wno-ambiguous-fields #-}
-- | Settings for using Azure Active Directory as OAuth identity provider
--
-- Both @Delegated@ (On-Behalf-Of) 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.
module Network.OAuth2.Provider.AzureAD (
AzureAD
-- * App flow
, azureADApp
-- * OAuth2 flow
, OAuthCfg(..)
, AzureADUser
, azureOAuthADApp
) where
-- * Delegated permissions OAuth2 flow
, OAuthCfg(..)
, AzureADUser
, azureOAuthADApp
) where

-- import Data.String (IsString(..))
-- import GHC.Generics
Expand All @@ -38,11 +41,18 @@ data AzureAD = AzureAD deriving (Eq, Show)

-- * App-only flow

-- | create app at https://go.microsoft.com/fwlink/?linkid=2083908
-- | Azure OAuth application (i.e. with user consent screen)
--
-- NB : scope @offline_access@ is ALWAYS requested
--
-- create app at https://go.microsoft.com/fwlink/?linkid=2083908
--
-- also be aware to find the right client id.
-- see https://stackoverflow.com/a/70670961
azureADApp :: TL.Text -> ClientId -> ClientSecret -> [Scope] -> IdpApplication 'ClientCredentials AzureAD
azureADApp :: TL.Text -- ^ application name
-> ClientId -> ClientSecret
-> [Scope] -- ^ scopes
-> IdpApplication 'ClientCredentials AzureAD
azureADApp appname clid sec scopes = defaultAzureADApp{
idpAppName = appname
, idpAppClientId = clid
Expand All @@ -55,15 +65,14 @@ defaultAzureADApp =
ClientCredentialsIDPAppConfig
{ idpAppClientId = ""
, idpAppClientSecret = ""
, idpAppScope = Set.fromList ["openid", "offline_access", "profile", "email"] -- https://learn.microsoft.com/EN-US/azure/active-directory/develop/scopes-oidc#openid-connect-scopes
, idpAppScope = Set.fromList ["offline_access"] -- https://learn.microsoft.com/EN-US/azure/active-directory/develop/scopes-oidc#openid-connect-scopes
, idpAppTokenRequestExtraParams = Map.empty
, idpAppName = "default-azure-app" --
, idp = defaultAzureADIdp
}


-- * OAuth flow

-- * Delegated permissions flow

type instance IdpUserInfo AzureAD = AzureADUser

Expand All @@ -83,7 +92,7 @@ data OAuthCfg = OAuthCfg {
--
-- Reference on Microsoft Graph permissions : https://learn.microsoft.com/en-us/graph/permissions-reference
--
-- | create app at https://go.microsoft.com/fwlink/?linkid=2083908
-- create app at https://go.microsoft.com/fwlink/?linkid=2083908
--
-- also be aware to find the right client id.
-- see https://stackoverflow.com/a/70670961
Expand Down
6 changes: 3 additions & 3 deletions ms-auth/src/Network/OAuth2/Session.hs
Original file line number Diff line number Diff line change
Expand Up @@ -149,11 +149,11 @@ readToken ts = atomically $ readTVar ts
--
-- https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow
fetchUpdateToken :: MonadIO m =>
Token OAuth2Token
-> IdpApplication 'ClientCredentials AzureAD
IdpApplication 'ClientCredentials AzureAD
-> Token OAuth2Token
-> Manager
-> m ()
fetchUpdateToken ts idpApp mgr = liftIO $ void $ forkFinally loop cleanup
fetchUpdateToken idpApp ts mgr = liftIO $ void $ forkFinally loop cleanup
where
cleanup = \case
Left e -> throwIO e
Expand Down

0 comments on commit 11fce44

Please sign in to comment.