Skip to content

Commit

Permalink
feat: Add new /zaas/zosmf endpoint to provide JWT and LTPA tokens fro…
Browse files Browse the repository at this point in the history
…m z/OSMF. (#3153)

* Add /zaas/zosmf endpoint to generate zosmf JWT and LTPA tokens
  • Loading branch information
weinfurt authored Nov 10, 2023
1 parent b12ab6a commit cc96c81
Show file tree
Hide file tree
Showing 34 changed files with 1,529 additions and 426 deletions.
68 changes: 67 additions & 1 deletion .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,72 @@ jobs:
- uses: ./.github/actions/teardown

CITestsZaas:
needs: PublishJibContainers
runs-on: ubuntu-latest
container: ubuntu:latest
timeout-minutes: 15

services:
caching-service:
image: ghcr.io/balhar-jakub/caching-service:${{ github.run_id }}-${{ github.run_number }}
env:
ZWE_CACHING_SERVICE_PERSISTENT: 'infinispan'
CACHING_STORAGE_MODE: "infinispan"
JGROUPS_BIND_ADDRESS: "caching-service"
JGROUPS_BIND_PORT: "7099"
cloud-gateway-service:
image: ghcr.io/balhar-jakub/cloud-gateway-service:${{ github.run_id }}-${{ github.run_number }}
discovery-service:
image: ghcr.io/balhar-jakub/discovery-service:${{ github.run_id }}-${{ github.run_number }}
gateway-service:
image: ghcr.io/balhar-jakub/gateway-service:${{ github.run_id }}-${{ github.run_number }}
env:
APIML_SECURITY_PERSONALACCESSTOKEN_ENABLED: true
APIML_SECURITY_X509_ENABLED: true
APIML_SECURITY_X509_ACCEPTFORWARDEDCERT: true
APIML_SECURITY_X509_CERTIFICATESURL: https://cloud-gateway-service:10023/gateway/certificates
APIML_SECURITY_OIDC_CLIENTID: ${{ secrets.OKTA_CLIENT_ID }}
APIML_SECURITY_OIDC_CLIENTSECRET: ${{ secrets.OKTA_CLIENT_PASSWORD }}
APIML_SECURITY_OIDC_ENABLED: true
APIML_SECURITY_OIDC_REGISTRY: zowe.okta.com
APIML_SECURITY_OIDC_JWKS_URI: ${{ secrets.OKTA_JWK_URI }}
APIML_SECURITY_OIDC_IDENTITYMAPPERUSER: APIMTST
APIML_SECURITY_OIDC_IDENTITYMAPPERURL: https://gateway-service:10010/zss/api/v1/certificate/dn
mock-services:
image: ghcr.io/balhar-jakub/mock-services:${{ github.run_id }}-${{ github.run_number }}
env:
ZOSMF_APPLIEDAPARS: PH34201

steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.head_ref }}

- uses: ./.github/actions/setup

- name: Build with Gradle
run: >
./gradlew :integration-tests:runZaasTest --info -Denvironment.config=-docker -Denvironment.offPlatform=true
-Partifactory_user=${{ secrets.ARTIFACTORY_USERNAME }} -Partifactory_password=${{ secrets.ARTIFACTORY_PASSWORD }}
-Dokta.client.id=${{ secrets.OKTA_CLIENT_ID }} -Doidc.test.user=${{ secrets.OIDC_TEST_USER }}
-Doidc.test.pass=${{ secrets.OIDC_TEST_PASS }}
- name: Dump DC jacoco data
run: >
java -jar ./scripts/jacococli.jar dump --address gateway-service --port 6300 --destfile ./results/gateway-service.exec
- name: Store results
uses: actions/upload-artifact@v3
if: always()
with:
name: ContainerCITestsZaas-${{ env.JOB_ID }}
path: |
integration-tests/build/reports/**
results/**
- uses: ./.github/actions/teardown

CITestsZosmfRsu2012:
needs: PublishJibContainers
runs-on: ubuntu-latest
Expand Down Expand Up @@ -1454,7 +1520,7 @@ jobs:

- name: Code coverage and publish results
run: >
./gradlew --info coverage sonar -Dresults="containercitests/results,citestswithinfinispan/results,containercitestszosmfrsu2012/results,ContainerCITestsWithRedisReplica/results,ContainerCITestsWithRedisSentinel/results,containercitestsinternalport/results,cloudgatewayproxy/results,citestswebsocketchaoticha/results,cloudgatewayservicerouting/results"
./gradlew --info coverage sonar -Dresults="containercitests/results,citestswithinfinispan/results,containercitestszosmfrsu2012/results,ContainerCITestsWithRedisReplica/results,ContainerCITestsWithRedisSentinel/results,containercitestsinternalport/results,cloudgatewayproxy/results,citestswebsocketchaoticha/results,cloudgatewayservicerouting/results,containercitestszaas/results"
-Psonar.host.url=$SONAR_HOST_URL -Dsonar.login=$SONAR_TOKEN
-Partifactory_user=$ARTIFACTORY_USERNAME -Partifactory_password=$ARTIFACTORY_PASSWORD
env:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@
import org.springframework.security.core.AuthenticationException;

public class AuthenticationTokenException extends AuthenticationException {

public AuthenticationTokenException(String msg) {
super(msg);
}

public AuthenticationTokenException(String msg, Throwable cause) {
super(msg, cause);
}

}
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,10 @@ task runZosmfAuthTest(dependsOn: ":integration-tests:runZosmfAuthTest") {
description "Run zOSMF dependant authentication tests only"
group "Integration tests"
}
task runZaasTest(dependsOn: ":integration-tests:runZaasTest") {
description "Run Zaas dependant authentication tests only"
group "Integration tests"
}
task runX509AuthTest(dependsOn: ":integration-tests:runX509AuthTest") {
description "Run x509 dependant authentication tests only"
group "Integration tests"
Expand Down
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.zaas.zosmf;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ZosmfResponse {

String cookieName;
String token;

}
10 changes: 5 additions & 5 deletions config/local/gateway-service.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ apiml:
webfinger:
fileLocation: config/local/webfinger.yml
personalAccessToken:
enabled: false
enabled: true
oidc:
enabled: false
clientId:
clientSecret:
registry:
identityMapperUrl:
identityMapperUser:
registry: zowe.okta.com
identityMapperUrl: https://localhost:10010/zss/api/v1/certificate/dn
identityMapperUser: APIMTST
jwks:
uri:
auth:
Expand All @@ -35,7 +35,7 @@ apiml:
verifySslCertificatesOfServices: true
x509:
enabled: true
acceptForwardedCert: false
acceptForwardedCert: true
certificatesUrl: https://localhost:10023/gateway/certificates

banner: console
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
@RequiredArgsConstructor
@Slf4j
public class ExtractAuthSourceFilter extends OncePerRequestFilter {
static final String AUTH_SOURCE_ATTR = "zaas.auth.source";
public static final String AUTH_SOURCE_ATTR = "zaas.auth.source";
public static final String AUTH_SOURCE_PARSED_ATTR = "zaas.auth.source.parsed";

private final AuthSourceService authSourceService;
private final AuthExceptionHandler authExceptionHandler;
Expand All @@ -39,7 +40,8 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
Optional<AuthSource> authSource = authSourceService.getAuthSourceFromRequest(request);
if (authSource.isPresent()) {
AuthSource.Parsed parsed = authSourceService.parse(authSource.get());
request.setAttribute(AUTH_SOURCE_ATTR, parsed);
request.setAttribute(AUTH_SOURCE_ATTR, authSource.get());
request.setAttribute(AUTH_SOURCE_PARSED_ATTR, parsed);
filterChain.doFilter(request, response);
} else {
throw new InsufficientAuthenticationException("No authentication source found in the request.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
import org.zowe.apiml.gateway.security.service.schema.source.AuthSourceService;
import org.zowe.apiml.gateway.security.ticket.SuccessfulTicketHandler;
import org.zowe.apiml.gateway.services.ServicesInfoController;
import org.zowe.apiml.gateway.zaas.ZaasAuthenticationFilter;
import org.zowe.apiml.security.common.config.AuthConfigurationProperties;
import org.zowe.apiml.security.common.config.CertificateAuthenticationProvider;
import org.zowe.apiml.security.common.config.HandlerInitializer;
Expand Down Expand Up @@ -309,24 +310,25 @@ private X509AuthenticationFilter x509AuthenticationFilter() {
@Configuration
@RequiredArgsConstructor
@Order(9)
class ZaasTicketEndpoint {
class ZaasEndpoints {

private final CompoundAuthProvider compoundAuthProvider;

@Bean
public SecurityFilterChain authZaasTicketEndpointFilterChain(HttpSecurity http) throws Exception {
public SecurityFilterChain authZaasEndpointsFilterChain(HttpSecurity http) throws Exception {
baseConfigure(http.requestMatchers().antMatchers( // no http method to catch all attempts to login and handle them here. Otherwise it falls to default filterchain and tries to route the calls, which doesnt make sense
authConfigurationProperties.getRevokeMultipleAccessTokens() + "/**",
authConfigurationProperties.getEvictAccessTokensAndRules(),
"/gateway/zaas/ticket"
"/gateway/zaas/**"
).and())
.authorizeRequests()
.anyRequest().authenticated()
.and()
.x509().userDetailsService(x509UserDetailsService())
.and()
.addFilterBefore(new CategorizeCertsFilter(publicKeyCertificatesBase64, certificateValidator), org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter.class)
.addFilterBefore(new ExtractAuthSourceFilter(authSourceService, authExceptionHandler), org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter.class);
.addFilterAfter(new CategorizeCertsFilter(publicKeyCertificatesBase64, certificateValidator), org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter.class)
.addFilterAfter(new ExtractAuthSourceFilter(authSourceService, authExceptionHandler), org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter.class)
.addFilterAfter(new ZaasAuthenticationFilter(authSourceService, authExceptionHandler), CategorizeCertsFilter.class);

return http.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AuthenticationService {

private static final String LTPA_CLAIM_NAME = "ltpa";
public static final String LTPA_CLAIM_NAME = "ltpa";
private static final String DOMAIN_CLAIM_NAME = "dom";
private static final String AUTH_PROV_CLAIM = "auth.prov";
private static final String SCOPES = "scopes";
Expand Down Expand Up @@ -130,9 +130,18 @@ private String createJWT(String username, String issuer, Map<String, Object> cla
.signWith(jwtSecurityInitializer.getJwtSecret(), jwtSecurityInitializer.getSignatureAlgorithm()).compact();
}

@SuppressWarnings("java:S5659") // It is checking the signature securely - https://github.com/zowe/api-layer/issues/3191
public QueryResponse parseJwtWithSignature(String jwt) throws SignatureException {
Jwt<DefaultJwsHeader, DefaultClaims> parsedJwt = Jwts.parserBuilder().setSigningKey(jwtSecurityInitializer.getJwtSecret()).build().parse(jwt);
return parseQueryResponse(parsedJwt.getBody());
try {
Jwt<DefaultJwsHeader, DefaultClaims> parsedJwt = Jwts.parserBuilder()
.setSigningKey(jwtSecurityInitializer.getJwtSecret())
.build()
.parse(jwt);

return parseQueryResponse(parsedJwt.getBody());
} catch (RuntimeException exception) {
throw handleJwtParserException(exception);
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@
import org.springframework.stereotype.Service;
import org.zowe.apiml.gateway.security.login.Providers;
import org.zowe.apiml.gateway.security.login.zosmf.ZosmfAuthenticationProvider;
import org.zowe.apiml.gateway.security.service.zosmf.ZosmfService;
import org.zowe.apiml.passticket.IRRPassTicketGenerationException;
import org.zowe.apiml.passticket.PassTicketService;
import org.zowe.apiml.security.common.error.AuthenticationTokenException;
import org.zowe.apiml.security.common.token.TokenAuthentication;

import java.util.Collections;
import java.util.Map;
import java.util.Optional;

@RequiredArgsConstructor
Expand All @@ -31,6 +34,7 @@
public class TokenCreationService {
private final Providers providers;
private final Optional<ZosmfAuthenticationProvider> zosmfAuthenticationProvider;
private final ZosmfService zosmfService;
private final PassTicketService passTicketService;
private final AuthenticationService authenticationService;

Expand All @@ -45,26 +49,13 @@ public class TokenCreationService {
* @return Valid JWT token or null
*/
public String createJwtTokenWithoutCredentials(String user) {
boolean isZosmfUsedAndAvailable = false;
try {
isZosmfUsedAndAvailable = providers.isZosfmUsed() && providers.isZosmfAvailable();
} catch (AuthenticationServiceException ex) {
// Intentionally do nothing. The issue is logged deeper.
}

if (isZosmfUsedAndAvailable) {
try {
log.debug("ZOSMF is available and used. Attempt to authenticate with PassTicket");
log.debug("Generating PassTicket for user: {} and ZOSMF applid: {}", user, zosmfApplId);
String passTicket = passTicketService.generate(user, zosmfApplId);
log.debug("Generated passticket: {}", passTicket);
return ((TokenAuthentication) zosmfAuthenticationProvider
.orElseThrow(() -> new IllegalStateException("The z/OSMF is not configured. The config value `apiml.security.auth.provider` should be set to `zosmf`."))
.authenticate(new UsernamePasswordAuthenticationToken(user, passTicket)))
.getCredentials();
} catch (IRRPassTicketGenerationException e) {
throw new AuthenticationTokenException("Problem with generating PassTicket");
}
if (isZosmfAvailable()) {
log.debug("ZOSMF is available and used. Attempt to authenticate with PassTicket");
final String passTicket = generatePassTicket(user);
return ((TokenAuthentication) zosmfAuthenticationProvider
.orElseThrow(() -> new IllegalStateException("The z/OSMF is not configured. The config value `apiml.security.auth.provider` should be set to `zosmf`."))
.authenticate(new UsernamePasswordAuthenticationToken(user, passTicket)))
.getCredentials();
} else {
final String domain = "security-domain";
log.debug("ZOSMF is not available or used. Generating APIML's JWT Token.");
Expand All @@ -73,4 +64,36 @@ public String createJwtTokenWithoutCredentials(String user) {
return authenticationService.createTokenAuthentication(user, jwtTokenString).getCredentials();
}
}

public Map<ZosmfService.TokenType, String> createZosmfTokensWithoutCredentials(String user) {
if (!isZosmfAvailable()) return Collections.emptyMap();

log.debug("ZOSMF is available and used. Attempt to authenticate with PassTicket");
final String passTicket = generatePassTicket(user);

return zosmfService.authenticate(new UsernamePasswordAuthenticationToken(user, passTicket)).getTokens();
}

private boolean isZosmfAvailable() {
try {
return providers.isZosfmUsed() && providers.isZosmfAvailable();
} catch (AuthenticationServiceException ex) {
// Intentionally do nothing. The issue is logged deeper.
}

return false;
}

private String generatePassTicket(String user) {
try {
log.debug("Generating PassTicket for user: {} and ZOSMF applid: {}", user, zosmfApplId);
String passTicket = passTicketService.generate(user, zosmfApplId);
log.debug("Generated PassTicket: {}", passTicket);

return passTicket;
} catch (IRRPassTicketGenerationException e) {
throw new AuthenticationTokenException("Generation of PassTicket failed", e);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,21 @@
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;

@RequiredArgsConstructor
@Getter
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class PATAuthSource implements AuthSource {

public static final AuthSource.AuthSourceType type = AuthSource.AuthSourceType.PAT;

@EqualsAndHashCode.Include
private final String source;

@Setter
private String defaultServiceId;

@Override
public Object getRawSource() {
return source;
Expand Down
Loading

0 comments on commit cc96c81

Please sign in to comment.