diff --git a/README.md b/README.md
index 749d07cb72..50ed89f290 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,7 @@
+## 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.
+
 ## 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).
diff --git a/core/src/main/java/se/su/dsv/scipro/system/AuthenticationContext.java b/core/src/main/java/se/su/dsv/scipro/system/AuthenticationContext.java
new file mode 100644
index 0000000000..9f46604358
--- /dev/null
+++ b/core/src/main/java/se/su/dsv/scipro/system/AuthenticationContext.java
@@ -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();
+}
diff --git a/daisy-integration/src/main/java/se/su/dsv/scipro/daisyExternal/impl/ImporterTransactionsImpl.java b/daisy-integration/src/main/java/se/su/dsv/scipro/daisyExternal/impl/ImporterTransactionsImpl.java
index 9e6ea05554..eaa367e3d0 100644
--- a/daisy-integration/src/main/java/se/su/dsv/scipro/daisyExternal/impl/ImporterTransactionsImpl.java
+++ b/daisy-integration/src/main/java/se/su/dsv/scipro/daisyExternal/impl/ImporterTransactionsImpl.java
@@ -157,6 +157,7 @@ public class ImporterTransactionsImpl implements ImporterTransactions {
                 username.setUsername(completeUsername);
                 username.setUser(local);
                 userNameService.save(username);
+                local.getUsernames().add(username);
             }
         }
     }
diff --git a/docker-compose.yml b/docker-compose.yml
index 637455a39e..aac221e274 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -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
diff --git a/view/src/main/java/se/su/dsv/scipro/CurrentUserFromWicketSession.java b/view/src/main/java/se/su/dsv/scipro/CurrentUserFromWicketSession.java
deleted file mode 100644
index 808992e771..0000000000
--- a/view/src/main/java/se/su/dsv/scipro/CurrentUserFromWicketSession.java
+++ /dev/null
@@ -1,13 +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;
-    }
-}
diff --git a/view/src/main/java/se/su/dsv/scipro/loginlogout/pages/SSOPage.java b/view/src/main/java/se/su/dsv/scipro/loginlogout/pages/SSOPage.java
index 5ebf9f0f0f..371c58afe8 100644
--- a/view/src/main/java/se/su/dsv/scipro/loginlogout/pages/SSOPage.java
+++ b/view/src/main/java/se/su/dsv/scipro/loginlogout/pages/SSOPage.java
@@ -4,11 +4,12 @@ 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.User;
+import se.su.dsv.scipro.system.AuthenticationContext;
 import se.su.dsv.scipro.system.UserImportService;
 import se.su.dsv.scipro.system.UserService;
 
 import jakarta.inject.Inject;
-import jakarta.servlet.http.HttpServletRequest;
+
 import java.util.Optional;
 import java.util.Set;
 
@@ -21,8 +22,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) {
diff --git a/view/src/main/java/se/su/dsv/scipro/security/auth/MockRemoteUserFilter.java b/view/src/main/java/se/su/dsv/scipro/security/auth/MockRemoteUserFilter.java
deleted file mode 100755
index b67ea8916d..0000000000
--- a/view/src/main/java/se/su/dsv/scipro/security/auth/MockRemoteUserFilter.java
+++ /dev/null
@@ -1,70 +0,0 @@
-package se.su.dsv.scipro.security.auth;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import jakarta.servlet.*;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletRequestWrapper;
-import java.io.IOException;
-
-/**
- * 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;
-		}
-	}
-}
diff --git a/view/src/main/java/se/su/dsv/scipro/session/SciProSession.java b/view/src/main/java/se/su/dsv/scipro/session/SciProSession.java
index 6d7d573de8..c0184059ff 100755
--- a/view/src/main/java/se/su/dsv/scipro/session/SciProSession.java
+++ b/view/src/main/java/se/su/dsv/scipro/session/SciProSession.java
@@ -1,18 +1,18 @@
 package se.su.dsv.scipro.session;
 
-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;
 
 import jakarta.inject.Inject;
+
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Locale;
@@ -20,10 +20,8 @@ import java.util.Set;
 
 public class SciProSession extends WebSession {
 
-    private final static 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<>();
@@ -38,15 +36,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) {
diff --git a/view/src/test/java/se/su/dsv/scipro/SciProTest.java b/view/src/test/java/se/su/dsv/scipro/SciProTest.java
index 2b469c39ec..0b9bbef520 100755
--- a/view/src/test/java/se/su/dsv/scipro/SciProTest.java
+++ b/view/src/test/java/se/su/dsv/scipro/SciProTest.java
@@ -111,12 +111,10 @@ 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.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;
@@ -124,6 +122,7 @@ import se.su.dsv.scipro.system.ProjectTypeService;
 import se.su.dsv.scipro.system.ResearchAreaService;
 import se.su.dsv.scipro.system.SystemModule;
 import se.su.dsv.scipro.system.User;
+import se.su.dsv.scipro.system.AuthenticationContext;
 import se.su.dsv.scipro.system.UserImportService;
 import se.su.dsv.scipro.system.UserNameService;
 import se.su.dsv.scipro.system.UserSearchService;
@@ -298,7 +297,7 @@ public abstract class SciProTest {
     @Mock
     protected ChecklistAnswerService checklistAnswerService;
     @Mock
-    protected CurrentUser currentUser;
+    protected AuthenticationContext authenticationContext;
     @Mock
     private Scheduler scheduler;
     @Mock
diff --git a/war/pom.xml b/war/pom.xml
index 2c4ad17242..456eeae393 100644
--- a/war/pom.xml
+++ b/war/pom.xml
@@ -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>
diff --git a/war/src/main/java/se/su/dsv/scipro/war/CurrentUserFromSpringSecurity.java b/war/src/main/java/se/su/dsv/scipro/war/CurrentUserFromSpringSecurity.java
new file mode 100644
index 0000000000..f10db76cd6
--- /dev/null
+++ b/war/src/main/java/se/su/dsv/scipro/war/CurrentUserFromSpringSecurity.java
@@ -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 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.User;
+import se.su.dsv.scipro.system.AuthenticationContext;
+import se.su.dsv.scipro.system.UserService;
+import se.su.dsv.scipro.system.Username;
+
+import java.security.Principal;
+import java.util.Collections;
+
+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;
+        }
+    }
+}
diff --git a/war/src/main/java/se/su/dsv/scipro/war/Main.java b/war/src/main/java/se/su/dsv/scipro/war/Main.java
index 026130dd7d..3f50c122c5 100644
--- a/war/src/main/java/se/su/dsv/scipro/war/Main.java
+++ b/war/src/main/java/se/su/dsv/scipro/war/Main.java
@@ -18,7 +18,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;
@@ -83,11 +82,6 @@ public class Main extends SpringBootServletInitializer implements ServletContain
         return currentProfile;
     }
 
-    @Bean
-    public CurrentUserFromWicketSession currentUserFromWicketSession() {
-        return new CurrentUserFromWicketSession();
-    }
-
     @Bean
     public FileStore fileStore() {
         return new FileSystemStore();
diff --git a/war/src/main/java/se/su/dsv/scipro/war/WicketConfiguration.java b/war/src/main/java/se/su/dsv/scipro/war/WicketConfiguration.java
index bb255d4915..bd9b3d02f7 100644
--- a/war/src/main/java/se/su/dsv/scipro/war/WicketConfiguration.java
+++ b/war/src/main/java/se/su/dsv/scipro/war/WicketConfiguration.java
@@ -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 {
@@ -47,6 +55,28 @@ 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.jee(Customizer.withDefaults()); // Shibboleth integration
+        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,
diff --git a/war/src/main/resources/application.properties b/war/src/main/resources/application.properties
index 753d0e323c..54f5c34fad 100644
--- a/war/src/main/resources/application.properties
+++ b/war/src/main/resources/application.properties
@@ -18,3 +18,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