Switch authentication to OAuth 2 #27
@ -1,3 +1,12 @@
|
||||
## Working with the web GUI (Wicket)
|
||||
The web GUI is protected by OAuth 2 log in. Run the Docker Compose containers with
|
||||
`docker compose up` to start the authorization server to be able to log in.
|
||||
|
||||
If you run SciPro in development mode (DEV profile) you will be able to log in
|
||||
as the "default" OAuth 2 user populated in the upper form. If you have other
|
||||
data in your database you will have to use the lower form and specify a valid
|
||||
username in the principal field.
|
||||
|
||||
## Working with the API
|
||||
The API is protected by OAuth 2 acting as a [resource server](https://www.oauth.com/oauth2-servers/the-resource-server/)
|
||||
verifying tokens using [token introspection](https://datatracker.ietf.org/doc/html/rfc7662).
|
||||
|
@ -206,6 +206,10 @@ public class DataInitializer implements Lifecycle {
|
||||
admin.addRole(Roles.SYSADMIN);
|
||||
createBeta(admin);
|
||||
passwordService.updatePassword(admin, "aey7ru8aefei0jaW2wo9eX8EiShi0aan");
|
||||
Username defaultOAuth2Principal = new Username();
|
||||
defaultOAuth2Principal.setUsername("dev@localhost");
|
||||
defaultOAuth2Principal.setUser(admin);
|
||||
save(defaultOAuth2Principal);
|
||||
}
|
||||
|
||||
private void createBeta(User user) {
|
||||
|
@ -0,0 +1,15 @@
|
||||
package se.su.dsv.scipro.system;
|
||||
|
||||
/**
|
||||
* Information about the current authentication context.
|
||||
* <p>
|
||||
* The difference between this and {@link CurrentUser} is that a user can be
|
||||
* authenticated without being a user in the system. This can happen when a
|
||||
* user logs in for the first time via SSO. The {@link #set(User)} method can
|
||||
* be used if the user can be imported based on the {@link #getPrincipalName()}.
|
||||
*/
|
||||
public interface AuthenticationContext extends CurrentUser {
|
||||
void set(User user);
|
||||
|
||||
String getPrincipalName();
|
||||
}
|
@ -171,6 +171,7 @@ public class ImporterTransactionsImpl implements ImporterTransactions {
|
||||
username.setUsername(completeUsername);
|
||||
username.setUser(local);
|
||||
userNameService.save(username);
|
||||
local.getUsernames().add(username);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,3 +13,14 @@ services:
|
||||
- CLIENT_REDIRECT_URI=http://localhost:59732/
|
||||
- RESOURCE_SERVER_ID=scipro-api-client
|
||||
- RESOURCE_SERVER_SECRET=scipro-api-secret
|
||||
oauth2-wicket:
|
||||
build:
|
||||
context: https://github.com/dsv-su/toker.git
|
||||
dockerfile: embedded.Dockerfile
|
||||
restart: on-failure
|
||||
ports:
|
||||
- '59734:8080'
|
||||
environment:
|
||||
- CLIENT_ID=scipro
|
||||
- CLIENT_SECRET=s3cr3t
|
||||
- CLIENT_REDIRECT_URI=http://localhost:8080/login/oauth2/code/scipro
|
||||
|
@ -1,14 +0,0 @@
|
||||
package se.su.dsv.scipro;
|
||||
|
||||
import org.apache.wicket.Session;
|
||||
import se.su.dsv.scipro.session.SciProSession;
|
||||
import se.su.dsv.scipro.system.CurrentUser;
|
||||
import se.su.dsv.scipro.system.User;
|
||||
|
||||
public class CurrentUserFromWicketSession implements CurrentUser {
|
||||
|
||||
@Override
|
||||
public User get() {
|
||||
return Session.exists() ? SciProSession.get().getUser() : null;
|
||||
}
|
||||
}
|
@ -1,12 +1,12 @@
|
||||
package se.su.dsv.scipro.loginlogout.pages;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import se.su.dsv.scipro.basepages.PublicPage;
|
||||
import se.su.dsv.scipro.security.auth.Authorization;
|
||||
import se.su.dsv.scipro.session.SciProSession;
|
||||
import se.su.dsv.scipro.system.AuthenticationContext;
|
||||
import se.su.dsv.scipro.system.User;
|
||||
import se.su.dsv.scipro.system.UserImportService;
|
||||
import se.su.dsv.scipro.system.UserService;
|
||||
@ -20,8 +20,11 @@ public class SSOPage extends PublicPage {
|
||||
@Inject
|
||||
private UserService userService;
|
||||
|
||||
@Inject
|
||||
private AuthenticationContext authenticationContext;
|
||||
|
||||
public SSOPage() {
|
||||
String remoteUserName = ((HttpServletRequest) getRequest().getContainerRequest()).getRemoteUser();
|
||||
String remoteUserName = authenticationContext.getPrincipalName();
|
||||
User user = userService.findByUsername(remoteUserName);
|
||||
|
||||
if (user != null) {
|
||||
|
@ -1,81 +0,0 @@
|
||||
package se.su.dsv.scipro.security.auth;
|
||||
|
||||
import jakarta.servlet.*;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequestWrapper;
|
||||
import java.io.IOException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Throw-away implementation of a servlet filter, main task is to fake the getRemoteUser() call for the request chain.
|
||||
*/
|
||||
public final class MockRemoteUserFilter implements Filter {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(MockRemoteUserFilter.class);
|
||||
//Default value unless supplied via init parameter
|
||||
private String fakedUser = "SOME_GUY";
|
||||
private FilterConfig cfg = null;
|
||||
|
||||
/**
|
||||
* Default constructor.
|
||||
*/
|
||||
public MockRemoteUserFilter() {}
|
||||
|
||||
/**
|
||||
* @see Filter#destroy()
|
||||
*/
|
||||
@Override
|
||||
public void destroy() {
|
||||
cfg = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the passed request and alters the behavior of getRemoteUser() for later links of the chain.
|
||||
* @see Filter#doFilter(ServletRequest, ServletResponse, FilterChain)
|
||||
*/
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
LOGGER.debug("Faking external authentication user: " + fakedUser);
|
||||
if (cfg != null) {
|
||||
HttpServletRequestWrapper wrapper = new ModifiedRemoteUserRequestWrapper(
|
||||
(HttpServletRequest) request,
|
||||
fakedUser
|
||||
);
|
||||
// pass the request along the filter chain
|
||||
chain.doFilter(wrapper, response);
|
||||
return;
|
||||
}
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see Filter#init(FilterConfig)
|
||||
*/
|
||||
@Override
|
||||
public void init(FilterConfig fConfig) {
|
||||
cfg = fConfig;
|
||||
if (cfg != null) {
|
||||
fakedUser = cfg.getInitParameter("fakedUser");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Private RequestWrapper, of no interest to anyone outside of this class.
|
||||
*/
|
||||
static class ModifiedRemoteUserRequestWrapper extends HttpServletRequestWrapper {
|
||||
|
||||
private final String fakedUser;
|
||||
|
||||
ModifiedRemoteUserRequestWrapper(final HttpServletRequest request, final String fakedUser) {
|
||||
super(request);
|
||||
this.fakedUser = fakedUser;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRemoteUser() {
|
||||
return fakedUser;
|
||||
}
|
||||
}
|
||||
}
|
@ -5,24 +5,21 @@ import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import org.apache.wicket.MetaDataKey;
|
||||
import org.apache.wicket.Session;
|
||||
import org.apache.wicket.injection.Injector;
|
||||
import org.apache.wicket.protocol.http.WebSession;
|
||||
import org.apache.wicket.request.Request;
|
||||
import se.su.dsv.scipro.security.auth.roles.IRole;
|
||||
import se.su.dsv.scipro.security.auth.roles.Roles;
|
||||
import se.su.dsv.scipro.system.AuthenticationContext;
|
||||
import se.su.dsv.scipro.system.ProjectModule;
|
||||
import se.su.dsv.scipro.system.SystemModule;
|
||||
import se.su.dsv.scipro.system.User;
|
||||
import se.su.dsv.scipro.system.UserService;
|
||||
|
||||
public class SciProSession extends WebSession {
|
||||
|
||||
private static final MetaDataKey<Long> LOGGED_IN_USER_ID = new MetaDataKey<>() {};
|
||||
|
||||
@Inject
|
||||
private UserService userService;
|
||||
private AuthenticationContext authenticationContext;
|
||||
|
||||
private Set<ProjectModule> projectModules = new HashSet<>();
|
||||
private Set<SystemModule> systemModules = new HashSet<>();
|
||||
@ -37,15 +34,15 @@ public class SciProSession extends WebSession {
|
||||
}
|
||||
|
||||
public synchronized void setUser(User user) {
|
||||
setMetaData(LOGGED_IN_USER_ID, user.getId());
|
||||
authenticationContext.set(user);
|
||||
}
|
||||
|
||||
public synchronized User getUser() {
|
||||
return isLoggedIn() ? userService.findOne(getMetaData(LOGGED_IN_USER_ID)) : null;
|
||||
return authenticationContext.get();
|
||||
}
|
||||
|
||||
public synchronized boolean isLoggedIn() {
|
||||
return getMetaData(LOGGED_IN_USER_ID) != null;
|
||||
return authenticationContext.get() != null;
|
||||
}
|
||||
|
||||
public synchronized boolean authorizedForRole(Roles role) {
|
||||
|
@ -3,7 +3,6 @@ package se.su.dsv.scipro;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.common.eventbus.EventBus;
|
||||
import java.lang.reflect.Field;
|
||||
@ -119,12 +118,11 @@ import se.su.dsv.scipro.springdata.services.UnitService;
|
||||
import se.su.dsv.scipro.springdata.services.UserProfileService;
|
||||
import se.su.dsv.scipro.supervisor.pages.SupervisorStartPage;
|
||||
import se.su.dsv.scipro.survey.SurveyService;
|
||||
import se.su.dsv.scipro.system.CurrentUser;
|
||||
import se.su.dsv.scipro.system.AuthenticationContext;
|
||||
import se.su.dsv.scipro.system.ExternalResourceService;
|
||||
import se.su.dsv.scipro.system.FooterAddressRepo;
|
||||
import se.su.dsv.scipro.system.FooterLinkService;
|
||||
import se.su.dsv.scipro.system.GenericService;
|
||||
import se.su.dsv.scipro.system.Lifecycle;
|
||||
import se.su.dsv.scipro.system.PasswordRepo;
|
||||
import se.su.dsv.scipro.system.PasswordService;
|
||||
import se.su.dsv.scipro.system.ProjectModule;
|
||||
@ -369,7 +367,7 @@ public abstract class SciProTest {
|
||||
protected ChecklistAnswerService checklistAnswerService;
|
||||
|
||||
@Mock
|
||||
protected CurrentUser currentUser;
|
||||
protected AuthenticationContext authenticationContext;
|
||||
|
||||
@Mock
|
||||
private Scheduler scheduler;
|
||||
|
@ -41,6 +41,10 @@
|
||||
<artifactId>spring-boot-starter-tomcat</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-oauth2-client</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-orm</artifactId>
|
||||
|
@ -0,0 +1,98 @@
|
||||
package se.su.dsv.scipro.war;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Provider;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.security.Principal;
|
||||
import java.util.Collections;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
||||
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
|
||||
import org.springframework.security.web.context.SecurityContextRepository;
|
||||
import se.su.dsv.scipro.system.AuthenticationContext;
|
||||
import se.su.dsv.scipro.system.User;
|
||||
import se.su.dsv.scipro.system.UserService;
|
||||
import se.su.dsv.scipro.system.Username;
|
||||
|
||||
public class CurrentUserFromSpringSecurity implements AuthenticationContext {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
// injecting providers since this is a singleton and the request and response are not
|
||||
private final Provider<HttpServletRequest> currentRequest;
|
||||
private final Provider<HttpServletResponse> currentResponse;
|
||||
|
||||
// hardcoded since that is what Spring Security does (see SwitchUserFilter)
|
||||
private final SecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository();
|
||||
|
||||
@Inject
|
||||
public CurrentUserFromSpringSecurity(
|
||||
UserService userService,
|
||||
Provider<HttpServletRequest> currentRequest,
|
||||
Provider<HttpServletResponse> currentResponse
|
||||
) {
|
||||
this.userService = userService;
|
||||
this.currentRequest = currentRequest;
|
||||
this.currentResponse = currentResponse;
|
||||
}
|
||||
|
||||
@Override
|
||||
public User get() {
|
||||
SecurityContext context = SecurityContextHolder.getContext();
|
||||
Authentication authentication = context.getAuthentication();
|
||||
if (authentication == null) {
|
||||
return null;
|
||||
}
|
||||
String username = authentication.getName();
|
||||
return userService.findByUsername(username);
|
||||
}
|
||||
|
||||
// Implementing switch user manually rather than using the built-in Spring Security switch user feature
|
||||
// due to compatibility with Wicket.
|
||||
// Wicket does not supply a form with a username field since it has some JavaScript based auto-complete
|
||||
// person finder.
|
||||
// See Spring's SwitchUserFilter for the built-in switch user feature from where most of the code is copied.
|
||||
@Override
|
||||
public void set(User user) {
|
||||
SecurityContextHolderStrategy strategy = SecurityContextHolder.getContextHolderStrategy();
|
||||
SecurityContext context = strategy.createEmptyContext();
|
||||
WicketControlledPrincipal principal = new WicketControlledPrincipal(user);
|
||||
UsernamePasswordAuthenticationToken targetUser = UsernamePasswordAuthenticationToken.authenticated(
|
||||
principal,
|
||||
null,
|
||||
Collections.emptyList()
|
||||
);
|
||||
context.setAuthentication(targetUser);
|
||||
strategy.setContext(context);
|
||||
this.securityContextRepository.saveContext(context, currentRequest.get(), currentResponse.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPrincipalName() {
|
||||
SecurityContext context = SecurityContextHolder.getContext();
|
||||
Authentication authentication = context.getAuthentication();
|
||||
if (authentication == null) {
|
||||
return null;
|
||||
}
|
||||
return authentication.getName();
|
||||
}
|
||||
|
||||
private static final class WicketControlledPrincipal implements Principal {
|
||||
|
||||
private final String username;
|
||||
|
||||
public WicketControlledPrincipal(User user) {
|
||||
// extract any username so that we can look it up later
|
||||
this.username = user.getUsernames().stream().findAny().map(Username::getUsername).orElse("<unknown>");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return username;
|
||||
}
|
||||
}
|
||||
}
|
@ -23,7 +23,6 @@ import org.springframework.core.task.SimpleAsyncTaskExecutor;
|
||||
import org.springframework.orm.jpa.SharedEntityManagerCreator;
|
||||
import org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter;
|
||||
import se.su.dsv.scipro.CoreConfig;
|
||||
import se.su.dsv.scipro.CurrentUserFromWicketSession;
|
||||
import se.su.dsv.scipro.FileSystemStore;
|
||||
import se.su.dsv.scipro.RepositoryConfiguration;
|
||||
import se.su.dsv.scipro.file.FileStore;
|
||||
@ -85,11 +84,6 @@ public class Main extends SpringBootServletInitializer implements ServletContain
|
||||
return currentProfile;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CurrentUserFromWicketSession currentUserFromWicketSession() {
|
||||
return new CurrentUserFromWicketSession();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public FileStore fileStore() {
|
||||
return new FileSystemStore();
|
||||
|
@ -1,6 +1,9 @@
|
||||
package se.su.dsv.scipro.war;
|
||||
|
||||
import com.google.common.eventbus.EventBus;
|
||||
import jakarta.inject.Provider;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.apache.wicket.protocol.http.WebApplication;
|
||||
import org.apache.wicket.protocol.http.WicketFilter;
|
||||
import org.apache.wicket.spring.injection.annot.SpringComponentInjector;
|
||||
@ -8,6 +11,10 @@ import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import se.su.dsv.scipro.SciProApplication;
|
||||
import se.su.dsv.scipro.crosscutting.ForwardPhase2Feedback;
|
||||
import se.su.dsv.scipro.crosscutting.NotifyFailedReflection;
|
||||
@ -21,6 +28,7 @@ import se.su.dsv.scipro.notifications.NotificationController;
|
||||
import se.su.dsv.scipro.profiles.CurrentProfile;
|
||||
import se.su.dsv.scipro.reviewing.FinalSeminarApprovalService;
|
||||
import se.su.dsv.scipro.reviewing.RoughDraftApprovalService;
|
||||
import se.su.dsv.scipro.system.UserService;
|
||||
|
||||
@Configuration
|
||||
public class WicketConfiguration {
|
||||
@ -49,6 +57,25 @@ public class WicketConfiguration {
|
||||
return new SciProApplication(currentProfile);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(3) // make sure it's after the API security filters
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http.authorizeHttpRequests(requests -> requests.anyRequest().authenticated());
|
||||
http.oauth2Login(Customizer.withDefaults());
|
||||
http.csrf(csrf -> csrf.disable()); // Wicket has its own CSRF protection
|
||||
http.logout(logout -> logout.logoutUrl("/logout").logoutSuccessUrl("/"));
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CurrentUserFromSpringSecurity currentUserFromSpringSecurity(
|
||||
UserService userService,
|
||||
Provider<HttpServletRequest> httpServletRequestProvider,
|
||||
Provider<HttpServletResponse> httpServletResponseProvider
|
||||
) {
|
||||
return new CurrentUserFromSpringSecurity(userService, httpServletRequestProvider, httpServletResponseProvider);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ReviewingNotifications reviewingNotifications(
|
||||
EventBus eventBus,
|
||||
|
@ -19,3 +19,14 @@ springdoc.swagger-ui.persist-authorization=true
|
||||
spring.security.oauth2.resourceserver.opaquetoken.client-id=scipro-api-client
|
||||
spring.security.oauth2.resourceserver.opaquetoken.client-secret=scipro-api-secret
|
||||
spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=http://localhost:59733/introspect
|
||||
|
||||
# Log in via local OAuth 2 authorization server
|
||||
spring.security.oauth2.client.provider.docker.user-info-uri=http://localhost:59734/verify
|
||||
spring.security.oauth2.client.provider.docker.user-name-attribute=sub
|
||||
spring.security.oauth2.client.provider.docker.token-uri=http://localhost:59734/exchange
|
||||
spring.security.oauth2.client.provider.docker.authorization-uri=http://localhost:59734/authorize
|
||||
spring.security.oauth2.client.registration.scipro.redirect-uri={baseUrl}/login/oauth2/code/{registrationId}
|
||||
spring.security.oauth2.client.registration.scipro.provider=docker
|
||||
spring.security.oauth2.client.registration.scipro.client-id=scipro
|
||||
spring.security.oauth2.client.registration.scipro.client-secret=s3cr3t
|
||||
spring.security.oauth2.client.registration.scipro.authorization-grant-type=authorization_code
|
||||
|
Loading…
x
Reference in New Issue
Block a user