44{-# LANGUAGE TypeApplications #-}
55{-# LANGUAGE TypeOperators #-}
66
7+ {- |
8+ Module : Azure.Auth
9+ Description : Azure authentication for Haskell applications
10+
11+ This module provides authentication mechanisms for Azure services,
12+ primarily through Managed Identity.
13+
14+ = Basic Usage
15+
16+ @
17+ import Azure.Auth
18+
19+ main :: IO ()
20+ main = do
21+ cache <- newEmptyTokenCache
22+ token <- defaultAzureCredential Nothing keyVaultResource cache
23+ print token
24+ @
25+
26+ = Forcing Token Refresh
27+
28+ If a token has been revoked or you need to force re-authentication:
29+
30+ @
31+ invalidateTokenCache cache
32+ newToken <- defaultAzureCredential Nothing keyVaultResource cache
33+ @
34+ -}
735module Azure.Auth
8- ( defaultAzureCredential
36+ ( -- * Credential Functions
37+ defaultAzureCredential
938 , withManagedIdentity
1039 , withManagedIdentityEither
40+
41+ -- * Exceptions
42+ , AccessTokenException (.. )
43+
44+ -- * Common Resource URIs
45+ , keyVaultResource
46+ , storageResource
47+ , managementResource
48+
49+ -- * Re-exports from Azure.Types
50+ , AccessToken (.. )
51+ , TokenType (.. )
52+ , ResourceUri (.. )
53+ , TokenCache
54+ , newEmptyTokenCache
55+ , invalidateTokenCache
56+ , readTokenCache
1157 ) where
1258
1359import Control.Exception (Exception )
1460import Data.Data (Proxy (.. ))
1561import Data.Text (Text )
1662import Data.Typeable (Typeable )
17- import Network.HTTP.Client (defaultManagerSettings , newManager )
63+ import Network.HTTP.Client (Manager , defaultManagerSettings , newManager )
1864import Servant.API (Get , Header' , JSON , Optional , QueryParam' , Required , Strict , (:>) )
1965import Servant.Client (BaseUrl (.. ), ClientM , Scheme (.. ), client , mkClientEnv , runClientM )
2066import UnliftIO (MonadIO (.. ), throwIO )
2167import UnliftIO.Environment (lookupEnv )
2268
23- import Azure.Types (AccessToken (.. ), Token , readToken , updateToken )
69+ import Azure.Types
70+ ( AccessToken (.. )
71+ , ResourceUri (.. )
72+ , TokenCache
73+ , TokenType (.. )
74+ , invalidateTokenCache
75+ , newEmptyTokenCache
76+ , readTokenCache
77+ , updateTokenCache
78+ )
2479import Azure.Utils (isExpired )
2580
2681import qualified Data.Text as Text
2782
28- {- | IMDS is a REST API that's available at a well-known, non-routable IP address ( 169.254. 169.254 ).
29- It is a local-only link can only be accessed from within the VM.
83+ -- | Resource URI for Azure Key Vault.
84+ keyVaultResource :: ResourceUri
85+ keyVaultResource = ResourceUri " https://vault.azure.net"
86+
87+ -- | Resource URI for Azure Blob Storage.
88+ storageResource :: ResourceUri
89+ storageResource = ResourceUri " https://storage.azure.com"
90+
91+ -- | Resource URI for Azure Resource Manager.
92+ managementResource :: ResourceUri
93+ managementResource = ResourceUri " https://management.azure.com"
94+
95+ {- | IMDS is a REST API that's available at a well-known, non-routable IP address (169.254.169.254).
96+ It is a local-only link that can only be accessed from within the VM.
3097Communication between the VM and IMDS never leaves the host.
3198-}
3299imdsHost :: String
@@ -37,6 +104,8 @@ imdsApiVersion = "2021-02-01"
37104
38105{- | Provides a default @TokenCredential@ authentication flow for applications that will be deployed to Azure.
39106
107+ Currently only Managed Identity authentication is implemented.
108+
40109TODO: Implement other auth flows such as @withAzureCli@ and @withEnvironment@ and then apply
41110 alternative instance to @defaultAzureCredential@
42111 It should be of the form:
@@ -52,90 +121,106 @@ TODO: Implement other auth flows such as @withAzureCli@ and @withEnvironment@ an
52121-}
53122defaultAzureCredential ::
54123 MonadIO m =>
55- -- | Client ID
124+ -- | Client ID (optional, for user-assigned managed identity)
56125 Maybe Text ->
57- -- | Azure Resource URI (required for @managed identity@)
58- Text ->
59- -- | Token (if empty, then a new one is fetched and stored into the token TVar)
60- Token ->
126+ -- | Azure Resource URI to get a token for
127+ ResourceUri ->
128+ -- | Token cache for storing and reusing tokens
129+ TokenCache ->
61130 m AccessToken
62- defaultAzureCredential = withManagedIdentity
131+ defaultAzureCredential = withManagedIdentity Nothing
132+
133+ {- | Fetches an Access token using Azure Managed Identity.
63134
64- {- | Fetches an Access token for autheticating different azure services
65135All errors are thrown in IO.
66136
67- For version where errors are returned in a @Left@ branch, use @ withManagedIdentityEither@
137+ For a version where errors are returned in a @Left@ branch, use ' withManagedIdentityEither'.
68138-}
69139withManagedIdentity ::
70140 MonadIO m =>
71- -- | ClientId
141+ -- | Optional HTTP Manager (a new one is created if not provided)
142+ Maybe Manager ->
143+ -- | Client ID (optional, for user-assigned managed identity)
72144 Maybe Text ->
73145 -- | Resource URI
74- Text ->
75- -- | Access Token
76- Token ->
146+ ResourceUri ->
147+ -- | Token cache
148+ TokenCache ->
77149 m AccessToken
78- withManagedIdentity clientId resourceUri tokenStore = do
79- token <- withManagedIdentityEither clientId resourceUri tokenStore
150+ withManagedIdentity mgr clientId resourceUri tokenCache = do
151+ token <- withManagedIdentityEither mgr clientId resourceUri tokenCache
80152 case token of
81153 Left err -> throwIO err
82154 Right tok -> pure tok
83155
156+ {- | Fetches an Access token using Azure Managed Identity.
157+
158+ Returns errors in an @Either@ instead of throwing.
159+ -}
84160withManagedIdentityEither ::
85161 MonadIO m =>
86- -- | ClientId
162+ -- | Optional HTTP Manager (a new one is created if not provided)
163+ Maybe Manager ->
164+ -- | Client ID (optional, for user-assigned managed identity)
87165 Maybe Text ->
88166 -- | Resource URI
89- Text ->
90- -- | Access Token
91- Token ->
167+ ResourceUri ->
168+ -- | Token cache
169+ TokenCache ->
92170 m (Either AccessTokenException AccessToken )
93- withManagedIdentityEither clientId resourceUri tokenStore = do
171+ withManagedIdentityEither mManager clientId resourceUri tokenCache = do
94172 identityEndpoint <- lookupEnv " IDENTITY_ENDPOINT"
95173 identityHeader <- lookupEnv " IDENTITY_HEADER"
96- case (,) <$> identityEndpoint <*> identityEndpoint of
174+ case (,) <$> identityEndpoint <*> identityHeader of
97175 -- TODO: incorporate @IDENTITY_ENDPOINT@ into this logic
98176 -- If it's present, we can directly make a call to
99- -- to it and retrieve the access token.
100- -- This functionality is only available on App service and not standalone
177+ -- it and retrieve the access token.
178+ -- This functionality is only available on App Service and not standalone
101179 -- VM instances.
102180 Just (_endpoint, _header) ->
103- pure . Left $ TokenEndpointNotAvailable " Fetching Access token on an app service is not yet supported"
104- -- We do not have the @IDENTITY_ENDPOINT@. Which means that that
105- -- the VM is possibly standalone and not inside an app service .
181+ pure . Left $ TokenEndpointNotAvailable " Fetching Access token on an App Service is not yet supported"
182+ -- We do not have the @IDENTITY_ENDPOINT@. Which means that
183+ -- the VM is possibly standalone and not inside an App Service .
106184 -- Therefore, in order to get the access token details, we need
107185 -- to make GET request to Azure Instance Metadata Service.
108186 -- But first, check for an existing token
109187 Nothing -> do
110- tk <- readToken tokenStore
188+ tk <- readTokenCache tokenCache
111189 case tk of
112190 -- In case there is no existing token, we fetch a new one
113- Nothing -> do
114- newToken <- callAzureIMDSEndpoint getAzureIMDSClient resourceUri clientId (Text. pack <$> identityHeader)
115- case newToken of
116- Left err -> pure . Left . TokenClientMismatch . Text. pack $ show err
117- Right tok -> do
118- updateToken tokenStore (Just tok)
119- pure $ Right tok
191+ Nothing ->
192+ fetchAndCacheToken mManager clientId resourceUri tokenCache (Text. pack <$> identityHeader)
120193 Just oldToken@ AccessToken {atExpiresOn} -> do
121- -- we do have a token but we should check for it's validity
194+ -- we do have a token but we should check for its validity
122195 isTokenExpired <- isExpired atExpiresOn
123196 if isTokenExpired
124- then do
125- -- get a new token and write to the env
126- newToken <- callAzureIMDSEndpoint getAzureIMDSClient resourceUri clientId (Text. pack <$> identityHeader)
127- case newToken of
128- Left err -> pure . Left . TokenClientMismatch . Text. pack $ show err
129- Right tok -> do
130- updateToken tokenStore (Just tok)
131- pure $ Right tok
197+ then fetchAndCacheToken mManager clientId resourceUri tokenCache (Text. pack <$> identityHeader)
132198 else pure $ Right oldToken
133199
134- -- | An exception that can occur when generating an @AccessToken@
200+ -- | Internal helper to fetch a new token and update the cache.
201+ fetchAndCacheToken ::
202+ MonadIO m =>
203+ Maybe Manager ->
204+ Maybe Text ->
205+ ResourceUri ->
206+ TokenCache ->
207+ Maybe Text ->
208+ m (Either AccessTokenException AccessToken )
209+ fetchAndCacheToken mManager clientId resourceUri tokenCache identityHeader = do
210+ newToken <- callAzureIMDSEndpoint mManager getAzureIMDSClient resourceUri clientId identityHeader
211+ case newToken of
212+ Left err -> pure . Left . TokenFetchFailed $ err
213+ Right tok -> do
214+ updateTokenCache tokenCache (Just tok)
215+ pure $ Right tok
216+
217+ -- | An exception that can occur when generating an 'AccessToken'.
135218data AccessTokenException
136- = TokenEndpointNotAvailable Text
137- | TokenClientMismatch Text -- TODO: The type is misleading. This is a generic error from servant client
138- deriving stock (Show , Typeable )
219+ = -- | The App Service token endpoint is not yet supported.
220+ TokenEndpointNotAvailable Text
221+ | -- | Failed to fetch token from IMDS.
222+ TokenFetchFailed Text
223+ deriving stock (Eq , Show , Typeable )
139224
140225instance Exception AccessTokenException
141226
@@ -162,13 +247,14 @@ getAzureIMDSClient = client (Proxy @AzureIMDSEndpoint)
162247
163248callAzureIMDSEndpoint ::
164249 MonadIO m =>
250+ Maybe Manager ->
165251 (Text -> Text -> Maybe Text -> Maybe Text -> Bool -> ClientM AccessToken ) ->
166- Text ->
252+ ResourceUri ->
167253 Maybe Text ->
168254 Maybe Text ->
169255 m (Either Text AccessToken )
170- callAzureIMDSEndpoint action resourceUri clientId identityHeader = do
171- manager <- liftIO $ newManager defaultManagerSettings
256+ callAzureIMDSEndpoint mManager action ( ResourceUri resourceUri) clientId identityHeader = do
257+ manager <- maybe ( liftIO $ newManager defaultManagerSettings) pure mManager
172258 res <-
173259 liftIO $
174260 runClientM
0 commit comments