Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.PostConstruct;
import lombok.AllArgsConstructor;
Expand Down Expand Up @@ -46,7 +47,9 @@
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.RestTemplate;
import org.zowe.apiml.security.common.config.AuthConfigurationProperties;
import org.zowe.apiml.security.common.auth.saf.PlatformReturned;
import org.zowe.apiml.security.common.error.ServiceNotAccessibleException;
import org.zowe.apiml.security.common.error.ZosAuthenticationException;
import org.zowe.apiml.security.common.login.ChangePasswordRequest;
import org.zowe.apiml.security.common.login.LoginRequest;
import org.zowe.apiml.security.common.token.TokenNotValidException;
Expand Down Expand Up @@ -306,6 +309,36 @@ private String getURI(String serviceId, String path) {
return UrlUtils.buildFullRequestUrl(url.getProtocol(), url.getHost(), url.getPort(), path, null);
}

/**
* Check whether a 401 response from z/OSMF indicates an expired password.
*
* @param e the HttpClientErrorException.Unauthorized exception with the response body
* @return true if the response body contains SAFReturnCode=8 and SAFReasonCode=24
*/
private boolean isExpiredPassword(HttpClientErrorException.Unauthorized e) {
byte[] responseBody = e.getResponseBodyAsByteArray();
if (responseBody == null || responseBody.length == 0) {
return false;
}
try {
JsonNode root = securityObjectMapper.readTree(responseBody);
JsonNode safMessages = root.get("safMessages");
if (safMessages != null && safMessages.isArray()) {
for (JsonNode message : safMessages) {
JsonNode safReturnCode = message.get("SAFReturnCode");
JsonNode safReasonCode = message.get("SAFReasonCode");
if (safReturnCode != null && safReturnCode.asInt() == 8
&& safReasonCode != null && safReasonCode.asInt() == 24) {
return true;
}
}
}
} catch (IOException e1) {
log.debug("Error parsing z/OSMF 401 response body: {}", e1.getMessage());
}
return false;
}

/**
* POST to provided url and return authentication response
*
Expand All @@ -324,6 +357,14 @@ protected AuthenticationResponse issueAuthenticationRequest(Authentication authe
httpMethod,
new HttpEntity<>(null, headers), String.class);
return getAuthenticationResponse(response);
} catch (HttpClientErrorException.Unauthorized e) {
if (isExpiredPassword(e)) {
throw new ZosAuthenticationException(PlatformReturned.builder()
.errno(168)
.errnoMsg("org.zowe.apiml.security.platform.errno.EMVSEXPIRE")
.build());
}
throw handleExceptionOnCall(url, e);
} catch (RuntimeException re) {
throw handleExceptionOnCall(url, re);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@
import org.springframework.web.client.RestTemplate;
import org.zowe.apiml.message.log.ApimlLogger;
import org.zowe.apiml.security.common.config.AuthConfigurationProperties;
import org.zowe.apiml.security.common.error.PlatformPwdErrno;
import org.zowe.apiml.security.common.error.ServiceNotAccessibleException;
import org.zowe.apiml.security.common.error.ZosAuthenticationException;
import org.zowe.apiml.security.common.login.ChangePasswordRequest;
import org.zowe.apiml.security.common.login.LoginRequest;
import org.zowe.apiml.security.common.token.TokenNotValidException;
Expand Down Expand Up @@ -1048,4 +1050,114 @@ void givenOtherAuthSourceAndZosmaIsNotAvailable_thenExceptionIsThrown() {

}

@Nested
class GivenExpiredPasswordResponse {

private ZosmfService zosmfService;
private Authentication authentication;

@BeforeEach
void setUp() {
zosmfService = getZosmfServiceSpy();
doReturn(true).when(zosmfService).loginEndpointExists();
authentication = new UsernamePasswordAuthenticationToken("user", "pass");
}

@Test
void whenZosmfReturnsExpiredPassword_thenThrowZosAuthenticationException() {
String responseBody = """
{
"safMessages": [
{
"SAFReturnCode": 8,
"SAFReasonCode": 24,
"SAFMessageText": "ICH408I USER(user) EXPIRED PASSWORD"
}
]
}
""";
HttpClientErrorException.Unauthorized exception = (HttpClientErrorException.Unauthorized)
HttpClientErrorException.create(
HttpStatus.UNAUTHORIZED, "Unauthorized", null,
responseBody.getBytes(), null);

doThrow(exception).when(restTemplate).exchange(
anyString(),
any(HttpMethod.class),
any(HttpEntity.class),
(Class<?>) any()
);

ZosAuthenticationException thrown = assertThrows(ZosAuthenticationException.class,
() -> zosmfService.authenticate(authentication));
assertEquals(PlatformPwdErrno.EMVSEXPIRE, thrown.getPlatformError());
}

@Test
void whenZosmfReturnsInvalidCredentials_thenThrowBadCredentialsException() {
String responseBody = """
{
"safMessages": [
{
"SAFReturnCode": 8,
"SAFReasonCode": 16,
"SAFMessageText": "ICH408I USER(user) INVALID PASSWORD"
}
]
}
""";
HttpClientErrorException.Unauthorized exception = (HttpClientErrorException.Unauthorized)
HttpClientErrorException.create(
HttpStatus.UNAUTHORIZED, "Unauthorized", null,
responseBody.getBytes(), null);

doThrow(exception).when(restTemplate).exchange(
anyString(),
any(HttpMethod.class),
any(HttpEntity.class),
(Class<?>) any()
);

assertThrows(BadCredentialsException.class,
() -> zosmfService.authenticate(authentication));
}

@Test
void whenZosmfReturnsEmptyBody_thenThrowBadCredentialsException() {
HttpClientErrorException.Unauthorized exception = (HttpClientErrorException.Unauthorized)
HttpClientErrorException.create(
HttpStatus.UNAUTHORIZED, "Unauthorized", null,
new byte[0], null);

doThrow(exception).when(restTemplate).exchange(
anyString(),
any(HttpMethod.class),
any(HttpEntity.class),
(Class<?>) any()
);

assertThrows(BadCredentialsException.class,
() -> zosmfService.authenticate(authentication));
}

@Test
void whenZosmfReturnsMalformedJson_thenThrowBadCredentialsException() {
String malformedJson = "{ not valid json }";
HttpClientErrorException.Unauthorized exception = (HttpClientErrorException.Unauthorized)
HttpClientErrorException.create(
HttpStatus.UNAUTHORIZED, "Unauthorized", null,
malformedJson.getBytes(), null);

doThrow(exception).when(restTemplate).exchange(
anyString(),
any(HttpMethod.class),
any(HttpEntity.class),
(Class<?>) any()
);

assertThrows(BadCredentialsException.class,
() -> zosmfService.authenticate(authentication));
}
}

}
Loading