Skip to content

Commit

Permalink
Merge branch 'nenaraab-decode-jwt-from-multiple-xsuaa'
Browse files Browse the repository at this point in the history
  • Loading branch information
mwdb committed May 13, 2019
2 parents 1e4e266 + 70221b8 commit 001d16b
Show file tree
Hide file tree
Showing 14 changed files with 173 additions and 44 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file.
## 1.4.0
* 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)
* Bugfix in basic authentication support: allow usage of JWT token or basic authentication with one configuration
* Allows overwrite / enhancement of XSUAA jwt token validators
* 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)

## 1.3.1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public interface Token extends UserDetails {
String CLAIM_XS_USER_ATTRIBUTES = "xs.user.attributes";
String CLAIM_SCOPES = "scope";
String GRANTTYPE_CLIENTCREDENTIAL = "client_credentials";
String CLIENT_ID = "cid";

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

/**
* Returns list of scopes with appId prefix, e.g.
* "<my-app!t123>.Display".
* Returns list of scopes with appId prefix, e.g. "<my-app!t123>.Display".
*
* @return all scopes
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}

@Override public Date getExpirationDate() {
@Override
public Date getExpirationDate() {
return jwt.getExpiresAt() != null ? Date.from(jwt.getExpiresAt()) : null;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,45 +1,74 @@
package com.sap.cloud.security.xsuaa.token.authentication;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import com.sap.cloud.security.xsuaa.XsuaaServiceConfiguration;
import com.sap.cloud.security.xsuaa.XsuaaServicesParser;
import com.sap.cloud.security.xsuaa.token.Token;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.util.Assert;

/**
* Validate audience using audience field content. in case this field is empty,
* the audience is derived from the scope field
*
*/
public class XsuaaAudienceValidator implements OAuth2TokenValidator<Jwt> {
private XsuaaServiceConfiguration xsuaaServiceConfiguration;
private Map<String, String> appIdClientIdMap = new HashMap<>();
private final Log logger = LogFactory.getLog(XsuaaServicesParser.class);

public XsuaaAudienceValidator(XsuaaServiceConfiguration xsuaaServiceConfiguration) {
this.xsuaaServiceConfiguration = xsuaaServiceConfiguration;
Assert.notNull(xsuaaServiceConfiguration, "'xsuaaServiceConfiguration' is required");
appIdClientIdMap.put(xsuaaServiceConfiguration.getAppId(), xsuaaServiceConfiguration.getClientId());
}

public void configureAnotherXsuaaInstance(String appId, String clientId) {
Assert.notNull(appId, "'appId' is required");
Assert.notNull(clientId, "'clientId' is required");
appIdClientIdMap.putIfAbsent(appId, clientId);
logger.info(String.format("configured XsuaaAudienceValidator with appId %s and clientId %s", appId, clientId));
}

@Override
public OAuth2TokenValidatorResult validate(Jwt token) {
String tokenClientId = token.getClaimAsString(Token.CLIENT_ID);
if (tokenClientId == null) {
OAuth2TokenValidatorResult.failure(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,
"Jwt token must contain 'cid' (client_id)", null));
}
List<String> allowedAudiences = getAllowedAudiences(token);

for (Map.Entry<String, String> xsuaaConfig : appIdClientIdMap.entrySet()) {
if (checkMatch(xsuaaConfig.getKey(), xsuaaConfig.getValue(), tokenClientId, allowedAudiences)) {
return OAuth2TokenValidatorResult.success();
}
}
return OAuth2TokenValidatorResult.failure(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,
"Jwt token audience matches none of these: " + appIdClientIdMap.keySet().toString(), null));
}

private boolean checkMatch(String appId, String clientId, String tokenClientId, List<String> allowedAudiences) {
// case 1 : token issued by own client (or master)
if (xsuaaServiceConfiguration.getClientId().equals(token.getClaimAsString("client_id"))
|| (xsuaaServiceConfiguration.getAppId().contains("!b")
&& token.getClaimAsString("client_id").contains("|")
&& token.getClaimAsString("client_id").endsWith("|" + xsuaaServiceConfiguration.getAppId()))) {
return OAuth2TokenValidatorResult.success();
if (clientId.equals(tokenClientId)
|| (appId.contains("!b")
&& tokenClientId.contains("|")
&& tokenClientId.endsWith("|" + appId))) {
return true;
} else {
// case 2: foreign token
List<String> allowedAudiences = getAllowedAudiences(token);
if (allowedAudiences.contains(xsuaaServiceConfiguration.getAppId())) {
return OAuth2TokenValidatorResult.success();
if (allowedAudiences.contains(appId)) {
return true;
} else {
return OAuth2TokenValidatorResult.failure(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,
"Missing audience " + xsuaaServiceConfiguration.getAppId(), null));
return false;
}
}
}
Expand All @@ -51,7 +80,7 @@ public OAuth2TokenValidatorResult validate(Jwt token) {
* @param token
* @return (empty) list of audiences
*/
List<String> getAllowedAudiences(Jwt token) {
static List<String> getAllowedAudiences(Jwt token) {
List<String> allAudiences = new ArrayList<>();
List<String> tokenAudiences = token.getAudience();

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

private List<String> getScopes(Jwt token) {
static List<String> getScopes(Jwt token) {
List<String> scopes = null;
scopes = token.getClaimAsStringList(Token.CLAIM_SCOPES);
return scopes != null ? scopes : new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.sap.cloud.security.xsuaa.token.authentication;

import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;

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

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

XsuaaJwtDecoder(XsuaaServiceConfiguration xsuaaServiceConfiguration, int cacheValidity, int cacheSize) {
cache = Caffeine.newBuilder().expireAfterWrite(5, TimeUnit.SECONDS).maximumSize(cacheSize).build();
XsuaaJwtDecoder(XsuaaServiceConfiguration xsuaaServiceConfiguration, int cacheValidityInSeconds, int cacheSize,
OAuth2TokenValidator<Jwt>... tokenValidators) {
cache = Caffeine.newBuilder().expireAfterWrite(cacheValidityInSeconds, TimeUnit.SECONDS).maximumSize(cacheSize)
.build();
this.xsuaaServiceConfiguration = xsuaaServiceConfiguration;
// configure token validators
this.tokenValidators.add(new JwtTimestampValidator());

if (tokenValidators == null) {
this.tokenValidators.add(new XsuaaAudienceValidator(xsuaaServiceConfiguration));
} else {
this.tokenValidators.addAll(Arrays.asList(tokenValidators));
}
}

@Override
Expand All @@ -45,12 +59,10 @@ public Jwt decode(String token) throws JwtException {
}
}

private JwtDecoder getDecoder(String zid, String subdomain) {
protected JwtDecoder getDecoder(String zid, String subdomain) {
String url = xsuaaServiceConfiguration.getTokenKeyUrl(zid, subdomain);
NimbusJwtDecoderJwkSupport decoder = new NimbusJwtDecoderJwkSupport(url);
OAuth2TokenValidator<Jwt> validators = new DelegatingOAuth2TokenValidator<>(new JwtTimestampValidator(),
new XsuaaAudienceValidator(xsuaaServiceConfiguration));
decoder.setJwtValidator(validators);
decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(tokenValidators));
return decoder;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
package com.sap.cloud.security.xsuaa.token.authentication;

import org.springframework.security.oauth2.jwt.JwtDecoder;

import com.sap.cloud.security.xsuaa.XsuaaServiceConfiguration;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;

public class XsuaaJwtDecoderBuilder {

private XsuaaServiceConfiguration configuration;
int decoderCacheValidity = 900;
int decoderCacheValidity = 900; // in seconds
int decoderCacheSize = 100;
OAuth2TokenValidator<Jwt>[] tokenValidators;

/**
* Utility for building a JWT decoder configuration
*
*
* @param configuration
* of the Xsuaa service
*/
Expand All @@ -22,11 +24,11 @@ public XsuaaJwtDecoderBuilder(XsuaaServiceConfiguration configuration) {

/**
* Assembles a JwtDecoder
*
*
* @return JwtDecoder
*/
public JwtDecoder build() {
return new XsuaaJwtDecoder(configuration, decoderCacheValidity, decoderCacheSize);
return new XsuaaJwtDecoder(configuration, decoderCacheValidity, decoderCacheSize, tokenValidators);
}

/**
Expand Down Expand Up @@ -55,4 +57,15 @@ public XsuaaJwtDecoderBuilder withDecoderCacheSize(int size) {
return this;
}

/**
* Configures clone token validator, in case of two xsuaa bindings (application
* and broker plan).
*
* @return this
*/
public XsuaaJwtDecoderBuilder withTokenValidators(OAuth2TokenValidator<Jwt>... tokenValidators) {
this.tokenValidators = tokenValidators;
return this;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,13 @@ static public UserInfo getUserInfo() throws UserInfoException {
* Obtain the Token object from the Spring SecurityContext
*
* @return Token object
* @throws AccessDeniedException in case there is no token, user is not authenticated
* @throws AccessDeniedException
* in case there is no token, user is not authenticated
*/
static public Token getToken() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

if(authentication == null) {
if (authentication == null) {
throw new AccessDeniedException("Access forbidden: not authenticated");
}

Expand All @@ -53,7 +54,7 @@ static public Token getToken() {
return (Token) principal;
}

static public void init(String appId , Jwt token, boolean extractLocalScopesOnly) {
static public void init(String appId, Jwt token, boolean extractLocalScopesOnly) {
TokenAuthenticationConverter authenticationConverter = new TokenAuthenticationConverter(appId);
authenticationConverter.setLocalScopeAsAuthorities(extractLocalScopesOnly);
Authentication authentication = authenticationConverter.convert(token);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public void extractCustomAuthoritiesWithScopes() {
@Test
public void authoritiesHaveLocalScopesWithoutAppIdPrefix() {
String scopeWithNamespace = xsAppName + ".iot.Delete";
String scopeWithOtherAppId = "anyAppId!200." + xsAppName + ".iot.Delete";
String scopeWithOtherAppId = "anyAppId!t200." + xsAppName + ".Delete";

Jwt jwt = new JwtGenerator()
.addScopes(xsAppName + "." + scopeAdmin, scopeRead, scopeWithNamespace, scopeWithOtherAppId)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.sap.cloud.security.xsuaa.token.authentication;

import static org.hamcrest.CoreMatchers.is;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import com.nimbusds.jwt.JWTClaimsSet;
import com.sap.cloud.security.xsuaa.XsuaaServiceConfiguration;
import com.sap.cloud.security.xsuaa.test.JwtGenerator;
import com.sap.cloud.security.xsuaa.token.Token;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;

public class XsuaaAudienceValidatorForCloneTokenTest {

private JWTClaimsSet.Builder claimsBuilder;
private String XSUAA_BROKER_XSAPPNAME = "brokerplanmasterapp!b123";
private String XSUAA_BROKER_CLIENT_ID = "sb-" + XSUAA_BROKER_XSAPPNAME;
private XsuaaAudienceValidator cut;

@Before
public void setup() {
XsuaaServiceConfiguration serviceConfiguration = new XsuaaAudienceValidatorTest.DummyXsuaaServiceConfiguration(
"sb-test1!t1", "test1!t1");
cut = new XsuaaAudienceValidator(serviceConfiguration);
cut.configureAnotherXsuaaInstance(XSUAA_BROKER_XSAPPNAME, XSUAA_BROKER_CLIENT_ID);

claimsBuilder = new JWTClaimsSet.Builder().issueTime(new Date()).expirationTime(JwtGenerator.NO_EXPIRE_DATE);
}

@Test
public void tokenWithClientId_like_brokerClientId_shouldBeIgnored() {
claimsBuilder.claim(Token.CLIENT_ID, XSUAA_BROKER_CLIENT_ID);

OAuth2TokenValidatorResult result = cut.validate(JwtGenerator.createFromClaims(claimsBuilder.build()));
Assert.assertFalse(result.hasErrors());
}

@Test
public void cloneTokenClientId_like_brokerClientId_shouldBeAccepted() {
claimsBuilder.claim(Token.CLIENT_ID, "sb-clone1!b22|" + XSUAA_BROKER_XSAPPNAME);

OAuth2TokenValidatorResult result = cut.validate(JwtGenerator.createFromClaims(claimsBuilder.build()));
Assert.assertFalse(result.hasErrors());
}

@Test
public void cloneTokenClientId_unlike_brokerClientId_raisesError() {
claimsBuilder.claim(Token.CLIENT_ID, "sb-clone1!b22|ANOTHERAPP!b12");

OAuth2TokenValidatorResult result = cut.validate(JwtGenerator.createFromClaims(claimsBuilder.build()));
Assert.assertTrue(result.hasErrors());

List<OAuth2Error> errors = new ArrayList<>(result.getErrors());
Assert.assertThat(errors.get(0).getDescription(),
is("Jwt token audience matches none of these: [test1!t1, brokerplanmasterapp!b123]"));
Assert.assertThat(errors.get(0).getErrorCode(), is(OAuth2ErrorCodes.INVALID_CLIENT));
}

}
Loading

0 comments on commit 001d16b

Please sign in to comment.