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

Implement SHA-256 in token based Remember-Me services #10675

Closed
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
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -33,6 +33,7 @@
import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.RememberMeHashingAlgorithm;
import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices;
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
import org.springframework.util.Assert;
Expand Down Expand Up @@ -91,6 +92,8 @@ public final class RememberMeConfigurer<H extends HttpSecurityBuilder<H>>

private String key;

private RememberMeHashingAlgorithm hashingAlgorithm = RememberMeHashingAlgorithm.MD5;

private RememberMeServices rememberMeServices;

private LogoutHandler logoutHandler;
Expand Down Expand Up @@ -186,6 +189,24 @@ public RememberMeConfigurer<H> key(String key) {
return this;
}

/**
* The algorithm to use with {@link TokenBasedRememberMeServices} for hashing the
* digital signature containing the key from {@link #key(String)} and the user's
* password. If unset, the MD5 message digest algorithm will be used.
* <p>
* This configuration is ignored if
* {@link #tokenRepository(PersistentTokenRepository)} or
* {@link #rememberMeServices(RememberMeServices)} are used.
* @param hashingAlgorithm the algorithm used when creating new cookies
* @return the {@link RememberMeConfigurer} for further customization
* @since 5.7
*/
public RememberMeConfigurer<H> hashingAlgorithm(RememberMeHashingAlgorithm hashingAlgorithm) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since RememberMeServices can be constructed with relative ease, I'd recommend leaving this out of the DSL for the time being.

Assert.notNull(hashingAlgorithm, "hashingAlgorithm cannot be null");
this.hashingAlgorithm = hashingAlgorithm;
return this;
}

/**
* The HTTP parameter used to indicate to remember the user at time of login.
* @param rememberMeParameter the HTTP parameter used to indicate to remember the user
Expand Down Expand Up @@ -380,7 +401,7 @@ private AbstractRememberMeServices createRememberMeServices(H http, String key)
*/
private AbstractRememberMeServices createTokenBasedRememberMeServices(H http, String key) {
UserDetailsService userDetailsService = getUserDetailsService(http);
return new TokenBasedRememberMeServices(key, userDetailsService);
return new TokenBasedRememberMeServices(key, userDetailsService, this.hashingAlgorithm);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -58,6 +58,8 @@ class RememberMeBeanDefinitionParser implements BeanDefinitionParser {

static final String ATT_TOKEN_VALIDITY = "token-validity-seconds";

static final String ATT_HASHING_ALGORITHM = "hashing-algorithm";

static final String ATT_SECURE_COOKIE = "use-secure-cookie";

static final String ATT_FORM_REMEMBERME_PARAMETER = "remember-me-parameter";
Expand Down Expand Up @@ -88,6 +90,7 @@ public BeanDefinition parse(Element element, ParserContext pc) {
String successHandlerRef = element.getAttribute(ATT_SUCCESS_HANDLER_REF);
String rememberMeServicesRef = element.getAttribute(ATT_SERVICES_REF);
String tokenValiditySeconds = element.getAttribute(ATT_TOKEN_VALIDITY);
String hashingAlgorithm = element.getAttribute(ATT_HASHING_ALGORITHM);
String useSecureCookie = element.getAttribute(ATT_SECURE_COOKIE);
String remembermeParameter = element.getAttribute(ATT_FORM_REMEMBERME_PARAMETER);
String remembermeCookie = element.getAttribute(ATT_REMEMBERME_COOKIE);
Expand All @@ -99,15 +102,16 @@ public BeanDefinition parse(Element element, ParserContext pc) {
boolean userServiceSet = StringUtils.hasText(userServiceRef);
boolean useSecureCookieSet = StringUtils.hasText(useSecureCookie);
boolean tokenValiditySet = StringUtils.hasText(tokenValiditySeconds);
boolean hashingAlgorithmSet = StringUtils.hasText(hashingAlgorithm);
boolean remembermeParameterSet = StringUtils.hasText(remembermeParameter);
boolean remembermeCookieSet = StringUtils.hasText(remembermeCookie);
if (servicesRefSet && (dataSourceSet || tokenRepoSet || userServiceSet || tokenValiditySet || useSecureCookieSet
|| remembermeParameterSet || remembermeCookieSet)) {
|| remembermeParameterSet || remembermeCookieSet || hashingAlgorithmSet)) {
pc.getReaderContext()
.error(ATT_SERVICES_REF + " can't be used in combination with attributes " + ATT_TOKEN_REPOSITORY
+ "," + ATT_DATA_SOURCE + ", " + ATT_USER_SERVICE_REF + ", " + ATT_TOKEN_VALIDITY + ", "
+ ATT_SECURE_COOKIE + ", " + ATT_FORM_REMEMBERME_PARAMETER + " or " + ATT_REMEMBERME_COOKIE,
source);
+ ATT_SECURE_COOKIE + ", " + ATT_FORM_REMEMBERME_PARAMETER + ", " + ATT_HASHING_ALGORITHM
+ " or " + ATT_REMEMBERME_COOKIE, source);
}
if (dataSourceSet && tokenRepoSet) {
pc.getReaderContext().error("Specify " + ATT_TOKEN_REPOSITORY + " or " + ATT_DATA_SOURCE + " but not both",
Expand All @@ -129,6 +133,9 @@ public BeanDefinition parse(Element element, ParserContext pc) {
}
else if (!servicesRefSet) {
services = new RootBeanDefinition(TokenBasedRememberMeServices.class);
if (hashingAlgorithmSet) {
services.getConstructorArgumentValues().addIndexedArgumentValue(2, hashingAlgorithm);
}
}
String servicesName;
if (services != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -790,6 +790,9 @@ remember-me.attlist &=
remember-me.attlist &=
## The name of cookie which store the token for remember-me authentication. Defaults to 'remember-me'.
attribute remember-me-cookie {xsd:token}?
remember-me.attlist &=
## The algorithm of cookie which store the token for remember-me authentication.
attribute hashing-algorithm {"MD5" | "SHA256"}?

token-repository-ref =
## Reference to a PersistentTokenRepository bean for use with the persistent token remember-me implementation.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2353,6 +2353,18 @@
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="hashing-algorithm">
<xs:annotation>
<xs:documentation>The algorithm of cookie which store the token for remember-me authentication.
</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:token">
<xs:enumeration value="MD5"/>
<xs:enumeration value="SHA256"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
</xs:attributeGroup>
<xs:attributeGroup name="token-repository-ref">
<xs:attribute name="token-repository-ref" use="required" type="xs:token">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,6 +16,7 @@

package org.springframework.security.config.annotation.web.configurers;

import java.util.Base64;
import java.util.Collections;

import javax.servlet.http.Cookie;
Expand All @@ -30,6 +31,7 @@
import org.springframework.mock.web.MockHttpSession;
import org.springframework.security.authentication.RememberMeAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
Expand All @@ -42,8 +44,10 @@
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.RememberMeServices;
import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.RememberMeHashingAlgorithm;
import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
Expand Down Expand Up @@ -100,7 +104,8 @@ public void postWhenNoUserDetailsServiceThenException() {
@Test
public void configureWhenRegisteringObjectPostProcessorThenInvokedOnRememberMeAuthenticationFilter() {
this.spring.register(ObjectPostProcessorConfig.class).autowire();
verify(ObjectPostProcessorConfig.objectPostProcessor).postProcess(any(RememberMeAuthenticationFilter.class));
verify(this.spring.getContext().getBean(ObjectPostProcessor.class))
.postProcess(any(RememberMeAuthenticationFilter.class));
}

@Test
Expand Down Expand Up @@ -130,6 +135,41 @@ public void loginWhenRememberMeTrueThenRespondsWithRememberMeCookie() throws Exc
this.mvc.perform(request).andExpect(cookie().exists("remember-me"));
}

@Test
public void loginWhenSha256HashingAlgorithmThenRespondsWithSha256RememberMeCookie() throws Exception {
this.spring.register(RememberMeWithSha256Config.class).autowire();
// @formatter:off
MockHttpServletRequestBuilder request = post("/login")
.with(csrf())
.param("username", "user")
.param("password", "password")
.param("remember-me", "true");
// @formatter:on
MvcResult result = this.mvc.perform(request).andReturn();
Cookie rememberMe = result.getResponse().getCookie("remember-me");
assertThat(rememberMe).isNotNull();
assertThat(new String(Base64.getDecoder().decode(rememberMe.getValue())))
.contains(RememberMeHashingAlgorithm.SHA256.name());
}

@Test
public void loginWhenSha256HashingAlgorithmConfiguredInLambdaThenRespondsWithSha256RememberMeCookie()
throws Exception {
this.spring.register(RememberMeWithSha256InLambdaConfig.class).autowire();
// @formatter:off
MockHttpServletRequestBuilder request = post("/login")
.with(csrf())
.param("username", "user")
.param("password", "password")
.param("remember-me", "true");
// @formatter:on
MvcResult result = this.mvc.perform(request).andReturn();
Cookie rememberMe = result.getResponse().getCookie("remember-me");
assertThat(rememberMe).isNotNull();
assertThat(new String(Base64.getDecoder().decode(rememberMe.getValue())))
.contains(RememberMeHashingAlgorithm.SHA256.name());
}

@Test
public void getWhenRememberMeCookieThenAuthenticationIsRememberMeAuthenticationToken() throws Exception {
this.spring.register(RememberMeConfig.class).autowire();
Expand Down Expand Up @@ -301,14 +341,14 @@ protected void configure(AuthenticationManagerBuilder auth) {
@EnableWebSecurity
static class ObjectPostProcessorConfig extends WebSecurityConfigurerAdapter {

static ObjectPostProcessor<Object> objectPostProcessor = spy(ReflectingObjectPostProcessor.class);
ObjectPostProcessor<Object> objectPostProcessor = spy(ReflectingObjectPostProcessor.class);

@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.rememberMe()
.userDetailsService(new AuthenticationManagerBuilder(objectPostProcessor).getDefaultUserDetailsService());
.userDetailsService(new AuthenticationManagerBuilder(this.objectPostProcessor).getDefaultUserDetailsService());
// @formatter:on
}

Expand All @@ -321,8 +361,8 @@ protected void configure(AuthenticationManagerBuilder auth) throws Exception {
}

@Bean
static ObjectPostProcessor<Object> objectPostProcessor() {
return objectPostProcessor;
ObjectPostProcessor<Object> objectPostProcessor() {
return this.objectPostProcessor;
}

}
Expand Down Expand Up @@ -397,6 +437,58 @@ void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {

}

@EnableWebSecurity
static class RememberMeWithSha256Config {

@Bean
SecurityFilterChain app(HttpSecurity http, UserDetailsService userDetailsService) throws Exception {
// @formatter:off
http
.authorizeRequests()
.anyRequest().hasRole("USER")
.and()
.formLogin()
.and()
.rememberMe()
.hashingAlgorithm(RememberMeHashingAlgorithm.SHA256)
.userDetailsService(userDetailsService);
// @formatter:on
return http.build();
}

@Bean
UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager(PasswordEncodedUser.user());
}

}

@EnableWebSecurity
static class RememberMeWithSha256InLambdaConfig {

@Bean
SecurityFilterChain app(HttpSecurity http, UserDetailsService userDetailsService) throws Exception {
// @formatter:off
http
.authorizeRequests((requests) -> requests
.anyRequest().hasRole("USER")
)
.formLogin(Customizer.withDefaults())
.rememberMe((rememberMe) -> rememberMe
.hashingAlgorithm(RememberMeHashingAlgorithm.SHA256)
.userDetailsService(userDetailsService)
);
// @formatter:on
return http.build();
}

@Bean
UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager(PasswordEncodedUser.user());
}

}

@EnableWebSecurity
static class RememberMeInLambdaConfig extends WebSecurityConfigurerAdapter {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,6 +16,7 @@

package org.springframework.security.config.http;

import java.util.Base64;
import java.util.Collections;

import javax.servlet.http.Cookie;
Expand All @@ -34,6 +35,7 @@
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.RememberMeHashingAlgorithm;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultActions;
Expand Down Expand Up @@ -158,6 +160,16 @@ public void logoutWhenUsingRememberMeDefaultsThenCookieIsCancelled() throws Exce
// @formatter:on
}

@Test
public void configureWithHashingAlgorithm() throws Exception {
this.spring.configLocations(xml("Sha256Config")).autowire();
MvcResult result = rememberAuthentication("user", "password").andReturn();
Cookie cookie = rememberMeCookie(result);
assertThat(cookie).isNotNull();
assertThat(new String(Base64.getDecoder().decode(cookie.getValue())))
.contains(RememberMeHashingAlgorithm.SHA256.name());
}

@Test
public void requestWithRememberMeWhenTokenValidityIsConfiguredThenCookieReflectsCorrectExpiration()
throws Exception {
Expand Down Expand Up @@ -303,7 +315,7 @@ public void configureWhenUsingRememberMeCookieAndServicesRefThenThrowsWiringExce
.withMessageContaining(
"Configuration problem: services-ref can't be used in combination with attributes "
+ "token-repository-ref,data-source-ref, user-service-ref, token-validity-seconds, "
+ "use-secure-cookie, remember-me-parameter or remember-me-cookie");
+ "use-secure-cookie, remember-me-parameter, hashing-algorithm or remember-me-cookie");
}

private ResultActions rememberAuthentication(String username, String password) throws Exception {
Expand Down
Loading