Skip to content

Commit

Permalink
[ANCHOR-618] Fix auth header not configurable for callback API and pl…
Browse files Browse the repository at this point in the history
…atform API (#1302)

### Description

[ANCHOR-618] Fix auth header not configurable for callback API and
platform API

### Context

Bug fixes.

### Testing

- `./gradlew test`

### Documentation

N/A

### Known limitations

N/A



[ANCHOR-618]:
https://stellarorg.atlassian.net/browse/ANCHOR-618?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
  • Loading branch information
lijamie98 authored Mar 23, 2024
1 parent 236b28b commit de2ab6e
Show file tree
Hide file tree
Showing 15 changed files with 179 additions and 48 deletions.
34 changes: 19 additions & 15 deletions core/src/main/java/org/stellar/anchor/auth/AuthHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,35 +10,39 @@

public class AuthHelper {
public final AuthType authType;

public final String authorizationHeader;
private JwtService jwtService;
private long jwtExpirationMilliseconds;
private String apiKey;

private AuthHelper(AuthType authType) {
this.authType = authType;
this(authType, "Authorization");
}

public static AuthHelper from(AuthType type, String secret, long jwtExpirationMilliseconds) {
switch (type) {
case JWT:
return AuthHelper.forJwtToken(
new JwtService(null, null, null, secret, secret, secret), jwtExpirationMilliseconds);
case API_KEY:
return AuthHelper.forApiKey(secret);
default:
return AuthHelper.forNone();
}
private AuthHelper(AuthType authType, String authorizationHeader) {
this.authType = authType;
this.authorizationHeader = authorizationHeader;
}

public static AuthHelper forJwtToken(JwtService jwtService, long jwtExpirationMilliseconds) {
AuthHelper authHelper = new AuthHelper(AuthType.JWT);
return forJwtToken("Authorization", jwtService, jwtExpirationMilliseconds);
}

public static AuthHelper forJwtToken(
String authorizationHeader, JwtService jwtService, long jwtExpirationMilliseconds) {
AuthHelper authHelper = new AuthHelper(AuthType.JWT, authorizationHeader);
authHelper.jwtService = jwtService;
authHelper.jwtExpirationMilliseconds = jwtExpirationMilliseconds;
return authHelper;
}

public static AuthHelper forApiKey(String apiKey) {
AuthHelper authHelper = new AuthHelper(AuthType.API_KEY);
return forApiKey("X-Api-Key", apiKey);
}

public static AuthHelper forApiKey(String authorizationHeader, String apiKey) {
AuthHelper authHelper = new AuthHelper(AuthType.API_KEY, authorizationHeader);
authHelper.apiKey = apiKey;
return authHelper;
}
Expand Down Expand Up @@ -67,9 +71,9 @@ private <T extends ApiAuthJwt> AuthHeader<String, String> createAuthHeader(Class
throws InvalidConfigException {
switch (authType) {
case JWT:
return new AuthHeader<>("Authorization", "Bearer " + createJwt(jwtClass));
return new AuthHeader<>(authorizationHeader, "Bearer " + createJwt(jwtClass));
case API_KEY:
return new AuthHeader<>("X-Api-Key", apiKey);
return new AuthHeader<>(authorizationHeader, apiKey);
default:
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ public abstract class AbstractJwtFilter implements Filter {
static final String APPLICATION_JSON_VALUE = "application/json";
static final Gson gson = GsonUtils.builder().setPrettyPrinting().create();
protected final JwtService jwtService;
protected final String authorizationHeader;

public AbstractJwtFilter(JwtService jwtService) {
public AbstractJwtFilter(JwtService jwtService, String authorizationHeader) {
this.jwtService = jwtService;
this.authorizationHeader = authorizationHeader;
}

@Override
Expand Down Expand Up @@ -52,7 +54,7 @@ public void doFilter(
return;
}

String authorization = request.getHeader("Authorization");
String authorization = request.getHeader(authorizationHeader);
if (authorization == null) {
sendForbiddenError(response);
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@
public class ApiKeyFilter implements Filter {
private static final String OPTIONS = "OPTIONS";
private static final String APPLICATION_JSON_VALUE = "application/json";
private static final String HEADER_NAME = "X-Api-Key";
private static final Gson gson = GsonUtils.builder().setPrettyPrinting().create();
private final String apiKey;
private final String authorizationHeader;

public ApiKeyFilter(@NotNull String apiKey) {
public ApiKeyFilter(@NotNull String apiKey, String authorizationHeader) {
this.apiKey = apiKey;
this.authorizationHeader = authorizationHeader;
}

@Override
Expand Down Expand Up @@ -53,7 +54,7 @@ public void doFilter(
return;
}

String gotApiKey = request.getHeader(HEADER_NAME);
String gotApiKey = request.getHeader(authorizationHeader);
if (!apiKey.equals(gotApiKey)) {
sendForbiddenError(response);
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@

public class CustodyAuthJwtFilter extends AbstractJwtFilter {

public CustodyAuthJwtFilter(JwtService jwtService) {
super(jwtService);
public CustodyAuthJwtFilter(JwtService jwtService, String authorizationHeader) {
super(jwtService, authorizationHeader);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
import org.stellar.anchor.auth.JwtService;

public class PlatformAuthJwtFilter extends AbstractJwtFilter {
public PlatformAuthJwtFilter(JwtService jwtService) {
super(jwtService);
public PlatformAuthJwtFilter(JwtService jwtService, String authorizationHeader) {
super(jwtService, authorizationHeader);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@

public class Sep10JwtFilter extends AbstractJwtFilter {
public Sep10JwtFilter(JwtService jwtService) {
super(jwtService);
// SEP-10 tokens are passed in the Authorization header.
super(jwtService, "Authorization");
}

@Override
Expand Down
39 changes: 28 additions & 11 deletions core/src/test/kotlin/org/stellar/anchor/auth/AuthHelperTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ package org.stellar.anchor.auth
import io.mockk.*
import java.time.Instant
import java.util.*
import java.util.stream.Stream
import kotlin.test.assertEquals
import kotlin.test.assertNull
import org.junit.jupiter.api.Order
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.EnumSource
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.MethodSource
import org.stellar.anchor.auth.ApiAuthJwt.*
import org.stellar.anchor.auth.AuthType.*
import org.stellar.anchor.lockAndMockStatic
Expand All @@ -17,11 +19,25 @@ import org.stellar.anchor.util.AuthHeader
class AuthHelperTest {
companion object {
const val JWT_EXPIRATION_MILLISECONDS: Long = 90000

@JvmStatic
fun authHelperTests(): Stream<Arguments> {
return Stream.of(
Arguments.of(JWT, "Authorization"),
Arguments.of(JWT, "Custom_Authorization"),
Arguments.of(API_KEY, "Authorization"),
Arguments.of(API_KEY, "Custom_Authorization"),
Arguments.of(NONE, null),
)
}
}

@ParameterizedTest
@EnumSource(AuthType::class)
fun `test AuthHeader creation based on the AuthType`(authType: AuthType) {
@MethodSource("authHelperTests")
fun `test AuthHeader creation with different AuthType and authorization headers`(
authType: AuthType,
headerName: String?,
) {
lockAndMockStatic(Calendar::class) {
val calendarSingleton = mockk<Calendar>(relaxed = true)
// Mock calendar to guarantee the jwt token format
Expand All @@ -37,36 +53,37 @@ class AuthHelperTest {
val wantPlatformJwt =
PlatformAuthJwt(
currentTimeMilliseconds / 1000L,
(currentTimeMilliseconds + JWT_EXPIRATION_MILLISECONDS) / 1000L
(currentTimeMilliseconds + JWT_EXPIRATION_MILLISECONDS) / 1000L,
)
val wantCallbackJwt =
CallbackAuthJwt(
currentTimeMilliseconds / 1000L,
(currentTimeMilliseconds + JWT_EXPIRATION_MILLISECONDS) / 1000L
(currentTimeMilliseconds + JWT_EXPIRATION_MILLISECONDS) / 1000L,
)
val wantCustodyJwt =
CustodyAuthJwt(
currentTimeMilliseconds / 1000L,
(currentTimeMilliseconds + JWT_EXPIRATION_MILLISECONDS) / 1000L
(currentTimeMilliseconds + JWT_EXPIRATION_MILLISECONDS) / 1000L,
)

val jwtService = JwtService(null, null, null, "secret", "secret", "secret")
val authHelper = AuthHelper.forJwtToken(jwtService, JWT_EXPIRATION_MILLISECONDS)
val authHelper =
AuthHelper.forJwtToken(headerName, jwtService, JWT_EXPIRATION_MILLISECONDS)
val gotPlatformAuthHeader = authHelper.createPlatformServerAuthHeader()
val wantPlatformAuthHeader =
AuthHeader("Authorization", "Bearer ${jwtService.encode(wantPlatformJwt)}")
AuthHeader(headerName, "Bearer ${jwtService.encode(wantPlatformJwt)}")
assertEquals(wantPlatformAuthHeader, gotPlatformAuthHeader)
val gotCallbackAuthHeader = authHelper.createCallbackAuthHeader()
val wantCallbackAuthHeader =
AuthHeader("Authorization", "Bearer ${jwtService.encode(wantCallbackJwt)}")
AuthHeader(headerName, "Bearer ${jwtService.encode(wantCallbackJwt)}")
assertEquals(wantCallbackAuthHeader, gotCallbackAuthHeader)
val gotCustodyAuthHeader = authHelper.createCustodyAuthHeader()
val wantCustodyAuthHeader =
AuthHeader("Authorization", "Bearer ${jwtService.encode(wantCustodyJwt)}")
AuthHeader(headerName, "Bearer ${jwtService.encode(wantCustodyJwt)}")
assertEquals(wantCustodyAuthHeader, gotCustodyAuthHeader)
}
API_KEY -> {
val authHelper = AuthHelper.forApiKey("secret")
val authHelper = AuthHelper.forApiKey("X-Api-Key", "secret")
val gotPlatformAuthHeader = authHelper.createPlatformServerAuthHeader()
val wantPlatformAuthHeader = AuthHeader("X-Api-Key", "secret")
assertEquals(wantPlatformAuthHeader, gotPlatformAuthHeader)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package org.stellar.anchor.filter

import io.mockk.Called
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import javax.servlet.FilterChain
import javax.servlet.ServletResponse
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
import org.apache.http.HttpStatus
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource
import org.stellar.anchor.auth.JwtService
import org.stellar.anchor.config.CustodySecretConfig
import org.stellar.anchor.config.SecretConfig

class AbstractJwtFilterTest {
private lateinit var jwtService: JwtService

@BeforeEach
fun setup() {
val secretConfig = mockk<SecretConfig>(relaxed = true)
val custodySecretConfig = mockk<CustodySecretConfig>(relaxed = true)
every { secretConfig.sep10JwtSecretKey } returns "secret"
this.jwtService = JwtService(secretConfig, custodySecretConfig)
}

@ParameterizedTest
@ValueSource(strings = ["GET", "PUT", "POST", "DELETE"])
fun `make sure FORBIDDEN is returned when the filter requires header names other than Authorization`(
method: String
) {
val request = mockk<HttpServletRequest>(relaxed = true)
val response = mockk<HttpServletResponse>(relaxed = true)
val filterChain = mockk<FilterChain>(relaxed = true)

every { request.method } returns method
every { request.getHeader("Authorization") } returns "Authorization_Header_Value"
val filter =
object : AbstractJwtFilter(jwtService, "Authorization-custom") {
@Throws(Exception::class)
override fun check(
jwtCipher: String?,
request: HttpServletRequest,
servletResponse: ServletResponse?,
) {}
}

filter.doFilter(request, response, filterChain)
verify(exactly = 1) {
response.setStatus(HttpStatus.SC_FORBIDDEN)
response.contentType = Sep10JwtFilter.APPLICATION_JSON_VALUE
}
verify { filterChain wasNot Called }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ internal class ApiKeyFilterTest {
fun setup() {
this.request = mockk(relaxed = true)
this.response = mockk(relaxed = true)
this.apiKeyFilter = ApiKeyFilter(API_KEY)
this.apiKeyFilter = ApiKeyFilter(API_KEY, "X-Api-Key")
this.mockFilterChain = mockk(relaxed = true)
}

Expand Down Expand Up @@ -110,4 +110,23 @@ internal class ApiKeyFilterTest {

verify { mockFilterChain.doFilter(request, response) }
}

@ParameterizedTest
@ValueSource(strings = ["GET", "PUT", "POST", "DELETE"])
fun `make sure FORBIDDEN is returned when the filter requires header names other than X-Api-Key`(
method: String
) {
val filterChain = mockk<FilterChain>(relaxed = true)

every { request.method } returns method
every { request.getHeader("X-Api-Key") } returns API_KEY
apiKeyFilter = ApiKeyFilter(API_KEY, "X-Api-Key-custom")

apiKeyFilter.doFilter(request, response, filterChain)
verify(exactly = 1) {
response.setStatus(HttpStatus.SC_FORBIDDEN)
response.contentType = APPLICATION_JSON_VALUE
}
verify { filterChain wasNot Called }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class CallbackApiTests : AbstractIntegrationTests(TestConfig()) {
)

private val authHelper =
AuthHelper.forJwtToken(platformToAnchorJwtService, JWT_EXPIRATION_MILLISECONDS)
AuthHelper.forJwtToken("Authorization", platformToAnchorJwtService, JWT_EXPIRATION_MILLISECONDS)

private val gson: Gson = GsonUtils.getInstance()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,14 @@ public FilterRegistrationBean<Filter> platformToCustodyTokenFilter(
switch (custodyApiConfig.getAuth().getType()) {
case JWT:
JwtService jwtService = new JwtService(null, null, null, null, null, authSecret);
platformToCustody = new CustodyAuthJwtFilter(jwtService);
platformToCustody =
new CustodyAuthJwtFilter(
jwtService, custodyApiConfig.getAuth().getJwt().getHttpHeader());
break;

case API_KEY:
platformToCustody = new ApiKeyFilter(authSecret);
platformToCustody =
new ApiKeyFilter(authSecret, custodyApiConfig.getAuth().getApiKey().getHttpHeader());
break;

default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,13 @@ public FilterRegistrationBean<Filter> platformTokenFilter(PlatformServerConfig c
switch (config.getAuth().getType()) {
case JWT:
JwtService jwtService = new JwtService(null, null, null, null, authSecret, null);
anchorToPlatformFilter = new PlatformAuthJwtFilter(jwtService);
anchorToPlatformFilter =
new PlatformAuthJwtFilter(jwtService, config.getAuth().getJwt().getHttpHeader());
break;

case API_KEY:
anchorToPlatformFilter = new ApiKeyFilter(authSecret);
anchorToPlatformFilter =
new ApiKeyFilter(authSecret, config.getAuth().getApiKey().getHttpHeader());
break;

default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,12 @@ AuthHelper buildAuthHelper(CustodyApiConfig custodyApiConfig) {
switch (custodyApiConfig.getAuth().getType()) {
case JWT:
return AuthHelper.forJwtToken(
custodyApiConfig.getAuth().getJwt().getHttpHeader(),
new JwtService(null, null, null, null, null, authSecret),
Long.parseLong(custodyApiConfig.getAuth().getJwt().getExpirationMilliseconds()));
case API_KEY:
return AuthHelper.forApiKey(authSecret);
return AuthHelper.forApiKey(
custodyApiConfig.getAuth().getApiKey().getHttpHeader(), authSecret);
default:
return AuthHelper.forNone();
}
Expand Down
Loading

0 comments on commit de2ab6e

Please sign in to comment.