Skip to content

Commit e4d42a9

Browse files
Petr WeinfurtachmeloPetr Weinfurt
authored
feat: revoke PAT by admin (#2476)
* tomcat security update Signed-off-by: achmelo <a.chmelo@gmail.com> * secure endpoints with basic auth and x509 Signed-off-by: achmelo <a.chmelo@gmail.com> * refactor, unauth IT Signed-off-by: achmelo <a.chmelo@gmail.com> * separate endpoints for scope and user Signed-off-by: achmelo <a.chmelo@gmail.com> * unit tests for controllers Signed-off-by: achmelo <a.chmelo@gmail.com> * chores Signed-off-by: achmelo <a.chmelo@gmail.com> * IT for revocation by scope Signed-off-by: achmelo <a.chmelo@gmail.com> * Separate cache rules for users and scopes. Signed-off-by: Petr Weinfurt <weipe03@ca.com> * default timestamp Signed-off-by: achmelo <a.chmelo@gmail.com> * Ignore new field in QueryResponse when building JSON Signed-off-by: Petr Weinfurt <weipe03@ca.com> * QueryResponse fix Signed-off-by: Petr Weinfurt <weipe03@ca.com> * QueryResponse fix Signed-off-by: Petr Weinfurt <weipe03@ca.com> * Resolve code smells Signed-off-by: Petr Weinfurt <weipe03@ca.com> * fix scope validation, add tests Signed-off-by: achmelo <a.chmelo@gmail.com> * address comments from code review Signed-off-by: achmelo <a.chmelo@gmail.com> Co-authored-by: achmelo <a.chmelo@gmail.com> Co-authored-by: Petr Weinfurt <weipe03@ca.com> Co-authored-by: achmelo <37397715+achmelo@users.noreply.github.com>
1 parent 79729ad commit e4d42a9

File tree

29 files changed

+558
-160
lines changed

29 files changed

+558
-160
lines changed

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public void invalidateToken(String token) {
3030
}
3131

3232
@Override
33-
public boolean isInvalidated(String token, String serviceId) {
33+
public boolean isInvalidated(String token) {
3434
throw new NotImplementedException();
3535
}
3636

@@ -45,7 +45,12 @@ public boolean isValidForScopes(String token, String serviceId) {
4545
}
4646

4747
@Override
48-
public void invalidateTokensUsingRules(String ruleId, long timeStamp) {
48+
public void invalidateAllTokensForUser(String userId, long timestamp) {
49+
throw new NotImplementedException();
50+
}
51+
52+
@Override
53+
public void invalidateAllTokensForService(String serviceId, long timestamp) {
4954
throw new NotImplementedException();
5055
}
5156
};

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ public class AuthConfigurationProperties {
4444

4545
private String gatewayAccessTokenEndpoint = "/gateway/api/v1/auth/access-token/generate";
4646

47+
private String revokeMultipleAccessTokens = "/gateway/auth/access-token/revoke/tokens";
48+
4749
private String gatewayRefreshEndpointOldFormat = "/api/v1/gateway/auth/refresh";
4850
private String gatewayRefreshEndpoint = "/gateway/api/v1/auth/refresh";
4951

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ 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."),
1919
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+
BAD_REVOKE_REQUEST_BODY("org.zowe.apiml.security.query.invalidRevokeRequestBody", "Body in the revoke request is not valid.", "Use a valid body in the request. Format of a message: {userId: string, (optional)timestamp: long} or {serviceId: string, (optional)timestamp: long}."),
2021
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."),
2122
TOKEN_NOT_PROVIDED("org.zowe.apiml.security.query.tokenNotProvided", "No authorization token provided.", "Provide a valid authorization token."),
2223
TOKEN_EXPIRED("org.zowe.apiml.security.expiredToken", "Token is expired.", "Obtain a new token by performing an authentication request."),
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* This program and the accompanying materials are made available under the terms of the
3+
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
4+
* https://www.eclipse.org/legal/epl-v20.html
5+
*
6+
* SPDX-License-Identifier: EPL-2.0
7+
*
8+
* Copyright Contributors to the Zowe Project.
9+
*/
10+
package org.zowe.apiml.security.common.login;
11+
12+
import com.fasterxml.jackson.databind.ObjectMapper;
13+
import org.springframework.security.authentication.AuthenticationManager;
14+
import org.springframework.security.core.Authentication;
15+
import org.springframework.security.core.context.SecurityContext;
16+
import org.springframework.security.core.context.SecurityContextHolder;
17+
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
18+
import org.zowe.apiml.security.common.error.ResourceAccessExceptionHandler;
19+
20+
import javax.servlet.FilterChain;
21+
import javax.servlet.ServletException;
22+
import javax.servlet.http.HttpServletRequest;
23+
import javax.servlet.http.HttpServletResponse;
24+
import java.io.IOException;
25+
import java.util.Optional;
26+
27+
public class BasicAuthFilter extends LoginFilter {
28+
29+
public BasicAuthFilter(String authEndpoint, AuthenticationFailureHandler failureHandler, ObjectMapper mapper, AuthenticationManager authenticationManager, ResourceAccessExceptionHandler resourceAccessExceptionHandler) {
30+
//no need for success handler implementation, we just need to continue in process chain, this is the reason for lambda rather than pass null
31+
super(authEndpoint, ((request, response, authentication) -> {
32+
}), failureHandler, mapper, authenticationManager, resourceAccessExceptionHandler);
33+
}
34+
35+
@Override
36+
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws ServletException {
37+
Optional<LoginRequest> credentialFromHeader = LoginFilter.getCredentialFromAuthorizationHeader(request);
38+
LoginRequest loginRequest = credentialFromHeader.orElse(null);
39+
return doAuth(request, response, loginRequest);
40+
}
41+
42+
/**
43+
* Calls successful login handler
44+
*/
45+
@Override
46+
protected void successfulAuthentication(HttpServletRequest request,
47+
HttpServletResponse response,
48+
FilterChain chain,
49+
Authentication authResult) throws ServletException, IOException {
50+
SecurityContext context = SecurityContextHolder.createEmptyContext();
51+
context.setAuthentication(authResult);
52+
SecurityContextHolder.setContext(context);
53+
chain.doFilter(request, response);
54+
}
55+
}

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

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313
import org.apache.commons.lang3.StringUtils;
1414
import org.springframework.http.HttpHeaders;
1515
import org.springframework.http.HttpMethod;
16-
import org.springframework.security.authentication.*;
16+
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
17+
import org.springframework.security.authentication.AuthenticationManager;
18+
import org.springframework.security.authentication.BadCredentialsException;
19+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
1720
import org.springframework.security.core.Authentication;
1821
import org.springframework.security.core.AuthenticationException;
1922
import org.springframework.security.core.context.SecurityContextHolder;
@@ -73,8 +76,12 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ
7376

7477
Optional<LoginRequest> credentialFromHeader = getCredentialFromAuthorizationHeader(request);
7578
Optional<LoginRequest> credentialsFromBody = getCredentialsFromBody(request);
76-
7779
LoginRequest loginRequest = credentialFromHeader.orElse(credentialsFromBody.orElse(null));
80+
return doAuth(request, response, loginRequest);
81+
82+
}
83+
84+
public Authentication doAuth(HttpServletRequest request, HttpServletResponse response, LoginRequest loginRequest) throws ServletException {
7885

7986
if (loginRequest == null) {
8087
return null;
@@ -95,7 +102,6 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ
95102
resourceAccessExceptionHandler.handleException(request, response, ex);
96103
}
97104
return auth;
98-
99105
}
100106

101107

@@ -127,16 +133,16 @@ protected void unsuccessfulAuthentication(HttpServletRequest request,
127133
* @param request the http request
128134
* @return the decoded credentials
129135
*/
130-
private Optional<LoginRequest> getCredentialFromAuthorizationHeader(HttpServletRequest request) {
136+
public static Optional<LoginRequest> getCredentialFromAuthorizationHeader(HttpServletRequest request) {
131137
return Optional.ofNullable(
132-
request.getHeader(HttpHeaders.AUTHORIZATION)
133-
).filter(
134-
header -> header.startsWith(ApimlConstants.BASIC_AUTHENTICATION_PREFIX)
135-
).map(
136-
header -> header.replaceFirst(ApimlConstants.BASIC_AUTHENTICATION_PREFIX, "").trim()
137-
)
138+
request.getHeader(HttpHeaders.AUTHORIZATION)
139+
).filter(
140+
header -> header.startsWith(ApimlConstants.BASIC_AUTHENTICATION_PREFIX)
141+
).map(
142+
header -> header.replaceFirst(ApimlConstants.BASIC_AUTHENTICATION_PREFIX, "").trim()
143+
)
138144
.filter(base64Credentials -> !base64Credentials.isEmpty())
139-
.map(this::mapBase64Credentials);
145+
.map(LoginFilter::mapBase64Credentials);
140146
}
141147

142148
/**
@@ -145,7 +151,7 @@ private Optional<LoginRequest> getCredentialFromAuthorizationHeader(HttpServletR
145151
* @param base64Credentials the credentials encoded in base64
146152
* @return the decoded credentials in {@link LoginRequest}
147153
*/
148-
private LoginRequest mapBase64Credentials(String base64Credentials) {
154+
private static LoginRequest mapBase64Credentials(String base64Credentials) {
149155
String credentials = new String(Base64.getDecoder().decode(base64Credentials), StandardCharsets.UTF_8);
150156
int i = credentials.indexOf(':');
151157
if (i > 0) {
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* This program and the accompanying materials are made available under the terms of the
3+
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
4+
* https://www.eclipse.org/legal/epl-v20.html
5+
*
6+
* SPDX-License-Identifier: EPL-2.0
7+
*
8+
* Copyright Contributors to the Zowe Project.
9+
*/
10+
package org.zowe.apiml.security.common.login;
11+
12+
import org.springframework.security.authentication.AuthenticationProvider;
13+
import org.springframework.security.core.Authentication;
14+
import org.springframework.security.core.AuthenticationException;
15+
import org.springframework.security.core.context.SecurityContext;
16+
import org.springframework.security.core.context.SecurityContextHolder;
17+
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
18+
19+
import javax.servlet.FilterChain;
20+
import javax.servlet.ServletException;
21+
import javax.servlet.http.HttpServletRequest;
22+
import javax.servlet.http.HttpServletResponse;
23+
import java.io.IOException;
24+
25+
public class X509AuthAwareFilter extends X509AuthenticationFilter {
26+
private final AuthenticationFailureHandler failureHandler;
27+
28+
public X509AuthAwareFilter(String endpoint, AuthenticationFailureHandler failureHandler, AuthenticationProvider authenticationProvider) {
29+
//no need for success handler implementation, we just need to continue in process chain, this is the reason for lambda rather than pass null
30+
super(endpoint, ((request, response, authentication) -> {
31+
}), authenticationProvider);
32+
this.failureHandler = failureHandler;
33+
}
34+
35+
@Override
36+
protected void successfulAuthentication(HttpServletRequest request,
37+
HttpServletResponse response,
38+
FilterChain chain,
39+
Authentication authResult) throws IOException, ServletException {
40+
if (SecurityContextHolder.getContext().getAuthentication() == null || !SecurityContextHolder.getContext().getAuthentication().isAuthenticated()) {
41+
SecurityContext context = SecurityContextHolder.createEmptyContext();
42+
context.setAuthentication(authResult);
43+
SecurityContextHolder.setContext(context);
44+
}
45+
chain.doFilter(request, response);
46+
}
47+
48+
@Override
49+
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
50+
failureHandler.onAuthenticationFailure(request, response, failed);
51+
}
52+
}

gateway-service/src/main/java/org/zowe/apiml/gateway/security/config/X509AuthenticationFilter.java renamed to apiml-security-common/src/main/java/org/zowe/apiml/security/common/login/X509AuthenticationFilter.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,12 @@
77
*
88
* Copyright Contributors to the Zowe Project.
99
*/
10-
package org.zowe.apiml.gateway.security.config;
10+
package org.zowe.apiml.security.common.login;
1111

1212
import lombok.extern.slf4j.Slf4j;
1313
import org.springframework.security.authentication.AuthenticationProvider;
1414
import org.springframework.security.core.Authentication;
1515
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
16-
import org.zowe.apiml.security.common.login.NonCompulsoryAuthenticationProcessingFilter;
1716
import org.zowe.apiml.security.common.token.X509AuthenticationToken;
1817

1918
import javax.servlet.FilterChain;

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@
99
*/
1010
package org.zowe.apiml.security.common.token;
1111

12+
import java.io.IOException;
1213
import java.util.Set;
1314

1415
public interface AccessTokenProvider {
1516

16-
void invalidateToken(String token) throws Exception;
17-
boolean isInvalidated(String token, String serviceId) throws Exception;
17+
void invalidateToken(String token) throws IOException;
18+
boolean isInvalidated(String token);
1819
String getToken(String username, int expirationTime, Set<String> scopes);
1920
boolean isValidForScopes(String token, String serviceId);
20-
void invalidateTokensUsingRules(String ruleId, long timeStamp) throws Exception;
21+
void invalidateAllTokensForUser(String userId, long timeStamp);
22+
void invalidateAllTokensForService(String serviceId, long timeStamp);
2123
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import org.zowe.apiml.cache.EntryExpiration;
1818

1919
import java.util.Date;
20+
import java.util.List;
2021

2122
/**
2223
* Represents the query JSON response with the token information
@@ -31,6 +32,8 @@ public class QueryResponse implements EntryExpiration {
3132
private Date creation;
3233
private Date expiration;
3334
@JsonIgnore
35+
private List<String> scopes;
36+
@JsonIgnore
3437
private Source source;
3538

3639
@Override

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,12 +128,21 @@ messages:
128128
action: "Use a valid body in the request. Format of a message: {validity: int , scopes: [string]}."
129129

130130
# Personal access token messages
131+
131132
- key: org.zowe.apiml.security.token.accessTokenBodyMissingScopes
132133
number: ZWEAT606
133134
type: ERROR
134135
text: "Body in the HTTP request for Personal Access Token does not contain scopes"
135136
reason: "The request body is not valid"
136137
action: "Provide a list of services for which this token will be valid"
138+
139+
# Revoke personal access token
140+
- key: org.zowe.apiml.security.query.invalidRevokeRequestBody
141+
number: ZWEAT607
142+
type: ERROR
143+
text: "Body in the revoke request is not valid."
144+
reason: "The request body is not valid"
145+
action: "Use a valid body in the request. Format of a message: {userId: string, (optional)timestamp: long} or {serviceId: string, (optional)timestamp: long}."
137146
# Service specific messages
138147
# 700-999
139148

0 commit comments

Comments
 (0)