Skip to content

Commit

Permalink
WWW-Authenticate resource parameter fails challenge verification even…
Browse files Browse the repository at this point in the history
… with custom hostname

- Uses default value of the LOWKEY_AUTH_RESOURCE property to keep existing logic in place
- Changes token endpoint to return WWW-Authenticate headers as well
- Allows configuration of the realm used by the token endpoint using LOWKEY_TOKEN_REALM
- Updates tests

Resolves #1149
{minor}

Signed-off-by: Esta Nagy <nagyesta@gmail.com>
  • Loading branch information
nagyesta committed Sep 21, 2024
1 parent 7e8185a commit 02f44d3
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 20 deletions.
22 changes: 19 additions & 3 deletions lowkey-vault-app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ Set `--server.port=<port>` as an argument as usual with Spring Boot apps:
java -jar lowkey-vault-app-<version>.jar --server.port=8443
```

## Challenge resource URI
### Overriding the challenge resource URI

The official Azure Key Vault clients verify the challenge resource URL returned by the server (see
[blog](https://devblogs.microsoft.com/azure-sdk/guidance-for-applications-using-the-key-vault-libraries/)). You can either set
Expand All @@ -104,8 +104,24 @@ The official Azure Key Vault clients verify the challenge resource URL returned
java -jar lowkey-vault-app-<version>.jar --LOWKEY_AUTH_RESOURCE="vault.azure.net"
```

You should be running the Lowkey Vault with a resolvable hostname as a subdomain of `vault.azure.net` (e.g. `lowkey.vault.azure.net`) and
have appropriate SSL certificates registered if you choose to configure the auth resource.
> [!NOTE]
> You should be running Lowkey Vault with a resolvable hostname as a subdomain of `vault.azure.net` (e.g. `lowkey.vault.azure.net`) and have appropriate SSL certificates registered if you choose to configure the auth resource.
> [!WARNING]
> This property is only intended to be used in case you absolutely cannot disable your challenge resource verification because it raises the complexity of your setup significantly and there are no guarantees that the clients will keep working with this workaround. Therefore, this is NOT recommended to be used. Please consider following [the official guidance](https://devblogs.microsoft.com/azure-sdk/guidance-for-applications-using-the-key-vault-libraries/) instead.
### Using the Token endpoint with a custom realm

By default, the Token endpoint includes the `WWW-Authenticate` response header with the `Basic realm=assumed-identity` value.
If you need to change the realm (for example because you are using Managed Identity authentication with the latest Python libraries)
you can use the `LOWKEY_TOKEN_REALM` configuration property to override it as seen in the example below:

```
java -jar lowkey-vault-app-<version>.jar --LOWKEY_TOKEN_REALM="local"
```

Using the configuration above, the value of the response header would change to `Basic realm=local`.


### Importing vault content at startup

Expand Down
2 changes: 1 addition & 1 deletion lowkey-vault-app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ test {
useJUnitPlatform()
systemProperty("junit.jupiter.extensions.autodetection.enabled", true)
systemProperty("junit.jupiter.execution.parallel.enabled", true)
systemProperty("junit.jupiter.execution.parallel.mode.default", "concurrent")
systemProperty("junit.jupiter.execution.parallel.mode.default", "same_thread")
systemProperty("junit.jupiter.execution.parallel.mode.classes.default", "concurrent")
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package com.github.nagyesta.lowkeyvault.controller;

import com.github.nagyesta.lowkeyvault.model.TokenResponse;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
Expand All @@ -13,10 +16,19 @@
@RestController
public class ManagedIdentityTokenController {

private final String tokenRealm;

public ManagedIdentityTokenController(
@NonNull @Value("${LOWKEY_TOKEN_REALM:assumed-identity}") final String tokenRealm) {
this.tokenRealm = tokenRealm;
}

@GetMapping(value = {"/metadata/identity/oauth2/token", "/metadata/identity/oauth2/token/"})
public ResponseEntity<TokenResponse> get(@RequestParam("resource") final URI resource) {
final TokenResponse body = new TokenResponse(resource);
log.info("Returning token: {}", body);
return ResponseEntity.ok(body);
return ResponseEntity.ok()
.header(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=" + tokenRealm)
.body(body);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,24 @@

import java.io.IOException;
import java.net.URI;
import java.util.Optional;
import java.util.Set;

@Component
@Slf4j
public class CommonAuthHeaderFilter extends OncePerRequestFilter {

static final String OMIT_DEFAULT = "";
private static final int DEFAULT_HTTPS_PORT = 443;
private static final String OMIT_DEFAULT = "";
private static final String PORT_SEPARATOR = ":";
private static final String HTTPS = "https://";
private static final String BEARER_FAKE_TOKEN = "Bearer resource=\"%s\", authorization_uri=\"%s\"";
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
private final Set<String> skipUrisIfMatch = Set.of("/ping", "/management/**", "/api/**", "/metadata/**");
private String authResource;
private final String authResource;

CommonAuthHeaderFilter(@Value("${LOWKEY_AUTH_RESOURCE:localhost}") final String authResource) {
public CommonAuthHeaderFilter(
@lombok.NonNull @Value("${LOWKEY_AUTH_RESOURCE:}") final String authResource) {
this.authResource = authResource;
}

Expand All @@ -43,8 +45,12 @@ protected void doFilterInternal(final HttpServletRequest request, final HttpServ
final String port = resolvePort(request.getServerPort());
final URI baseUri = URI.create(HTTPS + request.getServerName() + port);
request.setAttribute(ApiConstants.REQUEST_BASE_URI, baseUri);
final URI authResourceUri = Optional.of(authResource)
.filter(anObject -> !OMIT_DEFAULT.equals(anObject))
.map(res -> URI.create(HTTPS + res))
.orElse(baseUri);
response.setHeader(HttpHeaders.WWW_AUTHENTICATE,
String.format(BEARER_FAKE_TOKEN, URI.create(HTTPS + authResource), baseUri + request.getRequestURI()));
String.format(BEARER_FAKE_TOKEN, authResourceUri, baseUri + request.getRequestURI()));
if (!StringUtils.hasText(request.getHeader(HttpHeaders.AUTHORIZATION))) {
log.info("Sending token to client without processing payload: {}", request.getRequestURI());
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,20 @@
import com.github.nagyesta.lowkeyvault.model.TokenResponse;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

import java.net.URI;
import java.util.List;

class TokenControllerTest {

@Test
void testGetShouldReturnTokenWhenCalled() {
//given
final ManagedIdentityTokenController underTest = new ManagedIdentityTokenController();
final String tokenRealm = "realm-name";
final ManagedIdentityTokenController underTest = new ManagedIdentityTokenController(tokenRealm);
final URI resource = URI.create("https://localhost:8443/");

//when
Expand All @@ -26,5 +29,16 @@ void testGetShouldReturnTokenWhenCalled() {
Assertions.assertNotNull(actual.getBody());
Assertions.assertEquals(resource, actual.getBody().resource());
Assertions.assertEquals("dummy", actual.getBody().accessToken());
Assertions.assertEquals(List.of("Basic realm=" + tokenRealm), actual.getHeaders().get(HttpHeaders.WWW_AUTHENTICATE));
}

@Test
void testConstructorShouldThrowExceptionWhenCalledWithNull() {
//given

//when
Assertions.assertThrows(IllegalArgumentException.class, () -> new ManagedIdentityTokenController(null));

//then + exception
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,8 @@ void tearDown() throws Exception {
@ValueSource(strings = {EMPTY, HEADER_VALUE})
void testDoFilterInternalShouldNotCallNextOnChainWhenAuthorizationHeaderMissing(final String headerValue)
throws ServletException, IOException {
final CommonAuthHeaderFilter underTest = new CommonAuthHeaderFilter(LOCALHOST);

//given
final CommonAuthHeaderFilter underTest = new CommonAuthHeaderFilter(CommonAuthHeaderFilter.OMIT_DEFAULT);
when(request.getHeader(eq(HttpHeaders.AUTHORIZATION))).thenReturn(headerValue);

//when
Expand All @@ -102,9 +101,8 @@ void testDoFilterInternalShouldNotCallNextOnChainWhenAuthorizationHeaderMissing(
@ValueSource(strings = {EMPTY, HEADER_VALUE})
void testDoFilterInternalShouldAddTokenToResponseHeaderWhenCalled(final String headerValue)
throws ServletException, IOException {
final CommonAuthHeaderFilter underTest = new CommonAuthHeaderFilter(LOCALHOST);

//given
final CommonAuthHeaderFilter underTest = new CommonAuthHeaderFilter(CommonAuthHeaderFilter.OMIT_DEFAULT);
when(request.getHeader(eq(HttpHeaders.AUTHORIZATION))).thenReturn(headerValue);

//when
Expand All @@ -118,9 +116,8 @@ void testDoFilterInternalShouldAddTokenToResponseHeaderWhenCalled(final String h
@MethodSource("authResourceProvider")
void testDoFilterInternalShouldSetResourceOnResponseHeaderWhenCalled(final String authResource, final URI expected)
throws ServletException, IOException {
final CommonAuthHeaderFilter underTest = new CommonAuthHeaderFilter(authResource);

//given
final CommonAuthHeaderFilter underTest = new CommonAuthHeaderFilter(authResource);
when(request.getHeader(eq(HttpHeaders.AUTHORIZATION))).thenReturn(HEADER_VALUE);

//when
Expand All @@ -134,9 +131,8 @@ void testDoFilterInternalShouldSetResourceOnResponseHeaderWhenCalled(final Strin
@MethodSource("hostAndPortProvider")
void testDoFilterInternalShouldSetRequestBaseUriRequestAttributeWhenCalled(
final String hostName, final int port, final String path, final URI expected) throws ServletException, IOException {
final CommonAuthHeaderFilter underTest = new CommonAuthHeaderFilter(LOCALHOST);

//given
final CommonAuthHeaderFilter underTest = new CommonAuthHeaderFilter(CommonAuthHeaderFilter.OMIT_DEFAULT);
when(request.getServerName()).thenReturn(hostName);
when(request.getServerPort()).thenReturn(port);
when(request.getRequestURI()).thenReturn(path);
Expand All @@ -153,9 +149,8 @@ void testDoFilterInternalShouldSetRequestBaseUriRequestAttributeWhenCalled(

@Test
void testShouldNotFilterShouldReturnTrueWhenRequestBaseUriIsPing() {
final CommonAuthHeaderFilter underTest = new CommonAuthHeaderFilter(LOCALHOST);

//given
final CommonAuthHeaderFilter underTest = new CommonAuthHeaderFilter(CommonAuthHeaderFilter.OMIT_DEFAULT);
when(request.getRequestURI()).thenReturn("/ping");

//when
Expand All @@ -165,4 +160,14 @@ void testShouldNotFilterShouldReturnTrueWhenRequestBaseUriIsPing() {
Assertions.assertTrue(actual);
verify(request, atLeastOnce()).getRequestURI();
}

@Test
void testConstructorShouldThrowExceptionWhenCalledWithNull() {
//given

//when
Assertions.assertThrows(IllegalArgumentException.class, () -> new CommonAuthHeaderFilter(null));

//then + exception
}
}

0 comments on commit 02f44d3

Please sign in to comment.