Skip to content

Commit

Permalink
feat: add validate oidc token call to zaas client (#3897)
Browse files Browse the repository at this point in the history
---------

Signed-off-by: Pablo Hernán Carle <[email protected]>
Co-authored-by: Zowe Robot <[email protected]>
Co-authored-by: Pablo Hernán Carle <[email protected]>
  • Loading branch information
3 people authored Nov 13, 2024
1 parent 1fcda6f commit 3f0ac10
Show file tree
Hide file tree
Showing 9 changed files with 254 additions and 13 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1429,7 +1429,9 @@ jobs:
- name: Dump CGW jacoco data
run: >
java -jar ./scripts/jacococli.jar dump --address gateway-service --port 6300 --destfile ./results/gateway-service.exec
- name: Correct Permisions
run: |
chmod 755 -R .gradle
- name: Store results
uses: actions/upload-artifact@v4
if: always()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.zowe.apiml.util.SecurityUtils;
import org.zowe.apiml.util.TestWithStartedInstances;
import org.zowe.apiml.util.categories.GeneralAuthenticationTest;
import org.zowe.apiml.util.categories.NotForMainframeTest;
Expand Down Expand Up @@ -153,6 +154,7 @@ class WhenLoggingIn {

@Nested
class ValidTokenIsReturned {

@Test
void givenValidCredentials() throws ZaasClientException {
String token = tokenService.login(USERNAME, PASSWORD.toCharArray());
Expand All @@ -166,11 +168,13 @@ void givenValidCredentialsInHeader() throws ZaasClientException {
assertNotNull(token);
assertThat(token, is(not(EMPTY_STRING)));
}

}

@Nested
@TestsNotMeantForZowe
class ProperExceptionIsRaised {

@ParameterizedTest(name = "givenInvalidCredentials {index} {0} ")
@MethodSource("org.zowe.apiml.integration.authentication.services.ZaasClientIntegrationTest#provideInvalidUsernamePassword")
void givenInvalidCredentials(String username, String password, ZaasClientErrorCodes expectedCode) {
Expand Down Expand Up @@ -204,7 +208,9 @@ void givenInvalidPasswordInHeader(String authHeader, ZaasClientErrorCodes expect

assertThatExceptionContainValidCode(exception, expectedCode);
}

}

}

@Nested
Expand Down Expand Up @@ -249,8 +255,28 @@ void givenEmptyToken() {
}
}

@Nested
class WhenOidcQuery {

private static final String VALID_TOKEN_NO_MAPPING = SecurityUtils.validOktaAccessToken(false);

void givenValidOidcToken_thenValidDetailsAreProvided() throws ZaasClientException {
var validationResult = tokenService.validateOidc(VALID_TOKEN_NO_MAPPING);
assertNotNull(validationResult);
assertTrue(validationResult.isValid());
}

void givenInvalidOidcToken_thenNotValidIsIssued() throws ZaasClientException {
var validationResult = tokenService.validateOidc("invalidtoken");
assertNotNull(validationResult);
assertFalse(validationResult.isValid());
}

}

@Nested
class WhenPassTicketRequested {

@Test
void givenValidToken_thenValidPassTicketIsReturned() throws ZaasClientException, ZaasConfigurationException {
String token = tokenService.login(USERNAME, PASSWORD.toCharArray());
Expand Down Expand Up @@ -288,6 +314,7 @@ void givenValidTokenButInvalidApplicationId() throws ZaasClientException {
}

}

}

@Nested
Expand All @@ -305,5 +332,7 @@ void givenInvalidTokenBut_thenExceptionIsThrown() {
assertThrows(ZaasClientException.class, () ->
tokenService.logout(token));
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* This program and the accompanying materials are made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Copyright Contributors to the Zowe Project.
*/

package org.zowe.apiml.zaasclient.oidc;

import lombok.AllArgsConstructor;
import lombok.Data;

/**
*
*/
@Data
@AllArgsConstructor
public class ZaasOidcValidationResult {

private boolean isValid = false;

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import jakarta.servlet.http.HttpServletRequest;
import org.zowe.apiml.zaasclient.exception.ZaasClientException;
import org.zowe.apiml.zaasclient.exception.ZaasConfigurationException;
import org.zowe.apiml.zaasclient.oidc.ZaasOidcValidationResult;

/**
* Get JWT tokens, PaasTickets and details about the Tokens.
Expand Down Expand Up @@ -106,6 +107,21 @@ public interface ZaasClient {
*/
ZaasToken query(HttpServletRequest request) throws ZaasClientException;

/***
* Return details about the provided OIDC token by validating it against an API Mediation Layer instance.
*
* This method supports simple boolean validation against Zowe v2 and v2 as of v3.0
*
* A successful validation means the token was created using the same settings as found in the target API Mediation Layer.
* This version does not validate if the OIDC token is mapped to a user if the API Mediation Layer is running on the z platform.
*
* @param token OIDC Access Token to validate against an OIDC-enabled API Mediation Layer instance
* @return ZaasOidcValidationResult instance
* @throws ZaasClientException thrown if no token is provided in the request, or if the target API Mediation Layer
* returned a server error.
*/
ZaasOidcValidationResult validateOidc(String token) throws ZaasClientException;

/**
* Retrieve PassTicket based on the valid JWT Token and the application id. To succeed the Application ID must
* be known by the PassTicker provider and the JWT token must be valid and not expired.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import jakarta.servlet.http.HttpServletRequest;
import org.zowe.apiml.zaasclient.exception.ZaasClientException;
import org.zowe.apiml.zaasclient.exception.ZaasConfigurationException;
import org.zowe.apiml.zaasclient.oidc.ZaasOidcValidationResult;
import org.zowe.apiml.zaasclient.service.ZaasToken;

/**
Expand Down Expand Up @@ -75,6 +76,18 @@ interface TokenService {
*/
ZaasToken query(HttpServletRequest request) throws ZaasClientException;

/**
* Return details about a passed OIDC token.
*
* As of Zowe v2 and v3.0: valid OIDC token is a token confirmed to be issued by the OIDC provider configured in the target API
* Mediation Layer
*
* @param token The token to validate against an API Mediation Layer instance
* @return whether the token is valid or not for the target API Mediation Layer
* @throws ZaasClientException thrown if no token is passed to the method or a server error occurred in the target API Mediation Layer.
*/
ZaasOidcValidationResult validateOidc(String token) throws ZaasClientException;

/**
* Invalidate the provided JWT token in order to perform logout.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import org.zowe.apiml.zaasclient.exception.ZaasClientException;
import org.zowe.apiml.zaasclient.exception.ZaasConfigurationErrorCodes;
import org.zowe.apiml.zaasclient.exception.ZaasConfigurationException;
import org.zowe.apiml.zaasclient.oidc.ZaasOidcValidationResult;
import org.zowe.apiml.zaasclient.service.ZaasClient;
import org.zowe.apiml.zaasclient.service.ZaasToken;

Expand All @@ -26,6 +27,7 @@
import java.util.Objects;

public class ZaasClientImpl implements ZaasClient, Closeable {

private final TokenService tokens;
private final PassTicketService passTickets;
private final CloseableHttpClient httpClientWithoutCert;
Expand Down Expand Up @@ -149,4 +151,12 @@ public void close() throws IOException {
httpClient.close();
httpClientWithoutCert.close();
}

@Override
public ZaasOidcValidationResult validateOidc(String token) throws ZaasClientException {
if (token == null || token.isEmpty()) {
throw new ZaasClientException(ZaasClientErrorCodes.TOKEN_NOT_PROVIDED);
}
return tokens.validateOidc(token);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

package org.zowe.apiml.zaasclient.service.internal;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
Expand All @@ -33,6 +34,7 @@
import org.zowe.apiml.zaasclient.config.ConfigProperties;
import org.zowe.apiml.zaasclient.exception.ZaasClientErrorCodes;
import org.zowe.apiml.zaasclient.exception.ZaasClientException;
import org.zowe.apiml.zaasclient.oidc.ZaasOidcValidationResult;
import org.zowe.apiml.zaasclient.service.ZaasToken;
import org.zowe.apiml.zaasclient.util.SimpleHttpResponse;

Expand All @@ -52,6 +54,7 @@ class ZaasJwtService implements TokenService {
private final String loginEndpoint;
private final String queryEndpoint;
private final String logoutEndpoint;
private final String validateOidcEndpoint;
private final CloseableHttpClient httpClient;

private final ObjectMapper objectMapper = new ObjectMapper();
Expand All @@ -63,6 +66,7 @@ public ZaasJwtService(CloseableHttpClient client, String baseUrl, ConfigProperti
loginEndpoint = baseUrl + "/login";
queryEndpoint = baseUrl + "/query";
logoutEndpoint = baseUrl + "/logout";
validateOidcEndpoint = baseUrl + "/oidc-token/validate";
zaasConfigProperties = configProperties;
}

Expand All @@ -83,7 +87,6 @@ public String login(String userId, char[] password) throws ZaasClientException {
}

private ClassicHttpRequest loginWithCredentials(String userId, char[] password, char[] newPassword) throws IOException {

var httpPost = new HttpPost(loginEndpoint);
String json = objectMapper.writeValueAsString(new Credentials(userId, password, newPassword));
var entity = new StringEntity(json);
Expand Down Expand Up @@ -139,6 +142,40 @@ public void logout(String jwtToken) throws ZaasClientException {
doLogoutRequest(() -> logoutJwtToken(jwtToken));
}

@Override
public ZaasOidcValidationResult validateOidc(String token) throws ZaasClientException {
if (token == null || token.isEmpty()) {
throw new ZaasClientException(ZaasClientErrorCodes.TOKEN_NOT_PROVIDED, "No token provided for OIDC validation");
}

return (ZaasOidcValidationResult) doRequest(
() -> validateOidcToken(token),
SimpleHttpResponse::fromResponseWithBytesBodyOnSuccess,
this::extractZaasOidcValidate
);
}

private ClassicHttpRequest validateOidcToken(String oidcToken) throws JsonProcessingException {
var httpPost = new HttpPost(validateOidcEndpoint);
String json = objectMapper.writeValueAsString(new TokenRequest(oidcToken));
var entity = new StringEntity(json);
httpPost.setEntity(entity);
return httpPost;
}

private ZaasOidcValidationResult extractZaasOidcValidate(SimpleHttpResponse response) throws IOException, ZaasClientException {
int statusCode = response.getCode();
if (statusCode == 204) {
return new ZaasOidcValidationResult(true);
}

if (statusCode == 401) {
return new ZaasOidcValidationResult(false);
} else {
throw new ZaasClientException(ZaasClientErrorCodes.GENERIC_EXCEPTION, response.getStringBody());
}
}

/**
* Get the JWT token from the authorization header in the http request
* <p>
Expand Down Expand Up @@ -307,11 +344,18 @@ static class Credentials {
char[] newPassword;
}

@Data
@AllArgsConstructor
static class TokenRequest {
String token;
}

interface TokenExtractor {
Object extract(SimpleHttpResponse response) throws IOException, ZaasClientException;
}

interface OperationGenerator {
ClassicHttpRequest request() throws IOException;
}

}
Loading

0 comments on commit 3f0ac10

Please sign in to comment.