Skip to content

Commit 1d86c9c

Browse files
committed
Use Credentials object instead of 2 attributes for Basic Authentication
This commit changes the usage of two separate attributes (username and password) into one: a single `Credentials` object. Additionally, the attributes key under which the credentials are stored is changed to be specific to Basic Authentication, in order to allow for other sorts of authentication later. Issue: SPR-15764
1 parent 26de626 commit 1d86c9c

File tree

2 files changed

+94
-33
lines changed

2 files changed

+94
-33
lines changed

spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFilterFunctions.java

Lines changed: 84 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@
1616

1717
package org.springframework.web.reactive.function.client;
1818

19-
import java.nio.charset.Charset;
19+
import java.nio.charset.CharsetEncoder;
2020
import java.nio.charset.StandardCharsets;
2121
import java.util.Base64;
22+
import java.util.Map;
2223
import java.util.Optional;
24+
import java.util.function.Consumer;
2325
import java.util.function.Function;
2426
import java.util.function.Predicate;
2527

@@ -40,52 +42,45 @@
4042
public abstract class ExchangeFilterFunctions {
4143

4244
/**
43-
* Name of the {@link ClientRequest} attribute that contains the username, as used by
45+
* Name of the {@link ClientRequest} attribute that contains the {@link Credentials}, as used by
4446
* {@link #basicAuthentication()}
4547
*/
46-
public static final String USERNAME_ATTRIBUTE = ExchangeFilterFunctions.class.getName() + ".username";
47-
48-
/**
49-
* Name of the {@link ClientRequest} attribute that contains the password, as used by
50-
* {@link #basicAuthentication()}
51-
*/
52-
public static final String PASSWORD_ATTRIBUTE = ExchangeFilterFunctions.class.getName() + ".password";
48+
public static final String BASIC_AUTHENTICATION_CREDENTIALS_ATTRIBUTE = ExchangeFilterFunctions.class.getName() + ".basicAuthenticationCredentials";
5349

5450

5551
/**
5652
* Return a filter that adds an Authorization header for HTTP Basic Authentication, based on
5753
* the given username and password.
54+
* <p>Note that Basic Authentication only supports characters in the
55+
* {@link StandardCharsets#ISO_8859_1 ISO-8859-1} character set.
5856
* @param username the username to use
5957
* @param password the password to use
6058
* @return the {@link ExchangeFilterFunction} that adds the Authorization header
59+
* @throws IllegalArgumentException if either {@code username} or {@code password} contain
60+
* characters that cannot be encoded to ISO-8859-1
6161
*/
6262
public static ExchangeFilterFunction basicAuthentication(String username, String password) {
6363
Assert.notNull(username, "'username' must not be null");
6464
Assert.notNull(password, "'password' must not be null");
6565

66+
checkIllegalCharacters(username, password);
6667
return basicAuthenticationInternal(r -> Optional.of(new Credentials(username, password)));
6768
}
6869

6970
/**
7071
* Return a filter that adds an Authorization header for HTTP Basic Authentication, based on
71-
* the username and password provided in the
72-
* {@linkplain ClientRequest#attributes() request attributes}. If the attributes are not found,
73-
* no authorization header
72+
* the {@link Credentials} provided in the
73+
* {@linkplain ClientRequest#attributes() request attributes}. If the attribute is not found,
74+
* no authorization header is added.
75+
* <p>Note that Basic Authentication only supports characters in the
76+
* {@link StandardCharsets#ISO_8859_1 ISO-8859-1} character set.
7477
* @return the {@link ExchangeFilterFunction} that adds the Authorization header
75-
* @see #USERNAME_ATTRIBUTE
76-
* @see #PASSWORD_ATTRIBUTE
78+
* @see #BASIC_AUTHENTICATION_CREDENTIALS_ATTRIBUTE
79+
* @see Credentials#basicAuthenticationCredentials(String, String)
7780
*/
7881
public static ExchangeFilterFunction basicAuthentication() {
7982
return basicAuthenticationInternal(
80-
request -> {
81-
Optional<String> username = request.attribute(USERNAME_ATTRIBUTE).map(o -> (String)o);
82-
Optional<String> password = request.attribute(PASSWORD_ATTRIBUTE).map(o -> (String)o);
83-
if (username.isPresent() && password.isPresent()) {
84-
return Optional.of(new Credentials(username.get(), password.get()));
85-
} else {
86-
return Optional.empty();
87-
}
88-
});
83+
request -> request.attribute(BASIC_AUTHENTICATION_CREDENTIALS_ATTRIBUTE).map(o -> (Credentials)o));
8984
}
9085

9186
private static ExchangeFilterFunction basicAuthenticationInternal(
@@ -106,12 +101,28 @@ private static ExchangeFilterFunction basicAuthenticationInternal(
106101
}
107102

108103
private static String authorization(Credentials credentials) {
109-
byte[] credentialBytes = credentials.toByteArray(StandardCharsets.ISO_8859_1);
104+
String credentialsString = credentials.username + ":" + credentials.password;
105+
byte[] credentialBytes = credentialsString.getBytes(StandardCharsets.ISO_8859_1);
110106
byte[] encodedBytes = Base64.getEncoder().encode(credentialBytes);
111107
String encodedCredentials = new String(encodedBytes, StandardCharsets.ISO_8859_1);
112108
return "Basic " + encodedCredentials;
113109
}
114110

111+
/*
112+
* Basic authentication only supports ISO 8859-1, see
113+
* https://stackoverflow.com/questions/702629/utf-8-characters-mangled-in-http-basic-auth-username#703341
114+
*/
115+
private static void checkIllegalCharacters(String username, String password) {
116+
CharsetEncoder encoder = StandardCharsets.ISO_8859_1.newEncoder();
117+
if (!encoder.canEncode(username) || !encoder.canEncode(password)) {
118+
throw new IllegalArgumentException(
119+
"Username or password contains characters that cannot be encoded to ISO-8859-1");
120+
}
121+
122+
}
123+
124+
125+
115126
/**
116127
* Return a filter that returns a given {@link Throwable} as response if the given
117128
* {@link HttpStatus} predicate matches.
@@ -140,20 +151,63 @@ public static ExchangeFilterFunction statusError(Predicate<HttpStatus> statusPre
140151
}
141152

142153

143-
private static final class Credentials {
154+
/**
155+
* Represents a combination of username and password, as used by {@link #basicAuthentication()}.
156+
* @see #basicAuthenticationCredentials(String, String)
157+
*/
158+
public static final class Credentials {
144159

145-
private String username;
160+
private final String username;
146161

147-
private String password;
162+
private final String password;
148163

164+
/**
165+
* Create a new {@code Credentials} instance with the given username and password.
166+
* @param username the username
167+
* @param password the password
168+
*/
149169
public Credentials(String username, String password) {
170+
Assert.notNull(username, "'username' must not be null");
171+
Assert.notNull(password, "'password' must not be null");
172+
150173
this.username = username;
151174
this.password = password;
152175
}
153176

154-
public byte[] toByteArray(Charset charset) {
155-
String credentials = this.username + ":" + this.password;
156-
return credentials.getBytes(charset);
177+
/**
178+
* Return a consumer that stores the given username and password in the
179+
* {@linkplain ClientRequest.Builder#attributes(java.util.function.Consumer) request
180+
* attributes} as a {@code Credentials} object.
181+
* @param username the username
182+
* @param password the password
183+
* @return a consumer that adds the given credentials to the attribute map
184+
* @see ClientRequest.Builder#attributes(java.util.function.Consumer)
185+
* @see #BASIC_AUTHENTICATION_CREDENTIALS_ATTRIBUTE
186+
*/
187+
public static Consumer<Map<String, Object>> basicAuthenticationCredentials(String username, String password) {
188+
Credentials credentials = new Credentials(username, password);
189+
checkIllegalCharacters(username, password);
190+
191+
return attributes -> attributes.put(BASIC_AUTHENTICATION_CREDENTIALS_ATTRIBUTE, credentials);
192+
}
193+
194+
@Override
195+
public boolean equals(Object o) {
196+
if (this == o) {
197+
return true;
198+
}
199+
if (o instanceof Credentials) {
200+
Credentials other = (Credentials) o;
201+
return this.username.equals(other.username) &&
202+
this.password.equals(other.password);
203+
204+
}
205+
return false;
206+
}
207+
208+
@Override
209+
public int hashCode() {
210+
return 31 * this.username.hashCode() + this.password.hashCode();
157211
}
158212

159213
}

spring-webflux/src/test/java/org/springframework/web/reactive/function/client/ExchangeFilterFunctionsTests.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import static org.junit.Assert.*;
2929
import static org.mockito.Mockito.*;
3030
import static org.springframework.http.HttpMethod.GET;
31+
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials;
3132

3233
/**
3334
* @author Arjen Poutsma
@@ -84,7 +85,7 @@ public void apply() throws Exception {
8485
}
8586

8687
@Test
87-
public void basicAuthentication() throws Exception {
88+
public void basicAuthenticationUsernamePassword() throws Exception {
8889
ClientRequest request = ClientRequest.method(GET, URI.create("http://example.com")).build();
8990
ClientResponse response = mock(ClientResponse.class);
9091

@@ -100,11 +101,17 @@ public void basicAuthentication() throws Exception {
100101
assertEquals(response, result);
101102
}
102103

104+
@Test(expected = IllegalArgumentException.class)
105+
public void basicAuthenticationInvalidCharacters() throws Exception {
106+
107+
ExchangeFilterFunctions.basicAuthentication("foo", "\ud83d\udca9");
108+
}
109+
103110
@Test
104111
public void basicAuthenticationAttributes() throws Exception {
105112
ClientRequest request = ClientRequest.method(GET, URI.create("http://example.com"))
106-
.attribute(ExchangeFilterFunctions.USERNAME_ATTRIBUTE, "foo")
107-
.attribute(ExchangeFilterFunctions.PASSWORD_ATTRIBUTE, "bar").build();
113+
.attributes(basicAuthenticationCredentials("foo", "bar"))
114+
.build();
108115
ClientResponse response = mock(ClientResponse.class);
109116

110117
ExchangeFunction exchange = r -> {

0 commit comments

Comments
 (0)