Commit 4268a7c8 authored by Misagh Moayyed's avatar Misagh Moayyed
Browse files

Refactored service responses into individual components

parent 1b19d087
......@@ -22,8 +22,8 @@ package org.jasig.cas.authentication.principal;
/**
* Represents the task of building a CAS response
* that is returned by a service.
* @param <T> the type parameter
* @author Misagh Moayyed
* @param <T> the type parameter
* @since 4.2.0
*/
public interface ResponseBuilder<T extends WebApplicationService> {
......
......@@ -30,7 +30,7 @@ import java.util.Map;
* @author Misagh Moayyed
* @since 4.2
*/
public abstract class AbstractServiceResponseBuilder implements ResponseBuilder<WebApplicationService> {
public abstract class AbstractWebApplicationServiceResponseBuilder implements ResponseBuilder<WebApplicationService> {
protected final Logger logger = LoggerFactory.getLogger(getClass());
/**
......
......@@ -31,7 +31,7 @@ import java.util.Map;
* @author Misagh Moayyed
* @since 4.2
*/
public class WebApplicationServiceResponseBuilder extends AbstractServiceResponseBuilder {
public class WebApplicationServiceResponseBuilder extends AbstractWebApplicationServiceResponseBuilder {
private final Response.ResponseType responseType;
/**
......
......@@ -66,6 +66,12 @@
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jasig.cas</groupId>
<artifactId>cas-server-webapp-support</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
......
......@@ -19,24 +19,11 @@
package org.jasig.cas.support.openid.authentication.principal;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.jasig.cas.CentralAuthenticationService;
import org.jasig.cas.authentication.principal.AbstractWebApplicationService;
import org.jasig.cas.authentication.principal.DefaultResponse;
import org.jasig.cas.authentication.principal.Response;
import org.jasig.cas.support.openid.OpenIdProtocolConstants;
import org.jasig.cas.ticket.TicketException;
import org.jasig.cas.util.ApplicationContextProvider;
import org.jasig.cas.validation.Assertion;
import org.openid4java.association.Association;
import org.openid4java.message.AuthRequest;
import org.openid4java.message.Message;
import org.openid4java.message.MessageException;
import org.openid4java.message.ParameterList;
import org.openid4java.server.ServerManager;
import org.jasig.cas.authentication.principal.ResponseBuilder;
import org.jasig.cas.authentication.principal.WebApplicationService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
/**
* @author Scott Battaglia
......@@ -51,12 +38,6 @@ public final class OpenIdService extends AbstractWebApplicationService {
private final String identity;
private final String artifactId;
private final ParameterList requestParameters;
private final String openIdPrefixUrl;
/**
* Instantiates a new OpenID service.
*
......@@ -64,100 +45,15 @@ public final class OpenIdService extends AbstractWebApplicationService {
* @param originalUrl the original url
* @param artifactId the artifact id
* @param openIdIdentity the OpenID identity
* @param signature the signature
* @param parameterList the parameter list
* @param openIdPrefixUrl the prefix url for OpenID
* @param responseBuilder the response builder
*/
protected OpenIdService(final String id, final String originalUrl,
final String artifactId, final String openIdIdentity,
final String signature, final ParameterList parameterList,
final String openIdPrefixUrl) {
super(id, originalUrl, artifactId);
final String artifactId, final String openIdIdentity,
final ResponseBuilder<WebApplicationService> responseBuilder) {
super(id, originalUrl, artifactId, responseBuilder);
this.identity = openIdIdentity;
this.artifactId = artifactId;
this.requestParameters = parameterList;
this.openIdPrefixUrl = openIdPrefixUrl;
}
/**
* Generates an Openid response.
* If no ticketId is found, response is negative.
* If we have a ticket id, then we check if we have an association.
* If so, we ask OpenId server manager to generate the answer according with the existing association.
* If not, we send back an answer with the ticket id as association handle.
* This will force the consumer to ask a verification, which will validate the service ticket.
* @param ticketId the service ticket to provide to the service.
* @return the generated authentication answer
*/
@Override
public Response getResponse(final String ticketId) {
final Map<String, String> parameters = new HashMap<>();
if (ticketId != null) {
final ServerManager manager = (ServerManager) ApplicationContextProvider.getApplicationContext().getBean("serverManager");
final CentralAuthenticationService cas = ApplicationContextProvider.getApplicationContext()
.getBean("centralAuthenticationService", CentralAuthenticationService.class);
boolean associated = false;
boolean associationValid = true;
try {
final AuthRequest authReq = AuthRequest.createAuthRequest(requestParameters, manager.getRealmVerifier());
final Map parameterMap = authReq.getParameterMap();
if (parameterMap != null && !parameterMap.isEmpty()) {
final String assocHandle = (String) parameterMap.get(OpenIdProtocolConstants.OPENID_ASSOCHANDLE);
if (assocHandle != null) {
final Association association = manager.getSharedAssociations().load(assocHandle);
if (association != null) {
associated = true;
if (association.hasExpired()) {
associationValid = false;
}
}
}
}
} catch (final MessageException me) {
LOGGER.error("Message exception : {}", me.getMessage(), me);
}
boolean successFullAuthentication = true;
Assertion assertion = null;
try {
if (associated) {
if (associationValid) {
assertion = cas.validateServiceTicket(ticketId, this);
LOGGER.info("Validated openid ticket");
} else {
successFullAuthentication = false;
}
}
} catch (final TicketException te) {
LOGGER.error("Could not validate ticket : {}", te.getMessage(), te);
successFullAuthentication = false;
}
final String id;
if (assertion != null && OpenIdProtocolConstants.OPENID_IDENTIFIERSELECT.equals(this.identity)) {
id = this.openIdPrefixUrl + '/' + assertion.getPrimaryAuthentication().getPrincipal().getId();
} else {
id = this.identity;
}
// We sign directly (final 'true') because we don't add extensions
// response message can be either a DirectError or an AuthSuccess here.
// Anyway, handling is the same : send the response message
final Message response = manager.authResponse(requestParameters,
id,
id,
successFullAuthentication,
true);
parameters.putAll(response.getParameterMap());
if (!associated) {
parameters.put(OpenIdProtocolConstants.OPENID_ASSOCHANDLE, ticketId);
}
} else {
parameters.put(OpenIdProtocolConstants.OPENID_MODE, OpenIdProtocolConstants.CANCEL);
}
return DefaultResponse.getRedirectResponse(getOriginalUrl(), parameters);
}
/**
* Return that the service is already logged out.
......
......@@ -19,9 +19,13 @@
package org.jasig.cas.support.openid.authentication.principal;
import org.jasig.cas.CentralAuthenticationService;
import org.jasig.cas.authentication.principal.AbstractServiceFactory;
import org.jasig.cas.support.openid.OpenIdProtocolConstants;
import org.openid4java.message.ParameterList;
import org.openid4java.server.ServerManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
......@@ -45,6 +49,14 @@ public class OpenIdServiceFactory extends AbstractServiceFactory<OpenIdService>
@Value("${server.prefix}/openid")
private String openIdPrefixUrl;
@Autowired
@Qualifier("centralAuthenticationService")
private CentralAuthenticationService centralAuthenticationService;
@Autowired
@Qualifier("serverManager")
private ServerManager serverManager;
public String getOpenIdPrefixUrl() {
return openIdPrefixUrl;
}
......@@ -57,7 +69,6 @@ public class OpenIdServiceFactory extends AbstractServiceFactory<OpenIdService>
public OpenIdService createService(final HttpServletRequest request) {
final String service = request.getParameter(OpenIdProtocolConstants.OPENID_RETURNTO);
final String openIdIdentity = request.getParameter(OpenIdProtocolConstants.OPENID_IDENTITY);
final String signature = request.getParameter(OpenIdProtocolConstants.OPENID_SIG);
if (openIdIdentity == null || !StringUtils.hasText(service)) {
return null;
......@@ -67,12 +78,14 @@ public class OpenIdServiceFactory extends AbstractServiceFactory<OpenIdService>
final String artifactId = request.getParameter(OpenIdProtocolConstants.OPENID_ASSOCHANDLE);
final ParameterList paramList = new ParameterList(request.getParameterMap());
return new OpenIdService(id, service, artifactId, openIdIdentity,
signature, paramList, this.openIdPrefixUrl);
final OpenIdServiceResponseBuilder builder = new OpenIdServiceResponseBuilder(serverManager,
centralAuthenticationService, paramList, this.openIdPrefixUrl);
return new OpenIdService(id, service, artifactId, openIdIdentity, builder);
}
@Override
public OpenIdService createService(final String id) {
return new OpenIdService(id, id, null, null, null, null, this.openIdPrefixUrl);
return new OpenIdService(id, id, null, this.openIdPrefixUrl, null);
}
}
/*
* 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.support.openid.authentication.principal;
import org.apache.commons.lang3.StringUtils;
import org.jasig.cas.CentralAuthenticationService;
import org.jasig.cas.authentication.principal.AbstractWebApplicationServiceResponseBuilder;
import org.jasig.cas.authentication.principal.Response;
import org.jasig.cas.authentication.principal.WebApplicationService;
import org.jasig.cas.support.openid.OpenIdProtocolConstants;
import org.jasig.cas.ticket.TicketException;
import org.jasig.cas.validation.Assertion;
import org.openid4java.association.Association;
import org.openid4java.message.AuthRequest;
import org.openid4java.message.Message;
import org.openid4java.message.MessageException;
import org.openid4java.message.ParameterList;
import org.openid4java.server.ServerManager;
import java.util.HashMap;
import java.util.Map;
/**
* Builds responses to Openid authN requests.
*
* @author Misagh Moayyed
* @since 4.2
*/
public class OpenIdServiceResponseBuilder extends AbstractWebApplicationServiceResponseBuilder {
private final ServerManager serverManager;
private final CentralAuthenticationService centralAuthenticationService;
private final ParameterList parameterList;
private final String openIdPrefixUrl;
/**
* Instantiates a new Open id service response builder.
*
* @param serverManager the server manager
* @param centralAuthenticationService the central authentication service
* @param parameterList the parameter list
* @param openIdPrefixUrl the open id prefix url
*/
public OpenIdServiceResponseBuilder(final ServerManager serverManager,
final CentralAuthenticationService centralAuthenticationService,
final ParameterList parameterList,
final String openIdPrefixUrl) {
this.serverManager = serverManager;
this.centralAuthenticationService = centralAuthenticationService;
this.parameterList = parameterList;
this.openIdPrefixUrl = openIdPrefixUrl;
}
/**
* Generates an Openid response.
* If no ticketId is found, response is negative.
* If we have a ticket id, then we check if we have an association.
* If so, we ask OpenId server manager to generate the answer according with the existing association.
* If not, we send back an answer with the ticket id as association handle.
* This will force the consumer to ask a verification, which will validate the service ticket.
*
* @param ticketId the service ticket to provide to the service.
* @return the generated authentication answer
*/
@Override
public Response build(final WebApplicationService webApplicationService, final String ticketId) {
final OpenIdService service = (OpenIdService) webApplicationService;
final Map<String, String> parameters = new HashMap<>();
if (StringUtils.isBlank(ticketId)) {
parameters.put(OpenIdProtocolConstants.OPENID_MODE, OpenIdProtocolConstants.CANCEL);
return buildRedirect(service, parameters);
}
final Association association = getAssociation();
final boolean associated = association != null;
final boolean associationValid = isAssociationValid(association);
boolean successFullAuthentication = true;
Assertion assertion = null;
try {
if (associated && associationValid) {
assertion = centralAuthenticationService.validateServiceTicket(ticketId, service);
logger.debug("Validated openid ticket {} for {}", ticketId, service);
} else {
logger.warn("Association does not exist or is not valid");
successFullAuthentication = false;
}
} catch (final TicketException te) {
logger.error("Could not validate ticket : {}", te.getMessage(), te);
successFullAuthentication = false;
}
final String id = determineIdentity(service, assertion);
return buildAuthenticationResponse(ticketId, service, parameters, associated,
successFullAuthentication, id);
}
/**
* Determine identity.
*
* @param service the service
* @param assertion the assertion
* @return the string
*/
protected String determineIdentity(final OpenIdService service, final Assertion assertion) {
final String id;
if (assertion != null && OpenIdProtocolConstants.OPENID_IDENTIFIERSELECT.equals(service.getIdentity())) {
id = this.openIdPrefixUrl + '/' + assertion.getPrimaryAuthentication().getPrincipal().getId();
} else {
id = service.getIdentity();
}
return id;
}
/**
* We sign directly (final 'true') because we don't add extensions
* response message can be either a DirectError or an AuthSuccess here.
*
* @param ticketId the ticket id
* @param service the service
* @param parameters the parameters
* @param associated the associated
* @param successFullAuthentication the success full authentication
* @param id the id
* @return response
*/
protected Response buildAuthenticationResponse(final String ticketId, final OpenIdService service,
final Map<String, String> parameters,
final boolean associated, final boolean successFullAuthentication,
final String id) {
final Message response = serverManager.authResponse(this.parameterList, id, id,
successFullAuthentication, true);
parameters.putAll(response.getParameterMap());
if (!associated) {
parameters.put(OpenIdProtocolConstants.OPENID_ASSOCHANDLE, ticketId);
}
return buildRedirect(service, parameters);
}
/**
* Gets association.
*
* @return the association
*/
protected Association getAssociation() {
try {
final AuthRequest authReq = AuthRequest.createAuthRequest(this.parameterList,
this.serverManager.getRealmVerifier());
final Map parameterMap = authReq.getParameterMap();
if (parameterMap != null && !parameterMap.isEmpty()) {
final String assocHandle = (String) parameterMap.get(OpenIdProtocolConstants.OPENID_ASSOCHANDLE);
if (assocHandle != null) {
return serverManager.getSharedAssociations().load(assocHandle);
}
}
} catch (final MessageException me) {
logger.error("Message exception : {}", me.getMessage(), me);
}
return null;
}
/**
* Is association valid.
*
* @param association the association
* @return the boolean
*/
protected boolean isAssociationValid(final Association association) {
return association != null && !association.hasExpired();
}
}
/*
* 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.support.openid;
import org.jasig.cas.CentralAuthenticationService;
import org.jasig.cas.support.openid.authentication.principal.OpenIdServiceFactory;
import org.junit.runner.RunWith;
import org.openid4java.server.ServerAssociationStore;
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.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* Bootstrap context for openid tests.
* @author Misagh Moayyed
* @since 4.2
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath:/META-INF/spring/openid-config.xml"})
public class AbstractOpenIdTests {
protected final Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
@Qualifier("openIdServiceFactory")
protected OpenIdServiceFactory openIdServiceFactory;
@Autowired
@Qualifier("centralAuthenticationService")
protected CentralAuthenticationService centralAuthenticationService;
@Autowired
@Qualifier("serverAssociations")
protected ServerAssociationStore sharedAssociations;
}
......@@ -18,17 +18,13 @@
*/
package org.jasig.cas.support.openid.authentication.principal;
import org.jasig.cas.CentralAuthenticationService;
import org.jasig.cas.TestUtils;
import org.jasig.cas.authentication.principal.Response;
import org.jasig.cas.util.ApplicationContextProvider;
import org.jasig.cas.support.openid.AbstractOpenIdTests;
import org.jasig.cas.support.openid.OpenIdProtocolConstants;
import org.junit.Before;
import org.junit.Test;
import org.openid4java.association.Association;
import org.openid4java.server.ServerAssociationStore;
import org.openid4java.server.ServerManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.mock.web.MockHttpServletRequest;
import static org.junit.Assert.*;
......@@ -38,106 +34,82 @@ import static org.mockito.Mockito.*;
* @author Scott Battaglia
* @since 3.1
*/
public class OpenIdServiceTests {
public class OpenIdServiceTests extends AbstractOpenIdTests {
private static final Logger LOGGER = LoggerFactory.getLogger(OpenIdServiceTests.class);
private OpenIdService openIdService;
private ApplicationContext context;
private CentralAuthenticationService cas;
private ServerManager manager;
private ServerAssociationStore sharedAssociations;
private final MockHttpServletRequest request = new MockHttpServletRequest();
private Association association;
@Before
public void setUp() throws Exception {
request.addParameter("openid.identity", "http://openid.ja-sig.org/battags");
request.addParameter("openid.return_to", "http://www.ja-sig.org/?service=fa");
request.addParameter("openid.mode", "checkid_setup");
sharedAssociations = mock(ServerAssociationStore.class);
manager = new ServerManager();
manager.setOPEndpointUrl("https://localshot:8443/cas/login");
manager.setEnforceRpId(false);
manager.setSharedAssociations(sharedAssociations);
context = mock(ApplicationContext.class);
cas = mock(CentralAuthenticationService.class);
when(context.getBean("serverManager")).thenReturn(manager);
when(context.getBean("centralAuthenticationService", CentralAuthenticationService.class)).thenReturn(cas);
final ApplicationContextProvider contextProvider = new ApplicationContextProvider();
contextProvider.setApplicationContext(context);
<