Spring OAuth2 - URL Encoded of username within Basic auth header for token endpoint - spring-security-oauth2

It seems that Basic Auth for token endpoint is not compliant with OAuth2.0 spec, which specifies that the username and password should be URL encoded before being joined with a colon and the base 64 encoded. See rfc6749
Creation of the header should be:
auth_header = 'Basic' + base64 (urlEncode (client_id) + ':' + urlEncode (client_secret))
Hence there needs a URL decode step after splitting the username from the password.
Spring is just using the BasicAuthenticationFilter to extract the credentials and there doesn't seem a way of extending this to add the URL Decode step.
So, is this an omission from Spring security OAuth2? If so, guess a bug should be raised.
I could replace the BasicAuthenticationFilter, with one that has the URL Decode step, but is there an easier way?
I am currently using Spring Security 5.0.5

Spring security does comply with OAuth 2.0 spec. What you're asking is already covered in BasicAuthenticationFilter. Here's an excerpt from the code:
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
boolean debug = this.logger.isDebugEnabled();
String header = request.getHeader("Authorization");
if (header != null && header.startsWith("Basic ")) {
try {
String[] tokens = this.extractAndDecodeHeader(header, request);
assert tokens.length == 2;
Filter reads the header that has Basic authentication, then it extracts and decode the credentials.
In the following code, notice that index of colon is used as a String delimiter.
private String[] extractAndDecodeHeader(String header, HttpServletRequest request) throws IOException {
byte[] base64Token = header.substring(6).getBytes("UTF-8");
byte[] decoded;
try {
decoded = Base64.getDecoder().decode(base64Token);
} catch (IllegalArgumentException var7) {
throw new BadCredentialsException("Failed to decode basic authentication token");
}
String token = new String(decoded, this.getCredentialsCharset(request));
int delim = token.indexOf(":");
if (delim == -1) {
throw new BadCredentialsException("Invalid basic authentication token");
} else {
return new String[]{token.substring(0, delim), token.substring(delim + 1)};
}
}

Related

Basic Authentication with Resteasy client

I'm trying to perform an basic auth to the login-module which runs on my jboss using REST. I already found an StackOverflow topic which explains how to authenticate with credentials.
RESTEasy client framework authentication credentials
This does not work. Analysing the established connection with Wireshark I was not able to see an HTTP package with Authorization: Basic. After more research I found this article, http://docs.jboss.org/resteasy/docs/2.3.3.Final/userguide/html/RESTEasy_Client_Framework.html which describes how to append basic auth to ApacheHttpClient4Executor from resteasy.
// Configure HttpClient to authenticate preemptively
// by prepopulating the authentication data cache.
// 1. Create AuthCache instance
AuthCache authCache = new BasicAuthCache();
// 2. Generate BASIC scheme object and add it to the local auth cache
BasicScheme basicAuth = new BasicScheme();
authCache.put("com.bluemonkeydiamond.sippycups", basicAuth);
// 3. Add AuthCache to the execution context
BasicHttpContext localContext = new BasicHttpContext();
localContext.setAttribute(ClientContext.AUTH_CACHE, authCache);
// 4. Create client executor and proxy
httpClient = new DefaultHttpClient();
ApacheHttpClient4Executor executor = new ApacheHttpClient4Executor(httpClient, localContext);
client = ProxyFactory.create(BookStoreService.class, url, executor);
But this does not work either. There is no description how to append username and passwort for basic auth to the construct. Why is that information not associated with any class from httpcomponent?
One can use org.jboss.resteasy.client.jaxrs.BasicAuthentication which is packaged with resteasy-client 3.x and is meant specifically for basic authentication.
Client client = ClientBuilder.newClient();
ResteasyWebTarget resteasyWebTarget = (ResteasyWebTarget)client.target("http://mywebservice/rest/api");
resteasyWebTarget.register(new BasicAuthentication("username", "passwd"));
You can add a raw authorization header to your REST client by invoking .header(HttpHeaders.AUTHORIZATION, authHeader) in your client configuration.
The credentials must be packed in authorization header in the format of "user:pass", encoded as base64 byte array and then appended to the string "Basic " which identifies basic auth.
This is the whole snippet (inspired by this post on baeldung)
String auth = userName + ":" + password;
byte[] encodedAuth = Base64.encodeBase64(auth.getBytes(Charset.forName("ISO-8859-1")));
String authHeader = "Basic " + new String(encodedAuth);
authToken = restClient.target(restApiUrl + loginPath)
.request()
.accept(MediaType.TEXT_PLAIN)
.header(HttpHeaders.AUTHORIZATION, authHeader)
.get(String.class);
This worked for me in a Resteasy client. For information, when testing this with wget I had to use the --auth-no-challenge flag.
Consider the solution from Adam Bien:
You can attach an ClientRequestFilter to the RESTEasy Client, which adds the Authorization header to the request:
public class Authenticator implements ClientRequestFilter {
private final String user;
private final String password;
public Authenticator(String user, String password) {
this.user = user;
this.password = password;
}
public void filter(ClientRequestContext requestContext) throws IOException {
MultivaluedMap<String, Object> headers = requestContext.getHeaders();
final String basicAuthentication = getBasicAuthentication();
headers.add("Authorization", basicAuthentication);
}
private String getBasicAuthentication() {
String token = this.user + ":" + this.password;
try {
return "Basic " +
DatatypeConverter.printBase64Binary(token.getBytes("UTF-8"));
} catch (UnsupportedEncodingException ex) {
throw new IllegalStateException("Cannot encode with UTF-8", ex);
}
}
}
Client client = ClientBuilder.newClient()
.register(new Authenticator(user, password));
I recently upgraded to resteasy-client:4.0.0.Final to deal with some Jackson upgrade issues, and I noticed that setting headers seem to work differently (I was getting 401: Authorization Errors for every authenticated request that previously worked). I also couldn't find much documentation, (the 4.0.0.Final release is only a month old and has some dependency issues, if my experience is representative of the broader case).
The code previously injected headers into the ClientRequestContext:
public AddAuthHeadersRequestFilter(String username, String password) {
this.username = username;
this.password = password;
}
#Override
public void filter(ClientRequestContext requestContext) throws IOException {
String token = username + ":" + password;
String base64Token = Base64.encodeString(token);
requestContext.getHeaders().add("Authorization", "Basic " + base64Token);
}
}
then we set the filter on the ResteasyClient like so:
ResteasyClient client = new ResteasyClientBuilder()
.sslContext(buildSSLContext())
.hostnameVerifier(buildHostVerifier())
.build();
client.register(new AddAuthHeadersRequestFilter(user, pass));
However, this appears not to set the HeaderDelegate, which is where headers are retrieved in 4.x(?) and possibly earlier versions.
The trick was to register that filter on the ResteasyWebTarget instead of the client in the 4.0.0.Final version (you may notice the clientBuilder works a little differently now too).
ResteasyClient client = (ResteasyClient)ResteasyClientBuilder.newBuilder()
.sslContext(buildSSLContext())
.hostnameVerifier(buildHostVerifier())
.build();
ResteasyWebTarget target = client.target(url);
target.register(new AddAuthHeadersRequestFilter(user, pass));

Get UsernamePasswordCredential from HttpServletRequest

I'm using Spring SimpleFormController to handle a form submission . when client sets UsernamePasswordCredentials in httpclient by httpclient.getState().setCredentials on Preemptive Authentication, how can i get this UsernamePasswordCredential in server side from HttpServletRequest inside my "onSubmit" method.
we can take the 'authorization' header value from the request and Base64-decodes the value to show that the username and password are in fact very easily readable if someone intercepts the network communication. (This is why it's a very good idea to use HTTPS for authentication).
try {
String auth = request.getHeader("Authorization");
if(auth!=null&&auth.length()>6){
String userpassEncoded = auth.substring(6);
// Decode it, using any base 64 decoder
sun.misc.BASE64Decoder dec = new sun.misc.BASE64Decoder();
String userpassDecoded= new String(dec.decodeBuffer(userpassEncoded));
System.out.println(" userpassDecoded = "+userpassDecoded);
}
} catch (Exception e) {
e.printStackTrace();
}

How do I extract the username and password out of a URL in a servlet filter?

I've created a BasicAuthFilter and it has this signature:
#Override
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain chain) throws IOException, ServletException
This is working if someone calls the filter with an Authorization header set the right way. But, if someone on chrome or firefox visits the url like this:
http://username:password#localhost:8888
The browsers are not populating the Authorization header with that information (which surprised me). I looked at the information sent by chrome and the username and password are in the request URL but nowhere else.
I can't figure out how to extract that information from the URL. I've tried a lot of getters on the HttpServletRequest, but haven't found anything that gives me the username and password.
NOTE: Yes, I know this is insecure, it's just really convenient to use when you're trying to test your system.
URL url = new URL(custom_url);
String userInfo = url.getUserInfo();
String[] userInfoArray = userInfo.split(":");
System.out.println("username"+userInfoArray[0]);
System.out.println("password"+userInfoArray[1]);
My coworker found this thread that implies this isn't possible in modern browsers. They refuse to send the username:password part of a url over the wire for security reasons.
I'll add something to this answer
If the password contains the character :, you must specify a limit on your split.
So:
String[] userInfoArray = userInfo.split(":");
Becomes:
String[] userInfoArray = userInfo.split(":", 2);
2 means the pattern : is applied only one time (so the resulting length array is at maximum 2)
For passwords with '#', e.g. "http://user:p#ssw0rd#private.uri.org/some/service":
final String authority = uri.getAuthority();
if (authority != null) {
final String[] userInfo = authority.split(":", 2);
if (userInfo.length > 1) {
this.username = userInfo[0];
int passDelim = userInfo[1].lastIndexOf('#');
if (passDelim != -1) {
this.password = userInfo[1].substring(0, passDelim);
}
}
}
Note that in this case trying to use getUserInfo() won't help since userInfo of the URI is null.

Url Redirection error if url contains reserved characters

I am trying to access particular url
DefaultHttpClient httpclient = new DefaultHttpClient();
httpclient.getCredentialsProvider().setCredentials(new AuthScope("abc.com", 443),
new UsernamePasswordCredentials("user", "Passwd"));
HTTPHelper http = new HTTPHelper(httpclient);
http.get("http://abc.com/**aaa**/w/api.php?param=timestamp%7Cuser&format=xml");
where %7C= |
which is redirecting me to the following url internally
http://abc.com/**bbb**/w/api.php?param=timestamp%257Cuser&format=xml
and because of that I am not able to get the correct output...
| ==>%7C
%==> %25
%7C == %257C
I want query to be timestamp|user
but because of circular redirection it is changed into timestamp%7Cuser
Is there any way to avoid this??
I wrote my own Custom Redirect Strategy also
httpclient.setRedirectStrategy(new DefaultRedirectStrategy() {
public boolean isRedirected(HttpRequest request, HttpResponse response, HttpContext context) {
boolean isRedirect = false;
try {
isRedirect = super.isRedirected(request, response, context);
LOG.info(response.getStatusLine().getStatusCode());
LOG.info(request.getRequestLine().getUri().replaceAll("%25", "%"));
} catch (ProtocolException e) {
e.printStackTrace();
}
if (!isRedirect) {
int responseCode = response.getStatusLine().getStatusCode();
if (responseCode == 301 || responseCode == 302) {
return true;
}
}
return isRedirect;
}
});
But I am not sure how to replace %25C with %7C from redirected url
It looks like the site's URL rewrite rules are simply broken. If it's not your site, you may want to contact its maintainers and inform them about the issue.
In the mean time, is there some reason why you can't simply use the target URLs (i.e. http://abc.com/**bbb**/w/api.php?...) directly, avoiding the redirect?
Does just http.get("http://abc.com/**aaa**/w/api.php?param=timestamp|user&format=xml");
work?
What's your meaning "redirecting me to the following url internally"? It's like that your url are encoded again. Can you post code in HTTPHelper? My below test code work correctly.
Client Code:
HttpClient client = new DefaultHttpClient();
HttpGet get = new HttpGet("http://localhost:8080/test/myservlet?param=timestamp%7Cuser&format=xml");
client.execute(get);
Servlet Code:
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
String param = request.getParameter("param"); //get timestamp|user
String format = request.getParameter("format");
}

spring-mvc with resteasy character encoding problem on jetty server

I am trying to implement restful protocol on jetty server. I have runnable server and i can access it from my rest client. My server side project is a maven project. I have a problem about the character encoding.When i check response, before send it from controller, there is no encoding problem. But after i return response to client, i see broken data. Response header is UTF-8. Also i have a listener for this problem and i am setting to request and response to UTF-8. I guess problem happens when i try to write my response data to response.
#GET
#Path("/")
#Produces({"application/xml;charset=UTF-8","application/json;charset=UTF-8"})
public String getPersons(#Context HttpServletRequest request, #Context HttpServletResponse response) {
List<Person> persons = personService.getPersons(testUserId, collectionOption, null);
if (persons == null) {
persons = new ArrayList<Person>();
}
String result = JsonUtil.listToJson(persons);
//result doesnt has any encoding problem at this line
response.setContentType("application/json");
response.setContentLength(result.length());
response.setCharacterEncoding("utf-8");
//i guess problem happen after this line
return result;
}
Is there any jetty configuration or resteasy configuration for it? Or is there any way to solve this problem? Thanks for your helps.
Which resteasy version are you using? There is a known issue (RESTEASY-467) with Strings in 2.0.1 an prior.
These are your options:
1) force the encoding returning byte[]
public byte[] getPersons
and then
return result.getBytes("UTF8");
2) return List (or create a PersonListing if you need it)
public List<Person> getPersons
and let resteasy handle the json transformation.
3) return a StreamingOutput
NOTE: with this option the "Content-Length" header will be unknown.
return new StreamingOutput()
{
public void write(OutputStream outputStream) throws IOException, WebApplicationException
{
PrintStream writer = new PrintStream(outputStream, true, "UTF-8");
writer.println(result);
}
};
4) upgrade to 2.2-beta-1 or newer version.

Resources