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
170 * process of 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<ServletRequest> 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 Response samlResponse;
233 if (!loginContext.getAuthenticationOK()) {
235 .setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, StatusCode.AUTHN_FAILED_URI, null));
236 throw new ProfileException("User failed authentication");
239 ArrayList<Statement> statements = new ArrayList<Statement>();
240 statements.add(buildAuthnStatement(requestContext));
241 statements.add(buildAttributeStatement(requestContext));
243 Subject assertionSubject = buildSubject(requestContext, "urn:oasis:names:tc:SAML:2.0:cm:bearer");
245 samlResponse = buildResponse(requestContext, assertionSubject, statements);
246 } catch (ProfileException e) {
247 samlResponse = buildErrorResponse(requestContext);
250 requestContext.setSamlResponse(samlResponse);
251 encodeResponse(requestContext);
252 writeAuditLogEntry(requestContext);
256 * Creates an appropriate message decoder, populates it, and decodes the incoming request.
258 * @param request current request
260 * @return message decoder containing the decoded message and other stateful information
262 * @throws ProfileException thrown if the incomming message failed decoding
264 protected MessageDecoder<ServletRequest> decodeRequest(ProfileRequest<ServletRequest> request)
265 throws ProfileException {
266 MessageDecoder<ServletRequest> decoder = getMessageDecoderFactory().getMessageDecoder(decodingBinding);
267 if (decoder == null) {
268 log.error("No request decoder was registered for binding type: " + decodingBinding);
269 throw new ProfileException("No request decoder was registered for binding type: " + decodingBinding);
272 populateMessageDecoder(decoder);
273 decoder.setRequest(request.getRawRequest());
277 } catch (BindingException e) {
278 log.error("Error decoding authentication request message", e);
279 throw new ProfileException("Error decoding authentication request message", e);
280 } catch (SecurityPolicyException e) {
281 log.error("Message did not meet security policy requirements", e);
282 throw new ProfileException("Message did not meet security policy requirements", e);
287 * Creates an authentication request context from the current environmental information.
289 * @param loginContext current login context
290 * @param request current request
291 * @param response current response
293 * @return created authentication request context
295 * @throws ProfileException thrown if there is a problem creating the context
297 protected SSORequestContext buildRequestContext(Saml2LoginContext loginContext,
298 ProfileRequest<ServletRequest> request, ProfileResponse<ServletResponse> response) throws ProfileException {
299 SSORequestContext requestContext = new SSORequestContext(request, response);
302 String relyingPartyId = loginContext.getRelyingPartyId();
303 AuthnRequest authnRequest = loginContext.getAuthenticationRequest();
305 requestContext.setRelyingPartyId(relyingPartyId);
307 RelyingPartyConfiguration rpConfig = getRelyingPartyConfiguration(relyingPartyId);
308 requestContext.setRelyingPartyConfiguration(rpConfig);
310 requestContext.setRelyingPartyRole(SPSSODescriptor.DEFAULT_ELEMENT_NAME);
312 requestContext.setAssertingPartyId(requestContext.getRelyingPartyConfiguration().getProviderId());
314 requestContext.setAssertingPartyRole(IDPSSODescriptor.DEFAULT_ELEMENT_NAME);
316 requestContext.setProfileConfiguration((SSOConfiguration) rpConfig
317 .getProfileConfiguration(SSOConfiguration.PROFILE_ID));
319 requestContext.setSamlRequest(authnRequest);
321 return requestContext;
322 } catch (UnmarshallingException e) {
323 log.error("Unable to unmarshall authentication request context");
324 requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, null,
325 "Error recovering request state"));
326 throw new ProfileException("Error recovering request state", e);
331 * Creates an authentication statement for the current request.
333 * @param requestContext current request context
335 * @return constructed authentication statement
337 protected AuthnStatement buildAuthnStatement(SSORequestContext requestContext) {
338 Saml2LoginContext loginContext = requestContext.getLoginContext();
340 AuthnContext authnContext = buildAuthnContext(requestContext);
342 AuthnStatement statement = getAuthnStatementBuilder().buildObject();
343 statement.setAuthnContext(authnContext);
344 statement.setAuthnInstant(loginContext.getAuthenticationInstant());
347 statement.setSessionIndex(null);
349 if (loginContext.getAuthenticationDuration() > 0) {
350 statement.setSessionNotOnOrAfter(loginContext.getAuthenticationInstant().plus(
351 loginContext.getAuthenticationDuration()));
355 statement.setSubjectLocality(null);
361 * Creates an {@link AuthnContext} for a succesful authentication request.
363 * @param requestContext current request
365 * @return the built authn context
367 protected AuthnContext buildAuthnContext(SSORequestContext requestContext) {
368 AuthnContext authnContext = getAuthnContextBuilder().buildObject();
370 Saml2LoginContext loginContext = requestContext.getLoginContext();
371 AuthnRequest authnRequest = requestContext.getSamlRequest();
372 RequestedAuthnContext requestedAuthnContext = authnRequest.getRequestedAuthnContext();
373 if (requestedAuthnContext != null) {
374 if (requestedAuthnContext.getAuthnContextClassRefs() != null) {
375 for (AuthnContextClassRef classRef : requestedAuthnContext.getAuthnContextClassRefs()) {
376 if (classRef.getAuthnContextClassRef().equals(loginContext.getAuthenticationMethod())) {
377 AuthnContextClassRef ref = getAuthnContextClassRefBuilder().buildObject();
378 ref.setAuthnContextClassRef(loginContext.getAuthenticationMethod());
379 authnContext.setAuthnContextClassRef(ref);
382 } else if (requestedAuthnContext.getAuthnContextDeclRefs() != null) {
383 for (AuthnContextDeclRef declRef : requestedAuthnContext.getAuthnContextDeclRefs()) {
384 if (declRef.getAuthnContextDeclRef().equals(loginContext.getAuthenticationMethod())) {
385 AuthnContextDeclRef ref = getAuthnContextDeclRefBuilder().buildObject();
386 ref.setAuthnContextDeclRef(loginContext.getAuthenticationMethod());
387 authnContext.setAuthnContextDeclRef(ref);
392 AuthnContextDeclRef ref = getAuthnContextDeclRefBuilder().buildObject();
393 ref.setAuthnContextDeclRef(loginContext.getAuthenticationMethod());
394 authnContext.setAuthnContextDeclRef(ref);
401 * Encodes the request's SAML response and writes it to the servlet response.
403 * @param requestContext current request context
405 * @throws ProfileException thrown if no message encoder is registered for this profiles binding
407 protected void encodeResponse(SSORequestContext requestContext) throws ProfileException {
408 if (log.isDebugEnabled()) {
409 log.debug("Encoding response to SAML request " + requestContext.getSamlRequest().getID()
410 + " from relying party " + requestContext.getRelyingPartyId());
412 MessageEncoder<ServletResponse> encoder = getMessageEncoderFactory().getMessageEncoder(encodingBinding);
413 if (encoder == null) {
414 log.error("No response encoder was registered for binding type: " + encodingBinding);
415 throw new ProfileException("No response encoder was registered for binding type: " + encodingBinding);
418 super.populateMessageEncoder(encoder);
419 encoder.setResponse(requestContext.getProfileResponse().getRawResponse());
420 encoder.setSamlMessage(requestContext.getSamlResponse());
421 requestContext.setMessageEncoder(encoder);
425 } catch (BindingException e) {
426 throw new ProfileException("Unable to encode response to relying party: "
427 + requestContext.getRelyingPartyId(), e);
431 /** Represents the internal state of a SAML 2.0 SSO Request while it's being processed by the IdP. */
432 protected class SSORequestContext extends
433 SAML2ProfileRequestContext<AuthnRequest, Response, SSOConfiguration> {
435 /** Current login context. */
436 private Saml2LoginContext loginContext;
441 * @param request current profile request
442 * @param response current profile response
444 public SSORequestContext(ProfileRequest<ServletRequest> request,
445 ProfileResponse<ServletResponse> response) {
446 super(request, response);
450 * Gets the current login context.
452 * @return current login context
454 public Saml2LoginContext getLoginContext() {
459 * Sets the current login context.
461 * @param context current login context
463 public void setLoginContext(Saml2LoginContext context) {
464 loginContext = context;