Skip to content

Commit 7b89370

Browse files
authored
StorageBearerTokenChallengeAuthorizationPolicy now supports CAE (Azure#48974)
1 parent ce26920 commit 7b89370

File tree

3 files changed

+131
-15
lines changed

3 files changed

+131
-15
lines changed

sdk/storage/Azure.Storage.Common/CHANGELOG.md

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## 12.24.0-beta.1 (Unreleased)
44

55
### Features Added
6+
- `StorageBearerTokenChallengeAuthorizationPolicy` now will attempt to handle Continuous Access Evaluation (CAE) challenges, if present, by default.
67

78
### Breaking Changes
89

@@ -214,55 +215,55 @@
214215
## 12.5.0-preview.5 (2020-07-03)
215216
- This release contains bug fixes to improve quality.
216217

217-
## 12.5.0-preview.4
218+
## 12.5.0-preview.4
218219
- This preview contains bug fixes to improve quality.
219220

220-
## 12.5.0-preview.1
221+
## 12.5.0-preview.1
221222
- This preview adds support for client-side encryption, compatible with data uploaded in previous major versions.
222223

223-
## 12.4.3
224+
## 12.4.3
224225
- This release contains bug fixes to improve quality.
225226

226-
## 12.4.2
227+
## 12.4.2
227228
- This release contains bug fixes to improve quality.
228229

229-
## 12.4.1
230+
## 12.4.1
230231
- This release contains bug fixes to improve quality.
231232

232-
## 12.4.0
233+
## 12.4.0
233234
- This release contains bug fixes to improve quality.
234235

235-
## 12.3.0
236+
## 12.3.0
236237
- Added InitialTransferLength to StorageTransferOptions
237238

238-
## 12.2.0
239+
## 12.2.0
239240
- Added support for service version 2019-07-07.
240241
- Update StorageSharedKeyPipelinePolicy to upload the request date header each retry.
241242
- Sanitized header values in exceptions.
242243

243-
## 12.1.1
244+
## 12.1.1
244245
- Fixed issue where SAS content headers were not URL encoded when using Sas builders.
245246
- Fixed bug where using SAS connection string from portal would throw an exception if it included
246247
table endpoint.
247248

248-
## 12.1.0
249+
## 12.1.0
249250
- Add support for populating AccountName properties of the UriBuilders
250251
for non-IP style Uris.
251252

252-
## 12.0.0-preview.4
253+
## 12.0.0-preview.4
253254
- Bug fixes
254255

255-
## 12.0.0-preview.3
256+
## 12.0.0-preview.3
256257
- Support new for Blobs/Files features
257258
- Bug fixes
258259

259260
For more information, please visit: https://aka.ms/azure-sdk-preview3-net.
260261

261-
## 12.0.0-preview.2
262+
## 12.0.0-preview.2
262263
- Credential rolling
263264
- Bug fixes
264265

265-
## 12.0.0-preview.1
266+
## 12.0.0-preview.1
266267
This preview is the first release of a ground-up rewrite of our client
267268
libraries to ensure consistency, idiomatic design, productivity, and an
268269
excellent developer experience. It was created following the Azure SDK Design

sdk/storage/Azure.Storage.Common/src/Shared/StorageBearerTokenChallengeAuthorizationPolicy.cs

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Linq;
7+
using System.Text;
78
using System.Threading.Tasks;
89
using Azure.Core;
910
using Azure.Core.Pipeline;
@@ -60,7 +61,7 @@ private async ValueTask AuthorizeRequestInternal(HttpMessage message, bool async
6061
{
6162
if (tenantId != null || !_enableTenantDiscovery)
6263
{
63-
TokenRequestContext context = new TokenRequestContext(_scopes, message.Request.ClientRequestId, tenantId: tenantId);
64+
TokenRequestContext context = new TokenRequestContext(_scopes, message.Request.ClientRequestId, tenantId: tenantId, isCaeEnabled: true);
6465
if (async)
6566
{
6667
await base.AuthenticateAndAuthorizeRequestAsync(message, context).ConfigureAwait(false);
@@ -82,6 +83,23 @@ private async ValueTask<bool> AuthorizeRequestOnChallengeInternalAsync(HttpMessa
8283
{
8384
try
8485
{
86+
if (AuthorizationChallengeParser.IsCaeClaimsChallenge(message.Response))
87+
{
88+
if (TryGetTokenRequestContextForCaeChallenge(message, out var tokenRequestContext))
89+
{
90+
if (async)
91+
{
92+
await AuthenticateAndAuthorizeRequestAsync(message, tokenRequestContext).ConfigureAwait(false);
93+
}
94+
else
95+
{
96+
AuthenticateAndAuthorizeRequest(message, tokenRequestContext);
97+
}
98+
return true;
99+
}
100+
return false;
101+
}
102+
85103
var authUri = AuthorizationChallengeParser.GetChallengeParameterFromResponse(message.Response, "Bearer", "authorization_uri");
86104

87105
// tenantId should be the guid as seen in this example: https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47/oauth2/authorize
@@ -110,5 +128,32 @@ private async ValueTask<bool> AuthorizeRequestOnChallengeInternalAsync(HttpMessa
110128
return default;
111129
}
112130
}
131+
132+
internal bool TryGetTokenRequestContextForCaeChallenge(HttpMessage message, out TokenRequestContext tokenRequestContext)
133+
{
134+
string decodedClaims = null;
135+
string encodedClaims = AuthorizationChallengeParser.GetChallengeParameterFromResponse(message.Response, "Bearer", "claims");
136+
try
137+
{
138+
decodedClaims = encodedClaims switch
139+
{
140+
null => null,
141+
{ Length: 0 } => null,
142+
string enc => Encoding.UTF8.GetString(Convert.FromBase64String(enc))
143+
};
144+
}
145+
catch
146+
{
147+
// The claims are not in the expected format. This is not a CAE challenge.
148+
}
149+
if (decodedClaims == null)
150+
{
151+
tokenRequestContext = default;
152+
return false;
153+
}
154+
155+
tokenRequestContext = new TokenRequestContext(_scopes, message.Request.ClientRequestId, decodedClaims, isCaeEnabled: true);
156+
return true;
157+
}
113158
}
114159
}

sdk/storage/Azure.Storage.Common/tests/StorageBearerTokenChallengeAuthorizationPolicyTests.cs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
// Licensed under the MIT License.
33

44
using System;
5+
using System.Collections.Generic;
56
using System.Linq;
67
using System.Net;
78
using System.Threading;
89
using System.Threading.Tasks;
910
using Azure.Core;
11+
using Azure.Core.Diagnostics;
1012
using Azure.Core.TestFramework;
1113
using Azure.Storage.Shared;
1214
using Moq;
@@ -147,5 +149,73 @@ public async Task UsesScopeFromBearerChallange()
147149
await SendGetRequest(transport, tokenChallengeAuthorizationPolicy, uri: new Uri("https://example.com"));
148150
await SendGetRequest(transport, tokenChallengeAuthorizationPolicy, uri: new Uri("https://example.com"));
149151
}
152+
153+
[Test]
154+
[TestCaseSource(nameof(CaeTestDetails))]
155+
public async Task StorageBearerTokenChallengeAuthorizationPolicy_CAE_TokenRevocation(string description, string challenge, int expectedResponseCode, string expectedClaims, string encodedClaims)
156+
{
157+
string serviceChallengeResponseScope = "https://storage.azure.com";
158+
string[] serviceChallengeResponseScopes = new string[] { serviceChallengeResponseScope + "/.default" };
159+
string claims = null;
160+
int callCount = 0;
161+
162+
MockCredential mockCredential = new MockCredential()
163+
{
164+
GetTokenCallback = (trc, _) =>
165+
{
166+
// Assert.AreEqual(serviceChallengeResponseScopes, trc.Scopes);
167+
claims = trc.Claims;
168+
Interlocked.Increment(ref callCount);
169+
Assert.AreEqual(true, trc.IsCaeEnabled);
170+
}
171+
};
172+
173+
var transport = CreateMockTransport(req =>
174+
{
175+
if (callCount <= 1)
176+
{
177+
return challenge == null ? new(200) : new MockResponse(401).WithHeader("WWW-Authenticate", challenge);
178+
}
179+
else
180+
{
181+
return new(200);
182+
}
183+
});
184+
185+
var policy = new StorageBearerTokenChallengeAuthorizationPolicy(mockCredential, "scope", enableTenantDiscovery: false);
186+
187+
using AzureEventSourceListener listener = new((args, text) =>
188+
{
189+
TestContext.WriteLine(text);
190+
if (args.EventName == "FailedToDecodeCaeChallengeClaims")
191+
{
192+
Assert.That(text, Does.Contain($"'{encodedClaims}'"));
193+
}
194+
}, System.Diagnostics.Tracing.EventLevel.Error);
195+
196+
var response = await SendGetRequest(transport, policy, uri: new("https://example.com/1/Original"));
197+
Assert.AreEqual(expectedClaims, claims);
198+
Assert.AreEqual(expectedResponseCode, response.Status);
199+
200+
var response2 = await SendGetRequest(transport, policy, uri: new("https://example.com/1/Original"));
201+
if (expectedClaims != null)
202+
{
203+
Assert.IsNull(claims);
204+
}
205+
}
206+
207+
private static IEnumerable<object[]> CaeTestDetails()
208+
{
209+
//args: description, challenge, expectedResponseCode, expectedClaims, encodedClaims
210+
yield return new object[] { "no challenge", null, 200, null, null };
211+
yield return new object[] { "unexpected error value", """Bearer authorization_uri="https://login.windows.net/", error="invalid_token", claims="ey==" """, 401, null, "ey==" };
212+
yield return new object[] { "unexpected error value", """Bearer authorization_uri="https://login.windows.net/", error="invalid_token", claims="ey==" """, 401, null, "ey==" };
213+
yield return new object[] { "parsing error", """Bearer claims="not base64", error="insufficient_claims" """, 401, null, "not base64" };
214+
yield return new object[] { "no padding", """Bearer error="insufficient_claims", authorization_uri="http://localhost", claims="ey" """, 401, null, "ey" };
215+
yield return new object[] { "more parameters, different order", """Bearer realm="", authorization_uri="http://localhost", client_id="00000003-0000-0000-c000-000000000000", error="insufficient_claims", claims="ey==" """, 200, "{", "ey==" };
216+
yield return new object[] { "more parameters, different order", """Bearer realm="", authorization_uri="http://localhost", client_id="00000003-0000-0000-c000-000000000000", error="insufficient_claims", claims="ey==" """, 200, "{", "ey==" };
217+
yield return new object[] { "standard", """Bearer realm="", authorization_uri="https://login.microsoftonline.com/common/oauth2/authorize", error="insufficient_claims", claims="eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiIxNzI2MDc3NTk1In0sInhtc19jYWVlcnJvciI6eyJ2YWx1ZSI6IjEwMDEyIn19fQ==" """, 200, """{"access_token":{"nbf":{"essential":true,"value":"1726077595"},"xms_caeerror":{"value":"10012"}}}""", "eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiIxNzI2MDc3NTk1In0sInhtc19jYWVlcnJvciI6eyJ2YWx1ZSI6IjEwMDEyIn19fQ==" };
218+
yield return new object[] { "multiple challenges", """PoP realm="", authorization_uri="https://login.microsoftonline.com/common/oauth2/authorize", client_id="00000003-0000-0000-c000-000000000000", nonce="ey==", Bearer realm="", authorization_uri="https://login.microsoftonline.com/common/oauth2/authorize", client_id="00000003-0000-0000-c000-000000000000", error_description="Continuous access evaluation resulted in challenge with result: InteractionRequired and code: TokenIssuedBeforeRevocationTimestamp", error="insufficient_claims", claims="eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwgInZhbHVlIjoiMTcyNjI1ODEyMiJ9fX0=" """, 200, """{"access_token":{"nbf":{"essential":true, "value":"1726258122"}}}""", "eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwgInZhbHVlIjoiMTcyNjI1ODEyMiJ9fX0=" };
219+
}
150220
}
151221
}

0 commit comments

Comments
 (0)