Commit 40ac919b authored by Misagh Moayyed's avatar Misagh Moayyed
Browse files

Merge pull request #1112 from Unicon/oauth-views

Automatic module configuration
parents 91b8aaff 4106c61d
/*
* Licensed to Apereo under one or more contributor license
* agreements. See the NOTICE file distributed with this work
* for additional information regarding copyright ownership.
* Apereo licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a
* copy of the License at the following location:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.jasig.cas.web;
import org.jasig.cas.authentication.AuthenticationHandler;
import org.jasig.cas.authentication.principal.PrincipalResolver;
import org.jasig.cas.services.RegisteredService;
import org.jasig.cas.services.ServicesManager;
import org.jasig.cas.util.UniqueTicketIdGenerator;
import org.jasig.cas.web.support.ArgumentExtractor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
import org.springframework.web.servlet.mvc.Controller;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.ServletRegistration;
import java.util.List;
import java.util.Map;
/**
* Parent class for all servlet context initializers
* that provides commons methods for retrieving beans
* from the context dynamically.
* @author Misagh Moayyed
* @since 4.2
*/
@Component
public abstract class AbstractServletContextInitializer implements ServletContextListener, ApplicationContextAware {
/** Default CAS Servlet name. **/
private static final String CAS_SERVLET_NAME = "cas";
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
/** Application context. */
protected ApplicationContext applicationContext;
private final String contextInitializerName = getClass().getSimpleName();
/**
* Instantiates a new servlet context initializer.
*/
protected AbstractServletContextInitializer() {}
@Override
public final void contextInitialized(final ServletContextEvent sce) {
logger.info("Initializing {} context...", contextInitializerName);
initializeServletContext(sce);
logger.info("Initialized {} context...", contextInitializerName);
}
@Override
public final void contextDestroyed(final ServletContextEvent sce) {
logger.info("Destroying {} context...", contextInitializerName);
destroyServletContext(sce);
logger.info("Destroyed {} context...", contextInitializerName);
}
@Override
public final void setApplicationContext(final ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
try {
if (applicationContext.getParent() == null) {
logger.info("Initializing {} root application context", contextInitializerName);
initializeRootApplicationContext();
logger.info("Initialized {} root application context successfully", contextInitializerName);
} else {
logger.info("Initializing {} application context", contextInitializerName);
initializeServletApplicationContext();
logger.info("Initialized {} application context successfully", contextInitializerName);
}
} catch (final Exception e) {
logger.error(e.getMessage(), e);
}
}
/**
* Add authentication handler principal resolver.
*
* @param handler the handler
* @param resolver the resolver
*/
protected final void addAuthenticationHandlerPrincipalResolver(final AuthenticationHandler handler,
final PrincipalResolver resolver) {
final Map<AuthenticationHandler, PrincipalResolver> authenticationHandlersResolvers =
applicationContext.getBean("authenticationHandlersResolvers", Map.class);
authenticationHandlersResolvers.put(handler, resolver);
}
/**
* Gets cas servlet registration.
*
* @param sce the sce
* @return the cas servlet registration
*/
protected final ServletRegistration getCasServletRegistration(final ServletContextEvent sce) {
final ServletRegistration registration = sce.getServletContext().getServletRegistration(CAS_SERVLET_NAME);
return registration;
}
/**
* Add registered service to services manager.
*
* @param svc the svc
*/
protected final void addRegisteredServiceToServicesManager(final RegisteredService svc) {
final ServicesManager manager = this.applicationContext.getBean("servicesManager", ServicesManager.class);
}
/**
* Gets cas servlet handler mapping.
*
* @return the cas servlet handler mapping
*/
protected final SimpleUrlHandlerMapping getCasServletHandlerMapping() {
final SimpleUrlHandlerMapping handlerMappingC =
applicationContext.getBean("handlerMappingC", SimpleUrlHandlerMapping.class);
return handlerMappingC;
}
/**
* Add controller to cas servlet handler mapping.
*
* @param path the path
* @param controller the controller
*/
protected final void addControllerToCasServletHandlerMapping(final String path, final Controller controller) {
final SimpleUrlHandlerMapping handlerMappingC = getCasServletHandlerMapping();
final Map<String, Object> urlMap = (Map<String, Object>) handlerMappingC.getUrlMap();
urlMap.put(path, controller);
handlerMappingC.initApplicationContext();
}
/**
* Add controller to cas servlet handler mapping.
*
* @param path the path
* @param controller the controller
*/
protected final void addControllerToCasServletHandlerMapping(final String path, final String controller) {
addControllerToCasServletHandlerMapping(path, getController(controller));
}
/**
* Gets controller.
*
* @param id the id
* @return the controller
*/
protected final Controller getController(final String id) {
return applicationContext.getBean(id, Controller.class);
}
/**
* Add endpoint mapping to cas servlet.
*
* @param sce the sce
* @param mapping the mapping
*/
protected final void addEndpointMappingToCasServlet(final ServletContextEvent sce, final String mapping) {
final ServletRegistration registration = getCasServletRegistration(sce);
registration.addMapping(mapping);
logger.info("Added [{}] to {} servlet context", mapping, CAS_SERVLET_NAME);
}
/**
* Add argument extractor.
*
* @param ext the ext
*/
protected final void addArgumentExtractor(final ArgumentExtractor ext) {
final List<ArgumentExtractor> list = applicationContext.getBean("argumentExtractors", List.class);
list.add(ext);
}
/**
* Add service ticket unique id generator.
*
* @param serviceName the service name
* @param gen the gen
*/
protected final void addServiceTicketUniqueIdGenerator(final String serviceName, final UniqueTicketIdGenerator gen) {
final Map<String, UniqueTicketIdGenerator> map =
applicationContext.getBean("uniqueIdGeneratorsMap", Map.class);
map.put(serviceName, gen);
}
/**
* Initialize root application context.
*/
protected void initializeRootApplicationContext() {}
/**
* Initialize servlet application context.
*/
protected void initializeServletApplicationContext() {}
/**
* Initialize servlet context.
*
* @param event the event
*/
protected void initializeServletContext(final ServletContextEvent event) {}
/**
* Destroy servlet context.
*
* @param event the event
*/
protected void destroyServletContext(final ServletContextEvent event) {}
}
......@@ -29,6 +29,8 @@ import org.jasig.cas.ticket.ServiceTicket;
import org.jasig.cas.web.support.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.util.StringUtils;
import org.springframework.webflow.action.AbstractAction;
import org.springframework.webflow.execution.Event;
......@@ -56,6 +58,8 @@ public abstract class AbstractNonInteractiveCredentialsAction extends AbstractAc
/** Instance of CentralAuthenticationService. */
@NotNull
@Autowired
@Qualifier("centralAuthenticationService")
private CentralAuthenticationService centralAuthenticationService;
/**
......
......@@ -20,74 +20,7 @@ Support is enabled by including the following dependency in the Maven WAR overla
</dependency>
{% endhighlight %}
#Configuration
##Add the OAuth20WrapperController
To add the `OAuth20WrapperController`, you need to add the mapping between the /oauth2.0/* url and the CAS servlet in the *web.xml* file:
{% highlight xml %}
<servlet-mapping>
<servlet-name>cas</servlet-name>
<url-pattern>/oauth2.0/*</url-pattern>
</servlet-mapping>
{% endhighlight %}
You have to create the controller itself in the *cas-servlet.xml* file:
{% highlight xml %}
<bean
id="oauth20WrapperController"
class="org.jasig.cas.support.oauth.web.OAuth20WrapperController"
p:loginUrl="http://mycasserverwithoauthwrapper/login"
p:servicesManager-ref="servicesManager"
p:ticketRegistry-ref="ticketRegistry"
p:timeout="7200" />
{% endhighlight %}
The *loginUrl* is the login url of the CAS server. The timeout is the lifetime of a CAS ticket granting ticket (in seconds, not in milliseconds!) with its mapping in the `handlerMappingC` bean (*cas-servlet.xml* file):
{% highlight xml %}
<bean id="handlerMappingC" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/serviceValidate">serviceValidateController</prop>
...
<prop key="/statistics">statisticsController</prop>
<prop key="/oauth2.0/*">oauth20WrapperController</prop>
</props>
</property>
<property name="alwaysUseFullPath" value="true" />
</bean>
{% endhighlight %}
##Add the needed CAS services
###Callback Authorization
One service is needed to make the OAuth wrapper works in CAS. It defines the callback url after CAS authentication to return to the OAuth wrapper as a CAS service.
**Note**: the callback url must end with "callbackAuthorize".
{% highlight xml %}
<bean id="serviceRegistryDao" class="org.jasig.cas.services.InMemoryServiceRegistryDaoImpl">
<property name="registeredServices">
<list>
<!-- A dedicated component to recognize OAuth Callback Authorization requests -->
<!-- By default, service ids only support regex patterns if/when needed -->
<bean class="org.jasig.cas.support.oauth.services.OAuthCallbackAuthorizeService"
p:id="0"
p:name="HTTP"
p:description="oauth wrapper callback url"
p:serviceId="${server.prefix}/oauth2.0/callbackAuthorize" />
...
{% endhighlight %}
###OAuth Clients
##Add OAuth Clients
Every OAuth client must be defined as a CAS service (notice the new *clientId* and *clientSecret* properties, specific to OAuth):
......@@ -95,15 +28,18 @@ Every OAuth client must be defined as a CAS service (notice the new *clientId* a
<bean id="serviceRegistryDao" class="org.jasig.cas.services.InMemoryServiceRegistryDaoImpl">
<property name="registeredServices">
<list>
<!-- Supports regex patterns by default for service ids -->
<!-- Supports regex patterns by default for service ids -->
<bean class="org.jasig.cas.support.oauth.services.OAuthRegisteredService"
p:id="1"
p:name="serviceName"
p:name="serviceName"
p:description="Service Description"
p:serviceId="oauth client service url"
p:bypassApprovalPrompt="false"
p:clientId="client id goes here"
p:clientSecret="client secret goes here" />
p:bypassApprovalPrompt="false"
p:clientId="client id goes here"
p:clientSecret="client secret goes here" />
...
{% endhighlight %}
# OAuth Authentication
To configure CAS to act as an OpenID provider, please [see this page](../protocol/OpenID-Protocol.html).
\ No newline at end of file
......@@ -22,49 +22,6 @@ Support is enabled by including the following dependency in the Maven WAR overla
##Configuration
###Declare the OpenID endpoint
The OpenID discovery endpoint should be enabled during the configuration process. In the `web.xml` file, the following mapping must be added:
{% highlight xml %}
<servlet-mapping>
<servlet-name>cas</servlet-name>
<url-pattern>/openid/*</url-pattern>
</servlet-mapping>
{% endhighlight %}
In the `cas-servlet.xml` file, the following mapping and bean must be also added:
{% highlight xml %}
<bean id="handlerMappingC" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/logout">logoutController</prop>
...
<prop key="/openid/*">openIdProviderController</prop>
...
<bean
id="openIdProviderController"
class="org.jasig.cas.support.openid.web.OpenIdProviderController"
p:loginUrl="${server.prefix}/login"/>
{% endhighlight %}
###Add the OpenID entry in the unique id generator map
The OpenID entry should be added to the `uniqueIdGenerators.xml` file:
{% highlight xml %}
<util:map id="uniqueIdGeneratorsMap">
...
<entry
key="org.jasig.cas.support.openid.authentication.principal.OpenIdService"
value-ref="serviceTicketUniqueIdGenerator" />
</util:map>
{% endhighlight %}
###Update the webflow
CAS uses a spring webflow to describe the authentication process. We need to change it to switch to OpenID authentication if it recognizes one. This is done in the `login-webflow.xml` file. After the on-start element just add these two blocks:
......@@ -78,7 +35,7 @@ CAS uses a spring webflow to describe the authentication process. We need to cha
&amp;&amp; externalContext.requestParameterMap['openid.mode'] ne 'associate'"
then="openIdSingleSignOnAction" else="ticketGrantingTicketCheck" />
</decision-state>
<!-- The OpenID authentication action. If authentication is successful, send the ticket granting ticker. Otherwise, redirect to the login form. -->
<action-state id="openIdSingleSignOnAction">
<evaluate expression="openIdSingleSignOnAction" />
......@@ -88,133 +45,10 @@ CAS uses a spring webflow to describe the authentication process. We need to cha
</action-state>
{% endhighlight %}
The `openIdSingleSignOnAction` is itself defined in the `cas-servlet.xml` file:
{% highlight xml %}
<bean id="openIdSingleSignOnAction"
class="org.jasig.cas.support.openid.web.flow.OpenIdSingleSignOnAction"
p:centralAuthenticationService-ref="centralAuthenticationService"/>
{% endhighlight %}
###Enable OpenID in the AuthenticationManager
The authentication manager is the place where authentication takes place. We must provide it two elements needed for a successful OpenId authentication. The first thing to do is to detect the user name from the OpenID identifier. When your CAS server will work as an OP, users will authenticate with an OpenID identifier, looking like this: `http://localhost:8080/cas/openid/myusername`. We must provide the CAS server with a way to extract the user principal from the credentials provided. So add an `OpenIdCredentialsToPrincipalResolver` to the authentication manager. The next thing to give CAS is a specialized authentication handler.
Open the `deployerConfigContext.xml` file, and locate the `authenticationManager` bean definition. It has two properties containing beans. In the credentials to principal property, add this bean definition:
{% highlight xml %}
<!-- The openid credentials to principal resolver -->
<bean class="org.jasig.cas.support.openid.authentication.principal.OpenIdPrincipalResolver" />
{% endhighlight %}
Then, in the authentication handler property, add this bean definition:
{% highlight xml %}
<!-- The open id authentication handler -->
<bean class="org.jasig.cas.support.openid.authentication.handler.support.OpenIdCredentialsAuthenticationHandler"
p:ticketRegistry-ref="ticketRegistry" />
{% endhighlight %}
###Adapt the Spring CAS servlet configuration
We now have to make CAS handle the OpenID request that is presented. First, we'll add a handler for the `/login` url, when called to validate a ticket (CAS is implementing the dumb OpenID mode, which means it does not create an association at the beginning of the authentication process. It must then check the received authentication success notification, which is done by one extra HTTP request at the end of the process). Anywhere in the *`cas-servlet.xml`* file, add this bean definition:
{% highlight xml %}
<bean id="handlerMappingOpendId"
class="org.jasig.cas.support.openid.web.support.OpenIdPostUrlHandlerMapping">
<!-- Notice we set the order value to 2, which is the order of
the flow handler mapping. We'll fix that just next.
The OpenIDPostUrlHandlerMapping MUST be called before the login
webflow action is called, otherwise we will never be able to validate the authentication success. -->
<property name="order" value="2"/>
<property name="mappings">
<props>
<prop key="/login">delegatingController</prop>
</props>
</property>
</bean>
{% endhighlight %}
The ordering value is important here. You MUST make sure the order of all other handler mappings are incremented.
In the `handlerMappingOpenId`, we referenced a bean called `delegatingController`. This bean delegates the processing of a request to the first controller of its delegates which says it can handle it. So now we'll provide two delegate controllers. The first one is handling the Smart OpenId association, and the second process the authentication and ticket validation. Add this two beans in the file.
The Smart OpenId controller:
{% highlight xml %}
<bean id="smartOpenIdAssociationController" class="org.jasig.cas.support.openid.web.mvc.SmartOpenIdController"
p:serverManager-ref="serverManager"
p:successView="casOpenIdAssociationSuccessView" p:failureView="casOpenIdAssociationFailureView" />
{% endhighlight %}
The OpenID validation controller:
{% highlight xml %}
<bean id="openIdValidateController" class="org.jasig.cas.web.ServiceValidateController"
p:validationSpecificationClass="org.jasig.cas.validation.Cas20WithoutProxyingValidationSpecification"
p:centralAuthenticationService-ref="centralAuthenticationService"
p:proxyHandler-ref="proxy20Handler" p:argumentExtractor-ref="openIdArgumentExtractor"
p:successView="casOpenIdServiceSuccessView" p:failureView="casOpenIdServiceFailureView" />
{% endhighlight %}
We are done with the delegates. Now we must create the Delegating controller itself, and give it a list of delegates referencing the two delegates we just defined. So add this definition:
{% highlight xml %}
<bean id="delegatingController" class="org.jasig.cas.web.DelegatingController"
p:delegates-ref="delegateControllers"/>
<util:list id="delegateControllers">
<ref bean="smartOpenIdAssociationController"/>
<ref bean="openIdValidateController"/>
</util:list>
{% endhighlight %}
Don't forget to include the `util` Spring namespace if you don't have it already.
###Add an argument extractor
We must tell cas how to extract the OpenID information from the authentication request (`openid.mode`, `openid.sig`, `openid.assoc_handle`, etc). This is done in the *`argumentExtractorsConfiguration.xml`* file, located in the *`spring-configuration`* directory. Add this bean into the file:
{% highlight xml %}
<bean id="openIdArgumentExtractor" class="org.jasig.cas.support.openid.web.support.OpenIdArgumentExtractor">
<property name="openIdPrefixUrl" value="${server.prefix}/openid" />
</bean>
<util:list id="argumentExtractors">
<ref bean="casArgumentExtractor" />
<!-- The OpenId arguments extractor -->
<ref bean="openIdArgumentExtractor" />
<ref bean="samlArgumentExtractor" />
</util:list>
{% endhighlight %}
###Add the server manager
Next we must provide a `ServerManager`, which is a class from the [`openid4java` library](https://code.google.com/p/openid4java/), which allows us to handle the Diffie-Hellman algorithm used by the association process. In the `spring-configuration/applicationContext.xml` file, add this bean definition:
{% highlight xml %}
<bean id="serverManager"
class="org.openid4java.server.ServerManager"
p:oPEndpointUrl="${server.prefix}/login"
p:enforceRpId="false" />
{% endhighlight %}
And finally, we need an applicationContext provider in the `spring-configuration/applicationContext.xml` file again:
{% highlight xml %}
<bean id="applicationContextProvider"
class="org.jasig.cas.util.ApplicationContextProvider" />
{% endhighlight %}
##OpenID v2.0 support
By default, the CAS server is defined as an OpenID provider v1.0. This definition is held in the `user.jsp` file (in the `WEB-INF/view/jsp/protocol/openid` directory):
{% highlight xml %}
<html>
<head>
......@@ -250,10 +84,8 @@ And to add this Yadis definition on some publicly accessible url (in the above e
This XML content defines the CAS server available on `http://mycasserver/login` (to be changed for your server) as an OpenID provider v2.0 because of the type of service (`http://specs.openid.net/auth/2.0/signon`).
***
# Delegate To an OpenID Provider
Using the OpenID protocol, the CAS server can also be configured to [delegate the authentication](../integration/Delegate-Authentication.html) to an OpenID provider.
......@@ -26,6 +26,12 @@ package org.jasig.cas.support.oauth;
*/
public interface OAuthConstants {
/** OAuth 2 endpoint in CAS. */
String ENDPOINT_OAUTH2 = "/oauth2.0/*";
/** OAuth 2 endpoint in CAS. */
String ENDPOINT_OAUTH2_CALLBACK_AUTHORIZE = "/oauth2.0/callbackAuthorize";
/** The redirect uri. */
String REDIRECT_URI = "redirect_uri";
......
/*
* Licensed to Apereo under one or more contributor license
* agreements. See the NOTICE file distributed with this work
* for additional information regarding copyright ownership.
* Apereo licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a
* copy of the License at the following location:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations