Java tutorial
/* * Copyright (c) 2012-2015 VMware, Inc. All Rights Reserved. * * Licensed 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 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 com.vmware.identity; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang.StringEscapeUtils; import org.apache.commons.lang.Validate; import org.opensaml.xml.util.Base64; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.MessageSource; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import com.vmware.identity.diagnostics.DiagnosticsLoggerFactory; import com.vmware.identity.diagnostics.IDiagnosticsLogger; import com.vmware.identity.idm.RSAAgentConfig; import com.vmware.identity.idm.RelyingParty; import com.vmware.identity.samlservice.AuthenticationFilter; import com.vmware.identity.samlservice.AuthnRequestState; import com.vmware.identity.samlservice.AuthnTypesSupported; import com.vmware.identity.samlservice.DefaultIdmAccessorFactory; import com.vmware.identity.samlservice.IdmAccessor; import com.vmware.identity.samlservice.SamlValidator.ValidationResult; import com.vmware.identity.samlservice.Shared; import com.vmware.identity.session.SessionManager; /** * Handles requests for the application SSO page. */ @Controller public class SsoController extends BaseSsoController { @Autowired private MessageSource messageSource; @Autowired private AuthenticationFilter<AuthnRequestState> kerbAuthenticator; @Autowired private AuthenticationFilter<AuthnRequestState> passwordAuthenticator; @Autowired private AuthenticationFilter<AuthnRequestState> rsaamAuthenticator; @Autowired private AuthenticationFilter<AuthnRequestState> tlsClientAuthenticator; @Autowired private AuthenticationFilter<AuthnRequestState> cookieAuthenticator; @Autowired private SessionManager sessionManager; private static final IDiagnosticsLogger logger = DiagnosticsLoggerFactory.getLogger(SsoController.class); public SsoController() { } /** * Handle SAML AuthnRequest */ @RequestMapping(value = "/SAML2/SSO/{tenant:.*}", method = { RequestMethod.GET, RequestMethod.POST }) public String sso(Locale locale, @PathVariable(value = "tenant") String tenant, Model model, HttpServletRequest request, HttpServletResponse response) throws IOException { logger.info("Welcome to SP-initiated AuthnRequest handler! " + "The client locale is " + locale.toString() + ", tenant is " + tenant); logger.info("Request URL is " + request.getRequestURL().toString()); try { AuthenticationFilter<AuthnRequestState> authenticator = chooseAuthenticator(request); AuthnRequestState requestState = new AuthnRequestState(request, response, sessionManager, tenant); processSsoRequest(locale, tenant, request, response, authenticator, requestState, messageSource, sessionManager); model.addAttribute("tenant", tenant); model.addAttribute("protocol", "websso"); if (requestState.isChooseIDPViewRequired() != null && requestState.isChooseIDPViewRequired()) { setupChooseIDPModel(model, locale, tenant, requestState); return "chooseidp"; } else if (requestState.isLoginViewRequired() != null && requestState.isLoginViewRequired()) { setupAuthenticationModel(model, locale, tenant, request, requestState); return "unpentry"; } } catch (Exception e) { logger.error("Could not handle SAML Authentication request ", e); sendError(locale, response, e.getMessage()); } return null; } /** * Reverse proxy is expected to challenge for the client certificate with the request. */ @RequestMapping(value = "/SAML2/SSOCAC/{tenant:.*}", method = { RequestMethod.GET, RequestMethod.POST }) public String cacSso(Locale locale, @PathVariable(value = "tenant") String tenant, Model model, HttpServletRequest request, HttpServletResponse response) throws IOException { return this.sso(locale, tenant, model, request, response); } /** * Client sending request to this endpoint will be challenged to submit certificate by SSL protocal. * Thus a reverse proxy is not expected in between browser and this server. */ @RequestMapping(value = "/SAML2/SmartcardRealm/{tenant:.*}", method = { RequestMethod.GET, RequestMethod.POST }) public String smartcardRealmSso(Locale locale, @PathVariable(value = "tenant") String tenant, Model model, HttpServletRequest request, HttpServletResponse response) throws IOException { return this.sso(locale, tenant, model, request, response); } /** * Choose auth method to use based on castle auth header contents * @param request * @return */ private AuthenticationFilter<AuthnRequestState> chooseAuthenticator(HttpServletRequest request) { // default - try cookie AuthenticationFilter<AuthnRequestState> retval = this.getCookieAuthenticator(); if (request != null) { String castleAuthType = request.getParameter(Shared.REQUEST_AUTH_PARAM); if (castleAuthType != null) { if (castleAuthType.startsWith(Shared.KERB_AUTH_PREFIX)) { retval = this.getKerbAuthenticator(); logger.debug("Kerb authenticator chosen"); } else if (castleAuthType.startsWith(Shared.PASSWORD_AUTH_PREFIX)) { retval = this.getPasswordAuthenticator(); logger.debug("Password authenticator chosen"); } else if (castleAuthType.startsWith(Shared.RSAAM_AUTH_PREFIX)) { retval = this.getRsaAmAuthenticator(); logger.debug("Rsa am authenticator chosen"); } else if (castleAuthType.startsWith(Shared.TLSCLIENT_AUTH_PREFIX)) { retval = this.getTlsClientAuthenticator(); logger.debug("TLSClient authenticator chosen"); } } } return retval; } /** * Handle SSL for NGC. NGC is to host login page from iFrame which would fails silently if the server site is not trusted * by browser. This interactive exchange allow user to trust the ssl at the first time browsing this site. */ @RequestMapping(value = "/SAML2/SSOSSL/{tenant:.*}", method = { RequestMethod.GET, RequestMethod.POST }) public void ssoSSLDummy(Locale locale, Model model, @PathVariable(value = "tenant") String tenant, HttpServletRequest request, HttpServletResponse response) throws IOException { logger.info("Welcome to ssoSSLDummy handler! " + tenant); try { RelyingParty rp = validateRelyingParty(request, tenant); if (rp != null) { logger.info("Send ssl probing reponse for: " + rp.getUrl()); URL rpUrl = new URL(rp.getUrl()); //For ciswin environments, redirect without trailing slash will be on http // and this causes login issues. // TODO: if both NGC and WebSSO are on same machine, it will be nice to communicate // directly without going through reverse proxy. // see PR 1311527, 1281321 URL redirectUrl = new URL(rpUrl.getProtocol(), rpUrl.getHost(), rpUrl.getPort(), "/vsphere-client/"); String redirectUrlStr = redirectUrl.toString() + "?" + ssoSSLDummyQueryString(request); response.sendRedirect(redirectUrlStr); } else { logger.error("Not able to respond to the request posted to /SAML2/SSOSSL/" + tenant + ": No replying party was found to match the given query parameter!"); sendError(locale, response, "Not able to respond to the request posted to /SAML2/SSOSSL/"); } } catch (Exception e) { logger.error("Caught exception in ssoSSLDummy", e); sendError(locale, response, e.getLocalizedMessage()); } } /** * Handle SSL for default tenant. NGC is to host login page from iFrame which would fails silently if the server site is not trusted * by browser. This interactive exchange allow user to trust the ssl at the first time browsing this site. */ @RequestMapping(value = "/SAML2/SSOSSL", method = { RequestMethod.GET, RequestMethod.POST }) public void ssoSSLDummyDefault(Locale locale, Model model, HttpServletRequest request, HttpServletResponse response) throws IOException { ssoSSLDummy(locale, model, Shared.getDefaultTenant(), request, response); } /** * Handle SAML AuthnRequest for default tenant */ @RequestMapping(value = "/SAML2/SSO", method = { RequestMethod.GET, RequestMethod.POST }) public void ssoDefaultTenant(Locale locale, Model model, HttpServletRequest request, HttpServletResponse response) throws IOException { logger.info("Welcome to SP-initiated AuthnRequest handler! " + "The client locale is " + locale.toString() + ", DEFAULT tenant"); sso(locale, Shared.getDefaultTenant(), model, request, response); } /** * Reverse proxy is expected to challenge for the client certificate with the request of default tenant. */ @RequestMapping(value = "/SAML2/SSOCAC", method = { RequestMethod.GET, RequestMethod.POST }) public void cacSsoDefaultTenant(Locale locale, Model model, HttpServletRequest request, HttpServletResponse response) throws IOException { ssoDefaultTenant(locale, model, request, response); } /** * Client sending request of default tenant to this endpoint will be challenged to submit certificate by SSL protocal. * Thus a reverse proxy is not expected in between browser and this server. */ @RequestMapping(value = "/SAML2/SmartcardRealm", method = { RequestMethod.GET, RequestMethod.POST }) public void smartcardRealmSsoDefaultTenant(Locale locale, Model model, HttpServletRequest request, HttpServletResponse response) throws IOException { ssoDefaultTenant(locale, model, request, response); } /** * Handle request sent with a wrong binding */ @RequestMapping(value = "/SAML2/SSO/{tenant:.*}") public void ssoBindingError(Locale locale, @PathVariable(value = "tenant") String tenant, HttpServletResponse response) throws IOException { logger.info("SSO binding error! The client locale is " + locale.toString() + ", tenant is " + tenant); ssoDefaultTenantBindingError(locale, response); } /** * Handle default tenant request sent with a wrong binding */ @RequestMapping(value = "/SAML2/SSO") public void ssoDefaultTenantBindingError(Locale locale, HttpServletResponse response) throws IOException { logger.info("SSO binding error! The client locale is " + locale.toString() + ", DEFAULT tenant"); sendError(locale, response, "Binding"); } /** * Handle SAML AuthnRequest, UNP entry form */ @RequestMapping(value = "/SAML2/SSO/{tenant:.*}", method = RequestMethod.GET, params = Shared.PASSWORD_ENTRY) public String ssoPasswordEntry(Locale locale, @PathVariable(value = "tenant") String tenant, Model model, HttpServletRequest request, HttpServletResponse response) throws IOException { logger.info("Welcome to SP-initiated AuthnRequest handler, PASSWORD entry form! " + "The client locale is " + locale.toString() + ", tenant is " + tenant); try { // fix for PR 964366, check the cookie first if (Shared.hasSessionCookie(request, this.getSessionManager(), tenant)) { sso(locale, tenant, model, request, response); return null; } model.addAttribute("tenant", tenant); model.addAttribute("protocol", "websso"); setupAuthenticationModel(model, locale, tenant, request, null); } catch (Exception e) { logger.error("Found exception while populating model object ", e); sendError(locale, response, e.getLocalizedMessage()); return null; } return "unpentry"; } private void setupChooseIDPModel(Model model, Locale locale, String tenant, AuthnRequestState requestState) { model.addAttribute("tenant", tenant); model.addAttribute("protocol", "websso"); model.addAttribute("tenant_brandname", StringEscapeUtils.escapeJavaScript(getBrandName(tenant))); List<String> entityIdList = requestState.getIDPSelectionEntityIdList(); model.addAttribute("idp_entity_id_list", entityIdList); model.addAttribute("idp_display_name_list", requestState.getIDPSelectionDisplayNameList(entityIdList)); model.addAttribute("title", messageSource.getMessage("ChooseIDP.Title", null, locale)); } private void setupAuthenticationModel(Model model, Locale locale, String tenant, HttpServletRequest request, AuthnRequestState requestState) { model.addAttribute("spn", StringEscapeUtils.escapeJavaScript(getServerSPN())); model.addAttribute("tenant_brandname", StringEscapeUtils.escapeJavaScript(getBrandName(tenant))); model.addAttribute("username", messageSource.getMessage("LoginForm.UserName", null, locale)); model.addAttribute("username_placeholder", messageSource.getMessage("LoginForm.UserName.Placeholder", null, locale)); model.addAttribute("password", messageSource.getMessage("LoginForm.Password", null, locale)); model.addAttribute("passcode", messageSource.getMessage("LoginForm.Passcode", null, locale)); model.addAttribute("submit", messageSource.getMessage("LoginForm.Submit", null, locale)); model.addAttribute("error", StringEscapeUtils.escapeJavaScript(messageSource.getMessage("LoginForm.Error", null, locale))); model.addAttribute("login", messageSource.getMessage("LoginForm.Login", null, locale)); model.addAttribute("help", messageSource.getMessage("LoginForm.Help", null, locale)); model.addAttribute("winSession", messageSource.getMessage("LoginForm.WinSession", null, locale)); model.addAttribute("downloadCIP", messageSource.getMessage("LoginForm.DownloadCIP", null, locale)); model.addAttribute("unsupportedBrowserWarning", messageSource.getMessage("LoginForm.UnsupportedBrowserWarning", null, locale)); model.addAttribute("searchstring", Shared.PASSWORD_ENTRY); model.addAttribute("replacestring", Shared.PASSWORD_SUPPLIED); model.addAttribute("iAgreeTo", messageSource.getMessage("LoginForm.IAgreeTo", null, locale)); model.addAttribute("logonBannerAlertMessage", messageSource.getMessage("LoginForm.LogonBannerAlertMessage", null, locale)); setLogonBannerModelAttributes(tenant, model); AuthnTypesSupported supportedAuthnTypes; if (requestState != null) { supportedAuthnTypes = requestState.getAuthTypesSupportecd(); } else { supportedAuthnTypes = new AuthnTypesSupported(true, true, false, false); } model.addAttribute("enable_password_auth", supportedAuthnTypes.supportsPasswordProtectTransport()); model.addAttribute("enable_windows_auth", supportedAuthnTypes.supportsWindowsSession()); model.addAttribute("enable_tlsclient_auth", supportedAuthnTypes.supportsTlsClientCert()); model.addAttribute("enable_rsaam_auth", supportedAuthnTypes.supportsRsaSecureID()); model.addAttribute("smartcard", messageSource.getMessage("LoginForm.Smartcard", null, locale)); model.addAttribute("rsaam", messageSource.getMessage("LoginForm.RsaSecurID", null, locale)); String cacEndpoint = request.getAuthType() == SecurityRequestWrapper.VMWARE_CLIENT_CERT_AUTH ? Shared.ssoCACEndpoint : Shared.ssoSmartcardRealmEndpoint; model.addAttribute("cac_endpoint", cacEndpoint); model.addAttribute("sso_endpoint", Shared.ssoEndpoint); if (supportedAuthnTypes.supportsRsaSecureID()) { model.addAttribute("rsaam_reminder", StringEscapeUtils.escapeJavaScript(getRsaSecurIDLoginGuide(tenant))); } } /** * Handle SAML AuthnRequest for default tenant, UNP entry form */ @RequestMapping(value = "/SAML2/SSO", method = RequestMethod.GET, params = Shared.PASSWORD_ENTRY) public String ssoDefaultTenantPasswordEntry(Locale locale, Model model, HttpServletRequest request, HttpServletResponse response) throws IOException { logger.info("Welcome to SP-initiated AuthnRequest handler, PASSWORD entry form! " + "The client locale is " + locale.toString() + ", DEFAULT tenant"); return ssoPasswordEntry(locale, Shared.getDefaultTenant(), model, request, response); } /** * Handle SSL for landing page. */ @RequestMapping(value = "/", method = RequestMethod.GET) public String webssoRootPageEntry(Locale locale, Model model, HttpServletRequest request, HttpServletResponse response) throws IOException { logger.info("Welcome to webssoRootPage handler! "); return "index"; } public MessageSource getMessageSource() { return messageSource; } public void setMessageSource(MessageSource ms) { messageSource = ms; } public AuthenticationFilter<AuthnRequestState> getKerbAuthenticator() { return kerbAuthenticator; } public void setKerbAuthenticator(AuthenticationFilter<AuthnRequestState> kerbAuthenticator) { this.kerbAuthenticator = kerbAuthenticator; } /** * @return the passwordAuthenticator */ public AuthenticationFilter<AuthnRequestState> getPasswordAuthenticator() { return passwordAuthenticator; } /** * @param passwordAuthenticator * the passwordAuthenticator to set */ public void setPasswordAuthenticator(AuthenticationFilter<AuthnRequestState> passwordAuthenticator) { this.passwordAuthenticator = passwordAuthenticator; } /** * @return the rsaamAuthenticator */ public AuthenticationFilter<AuthnRequestState> getRsaAmAuthenticator() { return rsaamAuthenticator; } /** * @param rsaamAuthenticator * the rsaamAuthenticator to set */ public void setRsaAmAuthenticator(AuthenticationFilter<AuthnRequestState> rsaamAuthenticator) { this.rsaamAuthenticator = rsaamAuthenticator; } /** * @return the sessionManager */ public SessionManager getSessionManager() { return sessionManager; } /** * @param sessionManager * the sessionManager to set */ public void setSessionManager(SessionManager sessionManager) { this.sessionManager = sessionManager; } /** * @return the cookieAuthenticator */ public AuthenticationFilter<AuthnRequestState> getCookieAuthenticator() { return cookieAuthenticator; } /** * @param cookieAuthenticator the cookieAuthenticator to set */ public void setCookieAuthenticator(AuthenticationFilter<AuthnRequestState> cookieAuthenticator) { this.cookieAuthenticator = cookieAuthenticator; } /** * Allows view jsp to query brandname of the tenant * * @param tenantName * the cookieAuthenticator to set * @return tenant's brandname as ModelAttribute */ // @ModelAttribute("tenant_brandname") public String getBrandName(String tenantName) { //initiate idmAccessor if (tenantName == null || tenantName.isEmpty()) { return null; } DefaultIdmAccessorFactory idmFactory = new DefaultIdmAccessorFactory(); Validate.notNull(idmFactory, "idmFactory"); IdmAccessor idmAccessor = idmFactory.getIdmAccessor(); idmAccessor.setTenant(tenantName); String brand = idmAccessor.getBrandName(); logger.info("Accessing Tenant " + tenantName + ", brand name string " + brand); return brand; } /** * Allows view jsp to query logon banner attributes of the tenant * * @param tenant * the cookieAuthenticator to set */ // @ModelAttribute("tenant_logonbanner_title") // @ModelAttribute("tenant_logonbanner_content") // @ModelAttribute("enable_logonbanner_checkbox") private void setLogonBannerModelAttributes(String tenant, Model model) { if (tenant == null || tenant.isEmpty()) { return; } DefaultIdmAccessorFactory idmFactory = new DefaultIdmAccessorFactory(); Validate.notNull(idmFactory, "idmFactory"); IdmAccessor idmAccessor = idmFactory.getIdmAccessor(); idmAccessor.setTenant(tenant); String logonBannerTitle = idmAccessor.getLogonBannerTitle(); String logonBannerContent = idmAccessor.getLogonBannerContent(); // escape html and javascript logonBannerTitle = StringEscapeUtils.escapeJavaScript(StringEscapeUtils.escapeHtml(logonBannerTitle)); logonBannerContent = StringEscapeUtils.escapeJavaScript(StringEscapeUtils.escapeHtml(logonBannerContent)); logger.trace("Accessing Tenant " + tenant + ", logon banner title " + logonBannerTitle); logger.trace("Accessing Tenant " + tenant + ", logon banner content" + System.lineSeparator() + logonBannerContent); model.addAttribute("tenant_logonbanner_title", logonBannerTitle); model.addAttribute("tenant_logonbanner_content", logonBannerContent); model.addAttribute("enable_logonbanner_checkbox", idmAccessor.getLogonBannerCheckboxFlag()); } /** * return tenant setting of rsa login guide. * @param tenantName * @return guide string. if not defined. */ private String getRsaSecurIDLoginGuide(String tenantName) { if (tenantName == null || tenantName.isEmpty()) { return null; } DefaultIdmAccessorFactory idmFactory = new DefaultIdmAccessorFactory(); Validate.notNull(idmFactory, "idmFactory"); IdmAccessor idmAccessor = idmFactory.getIdmAccessor(); idmAccessor.setTenant(tenantName); RSAAgentConfig config = idmAccessor.getAuthnPolicy(tenantName).get_rsaAgentConfig(); String rsaLoginGuide = null; if (null != config) { rsaLoginGuide = config.get_loginGuide(); } return rsaLoginGuide; } private String getServerSPN() { DefaultIdmAccessorFactory idmFactory = new DefaultIdmAccessorFactory(); IdmAccessor idmAccessor = idmFactory.getIdmAccessor(); String spn = idmAccessor.getServerSPN(); logger.info("Server SPN is " + spn); return spn; } private void sendError(Locale locale, HttpServletResponse response, String subStatus) throws IOException { // use validation result code to return error to client ValidationResult vr = new ValidationResult(HttpServletResponse.SC_BAD_REQUEST, "BadRequest", subStatus); String message = vr.getMessage(messageSource, locale); response.sendError(vr.getResponseCode(), Shared.encodeString(message)); logger.info("Responded with ERROR " + vr.getResponseCode() + ", message " + message); } /** * This method validates the relying party. * @param request * @param tenantName * @return */ private RelyingParty validateRelyingParty(HttpServletRequest request, String tenantName) { Validate.notNull(request, "request"); Validate.notEmpty(tenantName, "tenantName"); DefaultIdmAccessorFactory idmFactory = new DefaultIdmAccessorFactory(); Validate.notNull(idmFactory, "idmFactory"); IdmAccessor idmAccessor = idmFactory.getIdmAccessor(); idmAccessor.setTenant(tenantName); String encodedEntityId = request.getParameter(Shared.RELYINGPARTY_ENTITYID); if (encodedEntityId == null || encodedEntityId.isEmpty()) { logger.error("No Relying Party's entity ID found. Ignore the request!"); return null; } String rpEntityId = new String(Base64.decode(encodedEntityId)); RelyingParty rp = idmAccessor.getRelyingPartyByUrl(rpEntityId); if (rp != null) { return rp; } else { logger.error("Unknown relying party: " + rpEntityId); return null; } } /** * reconstruct original query string minus RelyingPartyEntityId plus csp * @param request * @return * @throws MalformedURLException */ static String ssoSSLDummyQueryString(HttpServletRequest request) throws MalformedURLException { StringBuilder result = new StringBuilder(); Map<String, String[]> parameterMap = request.getParameterMap(); boolean appendAmpersand = false; for (Entry<String, String[]> entry : parameterMap.entrySet()) { String key = entry.getKey(); if (Shared.RELYINGPARTY_ENTITYID.equals(key)) { continue; } String[] values = entry.getValue(); for (String value : values) { if (appendAmpersand) { result.append("&"); } if (value == null || value.length() == 0) { result.append(String.format("%s", key)); } else { result.append(String.format("%s=%s", key, value)); } appendAmpersand = true; } } if (appendAmpersand) { result.append("&"); } result.append("csp"); return result.toString(); } public AuthenticationFilter<AuthnRequestState> getTlsClientAuthenticator() { return tlsClientAuthenticator; } public void setTlsClientAuthenticator(AuthenticationFilter<AuthnRequestState> tlsClientAuthenticator) { this.tlsClientAuthenticator = tlsClientAuthenticator; } }