/* * Copyright 2012 SURFnet bv, The Netherlands * * 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 teams.interceptor; import java.net.URLEncoder; import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Optional; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.StringUtils; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import teams.domain.Member; import teams.domain.MemberAttribute; import teams.domain.Person; import teams.migration.MigrationService; import teams.provision.UserDetailsManager; import teams.repository.PersonRepository; import teams.service.MemberAttributeService; import teams.util.AuditLog; /** * Intercepts calls to controllers to handle Single Sign On details from * Shibboleth and sets a Person object on the session when the user is logged in. */ public class LoginInterceptor extends HandlerInterceptorAdapter { private static final Logger logger = LoggerFactory.getLogger(LoginInterceptor.class); public static final String PERSON_SESSION_KEY = "person"; public static final String EXTERNAL_GROUPS_SESSION_KEY = "externalGroupsSessionKey"; public static final String USER_STATUS_SESSION_KEY = "userStatus"; private static final List<String> LOGIN_BYPASS = Arrays.asList("landingpage.shtml", "js", "css", "media", "teams.xml", "declineInvitation.shtml", "migrate"); private static final List<String> LANDING_BYPASS = Arrays.asList("acceptInvitation.shtml", "migrate"); public static final String STATUS_GUEST = "guest"; public static final String STATUS_MEMBER = "member"; public static final String TEAMS_COOKIE = "SURFconextTeams"; public static final String NOT_PROVIDED_SAML_ATTRIBUTES_SHTML = "/NotProvidedSamlAttributes.shtml"; public static final String API_VOOT_URL = "api/voot"; private final String teamsUrl; private final PersonRepository personRepository; public LoginInterceptor(String teamsURL, PersonRepository personRepository) { this.teamsUrl = teamsURL; this.personRepository = personRepository; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (request.getRequestURI().endsWith(NOT_PROVIDED_SAML_ATTRIBUTES_SHTML)) { return super.preHandle(request, response, handler); } HttpSession session = request.getSession(); String nameId = request.getHeader("name-id"); // Check session first: Person person = (Person) session.getAttribute(PERSON_SESSION_KEY); if (person == null || !person.getId().equals(nameId)) { if (StringUtils.hasText(nameId)) { Optional<Person> optionalPersonFromHeaders = constructPerson(request); if (optionalPersonFromHeaders.isPresent()) { person = optionalPersonFromHeaders.get(); Optional<teams.migration.Person> optionalPersonFromDatabase = personRepository.findByUrn(person.getId()); if (optionalPersonFromDatabase.isPresent()) { teams.migration.Person personFromDatabase = optionalPersonFromDatabase.get(); if (person.isGuest() != personFromDatabase.isGuest() || !person.getName().equals(personFromDatabase.getName()) || !person.getEmail().equals(personFromDatabase.getEmail())) { personFromDatabase.setGuest(person.isGuest()); personFromDatabase.setEmail(person.getEmail()); personFromDatabase.setName(person.getName()); personRepository.save(personFromDatabase); } } else { teams.migration.Person newPerson = new teams.migration.Person(person.getId(), person.getName(), person.getEmail(), person.isGuest(), Instant.now()); personRepository.save(newPerson); } } else { response.sendRedirect(teamsUrl + NOT_PROVIDED_SAML_ATTRIBUTES_SHTML); return false; } // Add person to session: session.setAttribute(PERSON_SESSION_KEY, person); AuditLog.log("Login by user {}", person.getId()); String userStatus = person.isGuest() ? STATUS_GUEST : STATUS_MEMBER; session.setAttribute(USER_STATUS_SESSION_KEY, userStatus); } else { // User is not logged in, and name-id header is empty. // Check whether the user is requesting the landing page, if not // redirect him to the landing page. String url = request.getRequestURI(); String[] urlSplit = url.split("/"); String urlPart = urlSplit.length < 1 ? "/" : urlSplit[1]; logger.debug("Request for '{}'", request.getRequestURI()); logger.debug("urlPart: '{}'", urlPart); if (LOGIN_BYPASS.contains(urlPart) || isApiCall(request.getRequestURI())) { logger.debug("Bypassing {}", urlPart); return super.preHandle(request, response, handler); } else if (getTeamsCookie(request).contains("skipLanding") || LANDING_BYPASS.contains(urlPart)) { String queryString = request.getQueryString() != null ? "?" + request.getQueryString() : ""; String target = URLEncoder.encode(request.getRequestURL().toString().replace("http://","https://") + queryString, "UTF-8"); response.sendRedirect(teamsUrl + "/Shibboleth.sso/Login?target=" + target); return false; } else { logger.debug("Redirect to landingpage"); response.sendRedirect(teamsUrl + "/landingpage.shtml"); return false; } } } return super.preHandle(request, response, handler); } protected boolean isApiCall(String requestURI) { return requestURI.contains(API_VOOT_URL); } private Optional<Person> constructPerson(HttpServletRequest request) { List<String> notProvidedSamlAttributes = new ArrayList<>(); String id = request.getHeader("name-id"); addNotProvidedSamlAttributes(id, notProvidedSamlAttributes, "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" ); String email = request.getHeader("Shib-InetOrgPerson-mail"); addNotProvidedSamlAttributes(email, notProvidedSamlAttributes, "urn:mace:dir:attribute-def:mail" ); String schacHomeOrganization = request.getHeader("schacHomeOrganization"); addNotProvidedSamlAttributes(schacHomeOrganization, notProvidedSamlAttributes, "urn:mace:terena.org:attribute-def:schacHomeOrganization" ); String displayName = request.getHeader("displayName"); addNotProvidedSamlAttributes(displayName, notProvidedSamlAttributes, "urn:mace:dir:attribute-def:displayName"); String status = request.getHeader("is-member-of"); if (!notProvidedSamlAttributes.isEmpty()) { request.getSession(true).setAttribute("notProvidedSamlAttributes", notProvidedSamlAttributes); } return notProvidedSamlAttributes.isEmpty() ? Optional.of(new Person(id, displayName, email, schacHomeOrganization, status, displayName)) : Optional.empty(); } private void addNotProvidedSamlAttributes(String requiredSamlAttribute, List<String> notProvidedSamlAttributes, String attributeName) { if (!StringUtils.hasText(requiredSamlAttribute)) { notProvidedSamlAttributes.add(attributeName); } } private String getTeamsCookie(HttpServletRequest request) { return Optional.ofNullable(request.getCookies()).flatMap(cookies -> Arrays.stream(cookies) .filter(c -> c.getName().equals(TEAMS_COOKIE)) .map(Cookie::getValue) .findFirst() ).orElse(""); } }