-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Assumed Identity endpoint in Lowkey Vault (#972)
- Adds configuration to listen to the 8080 HTTP port for token requests - Adds new ManagedIdentityTokenController to handle token requests - Adds filter to enforce functional separation between the 8080 and 8443 ports - Extends Testcontainers support with token endpoint related configuration - Adds new tests - Updates documentation Updates #960 {minor} Signed-off-by: Esta Nagy <nagyesta@gmail.com>
- Loading branch information
Showing
19 changed files
with
439 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
22 changes: 22 additions & 0 deletions
22
.../main/java/com/github/nagyesta/lowkeyvault/controller/ManagedIdentityTokenController.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package com.github.nagyesta.lowkeyvault.controller; | ||
|
||
import com.github.nagyesta.lowkeyvault.model.TokenResponse; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.web.bind.annotation.GetMapping; | ||
import org.springframework.web.bind.annotation.RequestParam; | ||
import org.springframework.web.bind.annotation.RestController; | ||
|
||
import java.net.URI; | ||
|
||
@Slf4j | ||
@RestController | ||
public class ManagedIdentityTokenController { | ||
|
||
@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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
32 changes: 32 additions & 0 deletions
32
...-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/filter/PortSeparationFilter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package com.github.nagyesta.lowkeyvault.filter; | ||
|
||
import jakarta.servlet.FilterChain; | ||
import jakarta.servlet.ServletException; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.stereotype.Component; | ||
import org.springframework.web.filter.OncePerRequestFilter; | ||
|
||
import java.io.IOException; | ||
|
||
@Component | ||
@Slf4j | ||
public class PortSeparationFilter extends OncePerRequestFilter { | ||
|
||
@Override | ||
protected void doFilterInternal( | ||
final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain) | ||
throws ServletException, IOException { | ||
final var secure = request.isSecure(); | ||
final boolean isTokenRequest = "/metadata/identity/oauth2/token".equals(request.getRequestURI()); | ||
final boolean unsecureTokenRequest = isTokenRequest && !secure; | ||
final boolean secureVaultRequest = !isTokenRequest && secure; | ||
if (unsecureTokenRequest || secureVaultRequest) { | ||
filterChain.doFilter(request, response); | ||
} else { | ||
response.sendError(HttpServletResponse.SC_NOT_FOUND); | ||
} | ||
} | ||
|
||
} |
26 changes: 26 additions & 0 deletions
26
lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/TokenResponse.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package com.github.nagyesta.lowkeyvault.model; | ||
|
||
import com.fasterxml.jackson.annotation.JsonProperty; | ||
import jakarta.validation.constraints.NotNull; | ||
import lombok.NonNull; | ||
import org.springframework.util.Assert; | ||
|
||
import java.net.URI; | ||
import java.time.Instant; | ||
|
||
public record TokenResponse( | ||
@NonNull @JsonProperty("resource") URI resource, | ||
@NonNull @JsonProperty("access_token") String accessToken, | ||
@NonNull @JsonProperty("refresh_token") String refreshToken, | ||
@JsonProperty("expires_in") long expiresIn, | ||
@JsonProperty("expires_on") long expiresOn, | ||
@JsonProperty("tokenType") int tokenType) { | ||
|
||
private static final long EXPIRES_IN = 48 * 3600L; | ||
private static final String TOKEN = "dummy"; | ||
|
||
public TokenResponse(@NotNull final URI resource) { | ||
this(resource, TOKEN, TOKEN, EXPIRES_IN, Instant.now().plusSeconds(EXPIRES_IN).getEpochSecond(), 1); | ||
Assert.hasText(resource.toString(), "Resource must not be empty"); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
30 changes: 30 additions & 0 deletions
30
.../src/test/java/com/github/nagyesta/lowkeyvault/controller/common/TokenControllerTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
package com.github.nagyesta.lowkeyvault.controller.common; | ||
|
||
import com.github.nagyesta.lowkeyvault.controller.ManagedIdentityTokenController; | ||
import com.github.nagyesta.lowkeyvault.model.TokenResponse; | ||
import org.junit.jupiter.api.Assertions; | ||
import org.junit.jupiter.api.Test; | ||
import org.springframework.http.HttpStatus; | ||
import org.springframework.http.ResponseEntity; | ||
|
||
import java.net.URI; | ||
|
||
class TokenControllerTest { | ||
|
||
@Test | ||
void testGetShouldReturnTokenWhenCalled() { | ||
//given | ||
final ManagedIdentityTokenController underTest = new ManagedIdentityTokenController(); | ||
final URI resource = URI.create("https://localhost:8443/"); | ||
|
||
//when | ||
final ResponseEntity<TokenResponse> actual = underTest.get(resource); | ||
|
||
//then | ||
Assertions.assertNotNull(actual); | ||
Assertions.assertEquals(HttpStatus.OK, actual.getStatusCode()); | ||
Assertions.assertNotNull(actual.getBody()); | ||
Assertions.assertEquals(resource, actual.getBody().resource()); | ||
Assertions.assertEquals("dummy", actual.getBody().accessToken()); | ||
} | ||
} |
83 changes: 83 additions & 0 deletions
83
...lt-app/src/test/java/com/github/nagyesta/lowkeyvault/filter/PortSeparationFilterTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
package com.github.nagyesta.lowkeyvault.filter; | ||
|
||
import jakarta.servlet.FilterChain; | ||
import jakarta.servlet.ServletException; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
import org.junit.jupiter.api.Test; | ||
|
||
import java.io.IOException; | ||
|
||
import static org.mockito.ArgumentMatchers.same; | ||
import static org.mockito.Mockito.*; | ||
|
||
class PortSeparationFilterTest { | ||
|
||
@Test | ||
void testDoFilterInternalShouldCallChainWhenInsecureAndTokenRequest() throws ServletException, IOException { | ||
//given | ||
final PortSeparationFilter underTest = new PortSeparationFilter(); | ||
final HttpServletRequest request = mock(HttpServletRequest.class); | ||
final HttpServletResponse response = mock(HttpServletResponse.class); | ||
final FilterChain chain = mock(FilterChain.class); | ||
when(request.isSecure()).thenReturn(false); | ||
when(request.getRequestURI()).thenReturn("/metadata/identity/oauth2/token"); | ||
|
||
//when | ||
underTest.doFilterInternal(request, response, chain); | ||
|
||
//then | ||
verify(chain).doFilter(same(request), same(response)); | ||
} | ||
|
||
@Test | ||
void testDoFilterInternalShouldCallChainWhenSecureAndNotTokenRequest() throws ServletException, IOException { | ||
//given | ||
final PortSeparationFilter underTest = new PortSeparationFilter(); | ||
final HttpServletRequest request = mock(HttpServletRequest.class); | ||
final HttpServletResponse response = mock(HttpServletResponse.class); | ||
final FilterChain chain = mock(FilterChain.class); | ||
when(request.isSecure()).thenReturn(true); | ||
when(request.getRequestURI()).thenReturn("/ping"); | ||
|
||
//when | ||
underTest.doFilterInternal(request, response, chain); | ||
|
||
//then | ||
verify(chain).doFilter(same(request), same(response)); | ||
} | ||
|
||
@Test | ||
void testDoFilterInternalShouldReturnNotFoundWhenSecureAndTokenRequest() throws ServletException, IOException { | ||
//given | ||
final PortSeparationFilter underTest = new PortSeparationFilter(); | ||
final HttpServletRequest request = mock(HttpServletRequest.class); | ||
final HttpServletResponse response = mock(HttpServletResponse.class); | ||
final FilterChain chain = mock(FilterChain.class); | ||
when(request.isSecure()).thenReturn(true); | ||
when(request.getRequestURI()).thenReturn("/metadata/identity/oauth2/token"); | ||
|
||
//when | ||
underTest.doFilterInternal(request, response, chain); | ||
|
||
//then | ||
verify(response).sendError(HttpServletResponse.SC_NOT_FOUND); | ||
} | ||
|
||
@Test | ||
void testDoFilterInternalShouldReturnNotFoundWhenInsecureAndNotTokenRequest() throws ServletException, IOException { | ||
//given | ||
final PortSeparationFilter underTest = new PortSeparationFilter(); | ||
final HttpServletRequest request = mock(HttpServletRequest.class); | ||
final HttpServletResponse response = mock(HttpServletResponse.class); | ||
final FilterChain chain = mock(FilterChain.class); | ||
when(request.isSecure()).thenReturn(false); | ||
when(request.getRequestURI()).thenReturn("/ping"); | ||
|
||
//when | ||
underTest.doFilterInternal(request, response, chain); | ||
|
||
//then | ||
verify(response).sendError(HttpServletResponse.SC_NOT_FOUND); | ||
} | ||
} |
85 changes: 85 additions & 0 deletions
85
lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/model/TokenResponseTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
package com.github.nagyesta.lowkeyvault.model; | ||
|
||
import org.junit.jupiter.api.Assertions; | ||
import org.junit.jupiter.api.Test; | ||
import org.junit.jupiter.params.ParameterizedTest; | ||
import org.junit.jupiter.params.provider.Arguments; | ||
import org.junit.jupiter.params.provider.MethodSource; | ||
|
||
import java.net.URI; | ||
import java.time.Instant; | ||
import java.util.stream.Stream; | ||
|
||
class TokenResponseTest { | ||
|
||
private static final int EXPECTED_EXPIRY = 48 * 3600; | ||
private static final long MIN_EXPIRES_ON = Instant.now().plusSeconds(EXPECTED_EXPIRY).getEpochSecond(); | ||
private static final int TOKEN_TYPE = 1; | ||
private static final String DUMMY_TOKEN = "dummy"; | ||
private static final URI RESOURCE = URI.create("https://localhost:8443/path"); | ||
|
||
public static Stream<Arguments> nullValuesProvider() { | ||
final URI resource = RESOURCE; | ||
final String token = DUMMY_TOKEN; | ||
final long expiresIn = EXPECTED_EXPIRY; | ||
final long expiresOn = MIN_EXPIRES_ON; | ||
final int tokenType = TOKEN_TYPE; | ||
|
||
return Stream.of( | ||
Arguments.of(null, token, token, expiresIn, expiresOn, tokenType), | ||
Arguments.of(resource, null, token, expiresIn, expiresOn, tokenType), | ||
Arguments.of(resource, token, null, expiresIn, expiresOn, tokenType) | ||
); | ||
} | ||
|
||
@Test | ||
void testConstructorShouldThrowExceptionWhenCalledWithNull() { | ||
//given | ||
|
||
//when | ||
Assertions.assertThrows(IllegalArgumentException.class, () -> new TokenResponse(null)); | ||
|
||
//then + expected | ||
} | ||
|
||
@Test | ||
void testConstructorShouldThrowExceptionWhenCalledWithEmptyResource() { | ||
//given | ||
final URI resource = URI.create(""); | ||
|
||
//when | ||
Assertions.assertThrows(IllegalArgumentException.class, () -> new TokenResponse(resource)); | ||
|
||
//then + expected | ||
} | ||
|
||
@ParameterizedTest | ||
@MethodSource("nullValuesProvider") | ||
void testConstructorShouldThrowExceptionWhenCalledWithNulls( | ||
final URI resource, final String accessToken, final String refreshToken, | ||
final long expiresIn, final long expiresOn, final int tokenType) { | ||
//given | ||
|
||
//when | ||
Assertions.assertThrows(IllegalArgumentException.class, | ||
() -> new TokenResponse(resource, accessToken, refreshToken, expiresIn, expiresOn, tokenType)); | ||
|
||
//then + expected | ||
} | ||
|
||
@Test | ||
void testConstructorShouldReturnNonExpiredTokenValidForTheProvidedResourceWhenCalledWithValidResource() { | ||
//given | ||
|
||
//when | ||
final TokenResponse actual = new TokenResponse(RESOURCE); | ||
|
||
//then | ||
Assertions.assertEquals(RESOURCE, actual.resource()); | ||
Assertions.assertEquals(DUMMY_TOKEN, actual.accessToken()); | ||
Assertions.assertEquals(DUMMY_TOKEN, actual.refreshToken()); | ||
Assertions.assertEquals(EXPECTED_EXPIRY, actual.expiresIn()); | ||
Assertions.assertTrue(MIN_EXPIRES_ON <= actual.expiresOn()); | ||
Assertions.assertEquals(TOKEN_TYPE, actual.tokenType()); | ||
} | ||
} |
1 change: 1 addition & 0 deletions
1
lowkey-vault-app/src/test/resources/test-application.properties
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
LOWKEY_DEBUG_REQUEST_LOG=true | ||
app.token.port=8080 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.