Skip to content

Commit 9be1f05

Browse files
Refinements to auth API + Bug fixes
1 parent 21b78f4 commit 9be1f05

8 files changed

Lines changed: 283 additions & 137 deletions

File tree

azure-auth/Azure/Auth.hs

Lines changed: 140 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,96 @@
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+
-}
735
module 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

1359
import Control.Exception (Exception)
1460
import Data.Data (Proxy (..))
1561
import Data.Text (Text)
1662
import Data.Typeable (Typeable)
17-
import Network.HTTP.Client (defaultManagerSettings, newManager)
63+
import Network.HTTP.Client (Manager, defaultManagerSettings, newManager)
1864
import Servant.API (Get, Header', JSON, Optional, QueryParam', Required, Strict, (:>))
1965
import Servant.Client (BaseUrl (..), ClientM, Scheme (..), client, mkClientEnv, runClientM)
2066
import UnliftIO (MonadIO (..), throwIO)
2167
import 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+
)
2479
import Azure.Utils (isExpired)
2580

2681
import 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.
3097
Communication between the VM and IMDS never leaves the host.
3198
-}
3299
imdsHost :: 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+
40109
TODO: 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
-}
53122
defaultAzureCredential ::
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
65135
All 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
-}
69139
withManagedIdentity ::
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+
-}
84160
withManagedIdentityEither ::
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'.
135218
data 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

140225
instance Exception AccessTokenException
141226

@@ -162,13 +247,14 @@ getAzureIMDSClient = client (Proxy @AzureIMDSEndpoint)
162247

163248
callAzureIMDSEndpoint ::
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

Comments
 (0)