Skip to content

Commit 26a84c0

Browse files
authored
feat: Support SAF IDT as authentication scheme (#1688)
* Add safIdt among the supported Schemas * Create stub for SafAuthenticationService providing the token * Create initial version of the Scheme providing the SafIdtCommand * Working test verifying the valid JWT token is exchanged for SAF IDT token as generated by Stub implementation at the time. * Reject expired token * Reject non-authenticated JWT token Signed-off-by: Jakub Balhar <jakub@balhar.net>
1 parent 8ef4546 commit 26a84c0

File tree

8 files changed

+328
-2
lines changed

8 files changed

+328
-2
lines changed

common-service-core/src/main/java/org/zowe/apiml/auth/Authentication.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public boolean supportsSso() {
4747
case ZOWE_JWT:
4848
case X509:
4949
case HTTP_BASIC_PASSTICKET:
50+
case SAF_IDT:
5051
case ZOSMF:
5152
return supportsSso == null || supportsSso;
5253
case BYPASS:

common-service-core/src/main/java/org/zowe/apiml/auth/AuthenticationScheme.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ public enum AuthenticationScheme {
2828
ZOSMF("zosmf"),
2929

3030
@JsonProperty("x509")
31-
X509("x509");
31+
X509("x509"),
32+
33+
@JsonProperty("safIdt")
34+
SAF_IDT("safIdt");
3235

3336
public final String scheme;
3437

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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.gateway.security.service;
11+
12+
import org.springframework.stereotype.Component;
13+
14+
/**
15+
* Authentication service
16+
*/
17+
@Component
18+
public class SafAuthenticationService {
19+
public String generateSafIdt(String jwtToken) {
20+
return "validToken" + jwtToken;
21+
}
22+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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.gateway.security.service.schema;
11+
12+
import com.netflix.appinfo.InstanceInfo;
13+
import com.netflix.zuul.context.RequestContext;
14+
import lombok.RequiredArgsConstructor;
15+
import org.springframework.stereotype.Component;
16+
import org.zowe.apiml.auth.Authentication;
17+
import org.zowe.apiml.auth.AuthenticationScheme;
18+
import org.zowe.apiml.gateway.security.service.AuthenticationService;
19+
import org.zowe.apiml.gateway.security.service.SafAuthenticationService;
20+
import org.zowe.apiml.security.common.token.QueryResponse;
21+
import org.zowe.apiml.security.common.token.TokenAuthentication;
22+
23+
import java.util.Date;
24+
import java.util.Optional;
25+
import java.util.function.Supplier;
26+
27+
/**
28+
* The scheme allowing for the safIdt authentication scheme.
29+
* It adds new header with the SAF IDT token in case of valid JWT provided.
30+
*/
31+
@Component
32+
@RequiredArgsConstructor
33+
public class SafIdtScheme implements AbstractAuthenticationScheme {
34+
private final AuthenticationService authenticationService;
35+
private final SafAuthenticationService safAuthenticationService;
36+
37+
@Override
38+
public AuthenticationScheme getScheme() {
39+
return AuthenticationScheme.SAF_IDT;
40+
}
41+
42+
@Override
43+
public AuthenticationCommand createCommand(Authentication authentication, Supplier<QueryResponse> tokenSupplier) {
44+
// Same behavior as for the ZosmfScheme.
45+
final QueryResponse queryResponse = tokenSupplier.get();
46+
final Date expiration = queryResponse == null ? null : queryResponse.getExpiration();
47+
final Long expirationTime = expiration == null ? null : expiration.getTime();
48+
return new SafIdtCommand(expirationTime);
49+
}
50+
51+
@RequiredArgsConstructor
52+
public class SafIdtCommand extends AuthenticationCommand {
53+
private final Long expireAt;
54+
55+
@Override
56+
public void apply(InstanceInfo instanceInfo) {
57+
final RequestContext context = RequestContext.getCurrentContext();
58+
59+
Optional<String> jwtToken = authenticationService.getJwtTokenFromRequest(context.getRequest());
60+
jwtToken.ifPresent(token -> {
61+
TokenAuthentication authentication = authenticationService.validateJwtToken(jwtToken.get());
62+
if (authentication.isAuthenticated()) {
63+
String safIdt = safAuthenticationService.generateSafIdt(token);
64+
65+
// TODO: Verify whether authentication should be used.
66+
context.addZuulRequestHeader("X-SAF-Token", safIdt);
67+
}
68+
});
69+
}
70+
71+
@Override
72+
public boolean isExpired() {
73+
if (expireAt == null) return false;
74+
75+
return System.currentTimeMillis() > expireAt;
76+
}
77+
78+
@Override
79+
public boolean isRequiredValidJwt() {
80+
return true;
81+
}
82+
}
83+
}

gateway-service/src/test/java/org/zowe/apiml/acceptance/DeterministicUserBasedRoutingTest.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
import org.apache.http.client.methods.CloseableHttpResponse;
1616
import org.apache.http.client.methods.HttpUriRequest;
1717
import org.apache.http.message.BasicStatusLine;
18-
import org.junit.jupiter.api.*;
18+
import org.junit.jupiter.api.Nested;
19+
import org.junit.jupiter.api.RepeatedTest;
20+
import org.junit.jupiter.api.RepetitionInfo;
1921
import org.mockito.ArgumentCaptor;
2022
import org.mockito.Mockito;
2123
import org.springframework.beans.factory.annotation.Autowired;
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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.acceptance;
11+
12+
import io.restassured.http.Cookie;
13+
import org.apache.http.HttpStatus;
14+
import org.apache.http.client.methods.HttpUriRequest;
15+
import org.junit.jupiter.api.BeforeEach;
16+
import org.junit.jupiter.api.Nested;
17+
import org.junit.jupiter.api.Test;
18+
import org.mockito.ArgumentCaptor;
19+
import org.zowe.apiml.acceptance.common.AcceptanceTest;
20+
import org.zowe.apiml.acceptance.common.AcceptanceTestWithTwoServices;
21+
22+
import java.io.IOException;
23+
24+
import static io.restassured.RestAssured.given;
25+
import static org.hamcrest.MatcherAssert.assertThat;
26+
import static org.hamcrest.core.Is.is;
27+
import static org.mockito.Mockito.*;
28+
29+
/**
30+
* This test verifies that the token was exchanged. The input is a valid apimlJwtToken. The output to be tested is
31+
* the saf idt token.
32+
*/
33+
@AcceptanceTest
34+
class SafIdtSchemeTest extends AcceptanceTestWithTwoServices {
35+
@Nested
36+
class GivenValidJwtToken {
37+
Cookie validJwtToken;
38+
39+
@BeforeEach
40+
void setUp() {
41+
validJwtToken = securityRequests.validJwtToken();
42+
}
43+
44+
@Nested
45+
class WhenSafIdtRequestedByService {
46+
@BeforeEach
47+
void prepareService() throws IOException {
48+
applicationRegistry.setCurrentApplication(serviceWithDefaultConfiguration.getId());
49+
50+
reset(mockClient);
51+
mockValid200HttpResponse();
52+
}
53+
54+
// Valid token is provided within the headers.
55+
@Test
56+
void thenValidTokenIsProvided() throws IOException {
57+
given()
58+
.cookie(validJwtToken)
59+
.when()
60+
.get(basePath + serviceWithDefaultConfiguration.getPath())
61+
.then()
62+
.statusCode(is(HttpStatus.SC_OK));
63+
64+
ArgumentCaptor<HttpUriRequest> captor = ArgumentCaptor.forClass(HttpUriRequest.class);
65+
verify(mockClient, times(1)).execute(captor.capture());
66+
67+
assertHeaderWithValue(captor.getValue(), "X-SAF-Token", "validToken" + validJwtToken.getValue());
68+
}
69+
}
70+
}
71+
72+
@Nested
73+
class ThenNoTokenIsProvided {
74+
@Nested
75+
class WhenSafIdtRequestedByService {
76+
@BeforeEach
77+
void prepareService() throws IOException {
78+
applicationRegistry.setCurrentApplication(serviceWithDefaultConfiguration.getId());
79+
80+
reset(mockClient);
81+
mockValid200HttpResponse();
82+
}
83+
84+
@Test
85+
void givenInvalidJwtToken() throws IOException {
86+
Cookie withInvalidToken = new Cookie.Builder("apimlAuthenticationToken=invalidValue").build();
87+
88+
given()
89+
.cookie(withInvalidToken)
90+
.when()
91+
.get(basePath + serviceWithDefaultConfiguration.getPath())
92+
.then()
93+
.statusCode(is(HttpStatus.SC_UNAUTHORIZED));
94+
95+
verify(mockClient, times(0)).execute(any());
96+
}
97+
}
98+
}
99+
100+
private void assertHeaderWithValue(HttpUriRequest request, String header, String value) {
101+
assertThat(request.getHeaders(header).length, is(1));
102+
assertThat(request.getFirstHeader(header).getValue(), is(value));
103+
}
104+
}

gateway-service/src/test/java/org/zowe/apiml/acceptance/netflix/ApplicationRegistry.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ private Map<String, String> createMetadata(boolean addRibbonConfig, boolean cors
161161
metadata.put("apiml.lb.cacheRecordExpirationTimeInHours", "8");
162162
metadata.put("apiml.corsEnabled", String.valueOf(corsEnabled));
163163
metadata.put("apiml.routes.gateway-url", "/");
164+
metadata.put("apiml.authentication.scheme", "safIdt");
164165
return metadata;
165166
}
166167

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
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.gateway.security.service.schema;
11+
12+
import com.netflix.appinfo.InstanceInfo;
13+
import com.netflix.zuul.context.RequestContext;
14+
import org.junit.jupiter.api.BeforeEach;
15+
import org.junit.jupiter.api.Nested;
16+
import org.junit.jupiter.api.Test;
17+
import org.zowe.apiml.gateway.security.service.AuthenticationService;
18+
import org.zowe.apiml.gateway.security.service.SafAuthenticationService;
19+
import org.zowe.apiml.security.common.token.QueryResponse;
20+
import org.zowe.apiml.security.common.token.TokenAuthentication;
21+
22+
import java.util.Optional;
23+
import java.util.function.Supplier;
24+
25+
import static org.hamcrest.CoreMatchers.is;
26+
import static org.hamcrest.CoreMatchers.nullValue;
27+
import static org.hamcrest.MatcherAssert.assertThat;
28+
import static org.mockito.ArgumentMatchers.any;
29+
import static org.mockito.Mockito.mock;
30+
import static org.mockito.Mockito.when;
31+
32+
class SafIdtSchemeTest {
33+
private SafIdtScheme underTest;
34+
private AuthenticationService authenticationService;
35+
private SafAuthenticationService safAuthenticationService;
36+
37+
@BeforeEach
38+
void setUp() {
39+
authenticationService = mock(AuthenticationService.class);
40+
safAuthenticationService = mock(SafAuthenticationService.class);
41+
underTest = new SafIdtScheme(authenticationService, safAuthenticationService);
42+
}
43+
44+
@Nested
45+
class WhenTokenIsRequested {
46+
AuthenticationCommand commandUnderTest;
47+
48+
@BeforeEach
49+
void setCommandUnderTest() {
50+
QueryResponse response = mock(QueryResponse.class);
51+
Supplier<QueryResponse> supplier = () -> response;
52+
commandUnderTest = underTest.createCommand(null, supplier);
53+
}
54+
55+
@Nested
56+
class ThenValidSafTokenIsProduced {
57+
@Test
58+
void givenAuthenticatedJwtToken() {
59+
InstanceInfo info = mock(InstanceInfo.class);
60+
61+
TokenAuthentication authentication = new TokenAuthentication("validJwtToken");
62+
authentication.setAuthenticated(true);
63+
64+
when(authenticationService.getJwtTokenFromRequest(any())).thenReturn(Optional.of("validJwtToken"));
65+
when(authenticationService.validateJwtToken("validJwtToken")).thenReturn(authentication);
66+
when(safAuthenticationService.generateSafIdt("validJwtToken")).thenReturn("validTokenValidJwtToken");
67+
68+
commandUnderTest.apply(info);
69+
70+
assertThat(getValueOfZuulHeader(), is("validTokenValidJwtToken"));
71+
}
72+
}
73+
74+
@Nested
75+
class ThenNoTokenIsProduced {
76+
@Test
77+
void givenNoJwtToken() {
78+
InstanceInfo info = mock(InstanceInfo.class);
79+
80+
when(authenticationService.getJwtTokenFromRequest(any())).thenReturn(Optional.empty());
81+
82+
commandUnderTest.apply(info);
83+
84+
assertThat(getValueOfZuulHeader(), is(nullValue()));
85+
}
86+
87+
@Test
88+
void givenInvalidJwtToken() {
89+
InstanceInfo info = mock(InstanceInfo.class);
90+
91+
when(authenticationService.getJwtTokenFromRequest(any())).thenReturn(Optional.of("invalidJwtToken"));
92+
when(authenticationService.validateJwtToken("invalidJwtToken")).thenReturn(new TokenAuthentication("invalidJwtToken"));
93+
94+
commandUnderTest.apply(info);
95+
96+
assertThat(getValueOfZuulHeader(), is(nullValue()));
97+
}
98+
}
99+
}
100+
101+
private String getValueOfZuulHeader() {
102+
final RequestContext context = RequestContext.getCurrentContext();
103+
String valueOfHeader = context.getZuulRequestHeaders().get("x-saf-token");
104+
if (valueOfHeader == null) {
105+
return null;
106+
}
107+
108+
return valueOfHeader.replace("x-saf-token=", "");
109+
}
110+
}

0 commit comments

Comments
 (0)