Skip to content

Commit 303087c

Browse files
achmeloPetr Weinfurttaban03
authored
feat: exchange client certificate for SAF IDT (#2455)
* resolve conflicts Signed-off-by: achmelo <a.chmelo@gmail.com> * fix: Use valid localca.cer (#2384) * Replace localca.cer Signed-off-by: at670475 <andrea.tabone@broadcom.com> * fix Signed-off-by: at670475 <andrea.tabone@broadcom.com> * revert Signed-off-by: at670475 <andrea.tabone@broadcom.com> * use certificate for dev instance Signed-off-by: at670475 <andrea.tabone@broadcom.com> Signed-off-by: achmelo <a.chmelo@gmail.com> * remove unused imports Signed-off-by: achmelo <a.chmelo@gmail.com> Co-authored-by: Petr Weinfurt <petr.weinfurt@broadcom.com> Co-authored-by: Andrea Tabone <39694626+taban03@users.noreply.github.com>
1 parent 7772401 commit 303087c

File tree

6 files changed

+282
-134
lines changed

6 files changed

+282
-134
lines changed

api-catalog-ui/frontend/.env.development

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@ REACT_APP_STATUS_UPDATE_MAX_RETRIES=3
44
REACT_APP_STATUS_UPDATE_DEBOUNCE=300
55
REACT_APP_STATUS_UPDATE_SCALING_DURATION=1000
66
REACT_APP_GATEWAY_URL=
7+
SSL_CRT_FILE=../../keystore/localhost/localhost.pem
8+
SSL_KEY_FILE=../../keystore/localhost/localhost.keystore.key

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

Lines changed: 79 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -11,32 +11,30 @@
1111

1212
import com.netflix.appinfo.InstanceInfo;
1313
import com.netflix.zuul.context.RequestContext;
14-
15-
import java.util.Arrays;
16-
import java.util.Date;
17-
import java.util.Optional;
14+
import io.jsonwebtoken.Claims;
15+
import lombok.Getter;
1816
import lombok.RequiredArgsConstructor;
19-
2017
import org.apache.commons.lang3.time.DateUtils;
2118
import org.springframework.beans.factory.annotation.Value;
2219
import org.springframework.stereotype.Component;
2320
import org.zowe.apiml.auth.Authentication;
2421
import org.zowe.apiml.auth.AuthenticationScheme;
25-
import org.zowe.apiml.gateway.security.service.PassTicketException;
22+
import org.zowe.apiml.gateway.security.service.saf.SafIdtAuthException;
2623
import org.zowe.apiml.gateway.security.service.saf.SafIdtException;
2724
import org.zowe.apiml.gateway.security.service.saf.SafIdtProvider;
25+
import org.zowe.apiml.gateway.security.service.schema.source.AuthSchemeException;
2826
import org.zowe.apiml.gateway.security.service.schema.source.AuthSource;
2927
import org.zowe.apiml.gateway.security.service.schema.source.AuthSourceService;
3028
import org.zowe.apiml.passticket.IRRPassTicketGenerationException;
3129
import org.zowe.apiml.passticket.PassTicketService;
3230
import org.zowe.apiml.security.common.config.AuthConfigurationProperties;
3331
import org.zowe.apiml.security.common.token.TokenExpireException;
3432
import org.zowe.apiml.security.common.token.TokenNotValidException;
35-
import org.zowe.apiml.util.CookieUtil;
36-
37-
import io.jsonwebtoken.Claims;
3833

39-
import javax.annotation.PostConstruct;
34+
import javax.validation.constraints.NotNull;
35+
import java.util.Arrays;
36+
import java.util.Date;
37+
import java.util.Optional;
4038

4139
import static org.zowe.apiml.gateway.security.service.JwtUtils.getJwtClaims;
4240

@@ -54,12 +52,6 @@ public class SafIdtScheme implements IAuthenticationScheme {
5452

5553
@Value("${apiml.security.saf.defaultIdtExpiration:10}")
5654
int defaultIdtExpiration;
57-
private String cookieName;
58-
59-
@PostConstruct
60-
public void initCookieName() {
61-
cookieName = authConfigurationProperties.getCookieProperties().getCookieName();
62-
}
6355

6456
@Override
6557
public AuthenticationScheme getScheme() {
@@ -68,73 +60,103 @@ public AuthenticationScheme getScheme() {
6860

6961
@Override
7062
public AuthenticationCommand createCommand(Authentication authentication, AuthSource authSource) {
71-
final AuthSource.Parsed parsedAuthSource = authSourceService.parse(authSource);
72-
73-
if (parsedAuthSource == null) {
74-
return AuthenticationCommand.EMPTY;
63+
// check the authentication source
64+
if (authSource == null || authSource.getRawSource() == null) {
65+
throw new AuthSchemeException("org.zowe.apiml.gateway.security.schema.missingAuthentication");
66+
}
67+
// parse the authentication source
68+
AuthSource.Parsed parsedAuthSource;
69+
try {
70+
parsedAuthSource = authSourceService.parse(authSource);
71+
if (parsedAuthSource == null) {
72+
throw new IllegalStateException("Error occurred while parsing authenticationSource");
73+
}
74+
} catch (TokenNotValidException e) {
75+
throw new AuthSchemeException("org.zowe.apiml.gateway.security.invalidToken");
76+
} catch (TokenExpireException e) {
77+
throw new AuthSchemeException("org.zowe.apiml.gateway.security.expiredToken");
7578
}
7679

77-
final String userId = parsedAuthSource.getUserId();
78-
final String applId = authentication.getApplid();
80+
String safIdentityToken;
81+
long expireAt;
82+
83+
String applId = getApplId(authentication);
84+
safIdentityToken = generateSafIdentityToken(parsedAuthSource, applId);
85+
expireAt = getSafIdtExpiration(safIdentityToken);
86+
87+
return new SafIdtCommand(safIdentityToken, expireAt);
88+
}
89+
90+
@Override
91+
public Optional<AuthSource> getAuthSource() {
92+
return authSourceService.getAuthSourceFromRequest();
93+
}
94+
95+
private String getApplId(Authentication authentication) {
96+
String applId = authentication == null ? null : authentication.getApplid();
7997
if (applId == null) {
80-
throw new PassTicketException(
81-
"Applid is required. Check the configuration of service"
82-
);
98+
throw new AuthSchemeException("org.zowe.apiml.gateway.security.scheme.missingApplid");
8399
}
100+
return applId;
101+
}
84102

103+
private String generateSafIdentityToken(@NotNull AuthSource.Parsed parsedAuthSource, @NotNull String applId) {
85104
String safIdentityToken;
105+
106+
String userId = parsedAuthSource.getUserId();
107+
if (userId == null) {
108+
throw new AuthSchemeException("org.zowe.apiml.gateway.security.schema.x509.mappingFailed");
109+
}
110+
111+
char[] passTicket = "".toCharArray();
86112
try {
87-
char[] passTicket = passTicketService.generate(userId, applId).toCharArray();
88-
try {
89-
safIdentityToken = safIdtProvider.generate(userId, passTicket, applId);
90-
} finally {
91-
Arrays.fill(passTicket, (char) 0);
92-
}
113+
passTicket = passTicketService.generate(userId, applId).toCharArray();
114+
safIdentityToken = safIdtProvider.generate(userId, passTicket, applId);
93115
} catch (IRRPassTicketGenerationException e) {
94-
throw new PassTicketException(
95-
String.format("Could not generate PassTicket for user ID '%s' and APPLID '%s'", userId, applId), e
96-
);
116+
throw new AuthSchemeException("org.zowe.apiml.security.ticket.generateFailed", e.getMessage());
117+
} catch (SafIdtException | SafIdtAuthException e) {
118+
throw new AuthSchemeException("org.zowe.apiml.security.idt.failed", e.getMessage());
119+
} finally {
120+
Arrays.fill(passTicket, (char) 0);
97121
}
122+
return safIdentityToken;
123+
}
98124

125+
private long getSafIdtExpiration(String safIdentityToken) {
126+
Date expirationTime;
99127
try {
100128
Claims claims = getJwtClaims(safIdentityToken);
101-
Date expirationDate = claims.getExpiration();
102-
if (expirationDate == null) {
103-
expirationDate = DateUtils.addMinutes(new Date(), defaultIdtExpiration);
129+
expirationTime = claims.getExpiration();
130+
if (expirationTime == null) {
131+
expirationTime = DateUtils.addMinutes(new Date(), defaultIdtExpiration);
104132
}
105-
106-
return new SafIdtCommand(safIdentityToken, cookieName, expirationDate.getTime());
107-
} catch (TokenNotValidException | TokenExpireException e) {
108-
throw new SafIdtException("Unable to parse Identity Token", e);
133+
} catch (TokenNotValidException e) {
134+
throw new AuthSchemeException("org.zowe.apiml.gateway.security.invalidToken");
135+
} catch (TokenExpireException e) {
136+
throw new AuthSchemeException("org.zowe.apiml.gateway.security.expiredToken");
109137
}
110-
}
111-
112-
@Override
113-
public Optional<AuthSource> getAuthSource() {
114-
return authSourceService.getAuthSourceFromRequest();
138+
return expirationTime.getTime();
115139
}
116140

117141
@RequiredArgsConstructor
118142
public class SafIdtCommand extends AuthenticationCommand {
119143
private static final long serialVersionUID = 8213192949049438897L;
120144

145+
@Getter
121146
private final String safIdentityToken;
122-
private final String cookieName;
147+
@Getter
123148
private final Long expireAt;
124149

125-
private static final String COOKIE_HEADER = "cookie";
126-
private static final String SAF_TOKEN_HEADER = "X-SAF-Token";
150+
protected static final String SAF_TOKEN_HEADER = "X-SAF-Token";
127151

128152
@Override
129153
public void apply(InstanceInfo instanceInfo) {
130-
final RequestContext context = RequestContext.getCurrentContext();
131-
context.addZuulRequestHeader(SAF_TOKEN_HEADER, safIdentityToken);
132-
context.addZuulRequestHeader(COOKIE_HEADER,
133-
CookieUtil.removeCookie(
134-
context.getZuulRequestHeaders().get(COOKIE_HEADER),
135-
cookieName
136-
)
137-
);
154+
if (safIdentityToken != null) {
155+
final RequestContext context = RequestContext.getCurrentContext();
156+
// add header with SafIdt token to request and remove APIML token from Cookie if exists
157+
context.addZuulRequestHeader(SAF_TOKEN_HEADER, safIdentityToken);
158+
JwtCommand.removeCookie(context, authConfigurationProperties.getCookieProperties().getCookieName());
159+
}
138160
}
139161

140162
@Override

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

Lines changed: 49 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import io.jsonwebtoken.security.Keys;
1515
import io.restassured.http.Cookie;
1616
import org.apache.commons.lang3.time.DateUtils;
17+
import org.apache.http.Header;
1718
import org.apache.http.HttpStatus;
1819
import org.apache.http.client.methods.HttpUriRequest;
1920
import org.junit.jupiter.api.BeforeEach;
@@ -24,27 +25,33 @@
2425
import org.springframework.beans.factory.annotation.Value;
2526
import org.springframework.http.HttpMethod;
2627
import org.springframework.http.ResponseEntity;
28+
import org.springframework.test.context.TestPropertySource;
2729
import org.springframework.test.util.ReflectionTestUtils;
2830
import org.springframework.web.client.RestTemplate;
2931
import org.zowe.apiml.acceptance.common.AcceptanceTest;
3032
import org.zowe.apiml.acceptance.common.AcceptanceTestWithTwoServices;
3133
import org.zowe.apiml.acceptance.netflix.MetadataBuilder;
3234
import org.zowe.apiml.gateway.security.service.saf.SafRestAuthenticationService;
35+
import org.zowe.apiml.util.config.SslContext;
36+
import org.zowe.apiml.util.config.SslContextConfigurer;
3337

3438
import java.io.IOException;
3539
import java.util.Date;
36-
import org.zowe.apiml.util.config.SslContext;
3740

3841
import static io.restassured.RestAssured.given;
3942
import static org.hamcrest.MatcherAssert.assertThat;
4043
import static org.hamcrest.core.Is.is;
44+
import static org.junit.jupiter.api.Assertions.assertEquals;
45+
import static org.junit.jupiter.api.Assertions.assertNotNull;
4146
import static org.mockito.Mockito.*;
47+
import static org.zowe.apiml.gateway.filters.pre.ServiceAuthenticationFilter.AUTH_FAIL_HEADER;
4248

4349
/**
4450
* This test verifies that the token/client certificate was exchanged. The input is a valid apimlJwtToken/client certificate.
4551
* The output to be tested is the saf idt token.
4652
*/
4753
@AcceptanceTest
54+
@TestPropertySource(properties = {"spring.profiles.active=debug", "apiml.security.x509.externalMapperUrl="})
4855
class SafIdtSchemeTest extends AcceptanceTestWithTwoServices {
4956
@Value("${server.ssl.keyStorePassword:password}")
5057
private char[] keystorePassword;
@@ -135,47 +142,35 @@ void prepareService() throws IOException {
135142
}
136143

137144
@Test
138-
void givenInvalidJwtToken() {
145+
void givenInvalidJwtToken() throws IOException {
139146
Cookie withInvalidToken = new Cookie.Builder("apimlAuthenticationToken=invalidValue").build();
140147

141148
//@formatter:off
142149
given()
143150
.cookie(withInvalidToken)
144-
.when()
151+
.when()
145152
.get(basePath + serviceWithDefaultConfiguration.getPath())
146153
.then()
147154
.statusCode(is(HttpStatus.SC_OK));
148155
//@formatter:on
149156

157+
ArgumentCaptor<HttpUriRequest> captor = ArgumentCaptor.forClass(HttpUriRequest.class);
158+
verify(mockClient, times(1)).execute(captor.capture());
159+
150160
verify(mockTemplate, times(0))
151-
.exchange(any(), eq(HttpMethod.POST), any(), eq(SafRestAuthenticationService.Token.class));
161+
.exchange(any(), eq(HttpMethod.POST), any(), eq(SafRestAuthenticationService.Token.class));
162+
163+
Header zoweAuthFailureHeader = captor.getValue().getFirstHeader(AUTH_FAIL_HEADER);
164+
assertNotNull(zoweAuthFailureHeader);
165+
assertEquals("ZWEAG102E Token is not valid", zoweAuthFailureHeader.getValue());
152166
}
153167
}
154168
}
155169

156-
@Nested
157-
class GivenServerCertificate {
158-
@Test
159-
void thenSafheaderInRequestHeaders() throws IOException {
160-
applicationRegistry.setCurrentApplication(serviceWithDefaultConfiguration.getId());
161-
mockValid200HttpResponse();
162-
163-
given()
164-
.config(SslContext.apimlRootCert)
165-
.when()
166-
.get(basePath + serviceWithDefaultConfiguration.getPath())
167-
.then()
168-
.statusCode(is(HttpStatus.SC_OK));
169-
170-
ArgumentCaptor<HttpUriRequest> captor = ArgumentCaptor.forClass(HttpUriRequest.class);
171-
verify(mockClient, times(1)).execute(captor.capture());
172-
173-
assertThat(captor.getValue().getHeaders("X-SAF-Token").length, is(0));
174-
}
175-
}
176-
/*
177170
@Nested
178171
class GivenClientCertificate {
172+
String resultSafToken;
173+
179174
@BeforeEach
180175
void setUp() throws Exception {
181176
SslContextConfigurer configurer = new SslContextConfigurer(keystorePassword, clientKeystore, keystore);
@@ -188,45 +183,63 @@ void setUp() throws Exception {
188183
applicationRegistry.setCurrentApplication(serviceWithDefaultConfiguration.getId());
189184

190185
reset(mockClient);
186+
187+
resultSafToken = Jwts.builder()
188+
.setExpiration(DateUtils.addMinutes(new Date(), 10))
189+
.signWith(Keys.secretKeyFor(SignatureAlgorithm.HS256))
190+
.compact();
191+
192+
ResponseEntity<SafRestAuthenticationService.Token> response = mock(ResponseEntity.class);
193+
when(mockTemplate.exchange(any(), eq(HttpMethod.POST), any(), eq(SafRestAuthenticationService.Token.class)))
194+
.thenReturn(response);
195+
SafRestAuthenticationService.Token responseBody =
196+
new SafRestAuthenticationService.Token(resultSafToken, "applid");
197+
when(response.getBody()).thenReturn(responseBody);
198+
199+
mockValid200HttpResponse();
191200
}
192201

193202
@Nested
194203
class WhenClientAuthInExtendedKeyUsage {
195-
// TODO: add checks for transformation once X509 -> SafIdt is implemented
196204
@Test
197-
@Ignore
198-
void thenOk() throws IOException {
199-
mockValid200HttpResponse();
200-
205+
void thenValidSafIdTokenProvided() throws IOException {
201206
given()
202207
.config(SslContext.clientCertUser)
203208
.when()
204209
.get(basePath + serviceWithDefaultConfiguration.getPath())
205210
.then()
206211
.statusCode(is(HttpStatus.SC_OK));
212+
213+
ArgumentCaptor<HttpUriRequest> captor = ArgumentCaptor.forClass(HttpUriRequest.class);
214+
verify(mockClient, times(1)).execute(captor.capture());
215+
assertHeaderWithValue(captor.getValue(), "X-SAF-Token", resultSafToken);
216+
assertThat(captor.getValue().getHeaders(AUTH_FAIL_HEADER).length, is(0));
207217
}
208218
}
209219

210220
/**
211221
* When client certificate from request does not have extended key usage set correctly and can't be used for
212-
* client authentication then request fails with response code 400 - BAD REQUEST
213-
* /
222+
* client authentication then request will continue with X-Zowe-Auth-Failure header only.
223+
*/
214224
@Nested
215225
class WhenNoClientAuthInExtendedKeyUsage {
216226
@Test
217-
@Ignore
218-
void thenBadRequest() {
227+
void thenNoSafIdTokenProvided() throws IOException {
219228

220229
given()
221230
.config(SslContext.apimlRootCert)
222231
.when()
223232
.get(basePath + serviceWithDefaultConfiguration.getPath())
224233
.then()
225-
.statusCode(is(HttpStatus.SC_BAD_REQUEST));
234+
.statusCode(is(HttpStatus.SC_OK));
235+
236+
ArgumentCaptor<HttpUriRequest> captor = ArgumentCaptor.forClass(HttpUriRequest.class);
237+
verify(mockClient, times(1)).execute(captor.capture());
238+
assertThat(captor.getValue().getHeaders("X-SAF-Token").length, is(0));
239+
assertHeaderWithValue(captor.getValue(), AUTH_FAIL_HEADER, "ZWEAG165E X509 certificate is missing the client certificate extended usage definition");
226240
}
227241
}
228242
}
229-
*/
230243

231244
private void assertHeaderWithValue(HttpUriRequest request, String header, String value) {
232245
assertThat(request.getHeaders(header).length, is(1));

0 commit comments

Comments
 (0)