Skip to content

Commit 047c54e

Browse files
authored
feat: Introduce rest provider for SAF tokens (#1714)
* Add mock implementation for the REST endpoint. * Initial implementation of the SAD IDT provider. * Use the actual JWT token * Generate the SAF token via REST call to defined API Signed-off-by: Jakub Balhar <jakub@balhar.net>
1 parent a7ee373 commit 047c54e

File tree

20 files changed

+783
-33
lines changed

20 files changed

+783
-33
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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.saf;
11+
12+
import java.util.Optional;
13+
14+
/**
15+
* It's possible to configure various SafIdtProviders. At the moment only one configured at the time is allowed. If there
16+
* are multiple providers configures, the behavior could be unpredictable.
17+
*/
18+
public interface SafIdtProvider {
19+
/**
20+
* If the current user has the proper rights generate the SAF token on its behalf and return it back.
21+
*
22+
* @return Either empty answer meaning the user is either unauthenticated or doesn't have the proper rights.
23+
*/
24+
Optional<String> generate(String username);
25+
26+
/**
27+
* Verify that the provided saf token is valid.
28+
*
29+
* @param safToken Token to validate.
30+
* @return true if the token is valid, false if it is invalid
31+
*/
32+
boolean verify(String safToken);
33+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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.saf;
11+
12+
import lombok.RequiredArgsConstructor;
13+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
14+
import org.springframework.context.annotation.Bean;
15+
import org.springframework.context.annotation.Configuration;
16+
import org.springframework.web.client.RestTemplate;
17+
import org.zowe.apiml.gateway.security.service.AuthenticationService;
18+
19+
@Configuration
20+
@RequiredArgsConstructor
21+
public class SafProviderBeansConfig {
22+
@Bean
23+
@ConditionalOnProperty(name = "apiml.security.saf.provider", havingValue = "rest")
24+
public SafIdtProvider restSafProvider(
25+
RestTemplate restTemplate,
26+
AuthenticationService authenticationService
27+
) {
28+
return new SafRestAuthenticationService(restTemplate, authenticationService);
29+
}
30+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
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.saf;
11+
12+
import com.netflix.zuul.context.RequestContext;
13+
import lombok.Data;
14+
import lombok.RequiredArgsConstructor;
15+
import lombok.extern.slf4j.Slf4j;
16+
import org.springframework.beans.factory.annotation.Value;
17+
import org.springframework.http.ResponseEntity;
18+
import org.springframework.web.client.HttpClientErrorException;
19+
import org.springframework.web.client.RestTemplate;
20+
import org.zowe.apiml.gateway.security.service.AuthenticationService;
21+
import org.zowe.apiml.security.common.token.TokenAuthentication;
22+
23+
import java.net.URI;
24+
import java.util.Optional;
25+
26+
import static org.springframework.util.StringUtils.isEmpty;
27+
28+
/**
29+
* Authentication provider implementation for the SafIdt Tokens that gets and verifies the tokens across the Restfull
30+
* interface
31+
* <p>
32+
* To work properly the implementation requires two urls:
33+
* <p>
34+
* - apiml.security.saf.urls.authenticate - URL to generate token
35+
* - apiml.security.saf.urls.verify - URL to verify the validity of the token
36+
*/
37+
@RequiredArgsConstructor
38+
@Slf4j
39+
public class SafRestAuthenticationService implements SafIdtProvider {
40+
private final RestTemplate restTemplate;
41+
private final AuthenticationService authenticationService;
42+
43+
@Value("${apiml.security.saf.urls.authenticate}")
44+
String authenticationUrl;
45+
@Value("${apiml.security.saf.urls.verify}")
46+
String verifyUrl;
47+
48+
@Override
49+
public Optional<String> generate(String username) {
50+
final RequestContext context = RequestContext.getCurrentContext();
51+
Optional<String> jwtToken = authenticationService.getJwtTokenFromRequest(context.getRequest());
52+
if (!jwtToken.isPresent()) {
53+
return Optional.empty();
54+
}
55+
56+
TokenAuthentication tokenAuthentication = authenticationService.validateJwtToken(jwtToken.get());
57+
if (!tokenAuthentication.isAuthenticated()) {
58+
return Optional.empty();
59+
}
60+
61+
try {
62+
Authentication authentication = new Authentication();
63+
authentication.setJwt(jwtToken.get());
64+
authentication.setUsername(username);
65+
66+
ResponseEntity<Token> re = restTemplate.postForEntity(URI.create(authenticationUrl), authentication, Token.class);
67+
68+
if (!re.getStatusCode().is2xxSuccessful()) {
69+
return Optional.empty();
70+
}
71+
72+
Token responseBody = re.getBody();
73+
if (responseBody == null) {
74+
return Optional.empty();
75+
}
76+
77+
return Optional.of(responseBody.getJwt());
78+
} catch (HttpClientErrorException.Unauthorized e) {
79+
return Optional.empty();
80+
}
81+
}
82+
83+
@Override
84+
public boolean verify(String safToken) {
85+
if (isEmpty(safToken)) {
86+
return false;
87+
}
88+
89+
try {
90+
Token token = new Token();
91+
token.setJwt(safToken);
92+
93+
ResponseEntity<String> re = restTemplate.postForEntity(URI.create(verifyUrl), token, String.class);
94+
95+
return re.getStatusCode().is2xxSuccessful();
96+
} catch (HttpClientErrorException.Unauthorized e) {
97+
return false;
98+
}
99+
}
100+
101+
@Data
102+
public static class Token {
103+
String jwt;
104+
}
105+
106+
@Data
107+
public static class Authentication {
108+
String jwt;
109+
String username;
110+
}
111+
}

gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/schema/SafIdtScheme.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import org.zowe.apiml.auth.Authentication;
1717
import org.zowe.apiml.auth.AuthenticationScheme;
1818
import org.zowe.apiml.gateway.security.service.AuthenticationService;
19-
import org.zowe.apiml.gateway.security.service.SafAuthenticationService;
19+
import org.zowe.apiml.gateway.security.service.saf.SafIdtProvider;
2020
import org.zowe.apiml.security.common.token.QueryResponse;
2121
import org.zowe.apiml.security.common.token.TokenAuthentication;
2222

@@ -32,7 +32,7 @@
3232
@RequiredArgsConstructor
3333
public class SafIdtScheme implements AbstractAuthenticationScheme {
3434
private final AuthenticationService authenticationService;
35-
private final SafAuthenticationService safAuthenticationService;
35+
private final SafIdtProvider safIdtProvider;
3636

3737
@Override
3838
public AuthenticationScheme getScheme() {
@@ -60,10 +60,10 @@ public void apply(InstanceInfo instanceInfo) {
6060
jwtToken.ifPresent(token -> {
6161
TokenAuthentication authentication = authenticationService.validateJwtToken(jwtToken.get());
6262
if (authentication.isAuthenticated()) {
63-
String safIdt = safAuthenticationService.generateSafIdt(token);
63+
// Get principal from the token?
64+
Optional<String> safIdt = safIdtProvider.generate(authentication.getPrincipal());
6465

65-
// TODO: Verify whether authentication should be used.
66-
context.addZuulRequestHeader("X-SAF-Token", safIdt);
66+
safIdt.ifPresent(safToken -> context.addZuulRequestHeader("X-SAF-Token", safToken));
6767
}
6868
});
6969
}

gateway-service/src/main/resources/application.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ apiml:
6262
x509:
6363
enabled: false
6464
externalMapperUrl:
65+
saf:
66+
provider: rest
67+
urls:
68+
authenticate: https://localhost:10013/zss/saf/authenticate
69+
verify: https://localhost:10013/zss/saf/verify
6570

6671
spring:
6772
application:

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,6 @@ class GivenAuthenticatedUserAndMoreInstancesOfService {
5454
@Nested
5555
class WhenCallingToServiceMultipleTimes {
5656

57-
boolean initialized = false;
58-
5957
@RepeatedTest(10)
6058
void thenCallTheSameInstance(RepetitionInfo repetitionInfo) throws IOException {
6159

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

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,13 @@
1616
import org.junit.jupiter.api.Nested;
1717
import org.junit.jupiter.api.Test;
1818
import org.mockito.ArgumentCaptor;
19+
import org.springframework.beans.factory.annotation.Autowired;
20+
import org.springframework.http.ResponseEntity;
21+
import org.springframework.test.util.ReflectionTestUtils;
22+
import org.springframework.web.client.RestTemplate;
1923
import org.zowe.apiml.acceptance.common.AcceptanceTest;
2024
import org.zowe.apiml.acceptance.common.AcceptanceTestWithTwoServices;
25+
import org.zowe.apiml.gateway.security.service.saf.SafRestAuthenticationService;
2126

2227
import java.io.IOException;
2328

@@ -32,6 +37,17 @@
3237
*/
3338
@AcceptanceTest
3439
class SafIdtSchemeTest extends AcceptanceTestWithTwoServices {
40+
@Autowired
41+
protected SafRestAuthenticationService safRestAuthenticationService;
42+
43+
private RestTemplate mockTemplate;
44+
45+
@BeforeEach
46+
void prepareTemplate() {
47+
mockTemplate = mock(RestTemplate.class);
48+
ReflectionTestUtils.setField(safRestAuthenticationService, "restTemplate", mockTemplate);
49+
}
50+
3551
@Nested
3652
class GivenValidJwtToken {
3753
Cookie validJwtToken;
@@ -45,6 +61,9 @@ void setUp() {
4561
class WhenSafIdtRequestedByService {
4662
@BeforeEach
4763
void prepareService() throws IOException {
64+
applicationRegistry.clearApplications();
65+
applicationRegistry.addApplication(serviceWithDefaultConfiguration, false, true, false, "authentication", true);
66+
applicationRegistry.addApplication(serviceWithCustomConfiguration, true, false, true, "authentication", true);
4867
applicationRegistry.setCurrentApplication(serviceWithDefaultConfiguration.getId());
4968

5069
reset(mockClient);
@@ -54,6 +73,15 @@ void prepareService() throws IOException {
5473
// Valid token is provided within the headers.
5574
@Test
5675
void thenValidTokenIsProvided() throws IOException {
76+
String resultSafToken = "resultSafToken";
77+
78+
ResponseEntity<Object> response = mock(ResponseEntity.class);
79+
when(mockTemplate.postForEntity(any(), any(), any())).thenReturn(response);
80+
when(response.getStatusCode()).thenReturn(org.springframework.http.HttpStatus.CREATED);
81+
SafRestAuthenticationService.Token responseBody = new SafRestAuthenticationService.Token();
82+
responseBody.setJwt(resultSafToken);
83+
when(response.getBody()).thenReturn(responseBody);
84+
5785
given()
5886
.cookie(validJwtToken)
5987
.when()
@@ -64,7 +92,7 @@ void thenValidTokenIsProvided() throws IOException {
6492
ArgumentCaptor<HttpUriRequest> captor = ArgumentCaptor.forClass(HttpUriRequest.class);
6593
verify(mockClient, times(1)).execute(captor.capture());
6694

67-
assertHeaderWithValue(captor.getValue(), "X-SAF-Token", "validToken" + validJwtToken.getValue());
95+
assertHeaderWithValue(captor.getValue(), "X-SAF-Token", resultSafToken);
6896
}
6997
}
7098
}
@@ -82,17 +110,17 @@ void prepareService() throws IOException {
82110
}
83111

84112
@Test
85-
void givenInvalidJwtToken() throws IOException {
113+
void givenInvalidJwtToken() {
86114
Cookie withInvalidToken = new Cookie.Builder("apimlAuthenticationToken=invalidValue").build();
87115

88116
given()
89117
.cookie(withInvalidToken)
90118
.when()
91119
.get(basePath + serviceWithDefaultConfiguration.getPath())
92120
.then()
93-
.statusCode(is(HttpStatus.SC_UNAUTHORIZED));
121+
.statusCode(is(HttpStatus.SC_OK));
94122

95-
verify(mockClient, times(0)).execute(any());
123+
verify(mockTemplate, times(0)).postForEntity(any(), any(), any());
96124
}
97125
}
98126
}

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

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ public void addApplication(Service service, boolean addTimeout, boolean corsEnab
4949
addApplication(service, addTimeout, corsEnabled, multipleInstances, "headerRequest");
5050
}
5151

52+
public void addApplication(Service service, boolean addTimeout, boolean corsEnabled, boolean multipleInstances, String loadBalancerStrategy) {
53+
addApplication(service, addTimeout, corsEnabled, multipleInstances, loadBalancerStrategy, false);
54+
}
55+
5256
/**
5357
* Add new route to a service.
5458
*
@@ -58,15 +62,15 @@ public void addApplication(Service service, boolean addTimeout, boolean corsEnab
5862
* @param loadBalancerStrategy What strategy should be applied by LoadBalancer. E.g use header, base the
5963
* authentication on the user and so on.
6064
*/
61-
public void addApplication(Service service, boolean addTimeout, boolean corsEnabled, boolean multipleInstances, String loadBalancerStrategy) {
65+
public void addApplication(Service service, boolean addTimeout, boolean corsEnabled, boolean multipleInstances, String loadBalancerStrategy, boolean safIdt) {
6266
String id = service.getId();
6367
String locationPattern = service.getLocationPattern();
6468
String serviceRoute = service.getServiceRoute();
6569

6670
Applications applications = new Applications();
6771
Application withMetadata = new Application(id);
6872

69-
Map<String, String> metadata = createMetadata(addTimeout, corsEnabled, loadBalancerStrategy);
73+
Map<String, String> metadata = createMetadata(addTimeout, corsEnabled, loadBalancerStrategy, safIdt);
7074

7175
withMetadata.addInstance(getStandardInstance(metadata, id, id));
7276
if (multipleInstances) {
@@ -149,7 +153,7 @@ private InstanceInfo getStandardInstance(Map<String, String> metadata, String se
149153
.build();
150154
}
151155

152-
private Map<String, String> createMetadata(boolean addRibbonConfig, boolean corsEnabled, String loadBalancerStrategy) {
156+
private Map<String, String> createMetadata(boolean addRibbonConfig, boolean corsEnabled, String loadBalancerStrategy, boolean safIdt) {
153157
Map<String, String> metadata = new HashMap<>();
154158
if (addRibbonConfig) {
155159
metadata.put("apiml.connectTimeout", "5000");
@@ -161,7 +165,9 @@ private Map<String, String> createMetadata(boolean addRibbonConfig, boolean cors
161165
metadata.put("apiml.lb.cacheRecordExpirationTimeInHours", "8");
162166
metadata.put("apiml.corsEnabled", String.valueOf(corsEnabled));
163167
metadata.put("apiml.routes.gateway-url", "/");
164-
metadata.put("apiml.authentication.scheme", "safIdt");
168+
if (safIdt) {
169+
metadata.put("apiml.authentication.scheme", "safIdt");
170+
}
165171
return metadata;
166172
}
167173

0 commit comments

Comments
 (0)