Skip to content

Commit

Permalink
feat: Limit scope of Personal Access Token (#2456)
Browse files Browse the repository at this point in the history
* Add scopes in the JWT

Signed-off-by: at670475 <[email protected]>

* Add validation for jwt

Signed-off-by: at670475 <[email protected]>

* Add validation for jwt

Signed-off-by: at670475 <[email protected]>

* test for validation of scopes in token

Signed-off-by: achmelo <[email protected]>

* Add IT

Signed-off-by: at670475 <[email protected]>

* Make scope value case non sensitive

Signed-off-by: at670475 <[email protected]>

* return immediately for incorrect request body

Signed-off-by: achmelo <[email protected]>

* resolve conflicts

Signed-off-by: achmelo <[email protected]>

* share object in request

Signed-off-by: achmelo <[email protected]>

* Fix check style

Signed-off-by: at670475 <[email protected]>

* Fix check style

Signed-off-by: at670475 <[email protected]>

* test for salt initialization

Signed-off-by: achmelo <[email protected]>

* format

Signed-off-by: achmelo <[email protected]>

* fix check style pt.3

Signed-off-by: at670475 <[email protected]>

* fix styles

Signed-off-by: achmelo <[email protected]>

* Add tests

Signed-off-by: at670475 <[email protected]>

* Refactoring of test

Signed-off-by: at670475 <[email protected]>

Co-authored-by: achmelo <[email protected]>
  • Loading branch information
taban03 and achmelo authored Jun 22, 2022
1 parent 7f0fd8d commit cc0aba4
Show file tree
Hide file tree
Showing 22 changed files with 361 additions and 77 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import java.io.IOException;
import java.util.Set;

import static org.zowe.apiml.security.common.filter.StoreAccessTokenInfoFilter.TOKEN_REQUEST;

@Component
@RequiredArgsConstructor
public class SuccessfulAccessTokenHandler implements AuthenticationSuccessHandler {
Expand All @@ -32,9 +34,8 @@ public class SuccessfulAccessTokenHandler implements AuthenticationSuccessHandle

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
Object expirationTime = request.getAttribute("expirationTime");
String validity = expirationTime == null || expirationTime.equals("") ? "0" : request.getAttribute("expirationTime").toString();
String token = accessTokenProvider.getToken(authentication.getPrincipal().toString(), Integer.parseInt(validity));
AccessTokenRequest accessTokenRequest = (AccessTokenRequest)request.getAttribute(TOKEN_REQUEST);
String token = accessTokenProvider.getToken(authentication.getPrincipal().toString(), accessTokenRequest.getValidity(), accessTokenRequest.getScopes());
response.getWriter().print(token);
response.getWriter().flush();
response.getWriter().close();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import org.springframework.context.annotation.Configuration;
import org.zowe.apiml.security.common.token.AccessTokenProvider;

import java.util.Set;

@Configuration
public class AccessTokenProviderConfig {

Expand All @@ -33,7 +35,12 @@ public boolean isInvalidated(String token) {
}

@Override
public String getToken(String username, int expirationTime) {
public String getToken(String username, int expirationTime, Set<String> scopes) {
throw new NotImplementedException();
}

@Override
public boolean isValidForScopes(String token, String serviceId) {
throw new NotImplementedException();
}
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ public void handleException(HttpServletRequest request, HttpServletResponse resp
handleTokenExpire(request, response, ex);
} else if (ex instanceof TokenFormatNotValidException) {
handleTokenFormatException(request, response, ex);
} else if (ex instanceof AccessTokenBodyNotValidException) {
handleInvalidAccessTokenBodyException(request, response, ex);
} else if (ex instanceof InvalidCertificateException) {
handleInvalidCertificate(response, ex);
} else if (ex instanceof ZosAuthenticationException) {
Expand Down Expand Up @@ -135,6 +137,11 @@ private void handleInvalidTokenTypeException(HttpServletRequest request, HttpSer
writeErrorResponse(ErrorType.INVALID_TOKEN_TYPE.getErrorMessageKey(), HttpStatus.UNAUTHORIZED, request, response);
}

private void handleInvalidAccessTokenBodyException(HttpServletRequest request, HttpServletResponse response, RuntimeException ex) throws ServletException {
log.debug(ERROR_MESSAGE_400, ex.getMessage());
writeErrorResponse(ex.getMessage(), HttpStatus.BAD_REQUEST, request, response);
}

//500
private void handleAuthenticationException(HttpServletRequest request, HttpServletResponse response, RuntimeException ex) throws ServletException {
log.debug(ERROR_MESSAGE_500, ex.getMessage());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
public enum ErrorType {
BAD_CREDENTIALS("org.zowe.apiml.security.login.invalidCredentials", "Invalid Credentials", "Provide a valid username and password."),
TOKEN_NOT_VALID("org.zowe.apiml.security.query.invalidToken", "Token is not valid.", "Provide a valid token."),
BAD_ACCESS_TOKEN_BODY("org.zowe.apiml.security.query.invalidAccessTokenBody", "Personal Access Token body in the request is not valid.", "Provide a valid body."),
BAD_ACCESS_TOKEN_BODY("org.zowe.apiml.security.query.invalidAccessTokenBody", "Personal Access Token body in the request is not valid.", "Use a valid body in the request. Format of a message: {validity: int , scopes: [string]}."),
ACCESS_TOKEN_BODY_MISSING_SCOPES("org.zowe.apiml.security.query.accessTokenBodyMissingScopes", "Body in the HTTP request for Personal Access Token does not contain scopes.", "Provide a list of services for which this token will be valid."),
TOKEN_NOT_PROVIDED("org.zowe.apiml.security.query.tokenNotProvided", "No authorization token provided.", "Provide a valid authorization token."),
TOKEN_EXPIRED("org.zowe.apiml.security.expiredToken", "Token is expired.", "Obtain a new token by performing an authentication request."),
AUTH_CREDENTIALS_NOT_FOUND("org.zowe.apiml.security.login.invalidInput", "Authorization header is missing, or request body is missing or invalid.", "Provide valid authentication."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,11 @@ public void handleException(HttpServletRequest request, HttpServletResponse resp
handleGatewayNotAvailable(request, response, ex);
} else if (ex instanceof ServiceNotAccessibleException) {
handleServiceNotAccessible(request, response, ex);
} else if (ex instanceof AccessTokenBodyNotValidException) {
handleInvalidAccessTokenBodyException(request, response, ex);
} else {
throw ex;
}
}

//400
private void handleInvalidAccessTokenBodyException(HttpServletRequest request, HttpServletResponse response, RuntimeException ex) throws ServletException {
log.debug(ERROR_MESSAGE_400, ex.getMessage());
writeErrorResponse(ErrorType.BAD_ACCESS_TOKEN_BODY.getErrorMessageKey(), HttpStatus.BAD_REQUEST, request, response);
}

//500
private void handleGatewayNotAvailable(HttpServletRequest request, HttpServletResponse response, RuntimeException ex) throws ServletException {
log.debug(ERROR_MESSAGE_500, ex.getMessage());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,36 +15,46 @@
import org.springframework.web.filter.OncePerRequestFilter;
import org.zowe.apiml.gateway.security.login.SuccessfulAccessTokenHandler;
import org.zowe.apiml.security.common.error.AccessTokenBodyNotValidException;
import org.zowe.apiml.security.common.error.ResourceAccessExceptionHandler;
import org.zowe.apiml.security.common.error.AuthExceptionHandler;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Set;
import java.util.stream.Collectors;

@RequiredArgsConstructor
/**
* This filter will store the personal access information from the body as request attribute
*/
@RequiredArgsConstructor
public class StoreAccessTokenInfoFilter extends OncePerRequestFilter {
private static final String EXPIRATION_TIME = "expirationTime";
private final ResourceAccessExceptionHandler resourceAccessExceptionHandler;
public static final String TOKEN_REQUEST = "tokenRequest";
private static final ObjectReader mapper = new ObjectMapper().reader();
private final AuthExceptionHandler authExceptionHandler;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException {
try {
ServletInputStream inputStream = request.getInputStream();
if (inputStream.available() != 0) {
int validity = mapper.readValue(inputStream, SuccessfulAccessTokenHandler.AccessTokenRequest.class).getValidity();
request.setAttribute(EXPIRATION_TIME, validity);
SuccessfulAccessTokenHandler.AccessTokenRequest accessTokenRequest = mapper.readValue(inputStream, SuccessfulAccessTokenHandler.AccessTokenRequest.class);
Set<String> scopes = accessTokenRequest.getScopes();
if (scopes == null || scopes.isEmpty()) {
authExceptionHandler.handleException(request, response, new AccessTokenBodyNotValidException("org.zowe.apiml.security.token.accessTokenBodyMissingScopes"));
return;
}
accessTokenRequest.setScopes(scopes.stream().map(String::toLowerCase).collect(Collectors.toSet()));
request.setAttribute(TOKEN_REQUEST, accessTokenRequest);
filterChain.doFilter(request, response);
} else {
authExceptionHandler.handleException(request, response, new AccessTokenBodyNotValidException("org.zowe.apiml.security.token.accessTokenBodyMissingScopes"));
}

filterChain.doFilter(request, response);
} catch (IOException e) {
resourceAccessExceptionHandler.handleException(request, response, new AccessTokenBodyNotValidException("The request body you provided is not valid"));
authExceptionHandler.handleException(request, response, new AccessTokenBodyNotValidException("org.zowe.apiml.security.query.invalidAccessTokenBody"));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*/
package org.zowe.apiml.security.common.handler;

import lombok.Getter;
import org.zowe.apiml.security.common.error.AuthExceptionHandler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -26,6 +27,7 @@
@Slf4j
@Component("plainAuth")
@RequiredArgsConstructor
@Getter
public class UnauthorizedHandler implements AuthenticationEntryPoint {
private final AuthExceptionHandler handler;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@
*/
package org.zowe.apiml.security.common.token;

import java.util.Set;

public interface AccessTokenProvider {

void invalidateToken(String token) throws Exception;
boolean isInvalidated(String token) throws Exception;
String getToken(String username, int expirationTime);
String getToken(String username, int expirationTime, Set<String> scopes);
boolean isValidForScopes(String token, String serviceId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,15 @@ messages:
type: ERROR
text: "Invalid body provided in request to create personal access token"
reason: "The request body is not valid"
action: "Use a valid body in the request"
action: "Use a valid body in the request. Format of a message: {validity: int , scopes: [string]}."

# Personal access token messages
- key: org.zowe.apiml.security.token.accessTokenBodyMissingScopes
number: ZWEAT606
type: ERROR
text: "Body in the HTTP request for Personal Access Token does not contain scopes"
reason: "The request body is not valid"
action: "Provide a list of services for which this token will be valid"
# Service specific messages
# 700-999

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,29 @@
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashSet;
import java.util.Set;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.zowe.apiml.security.common.filter.StoreAccessTokenInfoFilter.TOKEN_REQUEST;

class SuccessfulAccessTokenHandlerTest {
private final TokenAuthentication dummyAuth = new TokenAuthentication("user", "TEST_TOKEN_STRING");
private SuccessfulAccessTokenHandler underTest;
private AccessTokenProvider accessTokenProvider;
private MockHttpServletRequest httpServletRequest;
private MockHttpServletResponse httpServletResponse;
static Set<String> scopes = new HashSet<>();
static SuccessfulAccessTokenHandler.AccessTokenRequest accessTokenRequest = new SuccessfulAccessTokenHandler.AccessTokenRequest(80, scopes);

static {
scopes.add("gateway");
}

@BeforeEach
void setup() {
Expand All @@ -43,32 +53,30 @@ void setup() {
accessTokenProvider = mock(AccessTokenProvider.class);

underTest = new SuccessfulAccessTokenHandler(accessTokenProvider);
httpServletRequest.setAttribute(TOKEN_REQUEST, accessTokenRequest);
}

@Nested
class WhenCallingOnAuthentication {
@Test
void thenReturn200() throws ServletException, IOException {
httpServletRequest.setAttribute("expirationTime", 90);
when(accessTokenProvider.getToken(any(), anyInt())).thenReturn("jwtToken");
when(accessTokenProvider.getToken(any(), anyInt(), any())).thenReturn("jwtToken");
executeLoginHandler();

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

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

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

@Test
void givenResponseNotCommitted_thenThrowIOException() throws IOException {
httpServletRequest.setAttribute("expirationTime", 90);
when(accessTokenProvider.getToken(any(), anyInt())).thenReturn("jwtToken");
when(accessTokenProvider.getToken(any(), anyInt(), any())).thenReturn("jwtToken");
HttpServletResponse servletResponse = mock(HttpServletResponse.class);
PrintWriter mockWriter = mock(PrintWriter.class);
when(servletResponse.getWriter()).thenReturn(mockWriter);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,57 +13,88 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.http.HttpMethod;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.zowe.apiml.gateway.security.login.SuccessfulAccessTokenHandler;
import org.zowe.apiml.message.core.MessageService;
import org.zowe.apiml.message.yaml.YamlMessageService;
import org.zowe.apiml.security.common.error.ResourceAccessExceptionHandler;
import org.zowe.apiml.security.common.error.AuthExceptionHandler;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Set;
import java.util.stream.Stream;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.core.Is.is;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.*;
import static org.zowe.apiml.security.common.filter.StoreAccessTokenInfoFilter.TOKEN_REQUEST;

class StoreAccessTokenInfoFilterTest {
private StoreAccessTokenInfoFilter underTest;
private MockHttpServletRequest request;
private HttpServletResponse response;
private FilterChain chain;
private static final String VALID_JSON = "{\"validity\": 90, \"scopes\": [\"service\"]}";
private static final String VALID_JSON_NO_SCOPES = "{\"validity\": 90}";
private static final String INVALID_JSON = "{ \"notValid\", \"scopes\": [\"service\"]}";
private static final String INVALID_JSON2 = "{\"valissdity\": 90, \"scopes\": [\"service\"]}";
private final MessageService messageService = new YamlMessageService("/security-service-messages.yml");
private final ResourceAccessExceptionHandler resourceAccessExceptionHandler = new ResourceAccessExceptionHandler(messageService, new ObjectMapper());

private final AuthExceptionHandler authExceptionHandler = new AuthExceptionHandler(messageService, new ObjectMapper());

@BeforeEach
public void setUp() {
request = new MockHttpServletRequest();
response = mock(HttpServletResponse.class);
chain = mock(FilterChain.class);
underTest = new StoreAccessTokenInfoFilter(resourceAccessExceptionHandler);
underTest = new StoreAccessTokenInfoFilter(authExceptionHandler);
request.setMethod(HttpMethod.POST.name());

}

@Nested
class GivenRequestWithBody {
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class GivenRequestWithBodyTest {

@Test
void thenStoreExpirationTime() throws ServletException, IOException {
void thenStoreExpirationTimeAndScopes() throws ServletException, IOException {
request.setContent(VALID_JSON.getBytes());
underTest.doFilterInternal(request, response, chain);
String expirationTime = request.getAttribute("expirationTime").toString();
assertNotNull(expirationTime);
assertEquals("90", expirationTime);
SuccessfulAccessTokenHandler.AccessTokenRequest tokenRequest = (SuccessfulAccessTokenHandler.AccessTokenRequest) request.getAttribute(TOKEN_REQUEST);
Set<String> scopes = tokenRequest.getScopes();
assertEquals(90, tokenRequest.getValidity());
assertTrue(scopes.contains("service"));
verify(chain, times(1)).doFilter(request, response);
}

Stream<byte[]> values() {
return Stream.of(VALID_JSON_NO_SCOPES.getBytes(), null);
}

@ParameterizedTest
@MethodSource("values")
void givenNoScopesInRequest_thenThrowException(byte[] body) throws ServletException, IOException {

request.setContent(body);
StringWriter out = new StringWriter();
PrintWriter writer = new PrintWriter(out);
when(response.getWriter()).thenReturn(writer);
underTest.doFilterInternal(request, response, chain);
assertThat(out.toString(), containsString("ZWEAT606E"));
verify(chain, times(0)).doFilter(request, response);
}
}

@Nested
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,12 @@ messages:
type: ERROR
text: "Invalid body provided in request to create personal access token"
reason: "The request body is not valid"
action: "Use a valid body in the request"
action: "Use a valid body in the request. Format of a message: {validity: int , scopes: [string]}."

# Personal access token messages
- key: org.zowe.apiml.security.token.accessTokenBodyMissingScopes
number: ZWEAT606
type: ERROR
text: "Body in the HTTP request for Personal Access Token does not contain scopes"
reason: "The request body is not valid"
action: "Provide a list of services for which this token will be valid"
Loading

0 comments on commit cc0aba4

Please sign in to comment.