Skip to content

Commit 6412dcc

Browse files
committed
wip
Signed-off-by: Richard Salac <richard.salac@broadcom.com>
1 parent e2371c9 commit 6412dcc

File tree

6 files changed

+171
-62
lines changed

6 files changed

+171
-62
lines changed

apiml-security-common/src/main/java/org/zowe/apiml/security/common/util/JwtUtils.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,21 @@ public class JwtUtils {
4545
* @throws TokenNotValidException in case of invalid input, or TokenExpireException if JWT is expired
4646
*/
4747
public JWTClaimsSet getJwtClaims(String jwt) {
48+
return getJwtClaimsInternal(jwt, true);
49+
}
50+
51+
/**
52+
* This method reads the claims without validating the token signature. It should be used only if the validity was checked in the calling code. Ignores token expiration.
53+
*
54+
* @param jwt token to be parsed
55+
* @return parsed claims
56+
* @throws TokenNotValidException in case of invalid input
57+
*/
58+
public JWTClaimsSet getJwtClaimsIgnoreExpiration(String jwt) {
59+
return getJwtClaimsInternal(jwt, false);
60+
}
61+
62+
private JWTClaimsSet getJwtClaimsInternal(String jwt, boolean validateExpiration) {
4863
/*
4964
* Removes signature, because we don't have key to verify z/OS tokens, and we just need to read claim.
5065
* Verification is done by SAF itself. JWT library doesn't parse signed key without verification.
@@ -54,9 +69,10 @@ public JWTClaimsSet getJwtClaims(String jwt) {
5469
var token = JWTParser.parse(jwtWithoutSignature);
5570
var claims = token.getJWTClaimsSet();
5671

57-
if (claims.getExpirationTime().toInstant().isBefore(Instant.now())) {
72+
if (validateExpiration && claims.getExpirationTime().toInstant().isBefore(Instant.now())) {
5873
throw new ExpiredJWTException("JWT token is expired");
5974
}
75+
6076
return token.getJWTClaimsSet();
6177
} catch (RuntimeException | ParseException | BadJWTException exception) {
6278
throw handleJwtParserException(exception);

integration-tests/src/test/java/org/zowe/apiml/integration/authentication/schemes/PassticketSchemeTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,10 @@ public class PassticketSchemeTest implements TestWithStartedInstances {
6262
public static Stream<Arguments> getTokens() {
6363
return Stream.of(
6464
Arguments.of("validJwt", SC_OK, Matchers.blankOrNullString()),
65-
Arguments.of("noSignJwt", SC_OK, startsWith("ZWEAG160E")),
66-
Arguments.of("publicKeySignedJwt", SC_OK, startsWith("ZWEAG160E")),
65+
Arguments.of("noSignJwt", SC_OK, startsWith("ZWEAO402E")),
66+
Arguments.of("publicKeySignedJwt", SC_OK, startsWith("ZWEAO402E")),
6767
Arguments.of("changedRealmJwt", SC_OK, startsWith("ZWEAO402E")),
68-
Arguments.of("changedUserJwt", SC_OK, startsWith("ZWEAG160E")),
68+
Arguments.of("changedUserJwt", SC_OK, startsWith("ZWEAO402E")),
6969
Arguments.of("personalAccessToken", SC_OK, Matchers.blankOrNullString())
7070
);
7171
}

zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/AuthenticationService.java

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
import java.util.Set;
6969

7070
import static org.zowe.apiml.security.common.util.JwtUtils.getJwtClaims;
71+
import static org.zowe.apiml.security.common.util.JwtUtils.getJwtClaimsIgnoreExpiration;
7172
import static org.zowe.apiml.security.common.util.JwtUtils.handleJwtParserException;
7273
import static org.zowe.apiml.zaas.security.service.zosmf.ZosmfService.TokenType.JWT;
7374
import static org.zowe.apiml.zaas.security.service.zosmf.ZosmfService.TokenType.LTPA;
@@ -375,26 +376,35 @@ public TokenAuthentication validateJwtToken(String jwtToken) {
375376
if (jwtToken != null && validationJwtTokenCache != null) {
376377
Cache.ValueWrapper cached = validationJwtTokenCache.get(jwtToken);
377378
if (cached != null) {
378-
log.debug("JWT found in the cache.");
379-
return (TokenAuthentication) cached.get();
379+
var tokenAuthentication = (TokenAuthentication) cached.get();
380+
log.debug("JWT found in the cache. Is authenticated: {}", tokenAuthentication.isAuthenticated());
381+
return tokenAuthentication;
380382
}
381383
}
382384

383-
QueryResponse queryResponse = parseJwtToken(jwtToken);
384-
boolean isValid = switch (queryResponse.getSource()) {
385-
case ZOWE -> {
386-
validateAndParseLocalJwtToken(jwtToken);
387-
yield true;
385+
try {
386+
QueryResponse queryResponse = parseJwtToken(jwtToken);
387+
switch (queryResponse.getSource()) {
388+
case ZOWE -> validateAndParseLocalJwtToken(jwtToken);
389+
case ZOSMF -> zosmfService.validate(jwtToken);
390+
default -> throw new TokenNotValidException("Unknown token type.");
388391
}
389-
case ZOSMF -> zosmfService.validate(jwtToken);
390-
default -> throw new TokenNotValidException("Unknown token type.");
391-
};
392-
boolean notInvalidated = !isInvalidated(jwtToken);
392+
boolean notInvalidated = !isInvalidated(jwtToken);
393+
return processJWTvalidationResult(queryResponse, jwtToken, notInvalidated);
394+
} catch (TokenNotValidException | TokenExpireException ex) {
395+
log.debug("JWT token is not valid", ex);
396+
processJWTvalidationResult(parseJwtTokenIgnoreExpiration(jwtToken), jwtToken, false);
397+
throw ex;
398+
}
399+
}
400+
401+
private TokenAuthentication processJWTvalidationResult(QueryResponse queryResponse, String jwtToken, Boolean tokenValid) {
393402
TokenAuthentication tokenAuthentication = new TokenAuthentication(queryResponse.getUserId(), jwtToken, TokenAuthentication.Type.JWT);
394-
tokenAuthentication.setAuthenticated(notInvalidated && isValid);
403+
tokenAuthentication.setAuthenticated(tokenValid);
395404

396405
putValidationCache(jwtToken, tokenAuthentication);
397406
log.debug("JWT validation result: {}", tokenAuthentication.isAuthenticated());
407+
398408
return tokenAuthentication;
399409
}
400410

@@ -456,10 +466,9 @@ public TokenAuthentication validateJwtToken(TokenAuthentication token) {
456466
if (token == null) {
457467
throw new TokenNotValidException("Null token.");
458468
}
459-
parseJwtToken(token.getCredentials()); // throws on expired token, this needs to happen before cache
460-
469+
461470
var tokenAuth = validateJwtToken(token.getCredentials());
462-
471+
463472
return tokenAuth;
464473
}
465474

@@ -472,10 +481,22 @@ public TokenAuthentication validateJwtToken(TokenAuthentication token) {
472481
*/
473482
public QueryResponse parseJwtToken(String jwtToken) {
474483
log.debug("Parsing JWT: ...{}", StringUtils.right(jwtToken, 15));
475-
var claims = getJwtClaims(jwtToken);
476-
return parseQueryResponse(claims);
484+
return parseQueryResponse(getJwtClaims(jwtToken));
477485
}
478486

487+
/**
488+
* Parses the JWT token and return a {@link QueryResponse} object containing the domain, user id, type (Zowe / z/OSMF),
489+
* date of creation and date of expiration. Does not validate expiration.
490+
*
491+
* @param jwtToken the JWT token
492+
* @return the query response
493+
*/
494+
public QueryResponse parseJwtTokenIgnoreExpiration(String jwtToken) {
495+
log.debug("Parsing JWT: ...{}", StringUtils.right(jwtToken, 15));
496+
return parseQueryResponse(getJwtClaimsIgnoreExpiration(jwtToken));
497+
}
498+
499+
479500
public QueryResponse parseQueryResponse(JWTClaimsSet claims) {
480501
Object scopesObject = claims.getClaim(SCOPES);
481502
List<String> scopes = Collections.emptyList();
@@ -596,7 +617,6 @@ private Optional<String> extractJwtTokenFromAuthorizationHeader(String header) {
596617
return Optional.empty();
597618
}
598619

599-
600620
/**
601621
* Calculate the expiration time
602622
*

zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/zosmf/ZosmfService.java

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,7 @@
6060
import java.io.IOException;
6161
import java.net.MalformedURLException;
6262
import java.net.URL;
63-
import java.util.Collections;
64-
import java.util.EnumMap;
65-
import java.util.HashMap;
66-
import java.util.List;
67-
import java.util.Map;
63+
import java.util.*;
6864

6965
import static org.zowe.apiml.zaas.security.service.zosmf.ZosmfService.TokenType.JWT;
7066
import static org.zowe.apiml.zaas.security.service.zosmf.ZosmfService.TokenType.LTPA;
@@ -487,24 +483,47 @@ public boolean jwtBuilderEndpointExists() {
487483
return meAsProxy.jwtEndpointExists(headers);
488484
}
489485

486+
/**
487+
* Validates jwt token with all available strategies.
488+
*
489+
* @param token
490+
* @return true if at least one validation strategy evaluates token as valid
491+
* @throws ServiceNotAccessibleException if all validation strategies failed because of an error
492+
* @throws TokenNotValidException if all token validation strategies evaluate token as invalid
493+
*/
490494
public boolean validate(String token) {
491495
log.debug("ZosmfService validating token: ....{}", StringUtils.right(token, 15));
492496
TokenValidationRequest request = new TokenValidationRequest(TokenType.JWT, token, getURI(getZosmfServiceId()), getEndpointMap());
493497

498+
var isTokenValid = Optional.<Boolean>empty();
499+
494500
for (TokenValidationStrategy s : tokenValidationStrategy) {
495501
log.debug("Trying to validate token with strategy: {}", s.toString());
496502
try {
497503
s.validate(request);
498504
if (requestIsAuthenticated(request)) {
499505
log.debug("Token validity has been successfully determined: {}", request.getAuthenticated());
500-
return true;
506+
isTokenValid = Optional.of(true);
507+
break;
508+
} else {
509+
isTokenValid = Optional.of(false);
501510
}
502511
} catch (RuntimeException re) {
503512
log.debug("Exception during token validation:", re);
504513
}
505514
}
515+
506516
log.debug("Token validation strategies exhausted, final validation status: {}", request.getAuthenticated());
507-
return false;
517+
518+
if (isTokenValid.isPresent()) {
519+
if (isTokenValid.get()) {
520+
return true;
521+
} else {
522+
throw new TokenNotValidException("Token is not valid by any of zosmf validation strategies");
523+
}
524+
}
525+
526+
throw new ServiceNotAccessibleException("All token validation strategies has failed with " + request.getZosmfBaseUrl());
508527
}
509528

510529
private boolean requestIsAuthenticated(TokenValidationRequest request) {

0 commit comments

Comments
 (0)