Skip to content

Commit 5563de0

Browse files
authored
Support Athenz-Role-Auth header in Athenz module (#6341)
Motivation: I realized that Athenz has replaced the `Yahoo-Role-Auth` header with `Athenz-Role-Auth` header for sending role tokens. Modifications: - Raname the orignal `ROLE_TOKEN` to `YAHOO_ROLE_TOKEN` - Add `ATHENZ_ROLE_TOKEN` Result: Users can use `Athenz-Role-Auth` header for sending role tokens.
1 parent 69195de commit 5563de0

File tree

7 files changed

+119
-30
lines changed

7 files changed

+119
-30
lines changed

athenz/src/main/java/com/linecorp/armeria/client/athenz/AthenzClient.java

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
package com.linecorp.armeria.client.athenz;
1818

19-
import static com.linecorp.armeria.internal.common.athenz.AthenzHeaderNames.YAHOO_ROLE_AUTH;
2019
import static java.util.Objects.requireNonNull;
2120

2221
import java.time.Duration;
@@ -29,7 +28,6 @@
2928
import com.linecorp.armeria.client.ClientRequestContext;
3029
import com.linecorp.armeria.client.HttpClient;
3130
import com.linecorp.armeria.client.SimpleDecoratingHttpClient;
32-
import com.linecorp.armeria.common.HttpHeaderNames;
3331
import com.linecorp.armeria.common.HttpRequest;
3432
import com.linecorp.armeria.common.HttpResponse;
3533
import com.linecorp.armeria.common.RequestHeadersBuilder;
@@ -39,7 +37,7 @@
3937

4038
/**
4139
* An {@link HttpClient} that adds an Athenz token to the request headers.
42-
* {@link TokenType#ACCESS_TOKEN} and {@link TokenType#ROLE_TOKEN} are supported.
40+
* {@link TokenType#ACCESS_TOKEN} and {@link TokenType#YAHOO_ROLE_TOKEN} are supported.
4341
*
4442
* <p>The acquired token is cached and automatically refreshed before it expires based on the specified
4543
* duration. If not specified, the default refresh duration is 10 minutes before the token expires.
@@ -141,15 +139,10 @@ private AthenzClient(HttpClient delegate, ZtsBaseClient ztsBaseClient, String do
141139
List<String> roleNames, TokenType tokenType, Duration refreshBefore) {
142140
super(delegate);
143141
this.tokenType = tokenType;
144-
switch (tokenType) {
145-
case ROLE_TOKEN:
146-
tokenClient = new RoleTokenClient(ztsBaseClient, domainName, roleNames, refreshBefore);
147-
break;
148-
case ACCESS_TOKEN:
149-
tokenClient = new AccessTokenClient(ztsBaseClient, domainName, roleNames, refreshBefore);
150-
break;
151-
default:
152-
throw new Error("unknown auth type: " + tokenType);
142+
if (tokenType.isRoleToken()) {
143+
tokenClient = new RoleTokenClient(ztsBaseClient, domainName, roleNames, refreshBefore);
144+
} else {
145+
tokenClient = new AccessTokenClient(ztsBaseClient, domainName, roleNames, refreshBefore);
153146
}
154147
}
155148

@@ -158,11 +151,11 @@ public HttpResponse execute(ClientRequestContext ctx, HttpRequest req) throws Ex
158151
final CompletableFuture<HttpResponse> future = tokenClient.getToken().thenApply(token -> {
159152
final HttpRequest newReq = req.mapHeaders(headers -> {
160153
final RequestHeadersBuilder builder = headers.toBuilder();
161-
if (tokenType == TokenType.ROLE_TOKEN) {
162-
builder.set(YAHOO_ROLE_AUTH, token);
163-
} else {
164-
builder.set(HttpHeaderNames.AUTHORIZATION, "Bearer " + token);
154+
String token0 = token;
155+
if (tokenType.authScheme() != null) {
156+
token0 = tokenType.authScheme() + ' ' + token0;
165157
}
158+
builder.set(tokenType.headerName(), token0);
166159
return builder.build();
167160
});
168161
ctx.updateRequest(newReq);

athenz/src/main/java/com/linecorp/armeria/common/athenz/TokenType.java

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.linecorp.armeria.common.athenz;
1818

1919
import com.linecorp.armeria.common.HttpHeaderNames;
20+
import com.linecorp.armeria.common.annotation.Nullable;
2021
import com.linecorp.armeria.common.annotation.UnstableApi;
2122
import com.linecorp.armeria.internal.common.athenz.AthenzHeaderNames;
2223

@@ -28,24 +29,52 @@
2829
@UnstableApi
2930
public enum TokenType {
3031
/**
31-
* Athenz role token.
32+
* Athenz access token.
3233
*/
33-
ROLE_TOKEN(AthenzHeaderNames.YAHOO_ROLE_AUTH),
34+
ACCESS_TOKEN(HttpHeaderNames.AUTHORIZATION, false, "Bearer"),
3435
/**
35-
* Athenz access token.
36+
* Athenz role token. {@code "Athenz-Role-Auth"} is used as the header name for this token type.
37+
*/
38+
ATHENZ_ROLE_TOKEN(AthenzHeaderNames.ATHENZ_ROLE_AUTH, true, null),
39+
/**
40+
* The legacy Athenz role token used by Yahoo. {@code "Yahoo-Role-Auth"} is used as the
41+
* header name for this token type.
3642
*/
37-
ACCESS_TOKEN(HttpHeaderNames.AUTHORIZATION);
43+
YAHOO_ROLE_TOKEN(AthenzHeaderNames.YAHOO_ROLE_AUTH, true, null);
3844

39-
TokenType(AsciiString headerName) {
45+
TokenType(AsciiString headerName, boolean isRoleToken, @Nullable String authScheme) {
4046
this.headerName = headerName;
47+
this.isRoleToken = isRoleToken;
48+
this.authScheme = authScheme;
4149
}
4250

4351
private final AsciiString headerName;
52+
private final boolean isRoleToken;
53+
@Nullable
54+
private final String authScheme;
4455

4556
/**
4657
* Returns the header name used to pass the token.
4758
*/
4859
public AsciiString headerName() {
4960
return headerName;
5061
}
62+
63+
/**
64+
* Returns whether this token type is a role token.
65+
*/
66+
public boolean isRoleToken() {
67+
return isRoleToken;
68+
}
69+
70+
/**
71+
* Returns the authentication scheme used for this token type, or {@code null} if not applicable.
72+
* For example, {@link TokenType#ACCESS_TOKEN} uses "Bearer" as the authentication scheme,
73+
* while {@link TokenType#YAHOO_ROLE_TOKEN} and {@link TokenType#ATHENZ_ROLE_TOKEN} do not use
74+
* any authentication scheme.
75+
*/
76+
@Nullable
77+
public String authScheme() {
78+
return authScheme;
79+
}
5180
}

athenz/src/main/java/com/linecorp/armeria/internal/common/athenz/AthenzHeaderNames.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@
2222

2323
public final class AthenzHeaderNames {
2424

25-
public static final AsciiString YAHOO_ROLE_AUTH = HttpHeaderNames.of("yahoo-role-auth");
25+
public static final AsciiString YAHOO_ROLE_AUTH = HttpHeaderNames.of("Yahoo-Role-Auth");
26+
27+
public static final AsciiString ATHENZ_ROLE_AUTH = HttpHeaderNames.of("Athenz-Role-Auth");
2628

2729
private AthenzHeaderNames() {}
2830
}

athenz/src/main/java/com/linecorp/armeria/server/athenz/RequiresAthenzRole.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,9 @@
8181
/**
8282
* The required {@link TokenType}.
8383
*/
84-
TokenType[] tokenType() default { TokenType.ROLE_TOKEN, TokenType.ACCESS_TOKEN };
84+
TokenType[] tokenType() default {
85+
TokenType.ACCESS_TOKEN, TokenType.ATHENZ_ROLE_TOKEN, TokenType.YAHOO_ROLE_TOKEN
86+
};
8587

8688
/**
8789
* A special parameter in order to specify the order of a {@link Decorator}.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2025 LY Corporation
3+
*
4+
* LY Corporation licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
17+
package com.linecorp.armeria.common.athenz;
18+
19+
import static org.assertj.core.api.Assertions.assertThat;
20+
21+
import org.junit.jupiter.api.Test;
22+
23+
class TokenTypeTest {
24+
25+
@Test
26+
void testAccessToken() {
27+
final TokenType tokenType = TokenType.ACCESS_TOKEN;
28+
assertThat(tokenType.headerName().toString()).isEqualToIgnoringCase("Authorization");
29+
assertThat(tokenType.isRoleToken()).isFalse();
30+
assertThat(tokenType.authScheme()).isEqualTo("Bearer");
31+
}
32+
33+
@Test
34+
void testYahooRoleToken() {
35+
final TokenType tokenType = TokenType.YAHOO_ROLE_TOKEN;
36+
assertThat(tokenType.headerName().toString()).isEqualToIgnoringCase("Yahoo-Role-Auth");
37+
assertThat(tokenType.isRoleToken()).isTrue();
38+
assertThat(tokenType.authScheme()).isNull();
39+
}
40+
41+
@Test
42+
void testAthenzRoleToken() {
43+
final TokenType tokenType = TokenType.ATHENZ_ROLE_TOKEN;
44+
assertThat(tokenType.headerName().toString()).isEqualToIgnoringCase("Athenz-Role-Auth");
45+
assertThat(tokenType.isRoleToken()).isTrue();
46+
assertThat(tokenType.authScheme()).isNull();
47+
}
48+
}

athenz/src/test/java/com/linecorp/armeria/server/athenz/AthenzAnnotatedServiceTest.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,11 @@ protected void configure(ServerBuilder sb) {
7676
};
7777

7878
@CsvSource({
79-
"foo-service, ROLE_TOKEN",
79+
"foo-service, YAHOO_ROLE_TOKEN",
80+
"foo-service, ATHENZ_ROLE_TOKEN",
8081
"foo-service, ACCESS_TOKEN",
81-
"test-service, ROLE_TOKEN",
82+
"test-service, YAHOO_ROLE_TOKEN",
83+
"test-service, ATHENZ_ROLE_TOKEN",
8284
"test-service, ACCESS_TOKEN"
8385
})
8486
@ParameterizedTest
@@ -98,9 +100,11 @@ void testUserRole(String serviceName, TokenType tokenType) {
98100
}
99101

100102
@CsvSource({
101-
"foo-service, ROLE_TOKEN, false",
103+
"foo-service, YAHOO_ROLE_TOKEN, false",
104+
"foo-service, ATHENZ_ROLE_TOKEN, false",
102105
"foo-service, ACCESS_TOKEN, false",
103-
"test-service, ROLE_TOKEN, true",
106+
"test-service, YAHOO_ROLE_TOKEN, true",
107+
"test-service, ATHENZ_ROLE_TOKEN, true",
104108
"test-service, ACCESS_TOKEN, true"
105109
})
106110
@ParameterizedTest
@@ -122,7 +126,7 @@ void testAdminRole(String serviceName, TokenType tokenType, boolean shouldSuccee
122126
.isInstanceOf(AccessDeniedException.class)
123127
.hasMessage("Failed to obtain an Athenz %s token. " +
124128
"(domain: testing, roles: test_role_admin)",
125-
tokenType == TokenType.ROLE_TOKEN ? "role" : "access");
129+
tokenType.isRoleToken() ? "role" : "access");
126130
}
127131
}
128132
}

athenz/src/test/java/com/linecorp/armeria/server/athenz/AthenzIntegrationTest.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ protected void configure(ServerBuilder sb) {
7474
if (!authorization.isEmpty()) {
7575
return HttpResponse.of("YahooRoleAuth " + authorization);
7676
}
77+
authorization = req.headers().get(AthenzHeaderNames.ATHENZ_ROLE_AUTH, "");
78+
if (!authorization.isEmpty()) {
79+
return HttpResponse.of("AthenzRoleAuth " + authorization);
80+
}
7781
// Should not reach here.
7882
return HttpResponse.of(HttpStatus.INTERNAL_SERVER_ERROR);
7983
});
@@ -93,6 +97,10 @@ protected void configure(ServerBuilder sb) {
9397
if (!authorization.isEmpty()) {
9498
return HttpResponse.of("YahooRoleAuth " + authorization);
9599
}
100+
authorization = req.headers().get(AthenzHeaderNames.ATHENZ_ROLE_AUTH, "");
101+
if (!authorization.isEmpty()) {
102+
return HttpResponse.of("AthenzRoleAuth " + authorization);
103+
}
96104
// Should not reach here.
97105
return HttpResponse.of(HttpStatus.INTERNAL_SERVER_ERROR);
98106
});
@@ -119,9 +127,12 @@ void shouldObtainAdminRole(TokenType tokenType) {
119127
final AggregatedHttpResponse response = client.get("/admin");
120128
assertThat(response.status()).isEqualTo(HttpStatus.OK);
121129
switch (tokenType) {
122-
case ROLE_TOKEN:
130+
case YAHOO_ROLE_TOKEN:
123131
assertThat(response.contentUtf8()).startsWith("YahooRoleAuth ");
124132
break;
133+
case ATHENZ_ROLE_TOKEN:
134+
assertThat(response.contentUtf8()).startsWith("AthenzRoleAuth ");
135+
break;
125136
case ACCESS_TOKEN:
126137
assertThat(response.contentUtf8()).startsWith("Authorization ");
127138
break;
@@ -144,7 +155,7 @@ void shouldNotObtainAdminRole(TokenType tokenType) {
144155
client.get("/admin");
145156
}).isInstanceOf(AccessDeniedException.class)
146157
.hasMessage("Failed to obtain an Athenz " +
147-
(tokenType == TokenType.ROLE_TOKEN ? "role" : "access") +
158+
(tokenType.isRoleToken() ? "role" : "access") +
148159
" token. (domain: testing, roles: test_role_admin)");
149160
final ClientRequestContext ctx = captor.get();
150161
// Make sure the RequestLog is completed when the request was rejected by the decorator.

0 commit comments

Comments
 (0)