PageRenderTime 42ms CodeModel.GetById 10ms RepoModel.GetById 0ms app.codeStats 1ms

/support/cas-server-support-saml-idp-web/src/main/java/org/apereo/cas/support/saml/web/idp/profile/AbstractSamlProfileHandlerController.java

https://github.com/frett/cas
Java | 548 lines | 311 code | 57 blank | 180 comment | 17 complexity | cc17b05df95f25a546542fef1c71add6 MD5 | raw file
  1. package org.apereo.cas.support.saml.web.idp.profile;
  2. import org.apereo.cas.CasProtocolConstants;
  3. import org.apereo.cas.authentication.Authentication;
  4. import org.apereo.cas.authentication.AuthenticationSystemSupport;
  5. import org.apereo.cas.authentication.principal.Service;
  6. import org.apereo.cas.authentication.principal.ServiceFactory;
  7. import org.apereo.cas.authentication.principal.WebApplicationService;
  8. import org.apereo.cas.configuration.CasConfigurationProperties;
  9. import org.apereo.cas.services.RegisteredService;
  10. import org.apereo.cas.services.ServicesManager;
  11. import org.apereo.cas.services.UnauthorizedServiceException;
  12. import org.apereo.cas.support.saml.OpenSamlConfigBean;
  13. import org.apereo.cas.support.saml.SamlException;
  14. import org.apereo.cas.support.saml.SamlIdPUtils;
  15. import org.apereo.cas.support.saml.SamlProtocolConstants;
  16. import org.apereo.cas.support.saml.SamlUtils;
  17. import org.apereo.cas.support.saml.services.SamlRegisteredService;
  18. import org.apereo.cas.support.saml.services.idp.metadata.SamlRegisteredServiceServiceProviderMetadataFacade;
  19. import org.apereo.cas.support.saml.services.idp.metadata.cache.SamlRegisteredServiceCachingMetadataResolver;
  20. import org.apereo.cas.support.saml.web.idp.profile.builders.SamlProfileObjectBuilder;
  21. import org.apereo.cas.support.saml.web.idp.profile.builders.enc.SamlIdPObjectSigner;
  22. import org.apereo.cas.support.saml.web.idp.profile.builders.enc.SamlObjectSignatureValidator;
  23. import org.apereo.cas.util.DateTimeUtils;
  24. import org.apereo.cas.util.DigestUtils;
  25. import org.apereo.cas.util.EncodingUtils;
  26. import org.apereo.cas.web.support.WebUtils;
  27. import com.google.common.base.Splitter;
  28. import lombok.RequiredArgsConstructor;
  29. import lombok.SneakyThrows;
  30. import lombok.extern.slf4j.Slf4j;
  31. import lombok.val;
  32. import net.shibboleth.utilities.java.support.net.URLBuilder;
  33. import org.apache.commons.lang3.StringUtils;
  34. import org.apache.commons.lang3.tuple.Pair;
  35. import org.jasig.cas.client.authentication.AttributePrincipalImpl;
  36. import org.jasig.cas.client.authentication.DefaultAuthenticationRedirectStrategy;
  37. import org.jasig.cas.client.util.CommonUtils;
  38. import org.jasig.cas.client.validation.Assertion;
  39. import org.jasig.cas.client.validation.AssertionImpl;
  40. import org.opensaml.core.xml.util.XMLObjectSupport;
  41. import org.opensaml.messaging.context.MessageContext;
  42. import org.opensaml.saml.common.SAMLException;
  43. import org.opensaml.saml.common.SAMLObject;
  44. import org.opensaml.saml.common.SignableSAMLObject;
  45. import org.opensaml.saml.common.binding.BindingDescriptor;
  46. import org.opensaml.saml.common.binding.SAMLBindingSupport;
  47. import org.opensaml.saml.saml2.binding.decoding.impl.HTTPSOAP11Decoder;
  48. import org.opensaml.saml.saml2.core.AuthnRequest;
  49. import org.opensaml.saml.saml2.core.RequestAbstractType;
  50. import org.springframework.stereotype.Controller;
  51. import org.springframework.web.bind.annotation.ExceptionHandler;
  52. import org.springframework.web.servlet.ModelAndView;
  53. import javax.servlet.http.HttpServletRequest;
  54. import javax.servlet.http.HttpServletResponse;
  55. import java.io.ByteArrayInputStream;
  56. import java.nio.charset.StandardCharsets;
  57. import java.time.ZonedDateTime;
  58. import java.util.LinkedHashMap;
  59. import java.util.Map;
  60. import java.util.Optional;
  61. import java.util.TreeMap;
  62. /**
  63. * A parent controller to handle SAML requests.
  64. * Specific profile endpoints are handled by extensions.
  65. * This parent provides the necessary ops for profile endpoint
  66. * controllers to respond to end points.
  67. *
  68. * @author Misagh Moayyed
  69. * @since 5.0.0
  70. */
  71. @Controller
  72. @Slf4j
  73. @RequiredArgsConstructor
  74. public abstract class AbstractSamlProfileHandlerController {
  75. /**
  76. * The Saml object signer.
  77. */
  78. protected final SamlIdPObjectSigner samlObjectSigner;
  79. /**
  80. * Authentication support to handle credentials and authn subsystem calls.
  81. */
  82. protected final AuthenticationSystemSupport authenticationSystemSupport;
  83. /**
  84. * The Services manager.
  85. */
  86. protected final ServicesManager servicesManager;
  87. /**
  88. * The Web application service factory.
  89. */
  90. protected final ServiceFactory<WebApplicationService> webApplicationServiceFactory;
  91. /**
  92. * The Saml registered service caching metadata resolver.
  93. */
  94. protected final SamlRegisteredServiceCachingMetadataResolver samlRegisteredServiceCachingMetadataResolver;
  95. /**
  96. * The Config bean.
  97. */
  98. protected final OpenSamlConfigBean configBean;
  99. /**
  100. * The Response builder.
  101. */
  102. protected final SamlProfileObjectBuilder<? extends SAMLObject> responseBuilder;
  103. /**
  104. * The cas properties.
  105. */
  106. protected final CasConfigurationProperties casProperties;
  107. /**
  108. * Signature validator.
  109. */
  110. protected final SamlObjectSignatureValidator samlObjectSignatureValidator;
  111. /**
  112. * Callback service.
  113. */
  114. protected final Service callbackService;
  115. /**
  116. * Gets saml metadata adaptor for service.
  117. *
  118. * @param registeredService the registered service
  119. * @param authnRequest the authn request
  120. * @return the saml metadata adaptor for service
  121. */
  122. protected Optional<SamlRegisteredServiceServiceProviderMetadataFacade> getSamlMetadataFacadeFor(final SamlRegisteredService registeredService,
  123. final RequestAbstractType authnRequest) {
  124. return SamlRegisteredServiceServiceProviderMetadataFacade.get(this.samlRegisteredServiceCachingMetadataResolver, registeredService, authnRequest);
  125. }
  126. /**
  127. * Gets saml metadata adaptor for service.
  128. *
  129. * @param registeredService the registered service
  130. * @param entityId the entity id
  131. * @return the saml metadata adaptor for service
  132. */
  133. protected Optional<SamlRegisteredServiceServiceProviderMetadataFacade> getSamlMetadataFacadeFor(
  134. final SamlRegisteredService registeredService, final String entityId) {
  135. return SamlRegisteredServiceServiceProviderMetadataFacade.get(this.samlRegisteredServiceCachingMetadataResolver, registeredService, entityId);
  136. }
  137. /**
  138. * Gets registered service and verify.
  139. *
  140. * @param serviceId the service id
  141. * @return the registered service and verify
  142. */
  143. protected SamlRegisteredService verifySamlRegisteredService(final String serviceId) {
  144. if (StringUtils.isBlank(serviceId)) {
  145. throw new UnauthorizedServiceException(UnauthorizedServiceException.CODE_UNAUTHZ_SERVICE,
  146. "Could not verify/locate SAML registered service since no serviceId is provided");
  147. }
  148. LOGGER.debug("Checking service access in CAS service registry for [{}]", serviceId);
  149. val registeredService =
  150. this.servicesManager.findServiceBy(this.webApplicationServiceFactory.createService(serviceId));
  151. if (registeredService == null || !registeredService.getAccessStrategy().isServiceAccessAllowed()) {
  152. LOGGER.warn("[{}] is not found in the registry or service access is denied. Ensure service is registered in service registry", serviceId);
  153. throw new UnauthorizedServiceException(UnauthorizedServiceException.CODE_UNAUTHZ_SERVICE);
  154. }
  155. if (registeredService instanceof SamlRegisteredService) {
  156. val samlRegisteredService = (SamlRegisteredService) registeredService;
  157. LOGGER.debug("Located SAML service in the registry as [{}] with the metadata location of [{}]",
  158. samlRegisteredService.getServiceId(), samlRegisteredService.getMetadataLocation());
  159. return samlRegisteredService;
  160. }
  161. LOGGER.error("CAS has found a match for service [{}] in registry but the match is not defined as a SAML service", serviceId);
  162. throw new UnauthorizedServiceException(UnauthorizedServiceException.CODE_UNAUTHZ_SERVICE);
  163. }
  164. /**
  165. * Retrieve authn request authn request.
  166. *
  167. * @param request the request
  168. * @return the authn request
  169. * @throws Exception the exception
  170. */
  171. protected AuthnRequest retrieveSamlAuthenticationRequestFromHttpRequest(final HttpServletRequest request) throws Exception {
  172. LOGGER.debug("Retrieving authentication request from scope");
  173. val requestValue = request.getParameter(SamlProtocolConstants.PARAMETER_SAML_REQUEST);
  174. if (StringUtils.isBlank(requestValue)) {
  175. throw new IllegalArgumentException("SAML request could not be determined from the authentication request");
  176. }
  177. val encodedRequest = EncodingUtils.decodeBase64(requestValue.getBytes(StandardCharsets.UTF_8));
  178. val authnRequest = (AuthnRequest)
  179. XMLObjectSupport.unmarshallFromInputStream(this.configBean.getParserPool(), new ByteArrayInputStream(encodedRequest));
  180. return authnRequest;
  181. }
  182. /**
  183. * Build cas assertion.
  184. *
  185. * @param authentication the authentication
  186. * @param service the service
  187. * @param registeredService the registered service
  188. * @param attributesToCombine the attributes to combine
  189. * @return the assertion
  190. */
  191. protected Assertion buildCasAssertion(final Authentication authentication,
  192. final Service service,
  193. final RegisteredService registeredService,
  194. final Map<String, Object> attributesToCombine) {
  195. val attributes = registeredService.getAttributeReleasePolicy().getAttributes(authentication.getPrincipal(), service, registeredService);
  196. val principal = new AttributePrincipalImpl(authentication.getPrincipal().getId(), attributes);
  197. val authnAttrs = new LinkedHashMap(authentication.getAttributes());
  198. authnAttrs.putAll(attributesToCombine);
  199. return new AssertionImpl(principal, DateTimeUtils.dateOf(authentication.getAuthenticationDate()),
  200. null, DateTimeUtils.dateOf(authentication.getAuthenticationDate()),
  201. authnAttrs);
  202. }
  203. /**
  204. * Build cas assertion.
  205. *
  206. * @param principal the principal
  207. * @param registeredService the registered service
  208. * @param attributes the attributes
  209. * @return the assertion
  210. */
  211. protected Assertion buildCasAssertion(final String principal,
  212. final RegisteredService registeredService,
  213. final Map<String, Object> attributes) {
  214. val p = new AttributePrincipalImpl(principal, attributes);
  215. return new AssertionImpl(p, DateTimeUtils.dateOf(ZonedDateTime.now()),
  216. null, DateTimeUtils.dateOf(ZonedDateTime.now()), attributes);
  217. }
  218. /**
  219. * Log cas validation assertion.
  220. *
  221. * @param assertion the assertion
  222. */
  223. protected void logCasValidationAssertion(final Assertion assertion) {
  224. LOGGER.debug("CAS Assertion Valid: [{}]", assertion.isValid());
  225. LOGGER.debug("CAS Assertion Principal: [{}]", assertion.getPrincipal().getName());
  226. LOGGER.debug("CAS Assertion authentication Date: [{}]", assertion.getAuthenticationDate());
  227. LOGGER.debug("CAS Assertion ValidFrom Date: [{}]", assertion.getValidFromDate());
  228. LOGGER.debug("CAS Assertion ValidUntil Date: [{}]", assertion.getValidUntilDate());
  229. LOGGER.debug("CAS Assertion Attributes: [{}]", assertion.getAttributes());
  230. LOGGER.debug("CAS Assertion Principal Attributes: [{}]", assertion.getPrincipal().getAttributes());
  231. }
  232. /**
  233. * Redirect request for authentication.
  234. *
  235. * @param pair the pair
  236. * @param request the request
  237. * @param response the response
  238. * @throws Exception the exception
  239. */
  240. protected void issueAuthenticationRequestRedirect(final Pair<? extends SignableSAMLObject, MessageContext> pair,
  241. final HttpServletRequest request,
  242. final HttpServletResponse response) throws Exception {
  243. val authnRequest = (AuthnRequest) pair.getLeft();
  244. val serviceUrl = constructServiceUrl(request, response, pair);
  245. LOGGER.debug("Created service url [{}]", DigestUtils.abbreviate(serviceUrl));
  246. val initialUrl = CommonUtils.constructRedirectUrl(casProperties.getServer().getLoginUrl(),
  247. CasProtocolConstants.PARAMETER_SERVICE, serviceUrl, authnRequest.isForceAuthn(),
  248. authnRequest.isPassive());
  249. val urlToRedirectTo = buildRedirectUrlByRequestedAuthnContext(initialUrl, authnRequest, request);
  250. LOGGER.debug("Redirecting SAML authN request to [{}]", urlToRedirectTo);
  251. val authenticationRedirectStrategy = new DefaultAuthenticationRedirectStrategy();
  252. authenticationRedirectStrategy.redirect(request, response, urlToRedirectTo);
  253. }
  254. /**
  255. * Gets authentication context mappings.
  256. *
  257. * @return the authentication context mappings
  258. */
  259. protected Map<String, String> getAuthenticationContextMappings() {
  260. val mappings = new TreeMap<String, String>();
  261. casProperties.getAuthn().getSamlIdp().getAuthenticationContextClassMappings()
  262. .stream()
  263. .map(s -> {
  264. val bits = Splitter.on("->").splitToList(s);
  265. return Pair.of(bits.get(0), bits.get(1));
  266. })
  267. .forEach(p -> mappings.put(p.getKey(), p.getValue()));
  268. return mappings;
  269. }
  270. /**
  271. * Build redirect url by requested authn context.
  272. *
  273. * @param initialUrl the initial url
  274. * @param authnRequest the authn request
  275. * @param request the request
  276. * @return the redirect url
  277. */
  278. protected String buildRedirectUrlByRequestedAuthnContext(final String initialUrl, final AuthnRequest authnRequest, final HttpServletRequest request) {
  279. val authenticationContextClassMappings = this.casProperties.getAuthn().getSamlIdp().getAuthenticationContextClassMappings();
  280. if (authnRequest.getRequestedAuthnContext() == null || authenticationContextClassMappings == null || authenticationContextClassMappings.isEmpty()) {
  281. return initialUrl;
  282. }
  283. val mappings = getAuthenticationContextMappings();
  284. val p =
  285. authnRequest.getRequestedAuthnContext().getAuthnContextClassRefs()
  286. .stream()
  287. .filter(ref -> {
  288. val clazz = ref.getAuthnContextClassRef();
  289. return mappings.containsKey(clazz);
  290. })
  291. .findFirst();
  292. if (p.isPresent()) {
  293. val mappedClazz = mappings.get(p.get().getAuthnContextClassRef());
  294. return initialUrl + '&' + casProperties.getAuthn().getMfa().getRequestParameter() + '=' + mappedClazz;
  295. }
  296. return initialUrl;
  297. }
  298. /**
  299. * Construct service url string.
  300. *
  301. * @param request the request
  302. * @param response the response
  303. * @param pair the pair
  304. * @return the string
  305. * @throws SamlException the saml exception
  306. */
  307. @SneakyThrows
  308. protected String constructServiceUrl(final HttpServletRequest request,
  309. final HttpServletResponse response,
  310. final Pair<? extends SignableSAMLObject, MessageContext> pair) throws SamlException {
  311. val authnRequest = (AuthnRequest) pair.getLeft();
  312. val messageContext = pair.getRight();
  313. try (val writer = SamlUtils.transformSamlObject(this.configBean, authnRequest)) {
  314. val builder = new URLBuilder(this.callbackService.getId());
  315. builder.getQueryParams().add(
  316. new net.shibboleth.utilities.java.support.collection.Pair<>(SamlProtocolConstants.PARAMETER_ENTITY_ID,
  317. SamlIdPUtils.getIssuerFromSamlObject(authnRequest)));
  318. val samlRequest = EncodingUtils.encodeBase64(writer.toString().getBytes(StandardCharsets.UTF_8));
  319. builder.getQueryParams().add(
  320. new net.shibboleth.utilities.java.support.collection.Pair<>(SamlProtocolConstants.PARAMETER_SAML_REQUEST,
  321. samlRequest));
  322. builder.getQueryParams().add(
  323. new net.shibboleth.utilities.java.support.collection.Pair<>(SamlProtocolConstants.PARAMETER_SAML_RELAY_STATE,
  324. SAMLBindingSupport.getRelayState(messageContext)));
  325. val url = builder.buildURL();
  326. LOGGER.trace("Built service callback url [{}]", url);
  327. return CommonUtils.constructServiceUrl(request, response,
  328. url, casProperties.getServer().getName(),
  329. CasProtocolConstants.PARAMETER_SERVICE,
  330. CasProtocolConstants.PARAMETER_TICKET, false);
  331. }
  332. }
  333. /**
  334. * Initiate authentication request.
  335. *
  336. * @param pair the pair
  337. * @param response the response
  338. * @param request the request
  339. * @throws Exception the exception
  340. */
  341. protected void initiateAuthenticationRequest(final Pair<? extends SignableSAMLObject, MessageContext> pair,
  342. final HttpServletResponse response,
  343. final HttpServletRequest request) throws Exception {
  344. verifySamlAuthenticationRequest(pair, request);
  345. issueAuthenticationRequestRedirect(pair, request, response);
  346. }
  347. /**
  348. * Verify saml authentication request.
  349. *
  350. * @param authenticationContext the pair
  351. * @param request the request
  352. * @return the pair
  353. * @throws Exception the exception
  354. */
  355. protected Pair<SamlRegisteredService, SamlRegisteredServiceServiceProviderMetadataFacade> verifySamlAuthenticationRequest(
  356. final Pair<? extends SignableSAMLObject, MessageContext> authenticationContext,
  357. final HttpServletRequest request) throws Exception {
  358. val authnRequest = (AuthnRequest) authenticationContext.getKey();
  359. val issuer = SamlIdPUtils.getIssuerFromSamlObject(authnRequest);
  360. LOGGER.debug("Located issuer [{}] from authentication request", issuer);
  361. val registeredService = verifySamlRegisteredService(issuer);
  362. LOGGER.debug("Fetching saml metadata adaptor for [{}]", issuer);
  363. val adaptor = SamlRegisteredServiceServiceProviderMetadataFacade.get(this.samlRegisteredServiceCachingMetadataResolver, registeredService, authnRequest);
  364. if (!adaptor.isPresent()) {
  365. LOGGER.warn("No metadata could be found for [{}]", issuer);
  366. throw new UnauthorizedServiceException(UnauthorizedServiceException.CODE_UNAUTHZ_SERVICE, "Cannot find metadata linked to " + issuer);
  367. }
  368. val facade = adaptor.get();
  369. verifyAuthenticationContextSignature(authenticationContext, request, authnRequest, facade);
  370. SamlUtils.logSamlObject(this.configBean, authnRequest);
  371. return Pair.of(registeredService, facade);
  372. }
  373. /**
  374. * Verify authentication context signature.
  375. *
  376. * @param authenticationContext the authentication context
  377. * @param request the request
  378. * @param authnRequest the authn request
  379. * @param adaptor the adaptor
  380. * @throws Exception the exception
  381. */
  382. protected void verifyAuthenticationContextSignature(final Pair<? extends SignableSAMLObject, MessageContext> authenticationContext,
  383. final HttpServletRequest request, final RequestAbstractType authnRequest,
  384. final SamlRegisteredServiceServiceProviderMetadataFacade adaptor) throws Exception {
  385. val ctx = authenticationContext.getValue();
  386. verifyAuthenticationContextSignature(ctx, request, authnRequest, adaptor);
  387. }
  388. /**
  389. * Verify authentication context signature.
  390. *
  391. * @param ctx the authentication context
  392. * @param request the request
  393. * @param authnRequest the authn request
  394. * @param adaptor the adaptor
  395. * @throws Exception the exception
  396. */
  397. protected void verifyAuthenticationContextSignature(final MessageContext ctx,
  398. final HttpServletRequest request,
  399. final RequestAbstractType authnRequest,
  400. final SamlRegisteredServiceServiceProviderMetadataFacade adaptor) throws Exception {
  401. if (!SAMLBindingSupport.isMessageSigned(ctx)) {
  402. LOGGER.debug("The authentication context is not signed");
  403. if (adaptor.isAuthnRequestsSigned()) {
  404. LOGGER.error("Metadata for [{}] says authentication requests are signed, yet authentication request is not", adaptor.getEntityId());
  405. throw new SAMLException("AuthN request is not signed but should be");
  406. }
  407. LOGGER.debug("Authentication request is not signed, so there is no need to verify its signature.");
  408. } else {
  409. LOGGER.debug("The authentication context is signed; Proceeding to validate signatures...");
  410. this.samlObjectSignatureValidator.verifySamlProfileRequestIfNeeded(authnRequest, adaptor, request, ctx);
  411. }
  412. }
  413. /**
  414. * Build saml response.
  415. *
  416. * @param response the response
  417. * @param request the request
  418. * @param authenticationContext the authentication context
  419. * @param casAssertion the cas assertion
  420. * @param binding the binding
  421. */
  422. protected void buildSamlResponse(final HttpServletResponse response,
  423. final HttpServletRequest request,
  424. final Pair<AuthnRequest, MessageContext> authenticationContext,
  425. final Assertion casAssertion,
  426. final String binding) {
  427. val authnRequest = authenticationContext.getKey();
  428. val pair = getRegisteredServiceAndFacade(authnRequest);
  429. val entityId = pair.getValue().getEntityId();
  430. LOGGER.debug("Preparing SAML response for [{}]", entityId);
  431. this.responseBuilder.build(authnRequest, request, response, casAssertion,
  432. pair.getKey(), pair.getValue(), binding, authenticationContext.getValue());
  433. LOGGER.info("Built the SAML response for [{}]", entityId);
  434. }
  435. /**
  436. * Gets registered service and facade.
  437. *
  438. * @param request the request
  439. * @return the registered service and facade
  440. */
  441. protected Pair<SamlRegisteredService, SamlRegisteredServiceServiceProviderMetadataFacade> getRegisteredServiceAndFacade(final AuthnRequest request) {
  442. val issuer = SamlIdPUtils.getIssuerFromSamlObject(request);
  443. LOGGER.debug("Located issuer [{}] from authentication context", issuer);
  444. val registeredService = verifySamlRegisteredService(issuer);
  445. LOGGER.debug("Located SAML metadata for [{}]", registeredService.getServiceId());
  446. val adaptor =
  447. getSamlMetadataFacadeFor(registeredService, request);
  448. if (!adaptor.isPresent()) {
  449. throw new UnauthorizedServiceException(UnauthorizedServiceException.CODE_UNAUTHZ_SERVICE,
  450. "Cannot find metadata linked to " + issuer);
  451. }
  452. val facade = adaptor.get();
  453. return Pair.of(registeredService, facade);
  454. }
  455. /**
  456. * Decode soap 11 context.
  457. *
  458. * @param request the request
  459. * @return the soap 11 context
  460. */
  461. protected MessageContext decodeSoapRequest(final HttpServletRequest request) {
  462. try {
  463. val decoder = new HTTPSOAP11Decoder();
  464. decoder.setParserPool(this.configBean.getParserPool());
  465. decoder.setHttpServletRequest(request);
  466. val binding = new BindingDescriptor();
  467. binding.setId(getClass().getName());
  468. binding.setShortName(getClass().getName());
  469. binding.setSignatureCapable(true);
  470. binding.setSynchronous(true);
  471. decoder.setBindingDescriptor(binding);
  472. decoder.initialize();
  473. decoder.decode();
  474. return decoder.getMessageContext();
  475. } catch (final Exception e) {
  476. LOGGER.error(e.getMessage(), e);
  477. }
  478. return null;
  479. }
  480. /**
  481. * Handle unauthorized service exception.
  482. *
  483. * @param req the req
  484. * @param ex the ex
  485. * @return the model and view
  486. */
  487. @ExceptionHandler(UnauthorizedServiceException.class)
  488. public ModelAndView handleUnauthorizedServiceException(final HttpServletRequest req, final Exception ex) {
  489. return WebUtils.produceUnauthorizedErrorView();
  490. }
  491. }