Skip to content

Commit 70aacfd

Browse files
authored
BearerTokenChallengeAuthorizationPolicy extracting tenant ID (Azure#44280)
1 parent 6a7d2dc commit 70aacfd

File tree

2 files changed

+269
-5
lines changed

2 files changed

+269
-5
lines changed

sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/StorageBearerTokenChallengeAuthorizationPolicy.java

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@
1010
import com.azure.core.http.HttpResponse;
1111
import com.azure.core.http.policy.BearerTokenAuthenticationPolicy;
1212
import com.azure.core.util.CoreUtils;
13+
import com.azure.core.util.logging.ClientLogger;
1314
import reactor.core.publisher.Mono;
1415

16+
import java.net.URI;
17+
import java.net.URISyntaxException;
1518
import java.util.Collections;
1619
import java.util.HashMap;
1720
import java.util.Map;
@@ -22,8 +25,10 @@
2225
*/
2326
public class StorageBearerTokenChallengeAuthorizationPolicy extends BearerTokenAuthenticationPolicy {
2427

28+
private static final ClientLogger LOGGER = new ClientLogger(StorageBearerTokenChallengeAuthorizationPolicy.class);
29+
2530
private static final String DEFAULT_SCOPE = "/.default";
26-
private static final String BEARER_TOKEN_PREFIX = "Bearer ";
31+
static final String BEARER_TOKEN_PREFIX = "Bearer ";
2732

2833
private String[] scopes;
2934

@@ -64,29 +69,69 @@ public Mono<Boolean> authorizeRequestOnChallenge(HttpPipelineCallContext context
6469
String authHeader = response.getHeaderValue(HttpHeaderName.WWW_AUTHENTICATE);
6570
Map<String, String> challenges = extractChallengeAttributes(authHeader, BEARER_TOKEN_PREFIX);
6671

67-
String scope = challenges.get("resource_id");
72+
String scope = getScopeFromChallenges(challenges);
73+
String authorization = getAuthorizationFromChallenges(challenges);
74+
6875
if (scope != null) {
6976
scope += DEFAULT_SCOPE;
7077
scopes = new String[] { scope };
7178
scopes = getScopes(context, scopes);
72-
return setAuthorizationHeader(context, new TokenRequestContext().addScopes(scopes)).thenReturn(true);
7379
}
80+
81+
if (authorization != null) {
82+
String tenantId = extractTenantIdFromUri(authorization);
83+
TokenRequestContext tokenRequestContext = new TokenRequestContext().addScopes(scopes).setTenantId(tenantId);
84+
return setAuthorizationHeader(context, tokenRequestContext).thenReturn(true);
85+
}
86+
87+
if (scope != null) {
88+
TokenRequestContext tokenRequestContext = new TokenRequestContext().addScopes(scopes);
89+
return setAuthorizationHeader(context, tokenRequestContext).thenReturn(true);
90+
}
91+
7492
return Mono.just(false);
7593
}
7694

95+
String extractTenantIdFromUri(String uri) {
96+
try {
97+
String[] segments = new URI(uri).getPath().split("/");
98+
if (segments.length > 1) {
99+
return segments[1];
100+
} else {
101+
throw LOGGER.logExceptionAsError(new RuntimeException("Invalid authorization URI: tenantId not found"));
102+
}
103+
} catch (URISyntaxException e) {
104+
throw LOGGER.logExceptionAsError(new RuntimeException("Invalid authorization URI", e));
105+
}
106+
}
107+
77108
@Override
78109
public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, HttpResponse response) {
79110
String authHeader = response.getHeaderValue(HttpHeaderName.WWW_AUTHENTICATE);
80111
Map<String, String> challenges = extractChallengeAttributes(authHeader, BEARER_TOKEN_PREFIX);
81112

82-
String scope = challenges.get("resource_id");
113+
String scope = getScopeFromChallenges(challenges);
114+
String authorization = getAuthorizationFromChallenges(challenges);
115+
83116
if (scope != null) {
84117
scope += DEFAULT_SCOPE;
85118
scopes = new String[] { scope };
86119
scopes = getScopes(context, scopes);
87-
setAuthorizationHeaderSync(context, new TokenRequestContext().addScopes(scopes));
120+
}
121+
122+
if (authorization != null) {
123+
String tenantId = extractTenantIdFromUri(authorization);
124+
TokenRequestContext tokenRequestContext = new TokenRequestContext().addScopes(scopes).setTenantId(tenantId);
125+
setAuthorizationHeaderSync(context, tokenRequestContext);
126+
return true;
127+
}
128+
129+
if (scope != null) {
130+
TokenRequestContext tokenRequestContext = new TokenRequestContext().addScopes(scopes);
131+
setAuthorizationHeaderSync(context, tokenRequestContext);
88132
return true;
89133
}
134+
90135
return false;
91136
}
92137

@@ -117,4 +162,12 @@ static boolean isBearerChallenge(String authenticateHeader, String authChallenge
117162
return (!CoreUtils.isNullOrEmpty(authenticateHeader)
118163
&& authenticateHeader.toLowerCase(Locale.ROOT).startsWith(authChallengePrefix.toLowerCase(Locale.ROOT)));
119164
}
165+
166+
String getScopeFromChallenges(Map<String, String> challenges) {
167+
return challenges.get("resource_id");
168+
}
169+
170+
String getAuthorizationFromChallenges(Map<String, String> challenges) {
171+
return challenges.get("authorization_uri");
172+
}
120173
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.azure.storage.common.policy;
5+
6+
import com.azure.core.credential.TokenCredential;
7+
import com.azure.core.test.utils.MockTokenCredential;
8+
import org.junit.jupiter.api.BeforeEach;
9+
import org.junit.jupiter.api.Test;
10+
11+
import java.util.HashMap;
12+
import java.util.Map;
13+
14+
import static com.azure.storage.common.policy.StorageBearerTokenChallengeAuthorizationPolicy.BEARER_TOKEN_PREFIX;
15+
import static org.junit.jupiter.api.Assertions.assertEquals;
16+
import static org.junit.jupiter.api.Assertions.assertNotNull;
17+
import static org.junit.jupiter.api.Assertions.assertNull;
18+
import static org.junit.jupiter.api.Assertions.assertThrows;
19+
import static org.junit.jupiter.api.Assertions.assertTrue;
20+
21+
public class StorageBearerTokenChallengeAuthorizationPolicyTests {
22+
23+
private String[] scopes;
24+
private TokenCredential mockCredential;
25+
26+
@BeforeEach
27+
public void setup() {
28+
scopes = new String[] { "https://storage.azure.com/.default" };
29+
mockCredential = new MockTokenCredential();
30+
}
31+
32+
@Test
33+
public void testExtractChallengeAttributes() {
34+
StorageBearerTokenChallengeAuthorizationPolicy policy
35+
= new StorageBearerTokenChallengeAuthorizationPolicy(mockCredential, "https://storage.azure.com/.default");
36+
37+
String authHeader
38+
= "Bearer authorization_uri=https://login.microsoftonline.com/tenantId/oauth2/authorize resource_id=https://storage.azure.com";
39+
Map<String, String> challenges = policy.extractChallengeAttributes(authHeader, BEARER_TOKEN_PREFIX);
40+
41+
assertNotNull(challenges);
42+
assertEquals("https://login.microsoftonline.com/tenantid/oauth2/authorize",
43+
challenges.get("authorization_uri"));
44+
assertEquals("https://storage.azure.com", challenges.get("resource_id"));
45+
}
46+
47+
@Test
48+
public void testGetScopeFromChallenges() {
49+
StorageBearerTokenChallengeAuthorizationPolicy policy
50+
= new StorageBearerTokenChallengeAuthorizationPolicy(mockCredential, "https://storage.azure.com/.default");
51+
52+
Map<String, String> challenges = new HashMap<>();
53+
challenges.put("resource_id", "https://storage.azure.com");
54+
55+
String scope = policy.getScopeFromChallenges(challenges);
56+
57+
assertEquals("https://storage.azure.com", scope);
58+
}
59+
60+
@Test
61+
public void testGetAuthorizationFromChallenges() {
62+
StorageBearerTokenChallengeAuthorizationPolicy policy
63+
= new StorageBearerTokenChallengeAuthorizationPolicy(mockCredential, "https://storage.azure.com/.default");
64+
65+
Map<String, String> challenges = new HashMap<>();
66+
challenges.put("authorization_uri", "https://login.microsoftonline.com/tenantId/oauth2/authorize");
67+
68+
String authorization = policy.getAuthorizationFromChallenges(challenges);
69+
70+
assertEquals("https://login.microsoftonline.com/tenantId/oauth2/authorize", authorization);
71+
}
72+
73+
@Test
74+
public void usesTokenProvidedByCredentials() {
75+
StorageBearerTokenChallengeAuthorizationPolicy policy
76+
= new StorageBearerTokenChallengeAuthorizationPolicy(mockCredential, scopes);
77+
78+
Map<String, String> challenges = policy.extractChallengeAttributes(null, BEARER_TOKEN_PREFIX);
79+
80+
String scope = policy.getScopeFromChallenges(challenges);
81+
String authorization = policy.getAuthorizationFromChallenges(challenges);
82+
83+
assertNull(scope);
84+
assertNull(authorization);
85+
}
86+
87+
@Test
88+
public void doesNotSendUnauthorizedRequestWhenEnableTenantDiscoveryIsFalse() {
89+
StorageBearerTokenChallengeAuthorizationPolicy policy
90+
= new StorageBearerTokenChallengeAuthorizationPolicy(mockCredential, scopes);
91+
92+
Map<String, String> challenges = policy.extractChallengeAttributes(null, BEARER_TOKEN_PREFIX);
93+
94+
String scope = policy.getScopeFromChallenges(challenges);
95+
String authorization = policy.getAuthorizationFromChallenges(challenges);
96+
97+
assertNull(scope);
98+
assertNull(authorization);
99+
}
100+
101+
@Test
102+
public void sendsUnauthorizedRequestWhenEnableTenantDiscoveryIsTrue() {
103+
StorageBearerTokenChallengeAuthorizationPolicy policy
104+
= new StorageBearerTokenChallengeAuthorizationPolicy(mockCredential, scopes);
105+
106+
String expectedTenantId = "72f988bf-86f1-41af-91ab-2d7cd011db47";
107+
String authHeader = "Bearer authorization_uri=https://login.microsoftonline.com/" + expectedTenantId
108+
+ "/oauth2/authorize resource_id=https://storage.azure.com";
109+
Map<String, String> challenges = policy.extractChallengeAttributes(authHeader, BEARER_TOKEN_PREFIX);
110+
111+
String scope = policy.getScopeFromChallenges(challenges);
112+
String authorization = policy.getAuthorizationFromChallenges(challenges);
113+
114+
assertEquals("https://storage.azure.com", scope);
115+
assertEquals("https://login.microsoftonline.com/" + expectedTenantId + "/oauth2/authorize", authorization);
116+
}
117+
118+
@Test
119+
public void usesScopeFromBearerChallenge() {
120+
StorageBearerTokenChallengeAuthorizationPolicy policy = new StorageBearerTokenChallengeAuthorizationPolicy(
121+
mockCredential, "https://disk.compute.azure.com/.default");
122+
123+
String serviceChallengeResponseScope = "https://storage.azure.com";
124+
String authHeader
125+
= "Bearer authorization_uri=https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47/oauth2/authorize resource_id="
126+
+ serviceChallengeResponseScope;
127+
Map<String, String> challenges = policy.extractChallengeAttributes(authHeader, BEARER_TOKEN_PREFIX);
128+
129+
String scope = policy.getScopeFromChallenges(challenges);
130+
String authorization = policy.getAuthorizationFromChallenges(challenges);
131+
132+
assertEquals(serviceChallengeResponseScope, scope);
133+
assertEquals("https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47/oauth2/authorize",
134+
authorization);
135+
}
136+
137+
@Test
138+
public void testMultiTenantAuthentication() {
139+
StorageBearerTokenChallengeAuthorizationPolicy policy
140+
= new StorageBearerTokenChallengeAuthorizationPolicy(mockCredential, "https://storage.azure.com/.default");
141+
142+
String tenantId1 = "tenant1";
143+
String tenantId2 = "tenant2";
144+
145+
String authHeader1 = "Bearer authorization_uri=https://login.microsoftonline.com/" + tenantId1
146+
+ "/oauth2/authorize resource_id=https://storage.azure.com";
147+
String authHeader2 = "Bearer authorization_uri=https://login.microsoftonline.com/" + tenantId2
148+
+ "/oauth2/authorize resource_id=https://storage.azure.com";
149+
150+
Map<String, String> challenges1 = policy.extractChallengeAttributes(authHeader1, BEARER_TOKEN_PREFIX);
151+
Map<String, String> challenges2 = policy.extractChallengeAttributes(authHeader2, BEARER_TOKEN_PREFIX);
152+
153+
String scope1 = policy.getScopeFromChallenges(challenges1);
154+
String authorization1 = policy.getAuthorizationFromChallenges(challenges1);
155+
String scope2 = policy.getScopeFromChallenges(challenges2);
156+
String authorization2 = policy.getAuthorizationFromChallenges(challenges2);
157+
158+
assertEquals("https://storage.azure.com", scope1);
159+
assertEquals("https://login.microsoftonline.com/" + tenantId1 + "/oauth2/authorize", authorization1);
160+
assertEquals("https://storage.azure.com", scope2);
161+
assertEquals("https://login.microsoftonline.com/" + tenantId2 + "/oauth2/authorize", authorization2);
162+
}
163+
164+
@Test
165+
public void testExtractTenantIdFromUri() {
166+
StorageBearerTokenChallengeAuthorizationPolicy policy
167+
= new StorageBearerTokenChallengeAuthorizationPolicy(mockCredential, "https://storage.azure.com/.default");
168+
169+
String uri = "https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47/oauth2/authorize";
170+
String expectedTenantId = "72f988bf-86f1-41af-91ab-2d7cd011db47";
171+
172+
String actualTenantId = policy.extractTenantIdFromUri(uri);
173+
174+
assertEquals(expectedTenantId, actualTenantId);
175+
}
176+
177+
@Test
178+
public void testExtractTenantIdFromUriInvalidUri() {
179+
StorageBearerTokenChallengeAuthorizationPolicy policy
180+
= new StorageBearerTokenChallengeAuthorizationPolicy(mockCredential, "https://storage.azure.com/.default");
181+
182+
String invalidUri = "https://login.microsoftonline.com/";
183+
184+
Exception exception = assertThrows(RuntimeException.class, () -> {
185+
policy.extractTenantIdFromUri(invalidUri);
186+
});
187+
188+
String expectedMessage = "Invalid authorization URI: tenantId not found";
189+
String actualMessage = exception.getMessage();
190+
191+
assertTrue(actualMessage.contains(expectedMessage));
192+
}
193+
194+
@Test
195+
public void testExtractTenantIdFromUriMalformedUri() {
196+
StorageBearerTokenChallengeAuthorizationPolicy policy
197+
= new StorageBearerTokenChallengeAuthorizationPolicy(mockCredential, "https://storage.azure.com/.default");
198+
199+
String malformedUri = "ht!tp://invalid-uri";
200+
201+
Exception exception = assertThrows(RuntimeException.class, () -> {
202+
policy.extractTenantIdFromUri(malformedUri);
203+
});
204+
205+
String expectedMessage = "Invalid authorization URI";
206+
String actualMessage = exception.getMessage();
207+
208+
assertTrue(actualMessage.contains(expectedMessage));
209+
}
210+
211+
}

0 commit comments

Comments
 (0)