Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spring 6 Upgrade #37

Merged
merged 3 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions .github/workflows/maven.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,10 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- name: Set up JDK 1.8
uses: actions/setup-java@v3
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: 8
java-version-file: .java-version
distribution: temurin
cache: maven
- name: Build with Maven
Expand Down
1 change: 1 addition & 0 deletions .java-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
17
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,8 @@ then if that completes successfully a release bundle can be pushed to the stagin

We don't automatically close the staged artifacts so after checking that the files are ok you should login to the [repository](https://oss.sonatype.org/) and release it. The version in the README.md should also be updated so that people using the project get the latest version and the demo project should be updated to use the latest version.

## References

- Learning Tools Interoperability Core Specification - https://www.imsglobal.org/spec/lti/v1p3
- 1 EdTech Security Framework - https://www.imsglobal.org/spec/lti/v1p3
- OpenID Connect Core - https://openid.net/specs/openid-connect-core-1_0.html
65 changes: 39 additions & 26 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
<artifactId>spring-security-lti13</artifactId>
<version>0.1.12-SNAPSHOT</version>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring.version>5.3.25</spring.version>
<spring.security.version>5.7.7</spring.security.version>
<spring.security.version>6.3.2</spring.security.version>
<!-- This should mirror the version used in Spring Security -->
<spring.version>6.1.12</spring.version>
</properties>

<name>Spring Security LTI 1.3</name>
Expand All @@ -34,8 +35,8 @@
<developers>
<developer>
<name>Matthew Buckett</name>
<email>matthew.buckett@ctl.ox.ac.uk</email>
<organization>Centre for Teaching and Learning, University of Oxford</organization>
<email>matthew.buckett@it.ox.ac.uk</email>
<organization>IT Services, University of Oxford</organization>
</developer>
</developers>

Expand Down Expand Up @@ -100,46 +101,56 @@
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.4.2</version>
<version>2.17.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.0.1</version>
<scope>provided</scope>
<groupId>jakarta.activation</groupId>
<artifactId>jakarta.activation-api</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>jakarta.platform</groupId>
<artifactId>jakarta.jakartaee-api</artifactId>
<version>10.0.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.29</version>
<version>2.0.13</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.0.0-jre</version>
<version>33.3.0-jre</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.9.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-library</artifactId>
<version>2.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.1.0</version>
<version>5.11.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.29</version>
<version>2.0.13</version>
<scope>test</scope>
</dependency>
<dependency>
Expand Down Expand Up @@ -226,6 +237,13 @@
</plugins>
</build>

<repositories>
<repository>
<id>maven_central</id>
<name>Maven Central</name>
<url>https://repo.maven.apache.org/maven2/</url>
</repository>
</repositories>

<profiles>
<profile>
Expand All @@ -249,12 +267,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.4.0</version>
<!-- This is to fix building javadoc with JDK 11.0.2
https://bugs.openjdk.java.net/browse/JDK-8212233 -->
<configuration>
<source>8</source>
</configuration>
<version>3.8.0</version>
<executions>
<execution>
<id>attach-javadocs</id>
Expand Down
23 changes: 20 additions & 3 deletions src/main/java/uk/ac/ox/ctl/lti13/Lti13Configurer.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.security.web.context.SecurityContextRepository;
import uk.ac.ox.ctl.lti13.security.oauth2.OAuthAuthenticationFailureHandler;
import uk.ac.ox.ctl.lti13.security.oauth2.client.lti.authentication.OidcLaunchFlowAuthenticationProvider;
import uk.ac.ox.ctl.lti13.security.oauth2.client.lti.authentication.TargetLinkUriAuthenticationSuccessHandler;
Expand Down Expand Up @@ -43,6 +45,8 @@ public class Lti13Configurer extends AbstractHttpConfigurer<Lti13Configurer, Htt
protected ApplicationEventPublisher applicationEventPublisher;
protected GrantedAuthoritiesMapper grantedAuthoritiesMapper;
protected boolean limitIpAddresses;
protected SecurityContextRepository securityContextRepository;


public Lti13Configurer ltiPath(String ltiPath) {
this.ltiPath = ltiPath;
Expand All @@ -69,6 +73,15 @@ public Lti13Configurer grantedAuthoritiesMapper(GrantedAuthoritiesMapper granted
return this;
}

/**
* This security context repository to persist the authentication in. This is useful if you want to use
* HTTP sessions for authentication.
*/
public Lti13Configurer setSecurityContextRepository(SecurityContextRepository securityContextRepository) {
this.securityContextRepository = securityContextRepository;
return this;
}

/**
* Using this may cause problems for users who are behind a proxy or NAT setup that uses different IP addresses
* for different requests, even if they are close together in time.
Expand All @@ -86,12 +99,13 @@ public void init(HttpSecurity http) {
// Allow LTI launches to bypass CSRF protection
CsrfConfigurer<HttpSecurity> configurer = http.getConfigurer(CsrfConfigurer.class);
if (configurer != null) {
configurer.ignoringAntMatchers(ltiPath + "/**");
// I'm not sure about this.
configurer.ignoringRequestMatchers(ltiPath + "/**");
}
// In the future we should use CSP to limit the domains that can embed this tool
HeadersConfigurer<HttpSecurity> headersConfigurer = http.getConfigurer(HeadersConfigurer.class);
if (headersConfigurer != null) {
headersConfigurer.frameOptions().disable();
headersConfigurer.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable);
}
}

Expand Down Expand Up @@ -147,8 +161,11 @@ protected OAuth2LoginAuthenticationFilter configureLoginFilter(ClientRegistratio
if (applicationEventPublisher != null) {
authenticationManager.setAuthenticationEventPublisher(new DefaultAuthenticationEventPublisher(applicationEventPublisher));
}
if (securityContextRepository != null) {
loginFilter.setSecurityContextRepository(securityContextRepository);
}
loginFilter.setAuthenticationManager(authenticationManager);
return loginFilter;
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

public class OAuthAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoderJwkSupport;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestOperations;
Expand Down Expand Up @@ -161,26 +163,26 @@ private OidcIdToken createOidcToken(ClientRegistration clientRegistration, Strin
}

private JwtDecoder getJwtDecoder(ClientRegistration clientRegistration) {
String jwkSetUri = clientRegistration.getProviderDetails().getJwkSetUri();
if (!StringUtils.hasText(jwkSetUri)) {
OAuth2Error oauth2Error = new OAuth2Error(
MISSING_SIGNATURE_VERIFIER_ERROR_CODE,
"Failed to find a Signature Verifier for Client Registration: '" +
clientRegistration.getRegistrationId() + "'. Check to ensure you have configured the JwkSet URI.",
null
);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
JwtDecoder jwtDecoder = this.jwtDecoders.get(jwkSetUri);
JwtDecoder jwtDecoder = this.jwtDecoders.get(clientRegistration.getRegistrationId());
if (jwtDecoder == null) {
if (!StringUtils.hasText(clientRegistration.getProviderDetails().getJwkSetUri())) {
OAuth2Error oauth2Error = new OAuth2Error(
MISSING_SIGNATURE_VERIFIER_ERROR_CODE,
"Failed to find a Signature Verifier for Client Registration: '" +
clientRegistration.getRegistrationId() + "'. Check to ensure you have configured the JwkSet URI.",
null
);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
// TODO This should look at the Cache-Control header so to expire old jwtDecoders.
// Canvas looks to rotate it's keys monthly.
NimbusJwtDecoderJwkSupport nimbusJwtDecoderJwkSupport = new NimbusJwtDecoderJwkSupport(jwkSetUri);
String jwkSetUri = clientRegistration.getProviderDetails().getJwkSetUri();
NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder decoderBuilder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).jwsAlgorithm(SignatureAlgorithm.from(JwsAlgorithms.RS256));
if (restOperations != null) {
nimbusJwtDecoderJwkSupport.setRestOperations(restOperations);
decoderBuilder.restOperations(restOperations);
}
jwtDecoder = nimbusJwtDecoderJwkSupport;
this.jwtDecoders.put(jwkSetUri, jwtDecoder);
jwtDecoder = decoderBuilder.build();
this.jwtDecoders.put(clientRegistration.getRegistrationId(), jwtDecoder);
}
return jwtDecoder;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
import uk.ac.ox.ctl.lti13.security.oauth2.client.lti.web.OptimisticAuthorizationRequestRepository;
import uk.ac.ox.ctl.lti13.security.oauth2.client.lti.web.StateCheckingAuthenticationSuccessHandler;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

/**
* This looks for the target URI in the final request (as it's signed by the platform).
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package uk.ac.ox.ctl.lti13.security.oauth2.client.lti.web;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public interface AuthorizationRedirectHandler {
Expand All @@ -12,7 +12,8 @@ public interface AuthorizationRedirectHandler {
* Send a redirect to the user to start authorization but make the authorization request available.
* @param request The HttpServletRequest that came in.
* @param response The HttpServletResponse that we are writing the output to.
* @param authorizationRequest Details of the OAuth request.
* @param authorizationRequest Details of the OAuth request.
* @throws IOException If there's a IO problem sending the redirect.
*/
void sendRedirect(HttpServletRequest request, HttpServletResponse response, OAuth2AuthorizationRequest authorizationRequest) throws IOException;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@
import java.util.LinkedHashMap;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;

import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
Expand Down Expand Up @@ -86,15 +86,17 @@ public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationReq
}

@Override
public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request) {
public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request,
HttpServletResponse response) {
Assert.notNull(response, "response cannot be null");
Assert.notNull(request, "request cannot be null");
String stateParameter = this.getStateParameter(request);
if (stateParameter == null) {
return null;
}
Map<String, OAuth2AuthorizationRequest> authorizationRequests = this.getAuthorizationRequests(request);
OAuth2AuthorizationRequest originalRequest = authorizationRequests.remove(stateParameter);
if (authorizationRequests.size() == 0) {
if (authorizationRequests.isEmpty()) {
request.getSession().removeAttribute(this.sessionAttributeName);
}
else if (authorizationRequests.size() == 1) {
Expand All @@ -107,13 +109,6 @@ else if (authorizationRequests.size() == 1) {
return originalRequest;
}

@Override
public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request,
HttpServletResponse response) {
Assert.notNull(response, "response cannot be null");
return this.removeAuthorizationRequest(request);
}

/**
* Gets the state parameter from the {@link HttpServletRequest}
* @param request the request to use
Expand All @@ -136,8 +131,7 @@ private Map<String, OAuth2AuthorizationRequest> getAuthorizationRequests(HttpSer
if (sessionAttributeValue == null) {
return new HashMap<>();
}
else if (sessionAttributeValue instanceof OAuth2AuthorizationRequest) {
OAuth2AuthorizationRequest auth2AuthorizationRequest = (OAuth2AuthorizationRequest) sessionAttributeValue;
else if (sessionAttributeValue instanceof OAuth2AuthorizationRequest auth2AuthorizationRequest) {
Map<String, OAuth2AuthorizationRequest> authorizationRequests = createLRUMap(maxConcurrentLogins);
authorizationRequests.put(auth2AuthorizationRequest.getState(), auth2AuthorizationRequest);
return authorizationRequests;
Expand Down Expand Up @@ -167,7 +161,7 @@ public void setMaxConcurrentLogins(int maxConcurrentLogins) {
}

/**
* Creates a least recently used hashmap.
* Creates least recently used hashmap.
* @see <a href="https://stackoverflow.com/questions/11469045/how-to-limit-the-maximum-size-of-a-map-by-removing-oldest-entries-when-limit-rea">Stackoverflow</a>
* @param maxEntries Maximum number of entries in the map
* @param <K> Key type.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package uk.ac.ox.ctl.lti13.security.oauth2.client.lti.web;

import org.springframework.security.oauth2.core.AuthorizationGrantType;

public interface LTIAuthorizationGrantType {

/**
* The LTI grant type when launching a tool.This is needed because Spring Security itself
* doesn't support the implicit grant type anymore.
*/
AuthorizationGrantType IMPLICIT = new AuthorizationGrantType("implicit");
}
Loading