Skip to content

Commit 001d16b

Browse files
committed
Merge branch 'nenaraab-decode-jwt-from-multiple-xsuaa'
2 parents 1e4e266 + 70221b8 commit 001d16b

File tree

14 files changed

+173
-44
lines changed

14 files changed

+173
-44
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file.
44
## 1.4.0
55
* API method to query [token validity](https://github.com/SAP/cloud-security-xsuaa-integration/blob/master/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/Token.java#L167)
66
* Bugfix in basic authentication support: allow usage of JWT token or basic authentication with one configuration
7+
* Allows overwrite / enhancement of XSUAA jwt token validators
78
* Allow applications to initialize of Spring SecurityContext for non HTTP requests. As documented [here](https://github.com/SAP/cloud-security-xsuaa-integration/blob/master/spring-xsuaa/README.md)
89

910
## 1.3.1

spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/Token.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public interface Token extends UserDetails {
1313
String CLAIM_XS_USER_ATTRIBUTES = "xs.user.attributes";
1414
String CLAIM_SCOPES = "scope";
1515
String GRANTTYPE_CLIENTCREDENTIAL = "client_credentials";
16+
String CLIENT_ID = "cid";
1617

1718
/**
1819
* Subaccount identifier, which can be used as tenant guid
@@ -141,8 +142,7 @@ public interface Token extends UserDetails {
141142
String requestToken(XSTokenRequest tokenRequest) throws URISyntaxException;
142143

143144
/**
144-
* Returns list of scopes with appId prefix, e.g.
145-
* "<my-app!t123>.Display".
145+
* Returns list of scopes with appId prefix, e.g. "<my-app!t123>.Display".
146146
*
147147
* @return all scopes
148148
*/

spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/TokenImpl.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ public Collection<? extends GrantedAuthority> getAuthorities() {
6767
return this.authorities;
6868
}
6969

70-
@Override public Date getExpirationDate() {
70+
@Override
71+
public Date getExpirationDate() {
7172
return jwt.getExpiresAt() != null ? Date.from(jwt.getExpiresAt()) : null;
7273
}
7374

spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaAudienceValidator.java

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,74 @@
11
package com.sap.cloud.security.xsuaa.token.authentication;
22

33
import java.util.ArrayList;
4+
import java.util.HashMap;
45
import java.util.List;
6+
import java.util.Map;
57
import java.util.stream.Collectors;
68

79
import com.sap.cloud.security.xsuaa.XsuaaServiceConfiguration;
10+
import com.sap.cloud.security.xsuaa.XsuaaServicesParser;
811
import com.sap.cloud.security.xsuaa.token.Token;
12+
import org.apache.commons.logging.Log;
13+
import org.apache.commons.logging.LogFactory;
914
import org.springframework.security.oauth2.core.OAuth2Error;
1015
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
1116
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
1217
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
1318
import org.springframework.security.oauth2.jwt.Jwt;
19+
import org.springframework.util.Assert;
1420

1521
/**
1622
* Validate audience using audience field content. in case this field is empty,
1723
* the audience is derived from the scope field
18-
*
1924
*/
2025
public class XsuaaAudienceValidator implements OAuth2TokenValidator<Jwt> {
21-
private XsuaaServiceConfiguration xsuaaServiceConfiguration;
26+
private Map<String, String> appIdClientIdMap = new HashMap<>();
27+
private final Log logger = LogFactory.getLog(XsuaaServicesParser.class);
2228

2329
public XsuaaAudienceValidator(XsuaaServiceConfiguration xsuaaServiceConfiguration) {
24-
this.xsuaaServiceConfiguration = xsuaaServiceConfiguration;
30+
Assert.notNull(xsuaaServiceConfiguration, "'xsuaaServiceConfiguration' is required");
31+
appIdClientIdMap.put(xsuaaServiceConfiguration.getAppId(), xsuaaServiceConfiguration.getClientId());
32+
}
33+
34+
public void configureAnotherXsuaaInstance(String appId, String clientId) {
35+
Assert.notNull(appId, "'appId' is required");
36+
Assert.notNull(clientId, "'clientId' is required");
37+
appIdClientIdMap.putIfAbsent(appId, clientId);
38+
logger.info(String.format("configured XsuaaAudienceValidator with appId %s and clientId %s", appId, clientId));
2539
}
2640

2741
@Override
2842
public OAuth2TokenValidatorResult validate(Jwt token) {
43+
String tokenClientId = token.getClaimAsString(Token.CLIENT_ID);
44+
if (tokenClientId == null) {
45+
OAuth2TokenValidatorResult.failure(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,
46+
"Jwt token must contain 'cid' (client_id)", null));
47+
}
48+
List<String> allowedAudiences = getAllowedAudiences(token);
49+
50+
for (Map.Entry<String, String> xsuaaConfig : appIdClientIdMap.entrySet()) {
51+
if (checkMatch(xsuaaConfig.getKey(), xsuaaConfig.getValue(), tokenClientId, allowedAudiences)) {
52+
return OAuth2TokenValidatorResult.success();
53+
}
54+
}
55+
return OAuth2TokenValidatorResult.failure(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,
56+
"Jwt token audience matches none of these: " + appIdClientIdMap.keySet().toString(), null));
57+
}
58+
59+
private boolean checkMatch(String appId, String clientId, String tokenClientId, List<String> allowedAudiences) {
2960
// case 1 : token issued by own client (or master)
30-
if (xsuaaServiceConfiguration.getClientId().equals(token.getClaimAsString("client_id"))
31-
|| (xsuaaServiceConfiguration.getAppId().contains("!b")
32-
&& token.getClaimAsString("client_id").contains("|")
33-
&& token.getClaimAsString("client_id").endsWith("|" + xsuaaServiceConfiguration.getAppId()))) {
34-
return OAuth2TokenValidatorResult.success();
61+
if (clientId.equals(tokenClientId)
62+
|| (appId.contains("!b")
63+
&& tokenClientId.contains("|")
64+
&& tokenClientId.endsWith("|" + appId))) {
65+
return true;
3566
} else {
3667
// case 2: foreign token
37-
List<String> allowedAudiences = getAllowedAudiences(token);
38-
if (allowedAudiences.contains(xsuaaServiceConfiguration.getAppId())) {
39-
return OAuth2TokenValidatorResult.success();
68+
if (allowedAudiences.contains(appId)) {
69+
return true;
4070
} else {
41-
return OAuth2TokenValidatorResult.failure(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,
42-
"Missing audience " + xsuaaServiceConfiguration.getAppId(), null));
71+
return false;
4372
}
4473
}
4574
}
@@ -51,7 +80,7 @@ public OAuth2TokenValidatorResult validate(Jwt token) {
5180
* @param token
5281
* @return (empty) list of audiences
5382
*/
54-
List<String> getAllowedAudiences(Jwt token) {
83+
static List<String> getAllowedAudiences(Jwt token) {
5584
List<String> allAudiences = new ArrayList<>();
5685
List<String> tokenAudiences = token.getAudience();
5786

@@ -78,7 +107,7 @@ List<String> getAllowedAudiences(Jwt token) {
78107
return allAudiences.stream().distinct().filter(value -> !value.isEmpty()).collect(Collectors.toList());
79108
}
80109

81-
private List<String> getScopes(Jwt token) {
110+
static List<String> getScopes(Jwt token) {
82111
List<String> scopes = null;
83112
scopes = token.getClaimAsStringList(Token.CLAIM_SCOPES);
84113
return scopes != null ? scopes : new ArrayList<>();

spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaJwtDecoder.java

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package com.sap.cloud.security.xsuaa.token.authentication;
22

33
import java.text.ParseException;
4+
import java.util.ArrayList;
5+
import java.util.Arrays;
6+
import java.util.List;
47
import java.util.concurrent.TimeUnit;
58

69
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
@@ -24,10 +27,21 @@ public class XsuaaJwtDecoder implements JwtDecoder {
2427

2528
Cache<String, JwtDecoder> cache;
2629
private XsuaaServiceConfiguration xsuaaServiceConfiguration;
30+
private List<OAuth2TokenValidator<Jwt>> tokenValidators = new ArrayList<>();
2731

28-
XsuaaJwtDecoder(XsuaaServiceConfiguration xsuaaServiceConfiguration, int cacheValidity, int cacheSize) {
29-
cache = Caffeine.newBuilder().expireAfterWrite(5, TimeUnit.SECONDS).maximumSize(cacheSize).build();
32+
XsuaaJwtDecoder(XsuaaServiceConfiguration xsuaaServiceConfiguration, int cacheValidityInSeconds, int cacheSize,
33+
OAuth2TokenValidator<Jwt>... tokenValidators) {
34+
cache = Caffeine.newBuilder().expireAfterWrite(cacheValidityInSeconds, TimeUnit.SECONDS).maximumSize(cacheSize)
35+
.build();
3036
this.xsuaaServiceConfiguration = xsuaaServiceConfiguration;
37+
// configure token validators
38+
this.tokenValidators.add(new JwtTimestampValidator());
39+
40+
if (tokenValidators == null) {
41+
this.tokenValidators.add(new XsuaaAudienceValidator(xsuaaServiceConfiguration));
42+
} else {
43+
this.tokenValidators.addAll(Arrays.asList(tokenValidators));
44+
}
3145
}
3246

3347
@Override
@@ -45,12 +59,10 @@ public Jwt decode(String token) throws JwtException {
4559
}
4660
}
4761

48-
private JwtDecoder getDecoder(String zid, String subdomain) {
62+
protected JwtDecoder getDecoder(String zid, String subdomain) {
4963
String url = xsuaaServiceConfiguration.getTokenKeyUrl(zid, subdomain);
5064
NimbusJwtDecoderJwkSupport decoder = new NimbusJwtDecoderJwkSupport(url);
51-
OAuth2TokenValidator<Jwt> validators = new DelegatingOAuth2TokenValidator<>(new JwtTimestampValidator(),
52-
new XsuaaAudienceValidator(xsuaaServiceConfiguration));
53-
decoder.setJwtValidator(validators);
65+
decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(tokenValidators));
5466
return decoder;
5567
}
5668

spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaJwtDecoderBuilder.java

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
package com.sap.cloud.security.xsuaa.token.authentication;
22

3-
import org.springframework.security.oauth2.jwt.JwtDecoder;
4-
53
import com.sap.cloud.security.xsuaa.XsuaaServiceConfiguration;
4+
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
5+
import org.springframework.security.oauth2.jwt.Jwt;
6+
import org.springframework.security.oauth2.jwt.JwtDecoder;
67

78
public class XsuaaJwtDecoderBuilder {
89

910
private XsuaaServiceConfiguration configuration;
10-
int decoderCacheValidity = 900;
11+
int decoderCacheValidity = 900; // in seconds
1112
int decoderCacheSize = 100;
13+
OAuth2TokenValidator<Jwt>[] tokenValidators;
1214

1315
/**
1416
* Utility for building a JWT decoder configuration
15-
*
17+
*
1618
* @param configuration
1719
* of the Xsuaa service
1820
*/
@@ -22,11 +24,11 @@ public XsuaaJwtDecoderBuilder(XsuaaServiceConfiguration configuration) {
2224

2325
/**
2426
* Assembles a JwtDecoder
25-
*
27+
*
2628
* @return JwtDecoder
2729
*/
2830
public JwtDecoder build() {
29-
return new XsuaaJwtDecoder(configuration, decoderCacheValidity, decoderCacheSize);
31+
return new XsuaaJwtDecoder(configuration, decoderCacheValidity, decoderCacheSize, tokenValidators);
3032
}
3133

3234
/**
@@ -55,4 +57,15 @@ public XsuaaJwtDecoderBuilder withDecoderCacheSize(int size) {
5557
return this;
5658
}
5759

60+
/**
61+
* Configures clone token validator, in case of two xsuaa bindings (application
62+
* and broker plan).
63+
*
64+
* @return this
65+
*/
66+
public XsuaaJwtDecoderBuilder withTokenValidators(OAuth2TokenValidator<Jwt>... tokenValidators) {
67+
this.tokenValidators = tokenValidators;
68+
return this;
69+
}
70+
5871
}

spring-xsuaa/src/main/java/com/sap/xs2/security/container/SecurityContext.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,13 @@ static public UserInfo getUserInfo() throws UserInfoException {
3737
* Obtain the Token object from the Spring SecurityContext
3838
*
3939
* @return Token object
40-
* @throws AccessDeniedException in case there is no token, user is not authenticated
40+
* @throws AccessDeniedException
41+
* in case there is no token, user is not authenticated
4142
*/
4243
static public Token getToken() {
4344
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
4445

45-
if(authentication == null) {
46+
if (authentication == null) {
4647
throw new AccessDeniedException("Access forbidden: not authenticated");
4748
}
4849

@@ -53,7 +54,7 @@ static public Token getToken() {
5354
return (Token) principal;
5455
}
5556

56-
static public void init(String appId , Jwt token, boolean extractLocalScopesOnly) {
57+
static public void init(String appId, Jwt token, boolean extractLocalScopesOnly) {
5758
TokenAuthenticationConverter authenticationConverter = new TokenAuthenticationConverter(appId);
5859
authenticationConverter.setLocalScopeAsAuthorities(extractLocalScopesOnly);
5960
Authentication authentication = authenticationConverter.convert(token);

spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/token/TokenAuthenticationConverterTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ public void extractCustomAuthoritiesWithScopes() {
8181
@Test
8282
public void authoritiesHaveLocalScopesWithoutAppIdPrefix() {
8383
String scopeWithNamespace = xsAppName + ".iot.Delete";
84-
String scopeWithOtherAppId = "anyAppId!200." + xsAppName + ".iot.Delete";
84+
String scopeWithOtherAppId = "anyAppId!t200." + xsAppName + ".Delete";
8585

8686
Jwt jwt = new JwtGenerator()
8787
.addScopes(xsAppName + "." + scopeAdmin, scopeRead, scopeWithNamespace, scopeWithOtherAppId)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package com.sap.cloud.security.xsuaa.token.authentication;
2+
3+
import static org.hamcrest.CoreMatchers.is;
4+
5+
import java.util.ArrayList;
6+
import java.util.Date;
7+
import java.util.List;
8+
9+
import com.nimbusds.jwt.JWTClaimsSet;
10+
import com.sap.cloud.security.xsuaa.XsuaaServiceConfiguration;
11+
import com.sap.cloud.security.xsuaa.test.JwtGenerator;
12+
import com.sap.cloud.security.xsuaa.token.Token;
13+
import org.junit.Assert;
14+
import org.junit.Before;
15+
import org.junit.Test;
16+
import org.springframework.security.oauth2.core.OAuth2Error;
17+
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
18+
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
19+
20+
public class XsuaaAudienceValidatorForCloneTokenTest {
21+
22+
private JWTClaimsSet.Builder claimsBuilder;
23+
private String XSUAA_BROKER_XSAPPNAME = "brokerplanmasterapp!b123";
24+
private String XSUAA_BROKER_CLIENT_ID = "sb-" + XSUAA_BROKER_XSAPPNAME;
25+
private XsuaaAudienceValidator cut;
26+
27+
@Before
28+
public void setup() {
29+
XsuaaServiceConfiguration serviceConfiguration = new XsuaaAudienceValidatorTest.DummyXsuaaServiceConfiguration(
30+
"sb-test1!t1", "test1!t1");
31+
cut = new XsuaaAudienceValidator(serviceConfiguration);
32+
cut.configureAnotherXsuaaInstance(XSUAA_BROKER_XSAPPNAME, XSUAA_BROKER_CLIENT_ID);
33+
34+
claimsBuilder = new JWTClaimsSet.Builder().issueTime(new Date()).expirationTime(JwtGenerator.NO_EXPIRE_DATE);
35+
}
36+
37+
@Test
38+
public void tokenWithClientId_like_brokerClientId_shouldBeIgnored() {
39+
claimsBuilder.claim(Token.CLIENT_ID, XSUAA_BROKER_CLIENT_ID);
40+
41+
OAuth2TokenValidatorResult result = cut.validate(JwtGenerator.createFromClaims(claimsBuilder.build()));
42+
Assert.assertFalse(result.hasErrors());
43+
}
44+
45+
@Test
46+
public void cloneTokenClientId_like_brokerClientId_shouldBeAccepted() {
47+
claimsBuilder.claim(Token.CLIENT_ID, "sb-clone1!b22|" + XSUAA_BROKER_XSAPPNAME);
48+
49+
OAuth2TokenValidatorResult result = cut.validate(JwtGenerator.createFromClaims(claimsBuilder.build()));
50+
Assert.assertFalse(result.hasErrors());
51+
}
52+
53+
@Test
54+
public void cloneTokenClientId_unlike_brokerClientId_raisesError() {
55+
claimsBuilder.claim(Token.CLIENT_ID, "sb-clone1!b22|ANOTHERAPP!b12");
56+
57+
OAuth2TokenValidatorResult result = cut.validate(JwtGenerator.createFromClaims(claimsBuilder.build()));
58+
Assert.assertTrue(result.hasErrors());
59+
60+
List<OAuth2Error> errors = new ArrayList<>(result.getErrors());
61+
Assert.assertThat(errors.get(0).getDescription(),
62+
is("Jwt token audience matches none of these: [test1!t1, brokerplanmasterapp!b123]"));
63+
Assert.assertThat(errors.get(0).getErrorCode(), is(OAuth2ErrorCodes.INVALID_CLIENT));
64+
}
65+
66+
}

0 commit comments

Comments
 (0)