/*
* This file is part of ARSnova Backend.
* Copyright (C) 2012-2017 The ARSnova Team
*
* ARSnova Backend is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ARSnova Backend is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package de.thm.arsnova.controller;
import de.thm.arsnova.entities.ServiceDescription;
import de.thm.arsnova.entities.Session;
import de.thm.arsnova.entities.User;
import de.thm.arsnova.exceptions.UnauthorizedException;
import de.thm.arsnova.services.IUserService;
import de.thm.arsnova.services.UserSessionService;
import org.scribe.up.provider.impl.FacebookProvider;
import org.scribe.up.provider.impl.Google2Provider;
import org.scribe.up.provider.impl.TwitterProvider;
import org.scribe.up.session.HttpUserSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.cas.authentication.CasAuthenticationToken;
import org.springframework.security.cas.web.CasAuthenticationEntryPoint;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.token.Sha512DigestUtils;
import org.springframework.security.ldap.authentication.LdapAuthenticationProvider;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.view.RedirectView;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* Handles authentication specific requests.
*/
@Controller
public class LoginController extends AbstractController {
private static final int MAX_USERNAME_LENGTH = 15;
private static final int MAX_GUESTHASH_LENGTH = 10;
@Value("${api.path:}") private String apiPath;
@Value("${customization.path}") private String customizationPath;
@Value("${security.guest.enabled}") private boolean guestEnabled;
@Value("${security.guest.allowed-roles:speaker,student}") private String[] guestRoles;
@Value("${security.guest.order}") private int guestOrder;
@Value("${security.custom-login.enabled}") private boolean customLoginEnabled;
@Value("${security.custom-login.allowed-roles:speaker,student}") private String[] customLoginRoles;
@Value("${security.custom-login.title:University}") private String customLoginTitle;
@Value("${security.custom-login.login-dialog-path}") private String customLoginDialog;
@Value("${security.custom-login.image:}") private String customLoginImage;
@Value("${security.custom-login.order}") private int customLoginOrder;
@Value("${security.user-db.enabled}") private boolean dbAuthEnabled;
@Value("${security.user-db.allowed-roles:speaker,student}") private String[] dbAuthRoles;
@Value("${security.user-db.title:ARSnova}") private String dbAuthTitle;
@Value("${security.user-db.login-dialog-path}") private String dbAuthDialog;
@Value("${security.user-db.image:}") private String dbAuthImage;
@Value("${security.user-db.order}") private int dbAuthOrder;
@Value("${security.ldap.enabled}") private boolean ldapEnabled;
@Value("${security.ldap.allowed-roles:speaker,student}") private String[] ldapRoles;
@Value("${security.ldap.title:LDAP}") private String ldapTitle;
@Value("${security.ldap.login-dialog-path}") private String ldapDialog;
@Value("${security.ldap.image:}") private String ldapImage;
@Value("${security.ldap.order}") private int ldapOrder;
@Value("${security.cas.enabled}") private boolean casEnabled;
@Value("${security.cas.allowed-roles:speaker,student}") private String[] casRoles;
@Value("${security.cas.title:CAS}") private String casTitle;
@Value("${security.cas.image:}") private String casImage;
@Value("${security.cas.order}") private int casOrder;
@Value("${security.facebook.enabled}") private boolean facebookEnabled;
@Value("${security.facebook.allowed-roles:speaker,student}") private String[] facebookRoles;
@Value("${security.facebook.order}") private int facebookOrder;
@Value("${security.google.enabled}") private boolean googleEnabled;
@Value("${security.google.allowed-roles:speaker,student}") private String[] googleRoles;
@Value("${security.google.order}") private int googleOrder;
@Value("${security.twitter.enabled}") private boolean twitterEnabled;
@Value("${security.twitter.allowed-roles:speaker,student}") private String[] twitterRoles;
@Value("${security.twitter.order}") private int twitterOrder;
@Autowired(required = false)
private DaoAuthenticationProvider daoProvider;
@Autowired(required = false)
private TwitterProvider twitterProvider;
@Autowired(required = false)
private Google2Provider googleProvider;
@Autowired(required = false)
private FacebookProvider facebookProvider;
@Autowired(required = false)
private LdapAuthenticationProvider ldapAuthenticationProvider;
@Autowired(required = false)
private CasAuthenticationEntryPoint casEntryPoint;
@Autowired
private IUserService userService;
@Autowired
private UserSessionService userSessionService;
private static final Logger logger = LoggerFactory.getLogger(LoginController.class);
@RequestMapping(value = { "/auth/login", "/doLogin" }, method = { RequestMethod.POST, RequestMethod.GET })
public void doLogin(
@RequestParam("type") final String type,
@RequestParam(value = "user", required = false) String username,
@RequestParam(required = false) final String password,
@RequestParam(value = "role", required = false) final UserSessionService.Role role,
final HttpServletRequest request,
final HttpServletResponse response
) throws IOException {
String addr = request.getRemoteAddr();
if (userService.isBannedFromLogin(addr)) {
response.sendError(429, "Too Many Requests");
return;
}
userSessionService.setRole(role);
if (dbAuthEnabled && "arsnova".equals(type)) {
Authentication authRequest = new UsernamePasswordAuthenticationToken(username, password);
try {
Authentication auth = daoProvider.authenticate(authRequest);
if (auth.isAuthenticated()) {
SecurityContextHolder.getContext().setAuthentication(auth);
request.getSession(true).setAttribute(
HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY,
SecurityContextHolder.getContext());
return;
}
} catch (AuthenticationException e) {
logger.info("Database authentication failed.", e);
}
userService.increaseFailedLoginCount(addr);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
} else if (ldapEnabled && "ldap".equals(type)) {
if (!"".equals(username) && !"".equals(password)) {
org.springframework.security.core.userdetails.User user =
new org.springframework.security.core.userdetails.User(
username, password, true, true, true, true, this.getAuthorities()
);
Authentication token = new UsernamePasswordAuthenticationToken(user, password, getAuthorities());
try {
Authentication auth = ldapAuthenticationProvider.authenticate(token);
if (auth.isAuthenticated()) {
SecurityContextHolder.getContext().setAuthentication(auth);
request.getSession(true).setAttribute(
HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY,
SecurityContextHolder.getContext());
return;
}
logger.info("LDAP authentication failed.");
} catch (AuthenticationException e) {
logger.info("LDAP authentication failed.", e);
}
userService.increaseFailedLoginCount(addr);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
}
} else if (guestEnabled && "guest".equals(type)) {
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_GUEST"));
if (username == null || !username.startsWith("Guest") || username.length() != MAX_USERNAME_LENGTH) {
username = "Guest" + Sha512DigestUtils.shaHex(request.getSession().getId()).substring(0, MAX_GUESTHASH_LENGTH);
}
org.springframework.security.core.userdetails.User user =
new org.springframework.security.core.userdetails.User(
username, "", true, true, true, true, authorities
);
Authentication token = new UsernamePasswordAuthenticationToken(user, null, authorities);
SecurityContextHolder.getContext().setAuthentication(token);
request.getSession(true).setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY,
SecurityContextHolder.getContext());
} else {
response.setStatus(HttpStatus.BAD_REQUEST.value());
}
}
@RequestMapping(value = { "/auth/dialog" }, method = RequestMethod.GET)
@ResponseBody
public View dialog(
@RequestParam("type") final String type,
@RequestParam(value = "successurl", defaultValue = "/") String successUrl,
@RequestParam(value = "failureurl", defaultValue = "/") String failureUrl,
final HttpServletRequest request,
final HttpServletResponse response
) throws IOException, ServletException {
View result = null;
/* Use URLs from a request parameters for redirection as long as the
* URL is not absolute (to prevent abuse of the redirection). */
if (UrlUtils.isAbsoluteUrl(successUrl)) {
successUrl = "/";
}
if (UrlUtils.isAbsoluteUrl(failureUrl)) {
failureUrl = "/";
}
/* Handle proxy
* TODO: It might be better, to support the proposed standard: http://tools.ietf.org/html/rfc7239 */
String host = null != request.getHeader("X-Forwarded-Host")
? request.getHeader("X-Forwarded-Host") : request.getServerName();
int port = null != request.getHeader("X-Forwarded-Port")
? Integer.valueOf(request.getHeader("X-Forwarded-Port")) : request.getServerPort();
String scheme = null != request.getHeader("X-Forwarded-Proto")
? request.getHeader("X-Forwarded-Proto") : request.getScheme();
String serverUrl = scheme + "://" + host;
if ("https".equals(scheme)) {
if (443 != port) {
serverUrl = serverUrl + ":" + String.valueOf(port);
}
} else {
if (80 != port) {
serverUrl = serverUrl + ":" + String.valueOf(port);
}
}
request.getSession().setAttribute("ars-login-success-url", serverUrl + successUrl);
request.getSession().setAttribute("ars-login-failure-url", serverUrl + failureUrl);
if (casEnabled && "cas".equals(type)) {
casEntryPoint.commence(request, response, null);
} else if (twitterEnabled && "twitter".equals(type)) {
final String authUrl = twitterProvider.getAuthorizationUrl(new HttpUserSession(request));
result = new RedirectView(authUrl);
} else if (facebookEnabled && "facebook".equals(type)) {
facebookProvider.setFields("id,link");
facebookProvider.setScope("");
final String authUrl = facebookProvider.getAuthorizationUrl(new HttpUserSession(request));
result = new RedirectView(authUrl);
} else if (googleEnabled && "google".equals(type)) {
final String authUrl = googleProvider.getAuthorizationUrl(new HttpUserSession(request));
result = new RedirectView(authUrl);
} else {
response.setStatus(HttpStatus.BAD_REQUEST.value());
}
return result;
}
@RequestMapping(value = { "/auth/", "/whoami" }, method = RequestMethod.GET)
@ResponseBody
public User whoami() {
userSessionService.setUser(userService.getCurrentUser());
return userService.getCurrentUser();
}
@RequestMapping(value = { "/auth/logout", "/logout" }, method = { RequestMethod.POST, RequestMethod.GET })
public View doLogout(final HttpServletRequest request) {
final Authentication auth = SecurityContextHolder.getContext().getAuthentication();
userService.removeUserFromMaps(userService.getCurrentUser());
request.getSession().invalidate();
SecurityContextHolder.clearContext();
if (auth instanceof CasAuthenticationToken) {
if ("".equals(apiPath)) {
apiPath = request.getContextPath();
}
return new RedirectView(apiPath + "/j_spring_cas_security_logout");
}
return new RedirectView(request.getHeader("referer") != null ? request.getHeader("referer") : "/");
}
@RequestMapping(value = { "/auth/services" }, method = RequestMethod.GET)
@ResponseBody
public List<ServiceDescription> getServices(final HttpServletRequest request) {
List<ServiceDescription> services = new ArrayList<>();
if ("".equals(apiPath)) {
apiPath = request.getContextPath();
}
/* The first parameter is replaced by the backend, the second one by the frondend */
String dialogUrl = apiPath + "/auth/dialog?type={0}&successurl='{0}'";
if (guestEnabled) {
ServiceDescription sdesc = new ServiceDescription(
"guest",
"Guest",
null,
guestRoles
);
sdesc.setOrder(guestOrder);
services.add(sdesc);
}
if (customLoginEnabled && !"".equals(customLoginDialog)) {
ServiceDescription sdesc = new ServiceDescription(
"custom",
customLoginTitle,
customizationPath + "/" + customLoginDialog + "?redirect={0}",
customLoginRoles,
customLoginImage
);
sdesc.setOrder(customLoginOrder);
services.add(sdesc);
}
if (dbAuthEnabled && !"".equals(dbAuthDialog)) {
ServiceDescription sdesc = new ServiceDescription(
"arsnova",
dbAuthTitle,
customizationPath + "/" + dbAuthDialog + "?redirect={0}",
dbAuthRoles,
dbAuthImage
);
sdesc.setOrder(dbAuthOrder);
services.add(sdesc);
}
if (ldapEnabled && !"".equals(ldapDialog)) {
ServiceDescription sdesc = new ServiceDescription(
"ldap",
ldapTitle,
customizationPath + "/" + ldapDialog + "?redirect={0}",
ldapRoles,
ldapImage
);
sdesc.setOrder(ldapOrder);
services.add(sdesc);
}
if (casEnabled) {
ServiceDescription sdesc = new ServiceDescription(
"cas",
casTitle,
MessageFormat.format(dialogUrl, "cas"),
casRoles
);
sdesc.setOrder(casOrder);
services.add(sdesc);
}
if (facebookEnabled) {
ServiceDescription sdesc = new ServiceDescription(
"facebook",
"Facebook",
MessageFormat.format(dialogUrl, "facebook"),
facebookRoles
);
sdesc.setOrder(facebookOrder);
services.add(sdesc);
}
if (googleEnabled) {
ServiceDescription sdesc = new ServiceDescription(
"google",
"Google",
MessageFormat.format(dialogUrl, "google"),
googleRoles
);
sdesc.setOrder(googleOrder);
services.add(sdesc);
}
if (twitterEnabled) {
ServiceDescription sdesc = new ServiceDescription(
"twitter",
"Twitter",
MessageFormat.format(dialogUrl, "twitter"),
twitterRoles
);
sdesc.setOrder(twitterOrder);
services.add(sdesc);
}
return services;
}
private Collection<GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authList = new ArrayList<>();
authList.add(new SimpleGrantedAuthority("ROLE_USER"));
return authList;
}
@RequestMapping(value = { "/test/me" }, method = RequestMethod.GET)
@ResponseBody
public User me() {
final User me = userSessionService.getUser();
if (me == null) {
throw new UnauthorizedException();
}
return me;
}
@RequestMapping(value = { "/test/mysession" }, method = RequestMethod.GET)
@ResponseBody
public Session mysession() {
final Session mysession = userSessionService.getSession();
if (mysession == null) {
throw new UnauthorizedException();
}
return mysession;
}
@RequestMapping(value = { "/test/myrole" }, method = RequestMethod.GET)
@ResponseBody
public UserSessionService.Role myrole() {
final UserSessionService.Role myrole = userSessionService.getRole();
if (myrole == null) {
throw new UnauthorizedException();
}
return myrole;
}
}