/*
* Licensed to Jasig under one or more contributor license
* agreements. See the NOTICE file distributed with this work
* for additional information regarding copyright ownership.
* Jasig 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.flow;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.NotNull;
import org.jasig.cas.CentralAuthenticationService;
import org.jasig.cas.Message;
import org.jasig.cas.authentication.AuthenticationException;
import org.jasig.cas.authentication.Credential;
import org.jasig.cas.authentication.HandlerResult;
import org.jasig.cas.authentication.principal.Service;
import org.jasig.cas.ticket.TicketCreationException;
import org.jasig.cas.ticket.TicketException;
import org.jasig.cas.ticket.TicketGrantingTicket;
import org.jasig.cas.ticket.registry.TicketRegistry;
import org.jasig.cas.web.bind.CredentialsBinder;
import org.jasig.cas.web.support.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.binding.message.MessageBuilder;
import org.springframework.binding.message.MessageContext;
import org.springframework.util.StringUtils;
import org.springframework.web.util.CookieGenerator;
import org.springframework.webflow.core.collection.LocalAttributeMap;
import org.springframework.webflow.execution.Event;
import org.springframework.webflow.execution.RequestContext;
/**
* Action to authenticate credential and retrieve a TicketGrantingTicket for
* those credential. If there is a request for renew, then it also generates
* the Service Ticket required.
*
* @author Scott Battaglia
* @since 3.0.4
*/
public class AuthenticationViaFormAction {
/** Authentication success result. */
public static final String SUCCESS = "success";
/** Authentication succeeded with warnings from authn subsystem that should be displayed to user. */
public static final String SUCCESS_WITH_WARNINGS = "successWithWarnings";
/** Authentication success with "warn" enabled. */
public static final String WARN = "warn";
/** Authentication failure result. */
public static final String AUTHENTICATION_FAILURE = "authenticationFailure";
/** Error result. */
public static final String ERROR = "error";
/**
* Binder that allows additional binding of form object beyond Spring
* defaults.
*/
private CredentialsBinder credentialsBinder;
/** Core we delegate to for handling all ticket related tasks. */
@NotNull
private CentralAuthenticationService centralAuthenticationService;
/** Ticket registry used to retrieve tickets by ID. */
@NotNull
private TicketRegistry ticketRegistry;
@NotNull
private CookieGenerator warnCookieGenerator;
/** Flag indicating whether message context contains warning messages. */
private boolean hasWarningMessages;
/** Logger instance. **/
protected final Logger logger = LoggerFactory.getLogger(getClass());
public final void doBind(final RequestContext context, final Credential credential) throws Exception {
final HttpServletRequest request = WebUtils.getHttpServletRequest(context);
if (this.credentialsBinder != null && this.credentialsBinder.supports(credential.getClass())) {
this.credentialsBinder.bind(request, credential);
}
}
public final Event submit(final RequestContext context, final Credential credential,
final MessageContext messageContext) throws Exception {
// Validate login ticket
final String authoritativeLoginTicket = WebUtils.getLoginTicketFromFlowScope(context);
final String providedLoginTicket = WebUtils.getLoginTicketFromRequest(context);
if (!authoritativeLoginTicket.equals(providedLoginTicket)) {
logger.warn("Invalid login ticket {}", providedLoginTicket);
messageContext.addMessage(new MessageBuilder().code("error.invalid.loginticket").build());
return newEvent(ERROR);
}
final String ticketGrantingTicketId = WebUtils.getTicketGrantingTicketId(context);
final Service service = WebUtils.getService(context);
if (StringUtils.hasText(context.getRequestParameters().get("renew")) && ticketGrantingTicketId != null
&& service != null) {
try {
final String serviceTicketId = this.centralAuthenticationService.grantServiceTicket(
ticketGrantingTicketId, service, credential);
WebUtils.putServiceTicketInRequestScope(context, serviceTicketId);
putWarnCookieIfRequestParameterPresent(context);
return newEvent(WARN);
} catch (final AuthenticationException e) {
return newEvent(AUTHENTICATION_FAILURE, e);
} catch (final TicketCreationException e) {
logger.warn(
"Invalid attempt to access service using renew=true with different credential. "
+ "Ending SSO session.");
this.centralAuthenticationService.destroyTicketGrantingTicket(ticketGrantingTicketId);
} catch (final TicketException e) {
return newEvent(ERROR, e);
}
}
try {
final String tgtId = this.centralAuthenticationService.createTicketGrantingTicket(credential);
WebUtils.putTicketGrantingTicketInFlowScope(context, tgtId);
putWarnCookieIfRequestParameterPresent(context);
final TicketGrantingTicket tgt = (TicketGrantingTicket) this.ticketRegistry.getTicket(tgtId);
for (final Map.Entry<String, HandlerResult> entry : tgt.getAuthentication().getSuccesses().entrySet()) {
for (final Message message : entry.getValue().getWarnings()) {
addWarningToContext(messageContext, message);
}
}
if (this.hasWarningMessages) {
return newEvent(SUCCESS_WITH_WARNINGS);
}
return newEvent(SUCCESS);
} catch (final AuthenticationException e) {
return newEvent(AUTHENTICATION_FAILURE, e);
} catch (final Exception e) {
return newEvent(ERROR, e);
}
}
private void putWarnCookieIfRequestParameterPresent(final RequestContext context) {
final HttpServletResponse response = WebUtils.getHttpServletResponse(context);
if (StringUtils.hasText(context.getExternalContext().getRequestParameterMap().get("warn"))) {
this.warnCookieGenerator.addCookie(response, "true");
} else {
this.warnCookieGenerator.removeCookie(response);
}
}
private AuthenticationException getAuthenticationExceptionAsCause(final TicketException e) {
return (AuthenticationException) e.getCause();
}
private Event newEvent(final String id) {
return new Event(this, id);
}
private Event newEvent(final String id, final Exception error) {
return new Event(this, id, new LocalAttributeMap("error", error));
}
public final void setCentralAuthenticationService(final CentralAuthenticationService centralAuthenticationService) {
this.centralAuthenticationService = centralAuthenticationService;
}
public void setTicketRegistry(final TicketRegistry ticketRegistry) {
this.ticketRegistry = ticketRegistry;
}
/**
* Set a CredentialsBinder for additional binding of the HttpServletRequest
* to the Credential instance, beyond our default binding of the
* Credential as a Form Object in Spring WebMVC parlance. By the time we
* invoke this CredentialsBinder, we have already engaged in default binding
* such that for each HttpServletRequest parameter, if there was a JavaBean
* property of the Credential implementation of the same name, we have set
* that property to be the value of the corresponding request parameter.
* This CredentialsBinder plugin point exists to allow consideration of
* things other than HttpServletRequest parameters in populating the
* Credential (or more sophisticated consideration of the
* HttpServletRequest parameters).
*
* @param credentialsBinder the credential binder to set.
*/
public final void setCredentialsBinder(final CredentialsBinder credentialsBinder) {
this.credentialsBinder = credentialsBinder;
}
public final void setWarnCookieGenerator(final CookieGenerator warnCookieGenerator) {
this.warnCookieGenerator = warnCookieGenerator;
}
/**
* Adds a warning message to the message context.
*
* @param context Message context.
* @param warning Warning message.
*/
private void addWarningToContext(final MessageContext context, final Message warning) {
final MessageBuilder builder = new MessageBuilder()
.warning()
.code(warning.getCode())
.defaultText(warning.getDefaultMessage())
.args(warning.getParams());
context.addMessage(builder.build());
this.hasWarningMessages = true;
}
}