Skip to content

Commit 0baf3d9

Browse files
Canonicalize OAuth Bearer scheme when building Authorization header (#821)
Identity providers may return `token_type` in any case (e.g. `bearer`, `BEARER`) per RFC 6749/6750, but some downstream servers and proxies reject anything other than the canonical `Bearer`. This caused intermittent auth failures depending on the IdP's response casing. Adds `Token.getCanonicalTokenType()`, which returns `Bearer` whenever `tokenType` case-insensitively matches `bearer` and otherwise returns the original value untouched. Routes the three `Authorization` header construction sites through the new helper: `OAuthHeaderFactory.fromTokenSource`, `AzureCliCredentialsProvider`, and `ServingEndpointsDataPlaneImpl`. Non-Bearer schemes (e.g. `MAC`, custom) are unchanged. Original change authored by @mkazia in #788; this branch is the same change rebased onto current main with the changelog conflict resolved, opened from origin so CI runs with OIDC. #788 can be closed once this merges. Tests: `TokenTest` and `OAuthHeaderFactoryTest` cover Bearer casing normalization, non-Bearer pass-through, and the assembled `Authorization: Bearer <token>` header; 12 passed locally against current main. --------- Co-authored-by: Mubashir Kazia <mkazia@gmail.com> Co-authored-by: mkazia <3633226+mkazia@users.noreply.github.com>
1 parent 1b7fe25 commit 0baf3d9

7 files changed

Lines changed: 51 additions & 4 deletions

File tree

NEXT_CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
### Breaking Changes
88

99
### Bug Fixes
10+
* Canonicalize Bearer tokenType in Authorization headers
1011

1112
### Security Vulnerabilities
1213

@@ -15,3 +16,4 @@
1516
### Internal Changes
1617

1718
### API Changes
19+
* Add `getCanonicalTokenType()` method for `com.databricks.sdk.core.oauth.Token`

databricks-sdk-java/src/main/java/com/databricks/sdk/core/AzureCliCredentialsProvider.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,8 @@ public OAuthHeaderFactory configure(DatabricksConfig config) {
9999
() -> {
100100
Token token = tokenSource.getToken();
101101
Map<String, String> headers = new HashMap<>();
102-
headers.put("Authorization", token.getTokenType() + " " + token.getAccessToken());
102+
headers.put(
103+
"Authorization", token.getCanonicalTokenType() + " " + token.getAccessToken());
103104
if (finalMgmtTokenSource != null) {
104105
AzureUtils.addSpManagementToken(finalMgmtTokenSource, headers);
105106
}

databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/OAuthHeaderFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public Token getToken() {
5151
public Map<String, String> headers() {
5252
Token token = tokenSource.getToken();
5353
Map<String, String> headers = new HashMap<>();
54-
headers.put("Authorization", token.getTokenType() + " " + token.getAccessToken());
54+
headers.put("Authorization", token.getCanonicalTokenType() + " " + token.getAccessToken());
5555
return headers;
5656
}
5757
};

databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/Token.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,22 @@ public String getTokenType() {
5151
return tokenType;
5252
}
5353

54+
/**
55+
* Returns the token type canonicalized for use as the Authorization header scheme. Per RFC 6749
56+
* §5.1 / RFC 6750 §2.1, identity providers may return {@code token_type} in any case (e.g.
57+
* "bearer", "BEARER"). Some downstream servers and proxies reject anything other than the
58+
* canonical "Bearer" capitalization, so we normalize that scheme here. Other schemes are returned
59+
* unchanged.
60+
*
61+
* @return the canonicalized token type
62+
*/
63+
public String getCanonicalTokenType() {
64+
if ("bearer".equalsIgnoreCase(tokenType)) {
65+
return "Bearer";
66+
}
67+
return tokenType;
68+
}
69+
5470
/**
5571
* Returns the refresh token, if available. May be null for non-refreshable tokens.
5672
*

databricks-sdk-java/src/main/java/com/databricks/sdk/service/serving/ServingEndpointsDataPlaneImpl.java

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

databricks-sdk-java/src/test/java/com/databricks/sdk/core/oauth/OAuthHeaderFactoryTest.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,19 @@ private static Stream<Arguments> provideTokenSourceTestCases() {
3636
Arguments.of(
3737
"Token with custom type",
3838
new Token(TOKEN_VALUE, "Custom", expiry),
39-
Collections.singletonMap("Authorization", "Custom " + TOKEN_VALUE)));
39+
Collections.singletonMap("Authorization", "Custom " + TOKEN_VALUE)),
40+
Arguments.of(
41+
"Lowercase bearer is canonicalized",
42+
new Token(TOKEN_VALUE, "bearer", expiry),
43+
Collections.singletonMap("Authorization", "Bearer " + TOKEN_VALUE)),
44+
Arguments.of(
45+
"Uppercase BEARER is canonicalized",
46+
new Token(TOKEN_VALUE, "BEARER", expiry),
47+
Collections.singletonMap("Authorization", "Bearer " + TOKEN_VALUE)),
48+
Arguments.of(
49+
"Mixed-case BeArEr is canonicalized",
50+
new Token(TOKEN_VALUE, "BeArEr", expiry),
51+
Collections.singletonMap("Authorization", "Bearer " + TOKEN_VALUE)));
4052
}
4153

4254
@ParameterizedTest(name = "{0}")

databricks-sdk-java/src/test/java/com/databricks/sdk/core/oauth/TokenTest.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,20 @@ void createRefreshableToken() {
2929
assertEquals(refreshToken, token.getRefreshToken());
3030
assertEquals(currentInstant.plusSeconds(300), token.getExpiry());
3131
}
32+
33+
@Test
34+
void canonicalTokenTypeNormalizesBearerCasing() {
35+
Instant expiry = currentInstant.plusSeconds(300);
36+
assertEquals("Bearer", new Token(accessToken, "Bearer", expiry).getCanonicalTokenType());
37+
assertEquals("Bearer", new Token(accessToken, "bearer", expiry).getCanonicalTokenType());
38+
assertEquals("Bearer", new Token(accessToken, "BEARER", expiry).getCanonicalTokenType());
39+
assertEquals("Bearer", new Token(accessToken, "BeArEr", expiry).getCanonicalTokenType());
40+
}
41+
42+
@Test
43+
void canonicalTokenTypePreservesNonBearerSchemes() {
44+
Instant expiry = currentInstant.plusSeconds(300);
45+
assertEquals("Custom", new Token(accessToken, "Custom", expiry).getCanonicalTokenType());
46+
assertEquals("MAC", new Token(accessToken, "MAC", expiry).getCanonicalTokenType());
47+
}
3248
}

0 commit comments

Comments
 (0)