I try to implement a Security Token Service (Server side) with a requested UsernameToken and my service should response a token, which is generated by cxf.
I use JAVA, Spring Boot and a java-based configuration.
I have some problems to implement a custom Token Provider and using the default tokenstore from cxf.
My custom SCTProvider:
public class BiPROTokenProvider extends SCTProvider{
private static final String WSC_IDENTIFIER = "wsc:Identifier";
private static final String BIPRO_PRAEFIX = "bipro:";
#Override
public TokenProviderResponse createToken(TokenProviderParameters tokenParameters) {
TokenProviderResponse response = super.createToken(tokenParameters);
String biproId = BIPRO_PRAEFIX + response.getTokenId().split(":")[1];
//NodeList identifier = ((Element) response.getToken()).getElementsByTagName(WSC_IDENTIFIER);
//identifier.item(0).setTextContent(biproId);
//Element identifier = response.getTokenId().getElementsByTagName(WSC_IDENTIFIER);
//super.createToken(tokenParameters).setTokenId(biproId);
response.setTokenId(biproId);
return response;
}
}
My first problem is, that I do not know where I should include my custom SCT Provider? - is it possible do to it at my endpoint publish?
#Bean
public Endpoint endpoint() {
EndpointImpl endpoint = new EndpointImpl(springBus(), securityTokenServicePortType());
endpoint.setServiceName(securityTokenService26010().getServiceName());
endpoint.setWsdlLocation(securityTokenService26010().getWSDLDocumentLocation().toString());
endpoint.publish("/SecurityTokenService-2.6.0.1.0");
endpoint.getOutFaultInterceptors().add(soapInterceptor());
return endpoint;
}
#Bean
public DefaultInMemoryTokenStore defaulttokenStore(){
return new DefaultInMemoryTokenStore();
}
#Bean
SCTProvider customSCTProvider(){
return new BiPROTokenProvider();
}
Second problem:
I want to store my generated token in a default tokenstore from cxf. I read something about a tokenstore. http://cxf.apache.org/docs/ws-securitypolicy.html
In my opinion I have to include the tokenstore in the enpointproperties from service implementation.
#WebService
(
portName = "wst:UserPasswordLogin",
serviceName = "SecurityTokenService_2.6.0.1.0",
wsdlLocation = "src/main/resources/wsdl/SecurityTokenService- 2.6.0.1.0.wsdl",
endpointInterface = "net.bipro.namespace.SecurityTokenServicePortType"
)
#EndpointProperties({
#EndpointProperty(key = "ws-security.callback-handler", value="com.muki.endpoint.STSCallbackHandler"),
//#EndpointProperty(key = "ws-security.add.inclusive.prefixes", value="false"),
#EndpointProperty(key = "org.apache.cxf.ws.security.tokenstore.TokenStore", value="TOKEN_STORE_CACHE_INSTANCE"),
})
public class SecurityTokenEndpoint implements SecurityTokenServicePortType {
...
}
But if I include the tokenstore via the endpoint properties, I receive the following error.
<faultstring>java.lang.String cannot be cast to org.apache.cxf.ws.security.tokenstore.TokenStore</faultstring>
Can anybody help how I include a tokenstore and my custom SCT Provider?
I had similar issue but I use xml configuration. Instead of value I used value-ref and passed bean there:
<jaxws:endpoint
id="endpointId"
address="/foo/bar"
...
serviceName="ns1:ServiceName">
<jaxws:properties>
...
<entry key="org.apache.cxf.ws.security.tokenstore.TokenStore" value-ref="tokenStore" />
</jaxws:properties>
</jaxws:endpoint>
<bean id="tokenStore" class="org.apache.cxf.ws.security.tokenstore.MemoryTokenStore"/>
The error was gone but it wasn't working correctly - TokenStore wasn't set. So I tried another approach. Instead of editing endpoint I added that entry to bus config:
<cxf:bus>
<cxf:properties>
<entry key="org.apache.cxf.ws.security.tokenstore.TokenStore" value-ref="tokenStore" />
</cxf:properties>
</cxf:bus>
<bean id="tokenStore" class="org.apache.cxf.ws.security.tokenstore.MemoryTokenStore"/>
As for your question, I believe your syntax would look like:
#EndpointProperty(key = "org.apache.cxf.ws.security.tokenstore.TokenStore", ref="bean-name")
Related
How can I configure a spring boot config client microservice to fetch its configuration from an OAuth2 configServer which is #EnableResourceServer ?
I have one OAuth2 Authorization server (#EnableAuthorizationServer). There is a configServer (#EnableConfigServer) which I have configured to respond only to valid requests authorized by authorization server containing JWT tokens.
There is also a microservice client APP1 of the config server which needs to fetch its configuration upon startup from the aforementioned config server. Since the server only responds to requests containing valid access tokens (jwt tokens) I tried to inject OAuth2RestTemplate into ConfigServicePropertySourceLocator so that my config client (APP1) could fetch its config.
In order to do that I tried the partial solution which was discussed here.
This is my OAuth2 ready RestTemplate that I want to inject
#Configuration
public class OAuthConfig {
#Bean
public OAuth2RestTemplate oauth2RestTemplate(OAuth2ClientContext oauth2ClientContext,
OAuth2ProtectedResourceDetails details) {
return new OAuth2RestTemplate(details, oauth2ClientContext);
}}
and this is my custom property locator
public class CustomConfigServicePropertySourceLocator {
#Autowired
private ConfigurableEnvironment environment;
#Autowired
private RestTemplate restTemplate;
#Bean
public ConfigClientProperties configClientProperties() {
ConfigClientProperties client = new ConfigClientProperties(this.environment);
client.setEnabled(false);
return client;
}
#Bean
#Primary
public ConfigServicePropertySourceLocator configServicePropertySourceLocator() {
ConfigClientProperties clientProperties = configClientProperties();
ConfigServicePropertySourceLocator configServicePropertySourceLocator = new ConfigServicePropertySourceLocator(
clientProperties);
configServicePropertySourceLocator.setRestTemplate(restTemplate);
return configServicePropertySourceLocator;
}}
I Followed the instructions in Customizing the Bootstrap Configuration
I created a META_INF > spring.factories file containing
org.springframework.cloud.bootstrap.BootstrapConfiguration=com.company.mcapp.CustomConfigServicePropertySourceLocator
By debugging I can see that this custom locator will get called but my APP1 is failing to contact config server to fetch the configurations.
In initialize method of PropertySourceBootstrapConfiguration (below) I can see that the propertySourceLocators does not contain my CustomConfigServicePropertySourceLocator.
#Override
public void initialize(ConfigurableApplicationContext applicationContext) {
CompositePropertySource composite = new CompositePropertySource(
BOOTSTRAP_PROPERTY_SOURCE_NAME);
AnnotationAwareOrderComparator.sort(this.propertySourceLocators);
boolean empty = true;
ConfigurableEnvironment environment = applicationContext.getEnvironment();
for (PropertySourceLocator locator : this.propertySourceLocators) {
PropertySource<?> source = null;
source = locator.locate(environment);
if (source == null) {
continue;
}
logger.info("Located property source: " + source);
composite.addPropertySource(source);
empty = false;
}
.
.
.
UPDATE: The issue was a silly mistake That I made. Instead of creating META-INF I created META_INF.
Problem statement:
Spring amqp-outbound gateway to produce reply from a different thread (Like jms-outbound gateway, having different queue, correlate the request/response using correlation key).
Unable to correlate the message with this example.
Spring integration
<int:gateway id="outboundGateway" service-interface="com.amqp.outbound.gateway.OutboundGateway"
default-reply-channel="defaultReplyChannel" >
<int:method name="process" request-channel="inboundRequestChannel"/>
</int:gateway>
<int:channel id="defaultReplyChannel"/>
<int:channel id="inboundRequestChannel"/>
<int:channel id="enrichedInboundRequestChannel"/>
<int:channel id="processAuthRequestChannel"/>
<int:channel id="postProcessorChannel"/>
<int:chain input-channel="inboundRequestChannel" output-channel="enrichedInboundRequestChannel">
<int:service-activator id="serviceActivator"
ref="ouboundService" method="createRequest"/>
</int:chain>
<int-amqp:outbound-gateway id="outboundGtwyId" header-mapper="headerMapper"
request-channel="enrichedInboundRequestChannel"
reply-channel="defaultReplyChannel"
amqp-template="template"
reply-timeout="30000"
exchange-name="request_exchange"
routing-key="request_exchange_queue"/>
<int-amqp:inbound-channel-adapter id="amqpMessageDriven" queue-names="request_queue"
connection-factory="rabbitConnectionFactory" channel="processAuthRequestChannel"/>
<int:service-activator id="serviceActivator"
ref="ouboundService" input-channel="processAuthRequestChannel" output-channel="postProcessorChannel"
method="processRequest"/>
<int-amqp:outbound-channel-adapter amqp-template="template" channel="postProcessorChannel"
header-mapper="headerMapper" exchange-name="reply_exchange" routing-key="reply_exchange_queue"/>
<bean id="headerMapper" class="org.springframework.integration.amqp.support.DefaultAmqpHeaderMapper"/>
Config
#Bean
public RabbitTemplate template(ConnectionFactory rabbitConnectionFactory){
final RabbitTemplate template = new RabbitTemplate(rabbitConnectionFactory);
template.setQueue("reply_queue");
return template;
}
#Bean
public Binding binding(){
return BindingBuilder.bind(this.queue()).to(this.exchange()).with("request_exchange_queue");
}
#Bean
public DirectExchange exchange(){
return new DirectExchange("request_exchange");
}
#Bean
public Queue queue(){
return new Queue("request_queue", true, false, true);
}
#Bean
public Binding bindingReply(){
return BindingBuilder.bind(this.queue()).to(this.exchange()).with("reply_exchange_queue");
}
#Bean
public DirectExchange exchangeReply(){
return new DirectExchange("reply_exchange");
}
#Bean
public Queue replyQueue(){
return new Queue("reply_queue", true, false, true);
}
Service
#Service
public final class OuboundService {
public Message createRequest(String message){
System.out.println("Inside createRequest : "+ message);
final String transactionId = UUID.randomUUID().toString();
final Message builtMessage = MessageBuilder.withBody(message.getBytes())
.setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN)
.setHeader(AmqpHeaders.CORRELATION_ID, transactionId)
.build();
return builtMessage;
}
public Message processRequest(Message message){
System.out.println("Inside process Request : "+ new String(message.getBody()));
System.out.println("Header values : "+message.getMessageProperties().getHeaders());
final Message result = MessageBuilder.withBody("Successful".getBytes()).copyProperties(message.getMessageProperties())
.copyHeaders(message.getMessageProperties().getHeaders()).build();
return result;
}
}
Error:
org.springframework.integration.handler.ReplyRequiredException: No reply produced by handler 'outboundGtwyId', and its 'requiresReply' property is set to true.
GitHub source code (Resolved Solution)
https://github.com/kingkongprab/spring-amqp-outbound-gateway
The correlation is done in the Spring AMQP as well. See its RabbitTemplate#sendAndRecevie() for more info. Also there is a good documentation on the matter in the Reference Manual.
Spring Integration with its AbstractAmqpOutboundEndpoint and AmqpInboundGateway implementations provides out-of-the-box request-reply correlation solution. If you are not able to use AmqpInboundGateway on the server side, you should ensure the correlationId transfer from received request to the reply to send back. Yes, you can use dedicated exchange for replies and that is what supported by the RabbitTemplate#setQueue() to wait for replies on the client, outbound side. But that still isn't going to work without proper correlation transferring. Also see https://docs.spring.io/spring-integration/docs/4.3.12.RELEASE/reference/html/amqp.html#amqp-message-headers for the info how headers (including correlationId) are mapped in Spring Integration.
UPDATE
Thank you for sharing your application.
Well, now I see several problems:
You are definitely missing the replyQueue binding:
#Bean
public Binding bindingReply(){
return BindingBuilder.bind(this.replyQueue()).to(this.exchangeReply()).with("reply_exchange_queue");
}
RabbitTemplate must use setReplyAddress(). You have to configure MessageListenerContainer for the reply_queue and have RabbitTemplate as a listener:
#Bean
public RabbitTemplate template(ConnectionFactory rabbitConnectionFactory){
final RabbitTemplate template = new RabbitTemplate(rabbitConnectionFactory);
template.setReplyAddress(replyQueue().getName());
return template;
}
#Bean
public MessageListenerContainer replyContainer(RabbitTemplate template) {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setConnectionFactory(template.getConnectionFactory());
container.setQueues(replyQueue());
container.setMessageListener(template);
return container;
}
Your OuboundService with org.springframework.amqp.core.Message manipulation is useless. The Channel Adapters don't know about this type of payload and your custom Message just becomes as a serialized body of another org.springframework.amqp.core.Message. I have changed it to this and everything works well:
public String createRequest(String message){
System.out.println("Inside createRequest : "+ message);
return message;
}
public Message processRequest(Message message){
System.out.println("Inside process Request : " + message);
return message;
}
Anyway I suggest you to rethink your design and come back to the AmqpInboundGateway.
BTW in the final solution you don't need to care about any correlation. The Framework does that for you automatically.
How to configure Apache CXF client and server to pass additional classes to JAXBContext when it is serializing DTO to XML?
I can't use #XmlSeeAlso annotations because those classes are not known at compile time of jar with data contracts, but known when client compiles.
On client side I tried using:
Service service = Service.create(wsdlURL, serviceName, new UsesJAXBContextFeature(MyFactory.class));
T client = service.getPort(clazz);
But I got exception telling me that CXF doesn't support this feature.
You can do it with annotations also.
Works with Spring Boot CXF starter
#Autowired
private Bus bus;
#Bean
public Endpoint createMyEndpoint() {
JaxWsServerFactoryBean factory = new JaxWsServerFactoryBean();
Map<String, Object> properties = new HashMap<>();
properties.put("jaxb.additionalContextClasses", getExtraClasses());
factory.setProperties(properties);
Endpoint endpoint = new EndpointImpl(bus, new MyWebService(),factory);
endpoint.setProperties(new HashMap<>());
endpoint.publish("/v1/service");
return endpoint;
}
#SuppressWarnings("rawtypes")
private Class[] getExtraClasses() {
List<Class> extraClassList = new ArrayList<>();
extraClassList.add(A.class);
extraClassList.add(B.class);
return extraClassList.toArray(new Class[extraClassList.size()]);
}
...
#javax.jws.WebService
public class MyWebService implements MyPortType {
//...
}
I figured it out with
https://issues.apache.org/jira/browse/CXF-340
https://github.com/apache/cxf/blob/5578e0b82bcd4ea19c1de5b4a008af35f9c8451b/rt/frontend/jaxws/src/main/java/org/apache/cxf/jaxws/EndpointImpl.java#L164
if you configure cxf with cxf.xml (spring-xml) you can use the following:
<jaxws:endpoint/client>
<jaxws:properties>
<entry key="jaxb.additionalContextClasses">
<array value-type="java.lang.Class">
<value type="java.lang.Class">fullQualifiedClassName</value>
</array>
</entry>
</jaxws:properties>
</jaxws:endpoint>
or any other way to write the org.apache.cxf.jaxb.JAXBDataBinding property "extraClass" (a Class[]) like . See http://cxf.apache.org/docs/jaxb.html
I'm developing a web application to expose a number of RESTful services secured by OAuth 2.0. Here is the planned architecture:
1- OAuth Authorization Provider: WSO2 Identity Server (IS)
2- OAuth Resource Server: Java web application using the following technologies:
Jersey (to implement and expose the web services)
Spring Security (to implement the OAuth Resource Server part)
I've seen several examples (ex1, ex2, ex3, etc...) on how to secure RESTful services using WSO2 IS as an authorization server + WSO2 ESB as a resource server. This is NOT what I need in my case.
Unfortunately, the interaction between the authorization server and the resource server is beyond the scope of the OAuth2 RFC. So, I couldn't find much about how should it look like.
Here are my questions:
How to configure spring security to act as a resource server to validate an access token issued by an external OAuth provider (e.g. WSO2 IS)?
How should the resource server identify the scope of a given access token?
How to identify the resource owner given an access token from WSO2 IS?
Thanks
After doing some research, I figured out how to do it. The solution is divided into 2 main parts: WSO2 IS configuration & Resources server configuration.
The basic scenario goes as follows:
1- A client (e.g. mobile app) consume a secured resource (e.g. web service) by sending a request to the resources sever (Java web application in my case).
2- The resources server validates the "Authorization" header in the request and extracts the access token.
3- The resources server validates the access token by sending it to the authorization server (WSO2 IS).
4- The authorization server responds with validation response.
5- The resources server validates the response and decides whether to grant or deny access to the requested resource.
In my demo, I used WSO2 IS 5.0.0 and Spring security 3.1.0.
1- WSO2 IS Configuration
WSO2 IS will act as the authorization server. So, it should be configured to support OAuth 2.0. To do so, a new service provider should be added and configured as follows:
(a) Login to WSO2 IS management console.
(b) Add a new service provider and give it a name and description.
(c) Under Inbound Authentication Configuration >> OAuth/OpenID Connect Configuration >> Click Configure.
(d) Configure OAuth 2.0 provider as shown in the below screenshot and click Add. We'll need Password grant type which maps to Resource Owner Password Credentials grant type. It is best suited for my case (securing web services).
(e) Under OAuth/OpenID Connect Configuration, you'll find OAuth Client Key and OAuth Client Secret generated. They are used along with username, password, and scope to generate access tokens.
2- Resources Server Configuration
As mentioned earlier, the demo Java web application will act as Resources server and client at the same time. To act as resources server, Spring security needs to know how to validate access tokens. So, a token services implementation should be provided.
(a) Configure spring to act as resources server. Here is a sample configuration:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:security="http://www.springframework.org/schema/security"
xmlns:oauth2="http://www.springframework.org/schema/security/oauth2"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.1.xsd
http://www.springframework.org/schema/security/oauth2
http://www.springframework.org/schema/security/spring-security-oauth2.xsd">
<bean id="tokenServices" class="com.example.security.oauth2.wso2.TokenServiceWSO2" />
<bean id="authenticationEntryPoint" class="org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint" />
<security:authentication-manager alias="authenticationManager" />
<oauth2:resource-server id="resourcesServerFilter" token-services-ref="tokenServices" />
<security:http pattern="/services/**" create-session="stateless" entry-point-ref="authenticationEntryPoint" >
<security:anonymous enabled="false" />
<security:custom-filter ref="resourcesServerFilter" before="PRE_AUTH_FILTER" />
<security:intercept-url pattern="/services/**" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
</security:http>
</beans>
Here, a resource-server that uses a token services implementation TokenServiceWSO2 is configured. The resource-server tag is actually transformed to a security filter. An interception pattern is added to "/services/**" and the resources sever filter is added to the chain.
(b) Implement OAuth 2.0 token services ResourceServerTokenServices. The implementation will take an access token as an input, pass it to OAuth2TokenValidationService service exposed by WSO2 IS, validate the response and return a processed object containing the basic data about the token's issuer, validity, scope, corresponding JWT token, ...
public class TokenServiceWSO2 implements ResourceServerTokenServices {
#Autowired
TokenValidatorWSO2 tokenValidatorWSO2;
public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException {
try {
TokenValidationResponse validationResponse = tokenValidatorWSO2.validateAccessToken(accessToken);
OAuth2Request oAuth2Request = new OAuth2Request(null, null, null, true, validationResponse.getScope(), null, null, null,null);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(validationResponse.getAuthorizedUserIdentifier(), null, null);
OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication);
return oAuth2Authentication;
} catch (ApplicationException ex) {
// Handle exception
}
}
public OAuth2AccessToken readAccessToken(String accessToken) {
// TODO Add implementation
}
}
TokenValidatorWSO2 class implements the logic to call WSO2 IS's web service OAuth2TokenValidationService
#Component
public class TokenValidatorWSO2 implements OAuth2TokenValidator{
private static final Logger logger = Logger.getLogger(TokenValidatorWSO2.class);
#Value("${server_url}")
private String serverUrl;
#Value("${validation_service_name}")
private String validationServiceName;
#Value("${comsumer_key}")
private String consumerKey;
#Value("${admin_username}")
private String adminUsername;
#Value("${admin_password}")
private String adminPassword;
private OAuth2TokenValidationServiceStub stub;
private static final int TIMEOUT_IN_MILLIS = 15 * 60 * 1000;
public TokenValidationResponse validateAccessToken(String accessToken) throws ApplicationException {
logger.debug("validateAccessToken(String) - start");
if(stub == null) {
initializeValidationService();
}
OAuth2TokenValidationRequestDTO oauthRequest;
TokenValidationResponse validationResponse;
OAuth2TokenValidationRequestDTO_OAuth2AccessToken oAuth2AccessToken;
try {
oauthRequest = new OAuth2TokenValidationRequestDTO();
oAuth2AccessToken = new OAuth2TokenValidationRequestDTO_OAuth2AccessToken();
oAuth2AccessToken.setIdentifier(accessToken);
oAuth2AccessToken.setTokenType("bearer");
oauthRequest.setAccessToken(oAuth2AccessToken);
OAuth2TokenValidationResponseDTO response = stub.validate(oauthRequest);
if(!response.getValid()) {
throw new ApplicationException("Invalid access token");
}
validationResponse = new TokenValidationResponse();
validationResponse.setAuthorizedUserIdentifier(response.getAuthorizedUser());
validationResponse.setJwtToken(response.getAuthorizationContextToken().getTokenString());
validationResponse.setScope(new LinkedHashSet<String>(Arrays.asList(response.getScope())));
validationResponse.setValid(response.getValid());
} catch(Exception ex) {
logger.error("validateAccessToken() - Error when validating WSO2 token, Exception: {}", ex);
}
logger.debug("validateAccessToken(String) - end");
return validationResponse;
}
private void initializeValidationService() throws ApplicationException {
try {
String serviceURL = serverUrl + validationServiceName;
stub = new OAuth2TokenValidationServiceStub(null, serviceURL);
CarbonUtils.setBasicAccessSecurityHeaders(adminUsername, adminPassword, true, stub._getServiceClient());
ServiceClient client = stub._getServiceClient();
Options options = client.getOptions();
options.setTimeOutInMilliSeconds(TIMEOUT_IN_MILLIS);
options.setProperty(HTTPConstants.SO_TIMEOUT, TIMEOUT_IN_MILLIS);
options.setProperty(HTTPConstants.CONNECTION_TIMEOUT, TIMEOUT_IN_MILLIS);
options.setCallTransportCleanup(true);
options.setManageSession(true);
} catch(AxisFault ex) {
// Handle exception
}
}
}
TokenValidationResponse class holds the basic data returned in token validation response.
public class TokenValidationResponse {
private String jwtToken;
private boolean valid;
private Set<String> scope;
private String authorizedUserIdentifier;
public String getJwtToken() {
return jwtToken;
}
public void setJwtToken(String jwtToken) {
this.jwtToken = jwtToken;
}
public boolean isValid() {
return valid;
}
public void setValid(boolean valid) {
this.valid = valid;
}
public Set<String> getScope() {
return scope;
}
public void setScope(Set<String> scope) {
this.scope = scope;
}
public String getAuthorizedUserIdentifier() {
return authorizedUserIdentifier;
}
public void setAuthorizedUserIdentifier(String authorizedUserIdentifier) {
this.authorizedUserIdentifier = authorizedUserIdentifier;
}
}
3- Client Application Configuration
The last step is to configure the resources to be protected by OAuth 2.0. Basically, configure the web services to be secured with a root URL path "/services/**". In my demo, I used Jersey.
4- Test The Client Application
The last step is to consume the secured web services. This is done by adding Authorization header to the request with value " ", for example "bearer 7fbd71c5b28fdf0bdb922b07915c4d5".
P.S. The described sample is just for clarification purposes. It may be missing some implementations, exception handling, ... Kindly comment for further inquiries.
When working with Spring Security + CAS I keep hitting a small road block with the callback URL that is sent to CAS, ie the service property. I've looked at a bunch of examples such as this and this but they all use hard coded URLs (even Spring's CAS docs). A typical snip looks something like this...
<bean id="serviceProperties" class="org.springframework.security.ui.cas.ServiceProperties">
<property name="service" value="http://localhost:8080/click/j_spring_cas_security_check" />
</bean>
First, I don't want to hard code the server name or the port since I want this WAR to be deployable anywhere and I don't want my application tied to a particular DNS entry at compile time. Second, I don't understand why Spring can't auto detect my application's context and the request's URL to automagically build the URL. The first part of that statement still stand but As Raghuram pointed out below with this link, we can't trust the HTTP Host Header from the client for security reasons.
Ideally I would like service URL to be exactly what the user requested (as long as the request is valid such as a sub domain of mycompany.com) so it is seamless or at the very least I would like to only specify some path relative my applications context root and have Spring determine the service URL on the fly. Something like the following...
<bean id="serviceProperties" class="org.springframework.security.ui.cas.ServiceProperties">
<property name="service" value="/my_cas_callback" />
</bean>
OR...
<bean id="serviceProperties" class="org.springframework.security.ui.cas.ServiceProperties">
<property name="service" value="${container.and.app.derived.value.here}" />
</bean>
Is any of this possible or easy or have I missed the obvious?
I know this is a bit old but I just had to solve this very problem and couldn't really find anything in the newer stacks.
We have multiple environments sharing the same CAS service (think dev, qa, uat and local development environments); we have the ability to hit each environment from more than one url (via the client side web server over a reverse proxy and directly to the back-end server itself). This means that specifying a single url is difficult at best. Maybe there's a way to do this but being able to use a dynamic ServiceProperties.getService(). I'll probably add some kind of server suffix check to ensure that the url isn't hijacked at some point.
Here's what I did to get the basic CAS flow working regardless of the URL used to access the secured resource...
Override the CasAuthenticationFilter.
Override the CasAuthenticationProvider.
setAuthenticateAllArtifacts(true) on the ServiceProperties.
Here's the long form of my spring configuration bean:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true, jsr250Enabled = true)
public class CasSecurityConfiguration extends WebSecurityConfigurerAdapter {
Just the usual spring configuration bean.
#Value("${cas.server.url:https://localhost:9443/cas}")
private String casServerUrl;
#Value("${cas.service.validation.uri:/webapi/j_spring_cas_security_check}")
private String casValidationUri;
#Value("${cas.provider.key:whatever_your_key}")
private String casProviderKey;
Some externalized configuration parameters.
#Bean
public ServiceProperties serviceProperties() {
ServiceProperties serviceProperties = new ServiceProperties();
serviceProperties.setService(casValidationUri);
serviceProperties.setSendRenew(false);
serviceProperties.setAuthenticateAllArtifacts(true);
return serviceProperties;
}
The key thing above is the setAuthenticateAllArtifacts(true) call. This will make the service ticket validator use the AuthenticationDetailsSource implementation rather than a hard-coded ServiceProperties.getService() call
#Bean
public Cas20ServiceTicketValidator cas20ServiceTicketValidator() {
return new Cas20ServiceTicketValidator(casServerUrl);
}
Standard ticket validator..
#Resource
private UserDetailsService userDetailsService;
#Bean
public AuthenticationUserDetailsService authenticationUserDetailsService() {
return new AuthenticationUserDetailsService() {
#Override
public UserDetails loadUserDetails(Authentication token) throws UsernameNotFoundException {
String username = (token.getPrincipal() == null) ? "NONE_PROVIDED" : token.getName();
return userDetailsService.loadUserByUsername(username);
}
};
}
Standard hook to an existing UserDetailsService
#Bean
public CasAuthenticationProvider casAuthenticationProvider() {
CasAuthenticationProvider casAuthenticationProvider = new CasAuthenticationProvider();
casAuthenticationProvider.setAuthenticationUserDetailsService(authenticationUserDetailsService());
casAuthenticationProvider.setServiceProperties(serviceProperties());
casAuthenticationProvider.setTicketValidator(cas20ServiceTicketValidator());
casAuthenticationProvider.setKey(casProviderKey);
return casAuthenticationProvider;
}
Standard authentication provider
#Bean
public CasAuthenticationFilter casAuthenticationFilter() throws Exception {
CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter();
casAuthenticationFilter.setAuthenticationManager(authenticationManager());
casAuthenticationFilter.setServiceProperties(serviceProperties());
casAuthenticationFilter.setAuthenticationDetailsSource(dynamicServiceResolver());
return casAuthenticationFilter;
}
Key here is the dynamicServiceResolver() setting..
#Bean
AuthenticationDetailsSource<HttpServletRequest,
ServiceAuthenticationDetails> dynamicServiceResolver() {
return new AuthenticationDetailsSource<HttpServletRequest, ServiceAuthenticationDetails>() {
#Override
public ServiceAuthenticationDetails buildDetails(HttpServletRequest context) {
final String url = makeDynamicUrlFromRequest(serviceProperties());
return new ServiceAuthenticationDetails() {
#Override
public String getServiceUrl() {
return url;
}
};
}
};
}
Dynamically creates the service url from the makeDynamicUrlFromRequest() method. This bit is used upon ticket validation.
#Bean
public CasAuthenticationEntryPoint casAuthenticationEntryPoint() {
CasAuthenticationEntryPoint casAuthenticationEntryPoint = new CasAuthenticationEntryPoint() {
#Override
protected String createServiceUrl(final HttpServletRequest request, final HttpServletResponse response) {
return CommonUtils.constructServiceUrl(null, response, makeDynamicUrlFromRequest(serviceProperties())
, null, serviceProperties().getArtifactParameter(), false);
}
};
casAuthenticationEntryPoint.setLoginUrl(casServerUrl + "/login");
casAuthenticationEntryPoint.setServiceProperties(serviceProperties());
return casAuthenticationEntryPoint;
}
This part uses the same dynamic url creator when CAS wants to redirect to the login screen.
private String makeDynamicUrlFromRequest(ServiceProperties serviceProperties){
return "https://howeverYouBuildYourOwnDynamicUrl.com";
}
This is whatever you make of it. I only passed in the ServiceProperties to hold the URI of the service that we're configured for. We use HATEAOS on the back-side and have an implementation like:
return UriComponentsBuilder.fromHttpUrl(
linkTo(methodOn(ExposedRestResource.class)
.aMethodOnThatResource(null)).withSelfRel().getHref())
.replacePath(serviceProperties.getService())
.build(false)
.toUriString();
Edit: here's what I did for the list of valid server suffixes..
private List<String> validCasServerHostEndings;
#Value("${cas.valid.server.suffixes:company.com,localhost}")
private void setValidCasServerHostEndings(String endings){
validCasServerHostEndings = new ArrayList<>();
for (String ending : StringUtils.split(endings, ",")) {
if (StringUtils.isNotBlank(ending)){
validCasServerHostEndings.add(StringUtils.trim(ending));
}
}
}
private String makeDynamicUrlFromRequest(ServiceProperties serviceProperties){
UriComponents url = UriComponentsBuilder.fromHttpUrl(
linkTo(methodOn(ExposedRestResource.class)
.aMethodOnThatResource(null)).withSelfRel().getHref())
.replacePath(serviceProperties.getService())
.build(false);
boolean valid = false;
for (String validCasServerHostEnding : validCasServerHostEndings) {
if (url.getHost().endsWith(validCasServerHostEnding)){
valid = true;
break;
}
}
if (!valid){
throw new AccessDeniedException("The server is unable to authenticate the requested url.");
}
return url.toString();
}
In Spring 2.6.5 spring you could extend org.springframework.security.ui.cas.ServiceProperties
In spring 3 the method is final you could get around this by subclassing the CasAuthenticationProvider and CasEntryPoint and then use with your own version of ServiceProperties and override the getService() method with a more dynamic implementation.
You could use the host header to calculate the the required domain and make it more secure by validating that only domains/subdomains under your control are used. Then append to this some configurable value.
Of course you would be at risk that your implementation was insecure though... so be careful.
It could end up looking like:
<bean id="serviceProperties" class="my.ServiceProperties">
<property name="serviceRelativeUrl" value="/my_cas_callback" />
<property name="validDomainPattern" value="*.mydomain.com" />
</bean>
use maven, add a property placeholder, and configure it in your build process
I tried to subclass CasAuthenticationProvider as Pablojim suggest, but solution is very easier! with Spring Expression Language (SPEL) you can obtain the url dinamically.
Example: <property name="service"
value="https://#{T(java.net.InetAddress).getLocalHost().getHostName()}:${application.port}${cas.service}/login/cascheck"/>
I have not tried this myself, but it seems Spring Security has a solution to this with the SavedRequestAwareAuthenticationSuccessHandler shown in the update of Bob's blog.