/* * See LICENSE for licensing and NOTICE for copyright. */ package net.shibboleth.idp.cas.flow; import javax.annotation.Nonnull; import net.shibboleth.idp.authn.AuthenticationResult; import net.shibboleth.idp.cas.protocol.TicketValidationRequest; import net.shibboleth.idp.cas.protocol.TicketValidationResponse; import net.shibboleth.idp.session.IdPSession; import net.shibboleth.idp.session.context.SessionContext; import net.shibboleth.utilities.java.support.logic.Constraint; import net.shibboleth.utilities.java.support.security.IdentifierGenerationStrategy; import org.joda.time.DateTime; import org.opensaml.core.xml.schema.XSString; import org.opensaml.core.xml.schema.impl.XSStringBuilder; import org.opensaml.profile.context.ProfileRequestContext; import org.opensaml.saml.common.SAMLObject; import org.opensaml.saml.common.SAMLVersion; import org.opensaml.saml.saml1.core.Assertion; import org.opensaml.saml.saml1.core.Attribute; import org.opensaml.saml.saml1.core.AttributeStatement; import org.opensaml.saml.saml1.core.AttributeValue; import org.opensaml.saml.saml1.core.Audience; import org.opensaml.saml.saml1.core.AudienceRestrictionCondition; import org.opensaml.saml.saml1.core.AuthenticationStatement; import org.opensaml.saml.saml1.core.Conditions; import org.opensaml.saml.saml1.core.ConfirmationMethod; import org.opensaml.saml.saml1.core.NameIdentifier; import org.opensaml.saml.saml1.core.Response; import org.opensaml.saml.saml1.core.Status; import org.opensaml.saml.saml1.core.StatusCode; import org.opensaml.saml.saml1.core.Subject; import org.opensaml.saml.saml1.core.SubjectConfirmation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.webflow.execution.RequestContext; /** * Creates the SAML response message for successful ticket validation at the <code>/samlValidate</code> URI. * The SAML message is bound to the outgoing message context as needed by the * {@link org.opensaml.profile.action.impl.EncodeMessage} action. * * @author Marvin S. Addison */ public class BuildSamlValidationSuccessMessageAction extends AbstractOutgoingSamlMessageAction { /** Attribute namespace. */ private static final String NAMESPACE = "http://www.ja-sig.org/products/cas/"; /** Class logger. */ private final Logger log = LoggerFactory.getLogger(BuildSamlValidationSuccessMessageAction.class); /** Attribute value node builder. */ private final XSStringBuilder attrValueBuilder = new XSStringBuilder(); /** SAML identifier generation strategy. */ @Nonnull private final IdentifierGenerationStrategy identifierGenerationStrategy; /** IdP entity ID used to set issuer field of generated assertions. */ @Nonnull private final String entityID; /** * Creates a new instance with required parameters. * * @param strategy SAML identifier generation strategy. * @param entityID IdP entity ID. */ public BuildSamlValidationSuccessMessageAction(final IdentifierGenerationStrategy strategy, final String entityID) { Constraint.isNotNull(strategy, "IdentifierGenerationStrategy cannot be null"); Constraint.isNotNull(strategy, "EntityID cannot be null"); this.identifierGenerationStrategy = strategy; this.entityID = entityID; } @Nonnull @Override protected Response buildSamlResponse( final @Nonnull RequestContext springRequestContext, final @Nonnull ProfileRequestContext<SAMLObject, SAMLObject> profileRequestContext) { final DateTime now = DateTime.now(); final TicketValidationRequest request = FlowStateSupport.getTicketValidationRequest(springRequestContext); if (request == null) { log.info("TicketValidationRequest not found in flow state."); throw new IllegalStateException("TicketValidationRequest not found in flow state."); } final TicketValidationResponse ticketResponse = FlowStateSupport.getTicketValidationResponse(springRequestContext); if (ticketResponse == null) { log.info("TicketValidationResponse not found in flow state."); throw new IllegalStateException("TicketValidationResponse not found in flow state."); } final SessionContext sessionCtx = profileRequestContext.getSubcontext(SessionContext.class, false); if (sessionCtx == null || sessionCtx.getIdPSession() == null) { log.info("Cannot locate IdP session"); throw new IllegalStateException("Cannot locate IdP session"); } final IdPSession session = sessionCtx.getIdPSession(); final Response response = newSAMLObject(Response.class, Response.DEFAULT_ELEMENT_NAME); final Status status = newSAMLObject(Status.class, Status.DEFAULT_ELEMENT_NAME); final StatusCode code = newSAMLObject(StatusCode.class, StatusCode.DEFAULT_ELEMENT_NAME); code.setValue(StatusCode.SUCCESS); status.setStatusCode(code); response.setStatus(status); final Assertion assertion = newSAMLObject(Assertion.class, Assertion.DEFAULT_ELEMENT_NAME); assertion.setID(identifierGenerationStrategy.generateIdentifier()); assertion.setIssueInstant(now); assertion.setVersion(SAMLVersion.VERSION_11); assertion.setIssuer(entityID); final Conditions conditions = newSAMLObject(Conditions.class, Conditions.DEFAULT_ELEMENT_NAME); conditions.setNotBefore(now); conditions.setNotOnOrAfter(now.plusSeconds(60)); final AudienceRestrictionCondition audienceRestriction = newSAMLObject( AudienceRestrictionCondition.class, AudienceRestrictionCondition.DEFAULT_ELEMENT_NAME); final Audience audience = newSAMLObject(Audience.class, Audience.DEFAULT_ELEMENT_NAME); audience.setUri(request.getService()); audienceRestriction.getAudiences().add(audience); conditions.getAudienceRestrictionConditions().add(audienceRestriction); assertion.setConditions(conditions); // Create an AuthenticationStatement for every authentication bound to the IdP session // Use flow ID for authentication method for (AuthenticationResult result : session.getAuthenticationResults()) { assertion.getAuthenticationStatements().add( newAuthenticationStatement(now, result.getAuthenticationFlowId(), session.getPrincipalName())); } final AttributeStatement attrStatement = newSAMLObject( AttributeStatement.class, AttributeStatement.DEFAULT_ELEMENT_NAME); attrStatement.setSubject(newSubject(session.getPrincipalName())); for (final String attrName : ticketResponse.getAttributes().keySet()) { final Attribute attribute = newSAMLObject(Attribute.class, Attribute.DEFAULT_ELEMENT_NAME); attribute.setAttributeName(attrName); attribute.setAttributeNamespace(NAMESPACE); for (String value : ticketResponse.getAttributes().get(attrName)) { attribute.getAttributeValues().add(newAttributeValue(value)); } attrStatement.getAttributes().add(attribute); } assertion.getAttributeStatements().add(attrStatement); response.getAssertions().add(assertion); return response; } private Subject newSubject(final String identifier) { final SubjectConfirmation confirmation = newSAMLObject( SubjectConfirmation.class, SubjectConfirmation.DEFAULT_ELEMENT_NAME); final ConfirmationMethod method = newSAMLObject( ConfirmationMethod.class, ConfirmationMethod.DEFAULT_ELEMENT_NAME); method.setConfirmationMethod(ConfirmationMethod.METHOD_ARTIFACT); confirmation.getConfirmationMethods().add(method); final NameIdentifier nameIdentifier = newSAMLObject(NameIdentifier.class, NameIdentifier.DEFAULT_ELEMENT_NAME); nameIdentifier.setValue(identifier); final Subject subject = newSAMLObject(Subject.class, Subject.DEFAULT_ELEMENT_NAME); subject.setNameIdentifier(nameIdentifier); subject.setSubjectConfirmation(confirmation); return subject; } private AuthenticationStatement newAuthenticationStatement( final DateTime authnInstant, final String authnMethod, final String principal) { final AuthenticationStatement authnStatement = newSAMLObject( AuthenticationStatement.class, AuthenticationStatement.DEFAULT_ELEMENT_NAME); authnStatement.setAuthenticationInstant(authnInstant); authnStatement.setAuthenticationMethod(authnMethod); authnStatement.setSubject(newSubject(principal)); return authnStatement; } private XSString newAttributeValue(final String value) { final XSString stringValue = this.attrValueBuilder.buildObject( AttributeValue.DEFAULT_ELEMENT_NAME, XSString.TYPE_NAME); stringValue.setValue(value); return stringValue; } }