package com.sap.cloud.security.adapter.spring; import com.sap.cloud.security.config.Environments; import com.sap.cloud.security.config.OAuth2ServiceConfiguration; import com.sap.cloud.security.config.cf.CFConstants; import com.sap.cloud.security.token.*; import com.sap.cloud.security.token.validation.ValidationResult; import com.sap.cloud.security.token.validation.Validator; import com.sap.cloud.security.token.validation.validators.JwtValidatorBuilder; import com.sap.cloud.security.xsuaa.Assertions; import com.sap.cloud.security.xsuaa.client.SpringOAuth2TokenKeyService; import com.sap.cloud.security.xsuaa.client.SpringOidcConfigurationService; import org.springframework.beans.factory.InitializingBean; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; import org.springframework.security.oauth2.provider.AuthorizationRequest; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices; import org.springframework.web.client.RestOperations; import org.springframework.web.client.RestTemplate; import javax.annotation.Nonnull; import java.util.*; import java.util.stream.Collectors; /** * This constructor requires a dependency to Spring-security oauth, which will * be deprecated soon. * * <pre> * {@code * <dependency> * <groupId>org.springframework.security.oauth</groupId> * <artifactId>spring-security-oauth2</artifactId> * <scope>provided</scope> * </dependency> * <dependency> * <groupId>org.springframework</groupId> * <artifactId>spring-beans</artifactId> * <scope>provided</scope> * </dependency> * } * </pre> * * By default it used Apache Rest Client for communicating with the OAuth2 * Server.<br> * * Spring Security framework initializes the * {@link org.springframework.security.core.context.SecurityContext} with the * {@code OAuth2Authentication} which is provided as part of * {@link #loadAuthentication} method. <br> * This gives you the following options: * <ul> * <li>All Spring security features are supported that uses * {@link org.springframework.security.core.context.SecurityContext#getAuthentication()}</li> * <li>You can access the {@code Authentication} via * {@link SecurityContextHolder#getContext()} also within asynchronous * threads.</li> * <li>You can access the {@code Token} via * {@link SpringSecurityContext#getToken()} also within asynchronous * threads.</li> * </ul> * */ public class SAPOfflineTokenServicesCloud implements ResourceServerTokenServices, InitializingBean { private final OAuth2ServiceConfiguration serviceConfiguration; private Validator<Token> tokenValidator; private JwtValidatorBuilder jwtValidatorBuilder; private boolean useLocalScopeAsAuthorities; private ScopeConverter xsuaaScopeConverter; /** * Constructs an instance which is preconfigured for XSUAA service configuration * from SAP CP Environment. */ public SAPOfflineTokenServicesCloud() { this(Environments.getCurrent().getXsuaaConfiguration()); } /** * Constructs an instance with custom configuration. * * @param serviceConfiguration * the service configuration. You can use * {@link com.sap.cloud.security.config.Environments} in order to * load service configuration from the binding information in your * environment. */ public SAPOfflineTokenServicesCloud(OAuth2ServiceConfiguration serviceConfiguration) { this(serviceConfiguration, new RestTemplate()); } /** * Constructs an instance with custom configuration and rest template. * * @param serviceConfiguration * the service configuration. You can use * {@link com.sap.cloud.security.config.Environments} in order to * load service configuration from the binding information in your * environment. * @param restOperations * the spring rest template */ public SAPOfflineTokenServicesCloud(OAuth2ServiceConfiguration serviceConfiguration, RestOperations restOperations) { this(serviceConfiguration, JwtValidatorBuilder.getInstance(serviceConfiguration) .withOAuth2TokenKeyService(new SpringOAuth2TokenKeyService(restOperations)) .withOidcConfigurationService(new SpringOidcConfigurationService(restOperations))); } SAPOfflineTokenServicesCloud(OAuth2ServiceConfiguration serviceConfiguration, JwtValidatorBuilder jwtValidatorBuilder) { Assertions.assertNotNull(serviceConfiguration, "serviceConfiguration is required."); Assertions.assertNotNull(jwtValidatorBuilder, "jwtValidatorBuilder is required."); this.serviceConfiguration = serviceConfiguration; this.jwtValidatorBuilder = jwtValidatorBuilder; if (serviceConfiguration.hasProperty(CFConstants.XSUAA.APP_ID)) { this.xsuaaScopeConverter = new XsuaaScopeConverter( serviceConfiguration.getProperty(CFConstants.XSUAA.APP_ID)); } } /** * Configure another XSUAA instance, e.g. of plan broker. * * @param otherServiceConfiguration * another service configuration. You can use * {@link com.sap.cloud.security.config.cf.CFEnvironment#getXsuaaConfigurationForTokenExchange()} * in order to load additional broker service configuration from the * binding information in your environment. * @return the instance itself */ public SAPOfflineTokenServicesCloud withAnotherServiceConfiguration( OAuth2ServiceConfiguration otherServiceConfiguration) { jwtValidatorBuilder.configureAnotherServiceInstance(otherServiceConfiguration); return this; } @Override public OAuth2Authentication loadAuthentication(@Nonnull String accessToken) throws AuthenticationException, InvalidTokenException { Token token = checkAndCreateToken(accessToken); ValidationResult validationResult = tokenValidator.validate(token); if (validationResult.isErroneous()) { throw new InvalidTokenException(validationResult.getErrorDescription()); } SecurityContext.setToken(token); return getOAuth2Authentication(serviceConfiguration.getClientId(), getScopes(token)); } static OAuth2Authentication getOAuth2Authentication(String clientId, Set<String> scopes) { Authentication userAuthentication = null; // TODO no SAPUserDetails support. Using spring alternative? final AuthorizationRequest authorizationRequest = new AuthorizationRequest(clientId, scopes); authorizationRequest.setAuthorities(getAuthorities(scopes)); authorizationRequest.setApproved(true); return new OAuth2Authentication(authorizationRequest.createOAuth2Request(), userAuthentication); } private Set<String> getScopes(Token token) { Set<String> scopes = token instanceof AccessToken ? ((AccessToken) token).getScopes() : Collections.emptySet(); if (useLocalScopeAsAuthorities) { scopes = xsuaaScopeConverter.convert(scopes); } return scopes; } @Override public void afterPropertiesSet() { tokenValidator = jwtValidatorBuilder.build(); } @Override public OAuth2AccessToken readAccessToken(String accessToken) { throw new UnsupportedOperationException("Not supported: readAccessToken()"); } /** * This method allows to overwrite the default behavior of the authorities * converter implementation. * * @param extractLocalScopesOnly * true when only local scopes are extracted. Local scopes means that * non-application specific scopes are filtered out and scopes are * returned without appId prefix, e.g. "Display". * @return the token authenticator itself */ public SAPOfflineTokenServicesCloud setLocalScopeAsAuthorities(boolean extractLocalScopesOnly) { this.useLocalScopeAsAuthorities = extractLocalScopesOnly; return this; } private static Set<GrantedAuthority> getAuthorities(Collection<String> scopes) { return scopes.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toSet()); } private Token checkAndCreateToken(@Nonnull String accessToken) { try { switch (serviceConfiguration.getService()) { case XSUAA: return new XsuaaToken(accessToken).withScopeConverter(xsuaaScopeConverter); case IAS: return new SapIdToken(accessToken); default: // TODO support IAS throw new InvalidTokenException( "AccessToken of service " + serviceConfiguration.getService() + " is not supported."); } } catch (Exception e) { throw new InvalidTokenException(e.getMessage()); } } }