diff --git a/src/main/java/de/thm/arsnova/config/SecurityConfig.java b/src/main/java/de/thm/arsnova/config/SecurityConfig.java index 6f43c5b03318d87277d2ff1a0477ab7d8543cfcf..4f85089c3a28687a5ec52ac577baa698917f0d2f 100644 --- a/src/main/java/de/thm/arsnova/config/SecurityConfig.java +++ b/src/main/java/de/thm/arsnova/config/SecurityConfig.java @@ -584,6 +584,8 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { public OauthCallbackFilter oauthCallbackFilter() throws Exception { final OauthCallbackFilter callbackFilter = new OauthCallbackFilter(oauthConfig(), OAUTH_CALLBACK_PATH); callbackFilter.setAuthenticationManager(authenticationManager()); + callbackFilter.setAuthenticationSuccessHandler(successHandler()); + callbackFilter.setAuthenticationFailureHandler(failureHandler()); return callbackFilter; } diff --git a/src/main/java/de/thm/arsnova/controller/AuthenticationController.java b/src/main/java/de/thm/arsnova/controller/AuthenticationController.java index 7dead0ff264aa83cea65e9f328e1cb3c89f9d57b..eb7774be4b580855ac884958f19adfd6a5f1a4cc 100644 --- a/src/main/java/de/thm/arsnova/controller/AuthenticationController.java +++ b/src/main/java/de/thm/arsnova/controller/AuthenticationController.java @@ -18,44 +18,73 @@ package de.thm.arsnova.controller; +import java.io.IOException; +import java.util.Arrays; +import javax.servlet.ServletException; +import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.pac4j.core.context.J2EContext; +import org.pac4j.oidc.client.OidcClient; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.cas.web.CasAuthenticationEntryPoint; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.View; +import org.springframework.web.servlet.view.RedirectView; +import de.thm.arsnova.config.SecurityConfig; import de.thm.arsnova.model.ClientAuthentication; import de.thm.arsnova.model.LoginCredentials; import de.thm.arsnova.model.UserProfile; +import de.thm.arsnova.security.LoginAuthenticationSucessHandler; import de.thm.arsnova.service.UserService; @RestController @RequestMapping("/auth") public class AuthenticationController { private UserService userService; + private OidcClient oidcClient; + private CasAuthenticationEntryPoint casEntryPoint; public AuthenticationController(final UserService userService) { this.userService = userService; } - @PostMapping("/login") - public ClientAuthentication login() { - return userService.getCurrentClientAuthentication(); + @Autowired(required = false) + public void setOidcClient(final OidcClient oidcClient) { + this.oidcClient = oidcClient; } - @PostMapping("/login/registered") - public ClientAuthentication loginRegistered(@RequestBody final LoginCredentials loginCredentials, - final HttpServletRequest request) { - final String loginId = loginCredentials.getLoginId().toLowerCase(); - userService.authenticate(new UsernamePasswordAuthenticationToken(loginId, loginCredentials.getPassword()), - UserProfile.AuthProvider.ARSNOVA, request.getRemoteAddr()); - return userService.getCurrentClientAuthentication(); + @Autowired(required = false) + public void setCasEntryPoint(final CasAuthenticationEntryPoint casEntryPoint) { + this.casEntryPoint = casEntryPoint; + } + + @PostMapping("/login") + public ClientAuthentication login(@RequestParam(defaultValue = "false") final boolean refresh, + final HttpServletRequest request, final HttpServletResponse response) { + if (request.getCookies() != null && Arrays.stream(request.getCookies()) + .anyMatch(c -> c.getName().equalsIgnoreCase(LoginAuthenticationSucessHandler.AUTH_COOKIE_NAME))) { + /* Delete cookie */ + final Cookie cookie = new Cookie(LoginAuthenticationSucessHandler.AUTH_COOKIE_NAME, null); + cookie.setPath(request.getContextPath()); + cookie.setMaxAge(0); + response.addCookie(cookie); + } + + return userService.getCurrentClientAuthentication(refresh); } @PostMapping("/login/guest") public ClientAuthentication loginGuest(final HttpServletRequest request) { - final ClientAuthentication currentAuthentication = userService.getCurrentClientAuthentication(); + final ClientAuthentication currentAuthentication = userService.getCurrentClientAuthentication(false); if (currentAuthentication != null && currentAuthentication.getAuthProvider() == UserProfile.AuthProvider.ARSNOVA_GUEST) { return currentAuthentication; @@ -63,6 +92,51 @@ public class AuthenticationController { userService.authenticate(new UsernamePasswordAuthenticationToken(null, null), UserProfile.AuthProvider.ARSNOVA_GUEST, request.getRemoteAddr()); - return userService.getCurrentClientAuthentication(); + return userService.getCurrentClientAuthentication(false); + } + + @PostMapping("/login/{providerId}") + public ClientAuthentication loginViaProvider( + @PathVariable final String providerId, + @RequestBody final LoginCredentials loginCredentials, + final HttpServletRequest request) { + switch (providerId) { + case "registered": + final String loginId = loginCredentials.getLoginId().toLowerCase(); + userService.authenticate(new UsernamePasswordAuthenticationToken( + loginId, loginCredentials.getPassword()), + UserProfile.AuthProvider.ARSNOVA, request.getRemoteAddr()); + + return userService.getCurrentClientAuthentication(false); + case SecurityConfig.LDAP_PROVIDER_ID: + userService.authenticate(new UsernamePasswordAuthenticationToken( + loginCredentials.getLoginId(), loginCredentials.getPassword()), + UserProfile.AuthProvider.LDAP, request.getRemoteAddr()); + + return userService.getCurrentClientAuthentication(false); + default: + throw new IllegalArgumentException("Invalid provider ID."); + } + } + + @GetMapping("/sso/{providerId}") + public View redirectToSso(@PathVariable final String providerId, + final HttpServletRequest request, final HttpServletResponse response) throws IOException, ServletException { + switch (providerId) { + case SecurityConfig.OIDC_PROVIDER_ID: + if (oidcClient == null) { + throw new IllegalArgumentException("Invalid provider ID."); + } + return new RedirectView( + oidcClient.getRedirectAction(new J2EContext(request, response)).getLocation()); + case SecurityConfig.CAS_PROVIDER_ID: + if (casEntryPoint == null) { + throw new IllegalArgumentException("Invalid provider ID."); + } + casEntryPoint.commence(request, response, null); + return null; + default: + throw new IllegalArgumentException("Invalid provider ID."); + } } } diff --git a/src/main/java/de/thm/arsnova/controller/v2/AuthenticationController.java b/src/main/java/de/thm/arsnova/controller/v2/AuthenticationController.java index 9f2782e5242c3a7a864c76c0d7c391bdece9f1dc..dd0beb4fb9ccb2e10392cd129b528ba4280c51d6 100644 --- a/src/main/java/de/thm/arsnova/controller/v2/AuthenticationController.java +++ b/src/main/java/de/thm/arsnova/controller/v2/AuthenticationController.java @@ -66,6 +66,8 @@ import de.thm.arsnova.controller.AbstractController; import de.thm.arsnova.model.ServiceDescription; import de.thm.arsnova.model.UserProfile; import de.thm.arsnova.model.migration.v2.ClientAuthentication; +import de.thm.arsnova.security.LoginAuthenticationFailureHandler; +import de.thm.arsnova.security.LoginAuthenticationSucessHandler; import de.thm.arsnova.security.User; import de.thm.arsnova.service.UserService; import de.thm.arsnova.web.exceptions.UnauthorizedException; @@ -208,8 +210,8 @@ public class AuthenticationController extends AbstractController { } } - request.getSession().setAttribute("ars-login-success-url", serverUrl + successUrl); - request.getSession().setAttribute("ars-login-failure-url", serverUrl + failureUrl); + request.getSession().setAttribute(LoginAuthenticationSucessHandler.URL_ATTRIBUTE, serverUrl + successUrl); + request.getSession().setAttribute(LoginAuthenticationFailureHandler.URL_ATTRIBUTE, serverUrl + failureUrl); if (casProperties.isEnabled() && "cas".equals(type)) { casEntryPoint.commence(request, response, null); diff --git a/src/main/java/de/thm/arsnova/model/AuthenticationProvider.java b/src/main/java/de/thm/arsnova/model/AuthenticationProvider.java index 961e5f2e7bb3fa95a2c8c6e56760466fbcab073c..d455a785bcbf4f52eb8aad983320e94b601c24e3 100644 --- a/src/main/java/de/thm/arsnova/model/AuthenticationProvider.java +++ b/src/main/java/de/thm/arsnova/model/AuthenticationProvider.java @@ -28,7 +28,7 @@ public class AuthenticationProvider { public enum Type { ANONYMOUS, USERNAME_PASSWORD, - EXTERNAL + SSO } private String id; @@ -51,7 +51,7 @@ public class AuthenticationProvider { || provider instanceof AuthenticationProviderProperties.Ldap) { type = Type.USERNAME_PASSWORD; } else { - type = Type.EXTERNAL; + type = Type.SSO; } } diff --git a/src/main/java/de/thm/arsnova/security/LoginAuthenticationFailureHandler.java b/src/main/java/de/thm/arsnova/security/LoginAuthenticationFailureHandler.java index c3330f1eb8584a65b7df30059e258e77e66c6121..d5b5f1600c06d0a2544fdcdf8e15fcad0f9468e1 100644 --- a/src/main/java/de/thm/arsnova/security/LoginAuthenticationFailureHandler.java +++ b/src/main/java/de/thm/arsnova/security/LoginAuthenticationFailureHandler.java @@ -34,6 +34,7 @@ import org.springframework.security.web.authentication.SimpleUrlAuthenticationFa */ public class LoginAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { + public static final String URL_ATTRIBUTE = "ars-login-failure-url"; private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); private String failureUrl; @@ -45,8 +46,8 @@ public class LoginAuthenticationFailureHandler extends final AuthenticationException exception ) throws IOException, ServletException { final HttpSession session = request.getSession(); - if (session != null && session.getAttribute("ars-login-failure-url") != null) { - failureUrl = (String) session.getAttribute("ars-login-failure-url"); + if (session != null && session.getAttribute(URL_ATTRIBUTE) != null) { + failureUrl = (String) session.getAttribute(URL_ATTRIBUTE); } redirectStrategy.sendRedirect(request, response, failureUrl); diff --git a/src/main/java/de/thm/arsnova/security/LoginAuthenticationSucessHandler.java b/src/main/java/de/thm/arsnova/security/LoginAuthenticationSucessHandler.java index fe16ae086ac33c41e5e49466ac570a9ed877cec1..07c8d5f8a17427c60f4dc603dbec1ec3d66024ce 100644 --- a/src/main/java/de/thm/arsnova/security/LoginAuthenticationSucessHandler.java +++ b/src/main/java/de/thm/arsnova/security/LoginAuthenticationSucessHandler.java @@ -18,29 +18,63 @@ package de.thm.arsnova.security; +import java.io.IOException; +import javax.servlet.ServletException; +import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import de.thm.arsnova.security.jwt.JwtService; + /** * This class gets called when a user successfully logged in. */ public class LoginAuthenticationSucessHandler extends SimpleUrlAuthenticationSuccessHandler { + public static final String AUTH_COOKIE_NAME = "auth"; + public static final String URL_ATTRIBUTE = "ars-login-success-url"; + private JwtService jwtService; private String targetUrl; + @Autowired + public void setJwtService(final JwtService jwtService) { + this.jwtService = jwtService; + } + @Override protected String determineTargetUrl( final HttpServletRequest request, final HttpServletResponse response) { - final HttpSession session = request.getSession(); - if (session == null || session.getAttribute("ars-login-success-url") == null) { - return targetUrl; - } + final HttpSession session = request.getSession(false); + final String url = (String) session.getAttribute(URL_ATTRIBUTE); + session.removeAttribute(URL_ATTRIBUTE); - return (String) session.getAttribute("ars-login-success-url"); + return url; + } + + @Override + public void onAuthenticationSuccess(final HttpServletRequest request, final HttpServletResponse response, + final Authentication authentication) throws IOException, ServletException { + final HttpSession session = request.getSession(false); + if (session == null || session.getAttribute(URL_ATTRIBUTE) == null) { + final String token = jwtService.createSignedToken((User) authentication.getPrincipal(), true); + final Cookie cookie = new Cookie(AUTH_COOKIE_NAME, token); + cookie.setPath(request.getContextPath()); + cookie.setSecure(request.isSecure()); + cookie.setHttpOnly(true); + response.addCookie(cookie); + response.setContentType(MediaType.TEXT_HTML_VALUE); + response.getWriter().println("<!DOCTYPE html><script>if (window.opener) window.close()</script>"); + + return; + } + super.onAuthenticationSuccess(request, response, authentication); } public void setTargetUrl(final String url) { diff --git a/src/main/java/de/thm/arsnova/security/jwt/JwtService.java b/src/main/java/de/thm/arsnova/security/jwt/JwtService.java index 0c2dc71ecef4c4be8119efd63b8cf82b0217ca6f..f24a901895144a8e986d77ff654f4ec9017779da 100644 --- a/src/main/java/de/thm/arsnova/security/jwt/JwtService.java +++ b/src/main/java/de/thm/arsnova/security/jwt/JwtService.java @@ -46,6 +46,7 @@ public class JwtService { private String serverId; private TemporalAmount defaultValidityPeriod; private TemporalAmount guestValidityPeriod; + private TemporalAmount temporaryValidityPeriod; private JWTVerifier verifier; private UserService userService; @@ -56,19 +57,21 @@ public class JwtService { this.serverId = securityProperties.getJwt().getServerId(); this.defaultValidityPeriod = securityProperties.getJwt().getValidityPeriod(); guestValidityPeriod = Duration.parse("P180D"); + temporaryValidityPeriod = Duration.parse("PT30S"); algorithm = Algorithm.HMAC256(securityProperties.getJwt().getSecret()); verifier = JWT.require(algorithm) .withAudience(serverId) .build(); } - public String createSignedToken(final User user) { + public String createSignedToken(final User user, final boolean temporary) { final String[] roles = user.getAuthorities().stream() .map(ga -> ga.getAuthority()) .filter(ga -> ga.startsWith(ROLE_PREFIX)) .map(ga -> ga.substring(ROLE_PREFIX.length())).toArray(String[]::new); - final TemporalAmount expiresAt = user.getAuthProvider() == UserProfile.AuthProvider.ARSNOVA_GUEST - ? guestValidityPeriod : defaultValidityPeriod; + final TemporalAmount expiresAt = temporary ? temporaryValidityPeriod + : (user.getAuthProvider() == UserProfile.AuthProvider.ARSNOVA_GUEST + ? guestValidityPeriod : defaultValidityPeriod); return JWT.create() .withIssuer(serverId) .withAudience(serverId) diff --git a/src/main/java/de/thm/arsnova/security/jwt/JwtTokenFilter.java b/src/main/java/de/thm/arsnova/security/jwt/JwtTokenFilter.java index b90d3a60d0b20c0e8b423ad8aa8c239a16fa5683..3a216a6e176f84675d42f611d40876adba241a23 100644 --- a/src/main/java/de/thm/arsnova/security/jwt/JwtTokenFilter.java +++ b/src/main/java/de/thm/arsnova/security/jwt/JwtTokenFilter.java @@ -19,12 +19,15 @@ package de.thm.arsnova.security.jwt; import java.io.IOException; +import java.util.Arrays; +import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; +import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,6 +38,8 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.web.filter.GenericFilterBean; +import de.thm.arsnova.security.LoginAuthenticationSucessHandler; + @Component public class JwtTokenFilter extends GenericFilterBean { private static final Pattern BEARER_TOKEN_PATTERN = Pattern.compile("Bearer (.*)", Pattern.CASE_INSENSITIVE); @@ -51,28 +56,44 @@ public class JwtTokenFilter extends GenericFilterBean { filterChain.doFilter(servletRequest, servletResponse); return; } + + JwtToken token = null; final String jwtHeader = httpServletRequest.getHeader(HttpHeaders.AUTHORIZATION); if (jwtHeader != null) { final Matcher tokenMatcher = BEARER_TOKEN_PATTERN.matcher(jwtHeader); if (tokenMatcher.matches()) { - final JwtToken token = new JwtToken(tokenMatcher.group(1)); - try { - final Authentication authenticatedToken = jwtAuthenticationProvider.authenticate(token); - if (authenticatedToken != null) { - logger.debug("Storing JWT to SecurityContext: {}", authenticatedToken); - SecurityContextHolder.getContext().setAuthentication(authenticatedToken); - } else { - logger.debug("Could not authenticate JWT."); - } - } catch (final Exception e) { - logger.debug("JWT authentication failed", e); - } + token = new JwtToken(tokenMatcher.group(1)); } else { logger.debug("Unsupported authentication scheme."); } } else { logger.debug("No authentication header present."); + /* Look for auth cookie if Authorization header is not present. */ + if (httpServletRequest.getCookies() != null) { + final Optional<Cookie> cookie = Arrays.stream(httpServletRequest.getCookies()) + .filter(c -> c.getName().equalsIgnoreCase(LoginAuthenticationSucessHandler.AUTH_COOKIE_NAME)) + .findFirst(); + if (cookie.isPresent()) { + logger.debug("Trying to use authentication from cookie."); + token = new JwtToken(cookie.get().getValue()); + } + } } + + if (token != null) { + try { + final Authentication authenticatedToken = jwtAuthenticationProvider.authenticate(token); + if (authenticatedToken != null) { + logger.debug("Storing JWT to SecurityContext: {}", authenticatedToken); + SecurityContextHolder.getContext().setAuthentication(authenticatedToken); + } else { + logger.debug("Could not authenticate JWT."); + } + } catch (final Exception e) { + logger.debug("JWT authentication failed", e); + } + } + filterChain.doFilter(servletRequest, servletResponse); } diff --git a/src/main/java/de/thm/arsnova/service/UserService.java b/src/main/java/de/thm/arsnova/service/UserService.java index bbfd163811c4f2dce21f2fe3f21cc035c0d97e58..d2b442f032cd1b877b3ebe0882d126d57be11909 100644 --- a/src/main/java/de/thm/arsnova/service/UserService.java +++ b/src/main/java/de/thm/arsnova/service/UserService.java @@ -39,7 +39,7 @@ public interface UserService extends EntityService<UserProfile> { User getCurrentUser(); - de.thm.arsnova.model.ClientAuthentication getCurrentClientAuthentication(); + de.thm.arsnova.model.ClientAuthentication getCurrentClientAuthentication(boolean refresh); boolean isAdmin(String username); diff --git a/src/main/java/de/thm/arsnova/service/UserServiceImpl.java b/src/main/java/de/thm/arsnova/service/UserServiceImpl.java index 21110d04c633b7d2b097036a4580503e7c694986..e5723926553a36dc85f9b94c29374f2fbf78274d 100644 --- a/src/main/java/de/thm/arsnova/service/UserServiceImpl.java +++ b/src/main/java/de/thm/arsnova/service/UserServiceImpl.java @@ -203,14 +203,14 @@ public class UserServiceImpl extends DefaultEntityServiceImpl<UserProfile> imple } @Override - public de.thm.arsnova.model.ClientAuthentication getCurrentClientAuthentication() { + public de.thm.arsnova.model.ClientAuthentication getCurrentClientAuthentication(final boolean refresh) { final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null || !(authentication.getPrincipal() instanceof User)) { return null; } final User user = (User) authentication.getPrincipal(); - final String jwt = authentication instanceof JwtToken - ? (String) authentication.getCredentials() : jwtService.createSignedToken(user); + final String jwt = !refresh && authentication instanceof JwtToken + ? (String) authentication.getCredentials() : jwtService.createSignedToken(user, false); final ClientAuthentication clientAuthentication = new ClientAuthentication(user.getId(), user.getUsername(),