Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api-catalog-services/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ dependencies {
testImplementation libs.spring.boot.starter.test
testImplementation libs.spring.mock.mvc
testImplementation(testFixtures(project(":apiml-common")))
testImplementation(testFixtures(project(":apiml-security-common")))
testImplementation libs.reactor.test

compileOnly libs.lombok
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -348,15 +348,15 @@ WebFilter oidcAuthenticationFilter(
Optional.ofNullable(exchange.getRequest().getHeaders().getFirst(HEADER_OIDC_TOKEN))
.map(token -> {
try {
return Map.entry(token, gatewaySecurity.verifyOidc(token));
var tokenAuthentication = gatewaySecurity.verifyOidc(token);
tokenAuthentication.setAuthenticated(true);
return tokenAuthentication;
} catch (Exception e) {
log.debug("Cannot verify OIDC token: {}", token, e);
return null;
}
})
.map(pair -> ReactiveSecurityContextHolder.withAuthentication(
createAuthenticated(pair.getValue().getUserId(), pair.getKey(), TokenAuthentication.Type.OIDC)
))
.map(ReactiveSecurityContextHolder::withAuthentication)
.orElse(context)
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.springframework.web.server.WebFilterChain;
import org.zowe.apiml.security.common.config.AuthConfigurationProperties;
import org.zowe.apiml.security.common.token.TokenAuthentication;
import org.zowe.apiml.security.common.util.JWTTestUtils;
import reactor.test.StepVerifier;

import static org.junit.jupiter.api.Assertions.*;
Expand All @@ -31,8 +32,9 @@ class ApiCatalogLogoutSuccessHandlerTest {

@Test
void testOnLogoutSuccess() {
var token = JWTTestUtils.createDummyAPIMLToken("user");
var request = MockServerHttpRequest.get("/logout")
.header(HttpHeaders.AUTHORIZATION, "Bearer token123")
.header(HttpHeaders.AUTHORIZATION, "Bearer %s".formatted(token))
.build();
var exchange = MockServerWebExchange.from(request);
WebFilterChain mockChain = mock(WebFilterChain.class);
Expand All @@ -43,7 +45,7 @@ void testOnLogoutSuccess() {

StepVerifier.create(apiCatalogLogoutSuccessHandler.onLogoutSuccess(
webFilterExchange,
new TokenAuthentication("TEST_TOKEN_STRING")
new TokenAuthentication(token)
))
.verifyComplete();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.test.util.ReflectionTestUtils;
import org.zowe.apiml.security.common.token.TokenAuthentication;
import org.zowe.apiml.security.common.util.JWTTestUtils;

import java.io.IOException;
import java.nio.file.FileAlreadyExistsException;
Expand All @@ -44,7 +45,7 @@ class WhenStaticDefinitionGenerationResponse {

@BeforeEach
void setUp() {
TokenAuthentication authentication = new TokenAuthentication("token");
TokenAuthentication authentication = new TokenAuthentication(JWTTestUtils.createDummyAPIMLToken("user"));
authentication.setAuthenticated(true);
SecurityContextHolder.setContext(new SecurityContextImpl(authentication));
ReflectionTestUtils.setField(staticDefinitionGenerator, "staticApiDefinitionsDirectories", configFileLocation);
Expand Down Expand Up @@ -115,7 +116,7 @@ class WhenStaticDefinitionOverrideResponse {

@BeforeEach
void setUp() {
TokenAuthentication authentication = new TokenAuthentication("token");
TokenAuthentication authentication = new TokenAuthentication(JWTTestUtils.createDummyAPIMLToken("user"));
authentication.setAuthenticated(true);
SecurityContextHolder.setContext(new SecurityContextImpl(authentication));
ReflectionTestUtils.setField(staticDefinitionGenerator, "staticApiDefinitionsDirectories", "../config/local/api-defs");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,12 @@ protected boolean shouldNotFilter(HttpServletRequest request) {
*/
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException {
Optional<AbstractAuthenticationToken> authenticationToken = extractContent(request);
var authenticationToken = Optional.<AbstractAuthenticationToken>empty();
try {
authenticationToken = extractContent(request);
} catch (AuthenticationException authenticationException) {
failureHandler.onAuthenticationFailure(request, response, authenticationException);
}

if (authenticationToken.isPresent()) {
Authentication authentication = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import org.zowe.apiml.cache.EntryExpiration;
import org.zowe.apiml.util.UrlUtils;

import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
import java.util.List;

Expand All @@ -27,7 +29,10 @@
@Data
@AllArgsConstructor
@NoArgsConstructor
public class QueryResponse implements EntryExpiration {
public class QueryResponse implements EntryExpiration, Serializable {
Comment thread
richard-salac marked this conversation as resolved.

@Serial
private static final long serialVersionUID = 4282686067850298800L;

private String domain;
private String userId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,84 +10,155 @@

package org.zowe.apiml.security.common.token;

import com.nimbusds.jwt.*;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.zowe.apiml.security.common.util.JwtUtils;
import org.zowe.apiml.security.common.login.LoginFilter;

import java.util.Collections;
import java.util.Optional;
import java.io.Serial;
import java.text.ParseException;
import java.util.*;

/**
* This object is added to security context after successful authentication.
* Contains username and valid JWT token.
*/
@EqualsAndHashCode(callSuper = false)
@EqualsAndHashCode(callSuper = false, onlyExplicitlyIncluded = true)
@Slf4j
public class TokenAuthentication extends AbstractAuthenticationToken {

private static final long serialVersionUID = 9187160928171618141L;
@Serial
private static final long serialVersionUID = 82346593850419807L;

private final String username;
private final String token;
private static final String DOMAIN_CLAIM_NAME = "dom";
private static final String SCOPES = "scopes";

@Getter
private final JWT jwt;
private final JWTClaimsSet claims;
@Getter
private final QueryResponse queryResponse;
@Getter
private Type type;
private final Type type;

public TokenAuthentication(String token) {
this(token, (Type) null);
public enum Type {
JWT,
OIDC
}

public TokenAuthentication(String token, Type type) {
this(null, token, type);
public TokenAuthentication(String tokenString, Type type) {
super(Collections.emptyList());

try {
this.jwt = JWTParser.parse(tokenString);
this.claims = jwt.getJWTClaimsSet();
this.queryResponse = parseQueryResponse(claims);
this.type = type;
} catch (ParseException ex) {
throw JwtUtils.handleJwtParserException(ex);
}
}

public TokenAuthentication(String username, String token) {
this(username, token, (Type) null);
public TokenAuthentication(String tokenString) {
this(tokenString, Type.JWT);
}

public TokenAuthentication(String username, String token, Type type) {
super(Collections.emptyList());
this.username = username;
this.token = token;
this.type = type;
public TokenAuthentication(String userId, String tokenString, Type type) {
this(tokenString, type);
checkUserId(userId);
}

public static TokenAuthentication createAuthenticated(String tokenString, Type type) {
var tokenAuthentication = new TokenAuthentication(tokenString, type);
tokenAuthentication.setAuthenticated(true);
return tokenAuthentication;
}

public static TokenAuthentication createAuthenticated(String userId, String token, Type type) {
var tokenAuthentication = new TokenAuthentication(userId, token, type);
tokenAuthentication.setAuthenticated(true);
return tokenAuthentication;
}

@SuppressWarnings("squid:S3655")
public static TokenAuthentication createAuthenticatedFromHeader(String token, String authHeader) {
var loginRequest = LoginFilter.getCredentialFromAuthorizationHeader(Optional.of(authHeader));
return createAuthenticated(loginRequest.get().getUsername(), token, Type.JWT);
}

public boolean isExpired() {
return queryResponse.isExpired();
}

public Date getExpiration() {
return queryResponse.getExpiration();
}

public QueryResponse.Source getSource() {
return queryResponse.getSource();
}

public String getClaimAsString(String claimName) throws ParseException {
return claims.getClaimAsString(claimName);
}

/**
* @return the token that prove the username is correct
*/
@Override
@EqualsAndHashCode.Include
public String getCredentials() {
return token;
return jwt.getParsedString();
}

/**
* @return the username being authenticated
*/
@Override
public String getPrincipal() {
return username;
return queryResponse.getUserId();
}

/**
* Creates the TokenAuthentication with fulfilled username (principal), token and marked as authenticated.
* @param username Username, who is authenticated
* @param token Token, which authenticate the user
* @return TokenAuthentication marked as authenticated with username, token
*/
public static TokenAuthentication createAuthenticated(String username, String token, Type type) {
final TokenAuthentication out = new TokenAuthentication(username, token, type);
out.setAuthenticated(true);
return out;
@Override
public void setAuthenticated(boolean authenticated) {
if (authenticated && isExpired()) {
throw new TokenExpireException(
"Unable to set authentication as true because the token ...%s expired on %s"
.formatted(StringUtils.right(jwt.getParsedString(), 15), getExpiration()));
}
super.setAuthenticated(authenticated);
}

@SuppressWarnings("squid:S3655")
public static TokenAuthentication createAuthenticatedFromHeader(String token, String authHeader) {
var loginRequest = LoginFilter.getCredentialFromAuthorizationHeader(Optional.of(authHeader));
return createAuthenticated(loginRequest.get().getUsername(), token, Type.JWT);
private QueryResponse parseQueryResponse(JWTClaimsSet claims) {
Object scopesObject = claims.getClaim(SCOPES);
List<String> scopes = Collections.emptyList();
if (scopesObject instanceof List<?>) {
scopes = (List<String>) scopesObject;
}
try {
return new QueryResponse(
claims.getClaimAsString(DOMAIN_CLAIM_NAME),
claims.getSubject(),
claims.getIssueTime(),
claims.getExpirationTime(),
claims.getIssuer(),
scopes,
QueryResponse.Source.valueByIssuer(claims.getIssuer())
);
} catch (ParseException e) {
throw new TokenNotValidException(e.getMessage(), e);
}
}

public enum Type {
JWT,
OIDC
private void checkUserId(String userId) {
var principal = getPrincipal();
if (userId == null || !userId.equalsIgnoreCase(principal)) {
log.debug("Username '{}' does not match the one in token '{}' or is null", userId, principal);
throw new TokenNotValidException("Token is not valid for provided username");
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,6 @@ public RuntimeException handleJwtParserException(Exception exception) {
return new TokenNotValidException("An internal error occurred while validating the token therefore the token is no longer valid.", exception);
}

boolean verifyJwtSignatureWithJwk() {
return false;
}

/**
* Extracts value of a field from an OIDC token. The value is extracted from a custom path which supports nested objects.
* @param token to extract the field from
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import org.zowe.apiml.security.common.token.TokenAuthentication;

import jakarta.servlet.http.HttpServletResponse;
import org.zowe.apiml.security.common.util.JWTTestUtils;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashSet;
Expand All @@ -36,7 +38,8 @@
class SuccessfulAccessTokenHandlerTest {

private static final String USERNAME = "user";
private final TokenAuthentication dummyAuth = new TokenAuthentication(USERNAME, "TEST_TOKEN_STRING");
public static final String JWT_TOKEN = JWTTestUtils.createDummyAPIMLToken(USERNAME);
private final TokenAuthentication dummyAuth = new TokenAuthentication(JWT_TOKEN);
private SuccessfulAccessTokenHandler underTest;
private AccessTokenProvider accessTokenProvider;
private MockHttpServletRequest httpServletRequest;
Expand Down Expand Up @@ -71,23 +74,23 @@ void setup() {
class WhenCallingOnAuthentication {
@Test
void thenReturn200() throws IOException {
when(accessTokenProvider.getToken(any(), anyInt(), any())).thenReturn("jwtToken");
when(accessTokenProvider.getToken(any(), anyInt(), any())).thenReturn(JWT_TOKEN);
executeLoginHandler();

assertEquals(HttpStatus.OK.value(), httpServletResponse.getStatus());
}

@Test
void givenNullExpiration_thenReturn200() throws IOException {
when(accessTokenProvider.getToken(any(), anyInt(), any())).thenReturn("jwtToken");
when(accessTokenProvider.getToken(any(), anyInt(), any())).thenReturn(JWT_TOKEN);
executeLoginHandler();

assertEquals(HttpStatus.OK.value(), httpServletResponse.getStatus());
}

@Test
void givenResponseNotCommitted_thenThrowIOException() throws IOException {
when(accessTokenProvider.getToken(any(), anyInt(), any())).thenReturn("jwtToken");
when(accessTokenProvider.getToken(any(), anyInt(), any())).thenReturn(JWT_TOKEN);
HttpServletResponse servletResponse = mock(HttpServletResponse.class);
PrintWriter mockWriter = mock(PrintWriter.class);
when(servletResponse.getWriter()).thenReturn(mockWriter);
Expand All @@ -112,7 +115,7 @@ void verifyCommons() {

@Test
void whenProperInputs_thenRauditxIsGenerated() throws IOException {
doReturn("token").when(accessTokenProvider).getToken(anyString(), anyInt(), any());
doReturn(JWT_TOKEN).when(accessTokenProvider).getToken(anyString(), anyInt(), any());

underTest.onAuthenticationSuccess(httpServletRequest, httpServletResponse, dummyAuth);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import org.springframework.web.client.RestTemplate;
import org.zowe.apiml.security.common.config.AuthConfigurationProperties;
import org.zowe.apiml.security.common.token.TokenAuthentication;
import org.zowe.apiml.security.common.util.JWTTestUtils;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
Expand All @@ -41,7 +42,7 @@ class SafResourceAccessEndpointTest {
private static final String UNSUPPORTED_CLASS = "testClass";
private static final String RESOURCE = "resourceTest";
private static final String LEVEL = "READ";
private static final Authentication authentication = new TokenAuthentication(USER_ID, "token");
private static final Authentication authentication = new TokenAuthentication(JWTTestUtils.createDummyAPIMLToken(USER_ID));

@Mock
private RestTemplate restTemplate;
Expand Down
Loading
Loading