BASIC Authentication scheme assume that crdential are encoded with ISO-8859-1
I do not know if this is a deliberated choice or not, but we have discovered that the BASIC Authenticate HTTP header is decoded with the ISO-8859-1 charset avoiding the usage of non Latin character in the login and password when using this schema, in the org.restlet.engine.security.HttpBasicHelper class.
The code is coherent because the same charset is used to encode too, but it appears to give some problems to non-european people... As I said I do not know if this issue should be viewed as a bug or an enhancement request, but there is a optional parameter allowing for the server to indicate a preference for an UTF-8 encoding:
https://www.rfc-editor.org/rfc/rfc7617.html#section-2.1
It could be great if some parameter could be retrieved by HttpBasicHelper to insert this parameter and then use UTF-8 to encode and decode the header...
As a turn around, we can inject the charset preference with this turn around when generating the ChallengeRequest in the Authenticator class:
ChallengeRequest cr = new ChallengeRequest(ChallengeScheme.HTTP_BASIC, myRealm);
// Force the inclusion of the UTF-8 charset preference.
cr.setRawValue("Basic realm=\"" + myRealm + "\", charset=\"UTF-8\"");
By the way it appears thet HttpBasicHelper ignore the parameters added to the ChallengeRequest, the following code is useless:
cr.getParameters().add("charset", "UTF-8");
(I know that except "charset" and "realm" there is no other parameter existing for a BASIC but this could have been useful to manage them.)
And to correctly decode the credential we can add the following code after retreiving the ChallengeResponse:
ChallengeResponse challenge = request.getChallengeResponse();
byte[] credentialsEncoded = Base64.getDecoder().decode(challenge.getRawValue());
// Use UTF-8 as the default charset used for credential in BASIC HTTP authenticate header.
String credentials = new String(credentialsEncoded, StandardCharsets.UTF_8);
int separator = credentials.indexOf(':');
// Restlet already log an information about missing separator...
if (separator > 0) {
challenge.setIdentifier(credentials.substring(0, separator));
challenge.setSecret(credentials.substring(separator + 1));
} else {
// Assume that a missing separator imply that the password is empty... but we should throw a ResourceException here...
challenge.setIdentifier(credentials);
challenge.setSecret(new char[0]);
}
Thanks Marc for the detailled issue. This chartset parameters was added in 2015 with this RFC 7617 and never supported by Restlet Framework.
As part of a branch that I'm working on right now, I have enhanced the code to support charset="UTF-8": https://github.com/restlet/restlet-framework-java/blob/2.6-multipart-jetty/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/security/HttpBasicHelper.java
Could you test this revised HttpBasicHelper.java class and let me know if it works as expected?
Added @thboileau for visibility.
Sorry for the delay... I am testing it today !
I am afraid that it does not work completely,
On a request without challenge the server add correctly the charset parameter.
On the client side the Client encode the credential in utf-8 correctly to.
The problem come when the server get the request with the correct credential... the RFC 7617 is... let say... tricky, because the client request does not contain any information about the charset used to encode the raw value of the Authorization... so the HttpBasicHelper class the legacy charset.
the AuthenticatorUtils.parseResponse method create the ChallenceResponse with the schema and the raw value and call the helper in a raw, so there is no way to add the parameter to the it before it is pass to the helper.
Here is the test code I used, for the Client part:
package testsbasicunicode;
import java.io.IOException;
import org.restlet.data.ChallengeResponse;
import org.restlet.data.ChallengeScheme;
import org.restlet.data.Status;
import org.restlet.resource.ClientResource;
import org.restlet.resource.ResourceException;
public class ClientTest {
public static void main(String[] args) {
// Prepare the request
ClientResource resource = new ClientResource("http://localhost:8182/");
// Add the client authentication to the call
ChallengeResponse response = new ChallengeResponse(ChallengeScheme.HTTP_BASIC, "unicode", "aaa£€§");
response.getParameters().add("charset", "UTF-8");
resource.setChallengeResponse(response);
try {
// Send the HTTP GET request
resource.get();
// Manage the response
if (resource.getStatus().isSuccess()) {
// Output the response entity on the JVM console...
resource.getResponseEntity().write(System.out);
} else {
System.out.println("An unexpected status: " + resource.getStatus());
}
} catch (ResourceException e) {
if (Status.CLIENT_ERROR_UNAUTHORIZED.equals(e.getStatus())) {
System.out.println("Access unauthorized by the server !");
} else {
System.out.println("An unexpected status: " + e.getStatus());
}
} catch (IOException e) {
System.out.println("Broken connection: " + e.getLocalizedMessage());
}
}
}
Adding the charset parameter to the challenge response allow to the HttpBasicHelper to encode the crendential in UTF-8. (But the parameter is not sent through the HTTP call, as the RFC decribe it...)
And for the server side:
package testsbasicunicode;
import org.restlet.Application;
import org.restlet.Component;
import org.restlet.Restlet;
import org.restlet.data.ChallengeRequest;
import org.restlet.data.ChallengeScheme;
import org.restlet.data.Protocol;
import org.restlet.resource.Get;
import org.restlet.resource.ServerResource;
import org.restlet.security.ChallengeAuthenticator;
import org.restlet.security.MapVerifier;
public class ServerTest {
public static class RootResource extends ServerResource {
@Get("txt")
public String toString() {
return "Successful authentication !";
}
}
public static class GuardedApplication extends Application {
@Override
public Restlet createInboundRoot() {
// Create a simple password verifier with two users (one with unicode characters)
MapVerifier verifier = new MapVerifier();
verifier.getLocalSecrets().put("unicode", "aaa£€§".toCharArray());
verifier.getLocalSecrets().put("ascii", "abcd".toCharArray());
// Create a Guard
ChallengeAuthenticator guard = new ChallengeAuthenticator(getContext(), ChallengeScheme.HTTP_BASIC, "test") {
@Override
protected ChallengeRequest createChallengeRequest(boolean stale) {
ChallengeRequest challengeRequest = super.createChallengeRequest(stale);
// Add the charset requirement.
challengeRequest.getParameters().add("charset", "UTF-8");
return challengeRequest;
}
};
guard.setVerifier(verifier);
guard.setNext(RootResource.class);
return guard;
}
}
public static void main(String[] args) throws Exception {
Component component = new Component();
component.getServers().add(Protocol.HTTP, 8182);
component.getDefaultHost().attach(new GuardedApplication());
component.start();
}
}