<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-security</artifactId>
+ </dependency>
<dependency>
<groupId>org.springframework.social</groupId>
<artifactId>spring-social-facebook</artifactId>
<groupId>org.springframework.social</groupId>
<artifactId>spring-social-facebook-web</artifactId>
</dependency>
- <dependency>
- <groupId>org.springframework.security</groupId>
- <artifactId>spring-security-crypto</artifactId>
- <scope>runtime</scope>
- </dependency>
<!-- Httpclient -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
+++ /dev/null
-package de.juplo.yourshouter;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-
-/**
- * Simple SecurityContext that stores the currently signed-in connection in a
- * thread local.
- *
- * @author Kai Moritz
- */
-public final class SecurityContext
-{
- private final static Logger LOG = LoggerFactory.getLogger(SecurityContext.class);
- private final static ThreadLocal<String> CURRENT_USER = new ThreadLocal<>();
-
-
- /**
- * Fetches the ID of the current user from the thread-local.
- *
- * @return
- * The ID of the current user, or <code>null</code> if no user is known.
- */
- public static String getCurrentUser()
- {
- String user = CURRENT_USER.get();
- LOG.debug("current user: {}", user);
- return user;
- }
-
- /**
- * Stores the given ID as the ID of the current user in the thread-local.
- *
- * @param user
- * The ID to store as the ID of the current user.
- */
- public static void setCurrentUser(String user)
- {
- LOG.debug("setting current user: {}", user);
- CURRENT_USER.set(user);
- }
-
- /**
- * Checks, if a user is signed in. That is, if the ID of a user is stored in
- * the thread-local.
- *
- * @return
- * <code>true</code>, if a user is signed in, <code>false</code> otherwise.
- */
- public static boolean userSignedIn()
- {
- boolean signedIn = CURRENT_USER.get() != null;
- LOG.debug("user signed in: {}", signedIn);
- return signedIn;
- }
-
- /**
- * Removes the ID of the current user from the thread-local.
- */
- public static void remove()
- {
- LOG.debug("removing current user");
- CURRENT_USER.remove();
- }
-}
+++ /dev/null
-package de.juplo.yourshouter;
-
-import org.springframework.social.UserIdSource;
-import org.springframework.util.Assert;
-
-
-/**
- * Implementation of {@link UserIdSource}, that retrieves the ID of the current
- * user from the {@link SecurityContext}.
- *
- * @author Kai Moritz
- */
-public class SecurityContextUserIdSource implements UserIdSource
-{
- /**
- * Retrieves the ID of the current user from the {@link SecurityContext}.
- * If no ID is found, an exception is thrown.
- *
- * @return The ID of the current user
- * @throws IllegalStateException, if no current user is found.
- */
- @Override
- public String getUserId()
- {
- Assert.state(SecurityContext.userSignedIn(), "No user signed in!");
- return SecurityContext.getCurrentUser();
- }
-}
--- /dev/null
+package de.juplo.yourshouter;
+
+import java.io.IOException;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.AuthenticationEntryPoint;
+import org.springframework.stereotype.Service;
+
+
+/**
+ * Specialized implementation of {@link AuthenticationEntryPoint}, that
+ * redirects to the social sign-in-page, to let the user decide to sign in or
+ * not.
+ *
+ * @author Kai Moritz
+ */
+@Service
+public class SocialAuthenticationEntryPoint implements AuthenticationEntryPoint
+{
+ private static final Logger LOG =
+ LoggerFactory.getLogger(SocialAuthenticationEntryPoint.class);
+
+
+ /**
+ * {@inheritDoc}
+ *
+ * To commence the sign-in through the Graph-API, we only have to redirect
+ * to our already implemented sign-in-page.
+ */
+ @Override
+ public void commence(
+ HttpServletRequest request,
+ HttpServletResponse response,
+ AuthenticationException exception
+ )
+ throws
+ IOException,
+ ServletException
+ {
+ LOG.info(
+ "redirecting unauthenticated request {} to /signin.html",
+ request.getRequestURI()
+ );
+ response.sendRedirect("/signin.html");
+ }
+}
import org.springframework.social.UserIdSource;
import org.springframework.core.env.Environment;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
+import org.springframework.security.core.context.SecurityContext;
import org.springframework.social.config.annotation.ConnectionFactoryConfigurer;
import org.springframework.social.config.annotation.EnableSocial;
import org.springframework.social.config.annotation.SocialConfigurerAdapter;
@Override
public UserIdSource getUserIdSource()
{
- return new SecurityContextUserIdSource();
+ return new SpringSecurityContextUserIdSource();
}
--- /dev/null
+package de.juplo.yourshouter;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.social.UserIdSource;
+import org.springframework.util.Assert;
+
+
+/**
+ * Implementation of {@link UserIdSource}, that retrieves the ID of the current
+ * user from the {@link SecurityContext}.
+ *
+ * @author Kai Moritz
+ */
+public class SpringSecurityContextUserIdSource implements UserIdSource
+{
+ /**
+ * Retrieves the ID of the current user from the {@link SecurityContext}.
+ * If no ID is found, an exception is thrown.
+ *
+ * @return The ID of the current user
+ * @throws IllegalStateException, if no current user is found.
+ */
+ @Override
+ public String getUserId()
+ {
+ SecurityContext context = SecurityContextHolder.getContext();
+ Authentication authentication = context.getAuthentication();
+ Assert.state(authentication != null, "No user signed in!");
+ return authentication.getName();
+ }
+}
--- /dev/null
+package de.juplo.yourshouter;
+
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.social.connect.Connection;
+import org.springframework.social.connect.web.SignInAdapter;
+import org.springframework.stereotype.Service;
+import org.springframework.web.context.request.NativeWebRequest;
+
+
+/**
+ * Simple implementation of {@link SignInAdapter}.
+ *
+ * This implementation signes in the user by storing him in the
+ * {@link SecurityContext} provided by Spring-Security, using the user-ID as
+ * principal.
+ *
+ * We configured Spring-Social to call this implementation, to sign in the
+ * user, after he was authenticated by Facebook.
+ *
+ * @author Kai Moritz
+ */
+@Service
+public class SpringSecuritySignInAdapter implements SignInAdapter
+{
+ private final static Logger LOG =
+ LoggerFactory.getLogger(SpringSecuritySignInAdapter.class);
+
+ /**
+ * Stores the user in the {@link SecurityContext} provided by Spring Security
+ * to sign him in. Spring Security will automatically persist the
+ * authentication in the user-session for subsequent requests.
+ *
+ * @param user
+ * The user-ID. We configured Spring-Social to call
+ * {@link UserCookieSignInAdapter} to extract a user-ID from the
+ * connection.
+ * @param connection
+ * The connection. In our case a connection to Facebook.
+ * @param request
+ * The actual request. We need it, to store the cookie.
+ * @return
+ * We return <code>null</code>, to indicate, that the user should be
+ * redirected to the default-post-sign-in-URL (configured in
+ * {@link ProviderSinInController}) after a successfull authentication.
+ *
+ * @see {@link ProviderSignInController#postSignInUrl}
+ */
+ @Override
+ public String signIn(
+ String user,
+ Connection<?> connection,
+ NativeWebRequest request
+ )
+ {
+ LOG.info(
+ "signing in user {} (connected via {})",
+ user,
+ connection.getKey().getProviderId()
+ );
+
+ SecurityContextHolder.getContext().setAuthentication(
+ new UsernamePasswordAuthenticationToken(user, null, null));
+
+ // We return null to trigger a redirect to "/".
+ return null;
+ }
+}
+++ /dev/null
-package de.juplo.yourshouter;
-
-
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import org.springframework.web.util.CookieGenerator;
-import org.thymeleaf.util.StringUtils;
-
-
-/**
- * Utility class for managing the cookie that remembers the user.
- *
- * @author Kai Moritz
- */
-final class UserCookieGenerator
-{
- private final static Logger LOG =
- LoggerFactory.getLogger(UserCookieGenerator.class);
-
- public final static UserCookieGenerator INSTANCE = new UserCookieGenerator();
-
-
- private final CookieGenerator generator = new CookieGenerator();
-
-
- /**
- * Constructs an instance of this class, using <code>user</code> as the
- * cookie-name.
- */
- private UserCookieGenerator()
- {
- generator.setCookieName("user");
- }
-
-
- /**
- * Creates a cookie with the name <code>user</code>, that stores the ID of
- * the user for subsequent calls.
- *
- * @param user
- * The ID of the current user
- * @param response
- * The {@link HttpServletResponse} to store the cookie in.
- */
- public void addCookie(String user, HttpServletResponse response)
- {
- LOG.debug("adding cookie {}={}", generator.getCookieName(), user);
- generator.addCookie(response, user);
- }
-
- /**
- * Removes the cookie with the name <code>user</code> by storing an empty
- * string as its value.
- *
- * @param response
- * The {@link HttpServletResponse} to remove the cookie from.
- */
- public void removeCookie(HttpServletResponse response)
- {
- LOG.debug("removing cookie {}", generator.getCookieName());
- generator.addCookie(response, "");
- }
-
- /**
- * Reads the current value of the cookie with the name <code>user</code>.
- *
- * @param request
- * The {@link HttpServletRequest} to read the cookie-value from.
- * @return
- * The value of the cookie with the name <code>user</code>, or
- * <code>null</code>, if no cookie by that name can be found or the value
- * of the cookie is an empty string.
- */
- public String readCookieValue(HttpServletRequest request)
- {
- String name = generator.getCookieName();
- Cookie[] cookies = request.getCookies();
- if (cookies != null)
- {
- for (Cookie cookie : cookies)
- {
- if (cookie.getName().equals(name))
- {
- String value = cookie.getValue();
- if (!StringUtils.isEmptyOrWhitespace(value))
- {
- LOG.debug("found cookie {}={}", name, value);
- return value;
- }
- }
- }
- }
- LOG.debug("cookie \"{}\" not found!", name);
- return null;
- }
-}
+++ /dev/null
-package de.juplo.yourshouter;
-
-
-import java.io.IOException;
-import java.util.Collections;
-import java.util.regex.Pattern;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import org.springframework.social.connect.UsersConnectionRepository;
-import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
-
-
-/**
- * Intercepts all requests to handle the user-cookie.
- *
- * @author Kai Moritz
- */
-public final class UserCookieInterceptor extends HandlerInterceptorAdapter
-{
- private final static Logger LOG =
- LoggerFactory.getLogger(UserCookieInterceptor.class);
- private final static Pattern PATTERN = Pattern.compile("^/signin|canvas");
-
-
- private final UsersConnectionRepository repository;
-
-
- /**
- * Creates an instance of this class, that uses the given instance of
- * {@link UsersConnectionRepository}.
- *
- * @param repository
- * The instance of {@link UsersConnectionRepository} to use.
- */
- public UserCookieInterceptor(UsersConnectionRepository repository)
- {
- this.repository = repository;
- }
-
-
- /**
- * Before a request is handled, the current user is loaded from the cookie,
- * if the cookie is present and the user is known. If the user is not known,
- * the cookie is removed.
- *
- * @param request
- * The {@link HttpServletRequest} that is intercepted.
- * @param response
- * The {@link HttpServletResponse} that is intercepted.
- * @param handler
- * The handler, that handles the intercepted request.
- * @return
- * Always <code>true</code>, to indicate, that the intercepted request
- * should be handled normally.
- * @throws java.io.IOException
- * if something wents wrong, while sending the redirect to the
- * sign-in-page.
- */
- @Override
- public boolean preHandle(
- HttpServletRequest request,
- HttpServletResponse response,
- Object handler
- )
- throws
- IOException
- {
- if (PATTERN.matcher(request.getServletPath()).find())
- return true;
-
- String user = UserCookieGenerator.INSTANCE.readCookieValue(request);
- if (user != null)
- {
- if (!repository
- .findUserIdsConnectedTo("facebook", Collections.singleton(user))
- .isEmpty()
- )
- {
- LOG.info("loading user {} from cookie", user);
- SecurityContext.setCurrentUser(user);
- return true;
- }
- else
- {
- LOG.warn("user {} is not known!", user);
- UserCookieGenerator.INSTANCE.removeCookie(response);
- }
- }
-
- response.sendRedirect("/signin.html");
- return false;
- }
-
- /**
- * After a request, the user is removed from the security-context.
- *
- * @param request
- * The {@link HttpServletRequest} that is intercepted.
- * @param response
- * The {@link HttpServletResponse} that is intercepted.
- * @param handler
- * The handler, that handles the intercepted request.
- * @param exception
- * If an exception was thrown during the handling of this request, it is
- * handed in through this parameter.
- */
- @Override
- public void afterCompletion(
- HttpServletRequest request,
- HttpServletResponse response,
- Object handler,
- Exception exception
- )
- {
- SecurityContext.remove();
- }
-}
+++ /dev/null
-package de.juplo.yourshouter;
-
-import javax.servlet.http.HttpServletResponse;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.social.connect.Connection;
-import org.springframework.social.connect.web.SignInAdapter;
-import org.springframework.stereotype.Service;
-import org.springframework.web.context.request.NativeWebRequest;
-
-
-/**
- * Simple implementation of {@link SignInAdapter}.
- *
- * We configured Spring-Social to call this implementation, to sign in the
- * user, after he was authenticated by Facebook.
- *
- * @author Kai Moritz
- */
-@Service
-public class UserCookieSignInAdapter implements SignInAdapter
-{
- private final static Logger LOG =
- LoggerFactory.getLogger(UserCookieSignInAdapter.class);
-
-
- /**
- * Stores the user in the security-context to sign him in.
- * Also remembers the user for subsequent calls by storing the ID in the
- * cookie.
- *
- * @param user
- * The user-ID. We configured Spring-Social to call
- * {@link UserCookieSignInAdapter} to extract a user-ID from the
- * connection.
- * @param connection
- * The connection. In our case a connection to Facebook.
- * @param request
- * The actual request. We need it, to store the cookie.
- * @return
- * We return <code>null</code>, to indicate, that the user should be
- * redirected to the default-post-sign-in-URL (configured in
- * {@link ProviderSinInController}) after a successfull authentication.
- *
- * @see {@link UserCookieSignInAdapter}
- * @see {@link ProviderSignInController#postSignInUrl}
- */
- @Override
- public String signIn(
- String user,
- Connection<?> connection,
- NativeWebRequest request
- )
- {
- LOG.info(
- "signing in user {} (connected via {})",
- user,
- connection.getKey().getProviderId()
- );
- SecurityContext.setCurrentUser(user);
- UserCookieGenerator
- .INSTANCE
- .addCookie(user, request.getNativeResponse(HttpServletResponse.class));
-
- // We return null to trigger a redirect to "/".
- return null;
- }
-}
-import javax.inject.Inject;
import org.springframework.context.annotation.Configuration;
-import org.springframework.social.connect.UsersConnectionRepository;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
-import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter
{
- @Inject
- private UsersConnectionRepository usersConnectionRepository;
-
-
- /**
- * Configure the {@link UserCookieInterceptor} to intercept all requests.
- *
- * @param registry
- * The {@link InterceptorRegistry} to use.
- *
- * @see {@link UserCookieInterceptor}
- */
- @Override
- public void addInterceptors(InterceptorRegistry registry)
- {
- registry.addInterceptor(new UserCookieInterceptor(usersConnectionRepository));
- }
-
/**
* {@inheritDoc}
*/
--- /dev/null
+package de.juplo.yourshouter;
+
+import javax.inject.Inject;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.web.AuthenticationEntryPoint;
+import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
+
+
+@Configuration
+@EnableWebSecurity
+public class WebSecurityConfig extends WebSecurityConfigurerAdapter
+{
+ @Inject
+ AuthenticationEntryPoint authenticationEntryPoint;
+
+ /**
+ * We have to disable the default-configuration, because some of it does
+ * not work along with the canvas-page:
+ * <ul>
+ * <li>
+ * The support for CSRF-tokens consideres the initial call of Facebook to
+ * the canvas-page of our app as invalid, because it is issued as a post
+ * and the CSRF-token is missing.
+ * </li>
+ * <li>
+ * In the default-configuration, the <code>X-Frame-Options: DENY</code> is
+ * set for every response. This prevents the browser from showing our
+ * response inside Facebook, becaus that is an iFrame and the header
+ * forbidds to display our content in a frame.
+ * </li>
+ * </ul>
+ */
+ public WebSecurityConfig()
+ {
+ super(true);
+ }
+
+
+ /**
+ * @{@inheritDoc}
+ *
+ * Override the default-implementation to configure the authentication
+ * mechanism of Spring Security.
+ *
+ * We drop the support of CSRF-tokens, inject our specialized implementation
+ * of the {@link AuthenticationEntryPoint}-interface , disable the headers,
+ * that deny, to display our content insiede a frame and configure the
+ * pages, that should be accessible without authentication.
+ * We also drop support for a logout-page and the default-login-in-page.
+ */
+ @Override
+ protected void configure(HttpSecurity http) throws Exception
+ {
+ http
+ .addFilter(new WebAsyncManagerIntegrationFilter())
+ .exceptionHandling()
+ .authenticationEntryPoint(authenticationEntryPoint)
+ .and()
+ .headers()
+ .frameOptions().disable()
+ .and()
+ .sessionManagement().and()
+ .securityContext().and()
+ .requestCache().and()
+ .anonymous().and()
+ .servletApi().and()
+ .authorizeRequests()
+ .antMatchers("/signin.html", "/signin/*", "/canvas/*").permitAll()
+ .anyRequest().authenticated();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Override the default-implementation, to configure Spring Security to use
+ * in-memory authentication.
+ */
+ @Override
+ public void configure(AuthenticationManagerBuilder auth)
+ throws
+ Exception
+ {
+ auth.inMemoryAuthentication();
+ }
+}