Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
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
Loading