2 * Copyright [2007] [University Corporation for Advanced Internet Development, Inc.]
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package edu.internet2.middleware.shibboleth.idp.profile.saml2;
19 import java.io.IOException;
20 import java.util.ArrayList;
22 import javax.servlet.RequestDispatcher;
23 import javax.servlet.ServletException;
24 import javax.servlet.ServletRequest;
25 import javax.servlet.ServletResponse;
26 import javax.servlet.http.HttpServletRequest;
27 import javax.servlet.http.HttpSession;
29 import org.apache.log4j.Logger;
30 import org.opensaml.common.SAMLObjectBuilder;
31 import org.opensaml.common.binding.BindingException;
32 import org.opensaml.common.binding.decoding.MessageDecoder;
33 import org.opensaml.common.binding.encoding.MessageEncoder;
34 import org.opensaml.common.binding.security.SAMLSecurityPolicy;
35 import org.opensaml.saml2.core.AuthnContext;
36 import org.opensaml.saml2.core.AuthnContextClassRef;
37 import org.opensaml.saml2.core.AuthnContextDeclRef;
38 import org.opensaml.saml2.core.AuthnRequest;
39 import org.opensaml.saml2.core.AuthnStatement;
40 import org.opensaml.saml2.core.RequestedAuthnContext;
41 import org.opensaml.saml2.core.Response;
42 import org.opensaml.saml2.core.Statement;
43 import org.opensaml.saml2.core.StatusCode;
44 import org.opensaml.saml2.core.Subject;
45 import org.opensaml.saml2.metadata.IDPSSODescriptor;
46 import org.opensaml.saml2.metadata.SPSSODescriptor;
47 import org.opensaml.ws.security.SecurityPolicyException;
48 import org.opensaml.xml.io.MarshallingException;
49 import org.opensaml.xml.io.UnmarshallingException;
51 import edu.internet2.middleware.shibboleth.common.profile.ProfileException;
52 import edu.internet2.middleware.shibboleth.common.profile.ProfileRequest;
53 import edu.internet2.middleware.shibboleth.common.profile.ProfileResponse;
54 import edu.internet2.middleware.shibboleth.common.relyingparty.RelyingPartyConfiguration;
55 import edu.internet2.middleware.shibboleth.common.relyingparty.provider.saml2.SSOConfiguration;
56 import edu.internet2.middleware.shibboleth.idp.authn.LoginContext;
57 import edu.internet2.middleware.shibboleth.idp.authn.Saml2LoginContext;
59 /** SAML 2.0 SSO request profile handler. */
60 public class SSOProfileHandler extends AbstractSAML2ProfileHandler {
63 private final Logger log = Logger.getLogger(SSOProfileHandler.class);
65 /** Builder of AuthnStatement objects. */
66 private SAMLObjectBuilder<AuthnStatement> authnStatementBuilder;
68 /** Builder of AuthnContext objects. */
69 private SAMLObjectBuilder<AuthnContext> authnContextBuilder;
71 /** Builder of AuthnContextClassRef objects. */
72 private SAMLObjectBuilder<AuthnContextClassRef> authnContextClassRefBuilder;
74 /** Builder of AuthnContextDeclRef objects. */
75 private SAMLObjectBuilder<AuthnContextDeclRef> authnContextDeclRefBuilder;
77 /** URL of the authentication manager servlet. */
78 private String authenticationManagerPath;
80 /** URI of request decoder. */
81 private String decodingBinding;
83 /** URI of response encoder. */
84 private String encodingBinding;
89 * @param authnManagerPath path to the authentication manager servlet
90 * @param decoder URI of the request decoder to use
91 * @param encoder URI of the response encoder to use
93 @SuppressWarnings("unchecked")
94 public SSOProfileHandler(String authnManagerPath, String decoder, String encoder) {
97 if (authnManagerPath == null || decoder == null || encoder == null) {
98 throw new IllegalArgumentException("AuthN manager path, decoding, encoding bindings URI may not be null");
101 authenticationManagerPath = authnManagerPath;
102 decodingBinding = decoder;
103 encodingBinding = encoder;
105 authnStatementBuilder = (SAMLObjectBuilder<AuthnStatement>) getBuilderFactory().getBuilder(
106 AuthnStatement.DEFAULT_ELEMENT_NAME);
107 authnContextBuilder = (SAMLObjectBuilder<AuthnContext>) getBuilderFactory().getBuilder(
108 AuthnContext.DEFAULT_ELEMENT_NAME);
109 authnContextClassRefBuilder = (SAMLObjectBuilder<AuthnContextClassRef>) getBuilderFactory().getBuilder(
110 AuthnContextClassRef.DEFAULT_ELEMENT_NAME);
111 authnContextDeclRefBuilder = (SAMLObjectBuilder<AuthnContextDeclRef>) getBuilderFactory().getBuilder(
112 AuthnContextDeclRef.DEFAULT_ELEMENT_NAME);
116 * Convenience method for getting the SAML 2 AuthnStatement builder.
118 * @return SAML 2 AuthnStatement builder
120 public SAMLObjectBuilder<AuthnStatement> getAuthnStatementBuilder() {
121 return authnStatementBuilder;
125 * Convenience method for getting the SAML 2 AuthnContext builder.
127 * @return SAML 2 AuthnContext builder
129 public SAMLObjectBuilder<AuthnContext> getAuthnContextBuilder() {
130 return authnContextBuilder;
134 * Convenience method for getting the SAML 2 AuthnContextClassRef builder.
136 * @return SAML 2 AuthnContextClassRef builder
138 public SAMLObjectBuilder<AuthnContextClassRef> getAuthnContextClassRefBuilder() {
139 return authnContextClassRefBuilder;
143 * Convenience method for getting the SAML 2 AuthnContextDeclRef builder.
145 * @return SAML 2 AuthnContextDeclRef builder
147 public SAMLObjectBuilder<AuthnContextDeclRef> getAuthnContextDeclRefBuilder() {
148 return authnContextDeclRefBuilder;
152 public String getProfileId() {
153 return "urn:mace:shibboleth:2.0:idp:profiles:saml2:request:sso";
157 public void processRequest(ProfileRequest<ServletRequest> request, ProfileResponse<ServletResponse> response)
158 throws ProfileException {
160 HttpSession httpSession = ((HttpServletRequest) request.getRawRequest()).getSession(true);
161 if (httpSession.getAttribute(LoginContext.LOGIN_CONTEXT_KEY) == null) {
162 performAuthentication(request, response);
164 completeAuthenticationRequest(request, response);
169 * Creates a {@link Saml2LoginContext} an sends the request off to the AuthenticationManager to begin the process of
170 * authenticating the user.
172 * @param request current request
173 * @param response current response
175 * @throws ProfileException thrown if there is a problem creating the login context and transferring control to the
176 * authentication manager
178 protected void performAuthentication(ProfileRequest<ServletRequest> request,
179 ProfileResponse<ServletResponse> response) throws ProfileException {
180 HttpServletRequest httpRequest = (HttpServletRequest) request.getRawRequest();
182 AuthnRequest authnRequest = null;
184 MessageDecoder<ServletRequest> decoder = decodeRequest(request);
185 SAMLSecurityPolicy securityPolicy = decoder.getSecurityPolicy();
187 String relyingParty = securityPolicy.getIssuer();
188 authnRequest = (AuthnRequest) decoder.getSAMLMessage();
190 Saml2LoginContext loginContext = new Saml2LoginContext(relyingParty, authnRequest);
191 loginContext.setAuthenticationManagerURL(authenticationManagerPath);
192 loginContext.setProfileHandlerURL(httpRequest.getRequestURI());
194 HttpSession httpSession = httpRequest.getSession();
195 httpSession.setAttribute(Saml2LoginContext.LOGIN_CONTEXT_KEY, loginContext);
196 RequestDispatcher dispatcher = httpRequest.getRequestDispatcher(authenticationManagerPath);
197 dispatcher.forward(httpRequest, response.getRawResponse());
198 } catch (MarshallingException e) {
199 log.error("Unable to marshall authentication request context");
200 throw new ProfileException("Unable to marshall authentication request context", e);
201 } catch (IOException ex) {
202 log.error("Error forwarding SAML 2 AuthnRequest " + authnRequest.getID() + " to AuthenticationManager", ex);
203 throw new ProfileException("Error forwarding SAML 2 AuthnRequest " + authnRequest.getID()
204 + " to AuthenticationManager", ex);
205 } catch (ServletException ex) {
206 log.error("Error forwarding SAML 2 AuthnRequest " + authnRequest.getID() + " to AuthenticationManager", ex);
207 throw new ProfileException("Error forwarding SAML 2 AuthnRequest " + authnRequest.getID()
208 + " to AuthenticationManager", ex);
213 * Creates a response to the {@link AuthnRequest} and sends the user, with response in tow, back to the relying
214 * party after they've been authenticated.
216 * @param request current request
217 * @param response current response
219 * @throws ProfileException thrown if the response can not be created and sent back to the relying party
221 protected void completeAuthenticationRequest(ProfileRequest<ServletRequest> request,
222 ProfileResponse<ServletResponse> response) throws ProfileException {
224 HttpSession httpSession = ((HttpServletRequest) request.getRawRequest()).getSession(true);
226 Saml2LoginContext loginContext = (Saml2LoginContext) httpSession.getAttribute(LoginContext.LOGIN_CONTEXT_KEY);
227 httpSession.removeAttribute(LoginContext.LOGIN_CONTEXT_KEY);
229 SSORequestContext requestContext = buildRequestContext(loginContext, request, response);
231 checkSamlVersion(requestContext);
233 Response samlResponse;
235 if (!loginContext.isPrincipalAuthenticated()) {
237 .setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, StatusCode.AUTHN_FAILED_URI, null));
238 throw new ProfileException("User failed authentication");
241 ArrayList<Statement> statements = new ArrayList<Statement>();
242 statements.add(buildAuthnStatement(requestContext));
243 if (requestContext.getProfileConfiguration().includeAttributeStatement()) {
244 statements.add(buildAttributeStatement(requestContext));
247 Subject assertionSubject = buildSubject(requestContext, "urn:oasis:names:tc:SAML:2.0:cm:bearer");
249 samlResponse = buildResponse(requestContext, assertionSubject, statements);
250 } catch (ProfileException e) {
251 samlResponse = buildErrorResponse(requestContext);
254 requestContext.setSamlResponse(samlResponse);
255 encodeResponse(requestContext);
256 writeAuditLogEntry(requestContext);
260 * Creates an appropriate message decoder, populates it, and decodes the incoming request.
262 * @param request current request
264 * @return message decoder containing the decoded message and other stateful information
266 * @throws ProfileException thrown if the incomming message failed decoding
268 protected MessageDecoder<ServletRequest> decodeRequest(ProfileRequest<ServletRequest> request)
269 throws ProfileException {
270 MessageDecoder<ServletRequest> decoder = getMessageDecoderFactory().getMessageDecoder(decodingBinding);
271 if (decoder == null) {
272 log.error("No request decoder was registered for binding type: " + decodingBinding);
273 throw new ProfileException("No request decoder was registered for binding type: " + decodingBinding);
276 populateMessageDecoder(decoder);
277 decoder.setRequest(request.getRawRequest());
281 } catch (BindingException e) {
282 log.error("Error decoding authentication request message", e);
283 throw new ProfileException("Error decoding authentication request message", e);
284 } catch (SecurityPolicyException e) {
285 log.error("Message did not meet security policy requirements", e);
286 throw new ProfileException("Message did not meet security policy requirements", e);
291 * Creates an authentication request context from the current environmental information.
293 * @param loginContext current login context
294 * @param request current request
295 * @param response current response
297 * @return created authentication request context
299 * @throws ProfileException thrown if there is a problem creating the context
301 protected SSORequestContext buildRequestContext(Saml2LoginContext loginContext,
302 ProfileRequest<ServletRequest> request, ProfileResponse<ServletResponse> response) throws ProfileException {
303 SSORequestContext requestContext = new SSORequestContext(request, response);
306 String relyingPartyId = loginContext.getRelyingPartyId();
307 AuthnRequest authnRequest = loginContext.getAuthenticationRequest();
309 requestContext.setRelyingPartyId(relyingPartyId);
311 RelyingPartyConfiguration rpConfig = getRelyingPartyConfiguration(relyingPartyId);
312 requestContext.setRelyingPartyConfiguration(rpConfig);
314 requestContext.setRelyingPartyRole(SPSSODescriptor.DEFAULT_ELEMENT_NAME);
316 requestContext.setAssertingPartyId(requestContext.getRelyingPartyConfiguration().getProviderId());
318 requestContext.setAssertingPartyRole(IDPSSODescriptor.DEFAULT_ELEMENT_NAME);
320 requestContext.setProfileConfiguration((SSOConfiguration) rpConfig
321 .getProfileConfiguration(SSOConfiguration.PROFILE_ID));
323 requestContext.setSamlRequest(authnRequest);
325 return requestContext;
326 } catch (UnmarshallingException e) {
327 log.error("Unable to unmarshall authentication request context");
328 requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, null,
329 "Error recovering request state"));
330 throw new ProfileException("Error recovering request state", e);
335 * Creates an authentication statement for the current request.
337 * @param requestContext current request context
339 * @return constructed authentication statement
341 protected AuthnStatement buildAuthnStatement(SSORequestContext requestContext) {
342 Saml2LoginContext loginContext = requestContext.getLoginContext();
344 AuthnContext authnContext = buildAuthnContext(requestContext);
346 AuthnStatement statement = getAuthnStatementBuilder().buildObject();
347 statement.setAuthnContext(authnContext);
348 statement.setAuthnInstant(loginContext.getAuthenticationInstant());
351 statement.setSessionIndex(null);
353 if (loginContext.getAuthenticationDuration() > 0) {
354 statement.setSessionNotOnOrAfter(loginContext.getAuthenticationInstant().plus(
355 loginContext.getAuthenticationDuration()));
359 statement.setSubjectLocality(null);
365 * Creates an {@link AuthnContext} for a succesful authentication request.
367 * @param requestContext current request
369 * @return the built authn context
371 protected AuthnContext buildAuthnContext(SSORequestContext requestContext) {
372 AuthnContext authnContext = getAuthnContextBuilder().buildObject();
374 Saml2LoginContext loginContext = requestContext.getLoginContext();
375 AuthnRequest authnRequest = requestContext.getSamlRequest();
376 RequestedAuthnContext requestedAuthnContext = authnRequest.getRequestedAuthnContext();
377 if (requestedAuthnContext != null) {
378 if (requestedAuthnContext.getAuthnContextClassRefs() != null) {
379 for (AuthnContextClassRef classRef : requestedAuthnContext.getAuthnContextClassRefs()) {
380 if (classRef.getAuthnContextClassRef().equals(loginContext.getAuthenticationMethod())) {
381 AuthnContextClassRef ref = getAuthnContextClassRefBuilder().buildObject();
382 ref.setAuthnContextClassRef(loginContext.getAuthenticationMethod());
383 authnContext.setAuthnContextClassRef(ref);
386 } else if (requestedAuthnContext.getAuthnContextDeclRefs() != null) {
387 for (AuthnContextDeclRef declRef : requestedAuthnContext.getAuthnContextDeclRefs()) {
388 if (declRef.getAuthnContextDeclRef().equals(loginContext.getAuthenticationMethod())) {
389 AuthnContextDeclRef ref = getAuthnContextDeclRefBuilder().buildObject();
390 ref.setAuthnContextDeclRef(loginContext.getAuthenticationMethod());
391 authnContext.setAuthnContextDeclRef(ref);
396 AuthnContextDeclRef ref = getAuthnContextDeclRefBuilder().buildObject();
397 ref.setAuthnContextDeclRef(loginContext.getAuthenticationMethod());
398 authnContext.setAuthnContextDeclRef(ref);
405 * Encodes the request's SAML response and writes it to the servlet response.
407 * @param requestContext current request context
409 * @throws ProfileException thrown if no message encoder is registered for this profiles binding
411 protected void encodeResponse(SSORequestContext requestContext) throws ProfileException {
412 if (log.isDebugEnabled()) {
413 log.debug("Encoding response to SAML request " + requestContext.getSamlRequest().getID()
414 + " from relying party " + requestContext.getRelyingPartyId());
416 MessageEncoder<ServletResponse> encoder = getMessageEncoderFactory().getMessageEncoder(encodingBinding);
417 if (encoder == null) {
418 log.error("No response encoder was registered for binding type: " + encodingBinding);
419 throw new ProfileException("No response encoder was registered for binding type: " + encodingBinding);
422 super.populateMessageEncoder(encoder);
423 encoder.setResponse(requestContext.getProfileResponse().getRawResponse());
424 encoder.setSamlMessage(requestContext.getSamlResponse());
425 requestContext.setMessageEncoder(encoder);
429 } catch (BindingException e) {
430 throw new ProfileException("Unable to encode response to relying party: "
431 + requestContext.getRelyingPartyId(), e);
435 /** Represents the internal state of a SAML 2.0 SSO Request while it's being processed by the IdP. */
436 protected class SSORequestContext extends SAML2ProfileRequestContext<AuthnRequest, Response, SSOConfiguration> {
438 /** Current login context. */
439 private Saml2LoginContext loginContext;
444 * @param request current profile request
445 * @param response current profile response
447 public SSORequestContext(ProfileRequest<ServletRequest> request, ProfileResponse<ServletResponse> response) {
448 super(request, response);
452 * Gets the current login context.
454 * @return current login context
456 public Saml2LoginContext getLoginContext() {
461 * Sets the current login context.
463 * @param context current login context
465 public void setLoginContext(Saml2LoginContext context) {
466 loginContext = context;