|
2 | 2 | // Licensed under the MIT License. |
3 | 3 |
|
4 | 4 | using System; |
| 5 | +using System.Collections.Generic; |
5 | 6 | using System.Linq; |
6 | 7 | using System.Net; |
7 | 8 | using System.Threading; |
8 | 9 | using System.Threading.Tasks; |
9 | 10 | using Azure.Core; |
| 11 | +using Azure.Core.Diagnostics; |
10 | 12 | using Azure.Core.TestFramework; |
11 | 13 | using Azure.Storage.Shared; |
12 | 14 | using Moq; |
@@ -147,5 +149,73 @@ public async Task UsesScopeFromBearerChallange() |
147 | 149 | await SendGetRequest(transport, tokenChallengeAuthorizationPolicy, uri: new Uri("https://example.com")); |
148 | 150 | await SendGetRequest(transport, tokenChallengeAuthorizationPolicy, uri: new Uri("https://example.com")); |
149 | 151 | } |
| 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 | + } |
150 | 220 | } |
151 | 221 | } |
0 commit comments