Skip to content

Commit cc0aba4

Browse files
taban03achmelo
andauthored
feat: Limit scope of Personal Access Token (#2456)
* Add scopes in the JWT Signed-off-by: at670475 <andrea.tabone@broadcom.com> * Add validation for jwt Signed-off-by: at670475 <andrea.tabone@broadcom.com> * Add validation for jwt Signed-off-by: at670475 <andrea.tabone@broadcom.com> * test for validation of scopes in token Signed-off-by: achmelo <a.chmelo@gmail.com> * Add IT Signed-off-by: at670475 <andrea.tabone@broadcom.com> * Make scope value case non sensitive Signed-off-by: at670475 <andrea.tabone@broadcom.com> * return immediately for incorrect request body Signed-off-by: achmelo <a.chmelo@gmail.com> * resolve conflicts Signed-off-by: achmelo <a.chmelo@gmail.com> * share object in request Signed-off-by: achmelo <a.chmelo@gmail.com> * Fix check style Signed-off-by: at670475 <andrea.tabone@broadcom.com> * Fix check style Signed-off-by: at670475 <andrea.tabone@broadcom.com> * test for salt initialization Signed-off-by: achmelo <a.chmelo@gmail.com> * format Signed-off-by: achmelo <a.chmelo@gmail.com> * fix check style pt.3 Signed-off-by: at670475 <andrea.tabone@broadcom.com> * fix styles Signed-off-by: achmelo <a.chmelo@gmail.com> * Add tests Signed-off-by: at670475 <andrea.tabone@broadcom.com> * Refactoring of test Signed-off-by: at670475 <andrea.tabone@broadcom.com> Co-authored-by: achmelo <a.chmelo@gmail.com>
1 parent 7f0fd8d commit cc0aba4

File tree

22 files changed

+361
-77
lines changed

22 files changed

+361
-77
lines changed

apiml-security-common/src/main/java/org/zowe/apiml/gateway/security/login/SuccessfulAccessTokenHandler.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import java.io.IOException;
2525
import java.util.Set;
2626

27+
import static org.zowe.apiml.security.common.filter.StoreAccessTokenInfoFilter.TOKEN_REQUEST;
28+
2729
@Component
2830
@RequiredArgsConstructor
2931
public class SuccessfulAccessTokenHandler implements AuthenticationSuccessHandler {
@@ -32,9 +34,8 @@ public class SuccessfulAccessTokenHandler implements AuthenticationSuccessHandle
3234

3335
@Override
3436
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
35-
Object expirationTime = request.getAttribute("expirationTime");
36-
String validity = expirationTime == null || expirationTime.equals("") ? "0" : request.getAttribute("expirationTime").toString();
37-
String token = accessTokenProvider.getToken(authentication.getPrincipal().toString(), Integer.parseInt(validity));
37+
AccessTokenRequest accessTokenRequest = (AccessTokenRequest)request.getAttribute(TOKEN_REQUEST);
38+
String token = accessTokenProvider.getToken(authentication.getPrincipal().toString(), accessTokenRequest.getValidity(), accessTokenRequest.getScopes());
3839
response.getWriter().print(token);
3940
response.getWriter().flush();
4041
response.getWriter().close();

apiml-security-common/src/main/java/org/zowe/apiml/security/common/config/AccessTokenProviderConfig.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
import org.springframework.context.annotation.Configuration;
1616
import org.zowe.apiml.security.common.token.AccessTokenProvider;
1717

18+
import java.util.Set;
19+
1820
@Configuration
1921
public class AccessTokenProviderConfig {
2022

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

3537
@Override
36-
public String getToken(String username, int expirationTime) {
38+
public String getToken(String username, int expirationTime, Set<String> scopes) {
39+
throw new NotImplementedException();
40+
}
41+
42+
@Override
43+
public boolean isValidForScopes(String token, String serviceId) {
3744
throw new NotImplementedException();
3845
}
3946
};

apiml-security-common/src/main/java/org/zowe/apiml/security/common/error/AuthExceptionHandler.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ public void handleException(HttpServletRequest request, HttpServletResponse resp
6363
handleTokenExpire(request, response, ex);
6464
} else if (ex instanceof TokenFormatNotValidException) {
6565
handleTokenFormatException(request, response, ex);
66+
} else if (ex instanceof AccessTokenBodyNotValidException) {
67+
handleInvalidAccessTokenBodyException(request, response, ex);
6668
} else if (ex instanceof InvalidCertificateException) {
6769
handleInvalidCertificate(response, ex);
6870
} else if (ex instanceof ZosAuthenticationException) {
@@ -135,6 +137,11 @@ private void handleInvalidTokenTypeException(HttpServletRequest request, HttpSer
135137
writeErrorResponse(ErrorType.INVALID_TOKEN_TYPE.getErrorMessageKey(), HttpStatus.UNAUTHORIZED, request, response);
136138
}
137139

140+
private void handleInvalidAccessTokenBodyException(HttpServletRequest request, HttpServletResponse response, RuntimeException ex) throws ServletException {
141+
log.debug(ERROR_MESSAGE_400, ex.getMessage());
142+
writeErrorResponse(ex.getMessage(), HttpStatus.BAD_REQUEST, request, response);
143+
}
144+
138145
//500
139146
private void handleAuthenticationException(HttpServletRequest request, HttpServletResponse response, RuntimeException ex) throws ServletException {
140147
log.debug(ERROR_MESSAGE_500, ex.getMessage());

apiml-security-common/src/main/java/org/zowe/apiml/security/common/error/ErrorType.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
public enum ErrorType {
1717
BAD_CREDENTIALS("org.zowe.apiml.security.login.invalidCredentials", "Invalid Credentials", "Provide a valid username and password."),
1818
TOKEN_NOT_VALID("org.zowe.apiml.security.query.invalidToken", "Token is not valid.", "Provide a valid token."),
19-
BAD_ACCESS_TOKEN_BODY("org.zowe.apiml.security.query.invalidAccessTokenBody", "Personal Access Token body in the request is not valid.", "Provide a valid body."),
19+
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]}."),
20+
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."),
2021
TOKEN_NOT_PROVIDED("org.zowe.apiml.security.query.tokenNotProvided", "No authorization token provided.", "Provide a valid authorization token."),
2122
TOKEN_EXPIRED("org.zowe.apiml.security.expiredToken", "Token is expired.", "Obtain a new token by performing an authentication request."),
2223
AUTH_CREDENTIALS_NOT_FOUND("org.zowe.apiml.security.login.invalidInput", "Authorization header is missing, or request body is missing or invalid.", "Provide valid authentication."),

apiml-security-common/src/main/java/org/zowe/apiml/security/common/error/ResourceAccessExceptionHandler.java

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,19 +46,11 @@ public void handleException(HttpServletRequest request, HttpServletResponse resp
4646
handleGatewayNotAvailable(request, response, ex);
4747
} else if (ex instanceof ServiceNotAccessibleException) {
4848
handleServiceNotAccessible(request, response, ex);
49-
} else if (ex instanceof AccessTokenBodyNotValidException) {
50-
handleInvalidAccessTokenBodyException(request, response, ex);
5149
} else {
5250
throw ex;
5351
}
5452
}
5553

56-
//400
57-
private void handleInvalidAccessTokenBodyException(HttpServletRequest request, HttpServletResponse response, RuntimeException ex) throws ServletException {
58-
log.debug(ERROR_MESSAGE_400, ex.getMessage());
59-
writeErrorResponse(ErrorType.BAD_ACCESS_TOKEN_BODY.getErrorMessageKey(), HttpStatus.BAD_REQUEST, request, response);
60-
}
61-
6254
//500
6355
private void handleGatewayNotAvailable(HttpServletRequest request, HttpServletResponse response, RuntimeException ex) throws ServletException {
6456
log.debug(ERROR_MESSAGE_500, ex.getMessage());

apiml-security-common/src/main/java/org/zowe/apiml/security/common/filter/StoreAccessTokenInfoFilter.java

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,36 +15,46 @@
1515
import org.springframework.web.filter.OncePerRequestFilter;
1616
import org.zowe.apiml.gateway.security.login.SuccessfulAccessTokenHandler;
1717
import org.zowe.apiml.security.common.error.AccessTokenBodyNotValidException;
18-
import org.zowe.apiml.security.common.error.ResourceAccessExceptionHandler;
18+
import org.zowe.apiml.security.common.error.AuthExceptionHandler;
1919

2020
import javax.servlet.FilterChain;
2121
import javax.servlet.ServletException;
2222
import javax.servlet.ServletInputStream;
2323
import javax.servlet.http.HttpServletRequest;
2424
import javax.servlet.http.HttpServletResponse;
2525
import java.io.IOException;
26+
import java.util.Set;
27+
import java.util.stream.Collectors;
2628

27-
@RequiredArgsConstructor
2829
/**
2930
* This filter will store the personal access information from the body as request attribute
3031
*/
32+
@RequiredArgsConstructor
3133
public class StoreAccessTokenInfoFilter extends OncePerRequestFilter {
32-
private static final String EXPIRATION_TIME = "expirationTime";
33-
private final ResourceAccessExceptionHandler resourceAccessExceptionHandler;
34+
public static final String TOKEN_REQUEST = "tokenRequest";
3435
private static final ObjectReader mapper = new ObjectMapper().reader();
36+
private final AuthExceptionHandler authExceptionHandler;
3537

3638
@Override
3739
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException {
3840
try {
3941
ServletInputStream inputStream = request.getInputStream();
4042
if (inputStream.available() != 0) {
41-
int validity = mapper.readValue(inputStream, SuccessfulAccessTokenHandler.AccessTokenRequest.class).getValidity();
42-
request.setAttribute(EXPIRATION_TIME, validity);
43+
SuccessfulAccessTokenHandler.AccessTokenRequest accessTokenRequest = mapper.readValue(inputStream, SuccessfulAccessTokenHandler.AccessTokenRequest.class);
44+
Set<String> scopes = accessTokenRequest.getScopes();
45+
if (scopes == null || scopes.isEmpty()) {
46+
authExceptionHandler.handleException(request, response, new AccessTokenBodyNotValidException("org.zowe.apiml.security.token.accessTokenBodyMissingScopes"));
47+
return;
48+
}
49+
accessTokenRequest.setScopes(scopes.stream().map(String::toLowerCase).collect(Collectors.toSet()));
50+
request.setAttribute(TOKEN_REQUEST, accessTokenRequest);
51+
filterChain.doFilter(request, response);
52+
} else {
53+
authExceptionHandler.handleException(request, response, new AccessTokenBodyNotValidException("org.zowe.apiml.security.token.accessTokenBodyMissingScopes"));
4354
}
4455

45-
filterChain.doFilter(request, response);
4656
} catch (IOException e) {
47-
resourceAccessExceptionHandler.handleException(request, response, new AccessTokenBodyNotValidException("The request body you provided is not valid"));
57+
authExceptionHandler.handleException(request, response, new AccessTokenBodyNotValidException("org.zowe.apiml.security.query.invalidAccessTokenBody"));
4858
}
4959
}
5060
}

apiml-security-common/src/main/java/org/zowe/apiml/security/common/handler/UnauthorizedHandler.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
*/
1010
package org.zowe.apiml.security.common.handler;
1111

12+
import lombok.Getter;
1213
import org.zowe.apiml.security.common.error.AuthExceptionHandler;
1314
import lombok.RequiredArgsConstructor;
1415
import lombok.extern.slf4j.Slf4j;
@@ -26,6 +27,7 @@
2627
@Slf4j
2728
@Component("plainAuth")
2829
@RequiredArgsConstructor
30+
@Getter
2931
public class UnauthorizedHandler implements AuthenticationEntryPoint {
3032
private final AuthExceptionHandler handler;
3133

apiml-security-common/src/main/java/org/zowe/apiml/security/common/token/AccessTokenProvider.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@
99
*/
1010
package org.zowe.apiml.security.common.token;
1111

12+
import java.util.Set;
13+
1214
public interface AccessTokenProvider {
1315

1416
void invalidateToken(String token) throws Exception;
1517
boolean isInvalidated(String token) throws Exception;
16-
String getToken(String username, int expirationTime);
18+
String getToken(String username, int expirationTime, Set<String> scopes);
19+
boolean isValidForScopes(String token, String serviceId);
1720
}

apiml-security-common/src/main/resources/security-common-log-messages.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,15 @@ messages:
125125
type: ERROR
126126
text: "Invalid body provided in request to create personal access token"
127127
reason: "The request body is not valid"
128-
action: "Use a valid body in the request"
128+
action: "Use a valid body in the request. Format of a message: {validity: int , scopes: [string]}."
129129

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

apiml-security-common/src/test/java/org/zowe/apiml/gateway/security/login/SuccessfulAccessTokenHandlerTest.java

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,29 @@
2222
import javax.servlet.http.HttpServletResponse;
2323
import java.io.IOException;
2424
import java.io.PrintWriter;
25+
import java.util.HashSet;
26+
import java.util.Set;
2527

2628
import static org.junit.jupiter.api.Assertions.assertEquals;
2729
import static org.junit.jupiter.api.Assertions.assertThrows;
2830
import static org.mockito.ArgumentMatchers.any;
2931
import static org.mockito.ArgumentMatchers.anyInt;
30-
import static org.mockito.Mockito.*;
32+
import static org.mockito.Mockito.mock;
33+
import static org.mockito.Mockito.when;
34+
import static org.zowe.apiml.security.common.filter.StoreAccessTokenInfoFilter.TOKEN_REQUEST;
3135

3236
class SuccessfulAccessTokenHandlerTest {
3337
private final TokenAuthentication dummyAuth = new TokenAuthentication("user", "TEST_TOKEN_STRING");
3438
private SuccessfulAccessTokenHandler underTest;
3539
private AccessTokenProvider accessTokenProvider;
3640
private MockHttpServletRequest httpServletRequest;
3741
private MockHttpServletResponse httpServletResponse;
42+
static Set<String> scopes = new HashSet<>();
43+
static SuccessfulAccessTokenHandler.AccessTokenRequest accessTokenRequest = new SuccessfulAccessTokenHandler.AccessTokenRequest(80, scopes);
44+
45+
static {
46+
scopes.add("gateway");
47+
}
3848

3949
@BeforeEach
4050
void setup() {
@@ -43,32 +53,30 @@ void setup() {
4353
accessTokenProvider = mock(AccessTokenProvider.class);
4454

4555
underTest = new SuccessfulAccessTokenHandler(accessTokenProvider);
56+
httpServletRequest.setAttribute(TOKEN_REQUEST, accessTokenRequest);
4657
}
4758

4859
@Nested
4960
class WhenCallingOnAuthentication {
5061
@Test
5162
void thenReturn200() throws ServletException, IOException {
52-
httpServletRequest.setAttribute("expirationTime", 90);
53-
when(accessTokenProvider.getToken(any(), anyInt())).thenReturn("jwtToken");
63+
when(accessTokenProvider.getToken(any(), anyInt(), any())).thenReturn("jwtToken");
5464
executeLoginHandler();
5565

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

5969
@Test
6070
void givenNullExpiration_thenReturn200() throws ServletException, IOException {
61-
httpServletRequest.setAttribute("expirationTime", "");
62-
when(accessTokenProvider.getToken(any(), anyInt())).thenReturn("jwtToken");
71+
when(accessTokenProvider.getToken(any(), anyInt(), any())).thenReturn("jwtToken");
6372
executeLoginHandler();
6473

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

6877
@Test
6978
void givenResponseNotCommitted_thenThrowIOException() throws IOException {
70-
httpServletRequest.setAttribute("expirationTime", 90);
71-
when(accessTokenProvider.getToken(any(), anyInt())).thenReturn("jwtToken");
79+
when(accessTokenProvider.getToken(any(), anyInt(), any())).thenReturn("jwtToken");
7280
HttpServletResponse servletResponse = mock(HttpServletResponse.class);
7381
PrintWriter mockWriter = mock(PrintWriter.class);
7482
when(servletResponse.getWriter()).thenReturn(mockWriter);

0 commit comments

Comments
 (0)