rburgst / okhttp-digest Goto Github PK
View Code? Open in Web Editor NEWa digest authenticator for okhttp
License: Apache License 2.0
a digest authenticator for okhttp
License: Apache License 2.0
i.e. when the client is used to connect to multiple backends. This is possible by replacing the BasicAuthenticator, but this duplicates functionality e.g. terminating after a unsuccessful request.
final DigestAuthenticator authenticator = new DigestAuthenticator(new Credentials(unicomUserName, unicomPassword));
final Map<String, CachingAuthenticator> authCache = new ConcurrentHashMap<>();
final OkHttpClient client = new OkHttpClient.Builder()
.proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(unicomProxyHost, 8143))) //https:8143
.authenticator(new CachingAuthenticatorDecorator(authenticator, authCache))
.addInterceptor(new AuthenticationCacheInterceptor(authCache))
.build();
// String url = "http://www.google.com";
Request request = new Request.Builder()
.url(unicomUrl)
.build();
try (Response response = client.newCall(request).execute()) {
return response.body().string();
}
use proxy and digest lead to error:
Process: com.example.admin.mydemo, PID: 13867
java.lang.IllegalStateException: closed
at okio.RealBufferedSource.rangeEquals(RealBufferedSource.java:398)
at okio.RealBufferedSource.rangeEquals(RealBufferedSource.java:392)
at okhttp3.internal.Util.bomAwareCharset(Util.java:431)
at okhttp3.ResponseBody.string(ResponseBody.java:174)
at com.example.admin.mydemo.MainActivity.unicomHttpRequest(MainActivity.java:258)
at com.example.admin.mydemo.MainActivity.access$000(MainActivity.java:32)
at com.example.admin.mydemo.MainActivity$1.run(MainActivity.java:85)
at java.lang.Thread.run(Thread.java:761)
Since this library doesn't required backup and doesn't made use of the use, allowBackup and supportsRtl aren't needed and cause apps importing this library to use manifest merger to replace this properties.
For allowBackup isn't currently possible becasue there is a bug that prevents to replace the library property (https://code.google.com/p/android/issues/detail?id=70073).
I fixed it here jruesga@78fb09f (and fixed other lint warnings as well). Since i modify sdk versions and the lint warnings a didn't pushed a pull request, but i could do it easy if you one.
Error NoSuchMethodError happens when calling
Platform.get().log(Platform.WARN, "previous digest authentication with same nonce failed, returning null", null);
STACKS:
implementation "com.squareup.okhttp3:okhttp:4.3.0"
implementation "com.burgstaller:okhttp-digest:2.0"
NVIDIA SHIELD Android TV (foster), Android 9
java.lang.NoSuchMethodError:
at com.burgstaller.okhttp.basic.BasicAuthenticator.authFromRequest (BasicAuthenticator.java:39)
at com.burgstaller.okhttp.basic.BasicAuthenticator.authenticate (BasicAuthenticator.java:30)
LGE Qua tab PX (b3), Android 7.0
java.lang.NoSuchMethodError:
at com.burgstaller.okhttp.digest.DigestAuthenticator.authenticateWithState (DigestAuthenticator.java:227)
at com.burgstaller.okhttp.digest.DigestAuthenticator.authenticate (DigestAuthenticator.java:177)
FIX:
log() function in Platform.kt has changed in this commit
square/okhttp@16173e2#diff-f5b1ec0bc5e1662d90457f72acde2607
- open fun log(level: Int, message: String, t: Throwable?) {
+ open fun log(message: String, level: Int = INFO, t: Throwable? = null) {
Swapping params should fix the issue.
Runtime exception for a valid case seems severe. Should just not try to authenticate and return null.
Update README.md to reflect usage beyond version 1.15 This should include 1.9
When I downloaded the source ran with "./gradlew install" I got this:
* Where:
Build file 'C:\temp\okhttp-digest-1.12\okhttp-digest-1.12\build.gradle' line: 116
* What went wrong:
A problem occurred evaluating root project 'okhttp-digest-1.12'.
> SDK location not found. Define location with sdk.dir in the local.properties file or with an ANDROID_HOME environment variable.
I can't point it to ANDROID_HOME since I don't have android. Can I not build or use this library without android?
Hi, I'm trying out your lib w/ latest Android SDK M and trying a http digest authentication web service returns me: java.net.ProtocolException: Too many follow-up requests: 21
I used your sample code:
OkHttpClient client = new OkHttpClient();
final DigestAuthenticator authenticator = new DigestAuthenticator(new Credentials(username, password));
final Map<String, CachingAuthenticator> authCache = new ConcurrentHashMap<>();
client.interceptors().add(new AuthenticationCacheInterceptor(authCache));
client.setAuthenticator(new CachingAuthenticatorDecorator(authenticator, authCache));
Request request = new Request.Builder()
.url(url).get().build();
Response response = client.newCall(request).execute();
Have you run into such an issue?
Hi there,
I think that the fix for issue #8 may have introduced a bug when connecting with an incorrect password. I am talking to a third-party server, and every time I attempt to talk to it, it gives me back a unique nonce.
If I attempt to connect to the server using an invalid password, I get a 401 response, which contains a new WWW-Authenticate header (with a new nonce). This means that the new function havePreviousDigestAuthorizationWithSameNonce() is returning true, and so we keep retrying the same request again and again (I suspect eventually we will hit the 20 retries limit, but I dont have the patience).
Anyway, I am a HTTP Digest newbie, but my understanding is that the actual fix for #8 may have worked better if it looked for the 'stale="TRUE"' flag (and then verified that it has in fact received a new nonce to replace the stale one).
I have played around with this and seem to get the correct results but I am unable to test with a server that returns stale values, so I don't have a lot of confidence in my fix for the general case. Are you interested in looking at a patch or pull request to see what I mean?
Cheers,
Glen.
Error in okhttp-digest 1.14. It works at the same time in okhttp-digest 1.13.
W/System.err: java.lang.NullPointerException: Attempt to invoke interface method 'int java.util.Map.size()' on a null object reference
W/System.err: at java.util.HashMap.putMapEntries(HashMap.java:500)
W/System.err: at java.util.HashMap.<init>(HashMap.java:489)
W/System.err: at com.burgstaller.okhttp.digest.DigestAuthenticator.authenticateWithState(DigestAuthenticator.java:204)
W/System.err: at com.burgstaller.okhttp.DispatchingAuthenticator.authenticateWithState(DispatchingAuthenticator.java:55)
W/System.err: at com.burgstaller.okhttp.AuthenticationCacheInterceptor.intercept(AuthenticationCacheInterceptor.java:40)
W/System.err: at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:92)
W/System.err: at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:67)
W/System.err: at okhttp3.RealCall.getResponseWithInterceptorChain(RealCall.java:185)
W/System.err: at okhttp3.RealCall.execute(RealCall.java:69)
Version 1.6 now generates unneeded logs in Android.
08-12 14:16:41.513 28102-29171/com.project W/System.err: SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
08-12 14:16:41.513 28102-29171/com.project W/System.err: SLF4J: Defaulting to no-operation (NOP) logger implementation
08-12 14:16:41.513 28102-29171/com.project W/System.err: SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
Hey I am trying your implementation. But the OkHttpClient does not have a function called client.setAuthenticator(new CachingAuthenticatorDecorator(authenticator, authCache));?
Am I missing something? Or does it not support the newest version of OKHttp?
I am using compile 'com.squareup.retrofit2:retrofit:2.2.0'
and try to set the Client like:
`OkHttpClient httpClient = new OkHttpClient();
final DigestAuthenticator authenticator = new DigestAuthenticator(new Credentials("username", "pass"));
final Map<String, CachingAuthenticator> authCache = new ConcurrentHashMap<>();
httpClient.interceptors().add(new AuthenticationCacheInterceptor(authCache));
httpClient.authenticator().add(new CachingAuthenticatorDecorator(authenticator, authCache)); //this line is not working
Retrofit.Builder retrofitBuilder = NetworkHelper.create().retrofit();
retrofitBuilder.client(httpClient);
Retrofit retrofit = retrofitBuilder.build();`
Hello. Some servers sends 2 WWW-Authenticate headers.
WWW-Authenticate:Digest realm="DVRNVRDVS", domain="httpsHost", qop="auth", nonce="7676767677:676767676767", opaque="", algorithm="MD5", stale="FALSE"
WWW-Authenticate:Basic realm="DVRNVRDVS"
You must use response.headers("WWW-Authenticate") instead of response.header("WWW-Authenticate")
Seems like two concurrent requests could get confused
I tried your digest authenticator, however the authentication always fails. From my understanding the bug is in DigestAuthenticator line 332:
if (qop == QOP_AUTH) {
// Method ":" digest-uri-value
a2 = method + ':' + uri; // <-- here
The value uri is at runtime a full URL like http://example.org/path
- however the request Uri used by OkHttp in the HTTP request (first line) is /path
(no host and no protocol). I have read that fully qualified uris including protocol and host are allowed as request line, however from my knowledge the common style is to send only the path and use the Host field instead in the request.
In rfc2617 I found the following statement:
The authenticating server must assure that the resource designated by the "uri" directive is the same as the resource specified in the Request-Line; if they are not, the server SHOULD return a 400 Bad Request error.
Hello!,
The examples for this library seem outdated, but I have been trying to use it on the last okhttp release (3.0.0-RC1) as such:
DigestAuthenticator authenticator = new DigestAuthenticator(new Credentials("user", "password"));
final Map<String, CachingAuthenticator> authCache = new ConcurrentHashMap<>();
client = new OkHttpClient.Builder()
.addInterceptor(new AuthenticationCacheInterceptor(authCache))
.authenticator(new CachingAuthenticatorDecorator(authenticator, authCache))
.build();
The problem is that neither .addInterceptor() or .authenticator() accepts AuthenticationCacheInterceptor or CachingAuthenticatorDecorator as values.
How is it supposed to be used now?
Thanks and best regards!
Thanks four your hard work to making this repo possible!
I've a question using the okhttp-digest
for authentication:
Connecting to a specific server (https://www.pcloud.com/) I'm getting often (not always) 401 responses while presenting the correct credentials, around every randomly 5 request using the same setup.
Often during the communiction with this server I get an an W/OkHttp: previous basic authentication failed, returning null
in the log but everything seems to be fine. Only if after that, a Cached authentication expired. Sending a new request.
and followed again by a previous basic authentication failed, returning null
appears in the log, the server responds with a 401.
03-10 10:24:10.586 5144-5180 W/OkHttp: previous basic authentication failed, returning null
03-10 10:24:10.586 5144-5180 D/OkHttp: Cached authentication expired. Sending a new request.
03-10 10:24:10.916 5144-5180 W/OkHttp: previous basic authentication failed, returning null
Can you imagine what I'm doing wrong or is just the server bad configured? Communicating with other servers I'm facing no errors and never saw this logs before.
The complete HTTP-response when this error happen:
D 20180308141040.447 OkHttp <-- 401 Unauthorized https://webdav.pcloud.com/[_path_] (411ms)
D 20180308141040.448 OkHttp Date: Thu, 08 Mar 2018 20:10:41 GMT
D 20180308141040.449 OkHttp Server: Apache/2.4.10 (Debian)
D 20180308141040.450 OkHttp Keep-Alive: timeout=5, max=98
D 20180308141040.455 OkHttp Connection: Keep-Alive
D 20180308141040.463 OkHttp <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>401 Unauthorized</title>
</head><body>
<h1>Unauthorized</h1>
<p>This server could not verify that you
are authorized to access the document
requested. Either you supplied the wrong
credentials (e.g., bad password), or your
browser doesn't understand how to supply
the credentials required.</p>
<hr>
<address>Apache/2.4.10 (Debian) Server at webdav.pcloud.com Port 443</address>
</body></html>
THANKS!
Sooo while scanning through the code I came across this gem in the DigestAuthenticator
class.
// Method ":" digest-uri-value ":" H(entity-body)
RequestBody entity = null;
if (request.body() != null) {
entity = request.body();
}
if (entity != null) {
// If the entity is not repeatable, try falling back onto QOP_AUTH
if (qopset.contains("auth")) {
qop = QOP_AUTH;
a2 = method + ':' + uri;
} else {
throw new AuthenticationException("Qop auth-int cannot be used with " +
"a non-repeatable entity");
}
} else {
final HttpEntityDigester entityDigester = new HttpEntityDigester(digester);
try {
if (entity != null) {
entity.writeTo(entityDigester);
}
entityDigester.close();
} catch (final IOException ex) {
throw new AuthenticationException("I/O error reading entity content", ex);
}
a2 = method + ':' + uri + ':' + encode(entityDigester.getDigest());
}
You will never ever get the entity
to writeTo
the entityDigester.
Also checking body()
is not null before applying it to something that can be null is a bit weird...
I think there is a problem with your method havePreviousDigestAuthorizationAndShouldAbort.
It receives a "String nonce" but it never checks if the nonce is available in the previous request header.
That way you will log "previous digest authentication with same nonce failed, returning null" even when the nonce was never present in the request.
Hello everyone.
I need to change the Credentials on runtime without having to build a new instance of my OkHttpClient.
Is it possible? What would be a good alternative?
I am using Dagger2 to keep singleton instances of my Retrofit and OkHttpClient. It works flawlessly with the credentials of the 'default' user but I need to change them later on to access other methods.
@Provides
@Singleton
@DigestAuth
Credentials provideDigestAuthCredentials(){
return new Credentials("default", "default");
}
@Provides
@Singleton
@DigestAuth
OkHttpClient provideDigestAuthOkHttpClient(@DigestAuth Credentials credentials,
@DigestAuth UrlHostSelectionInterceptor urlHostSelectionInterceptor){
final DigestAuthenticator digestAuthenticator = new DigestAuthenticator(credentials);
final Map<String, CachingAuthenticator> authCache = new ConcurrentHashMap<>();
return new OkHttpClient.Builder()
.addInterceptor(urlHostSelectionInterceptor)
.authenticator(new CachingAuthenticatorDecorator(digestAuthenticator,authCache))
.addInterceptor(new AuthenticationCacheInterceptor(authCache))
.build();
}
@Provides
@Singleton
@DigestAuth
Retrofit provideDigestAuthRetrofit(@DigestAuth OkHttpClient okHttpClient, Gson gson){
return new Retrofit.Builder()
.baseUrl(GlobalConstants.LAN_DEFAULT_URL)
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create(gson))
.client(okHttpClient)
.build();
}
Hi,
I run into problems with the AuthenticationCacheInterceptor: The CachingAuthenticator seems to cache a successful digest authentication (which is of course correct so far) and tries to reuse it on subsequent request (also correct).
The problem is that the request might be several seconds later than the original request thus invalidating the nonce.
In previous versions, this was fine as the authenticator just tried again until OkHttp's request limit (20) was reached. As the authentication itself was correct, the next request worked fine.
But the following code in DigestAuthenticator prevents any request after an unsuccessful authentication to be executed:
// prevent infinite loops when the password is wrong
final String authorizationHeader = request.header("Authorization");
if (authorizationHeader != null && authorizationHeader.startsWith("Digest")) {
Log.w(TAG, "previous digest authentication failed, returning null");
return null;
}
It does prevent infinite loops (well 'infinite' as in a maximum of 20 requests) on invalid passwords but it doesn't take into consideration that something else (e. g. the previously cached nonce) was wrong.
Is there a simple way to fix this? I've removed the code and my authentication works fine again.
Thanks!
Here's that new issue. My hybrid web app is a complicated use case. I tried a singleton of the DispatchingAuthenticator to support both Basic and Digest auth schemes. Currently, it's set up with conditional logic to chose either BasicAuthenticator or digest. An instance for each one is cached in the sAuthenticator (see client setup) instance from the client setup below.
sClient = new OkHttpClient.Builder()
.followRedirects(false)
.authenticator(sAuthenticator)
.addNetworkInterceptor(logging)
.addInterceptor(new AuthenticationCacheInterceptor(sAuthenticator.authCache))
.addInterceptor(new OKGoInterceptor())
.cache(sCache)
.cookieJar(sCookieJar)
.connectTimeout(45, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build();
It's the dynamic setting of credentials from issue #41 that I want to ask about. From that question, what exactly needs to be cleared? The entire cache or the Authenticator that is being reset?
The custom authenticator logic for my use case is using one, static Credentials object for both authenticators and sets the username and password from user input before calling authenticate. This is working (both basic and digest) when I test for success. When I give the wrong password and get new input from the user and try again the right credentials, a call to the authenticator's authenticateWithState gets back null with this warning:
"W/OkHttp: previous basic authentication failed, returning null"
I could use some help. Thanks in advance for any suggestions.
Thanks for this awesome software.
Using the latest version of OkHttp 4.0.1 or 4.0.0 I get the following exception when trying to connect to a WebDAV source using digest access authentication:
java.lang.IncompatibleClassChangeError: The method 'java.lang.String okhttp3.internal.http.RequestLine.requestPath(okhttp3.HttpUrl)' was expected to be of type static but instead was found to be of type virtual (declaration of 'com.burgstaller.okhttp.digest.DigestAuthenticator' appears in /data/app/org.cryptomator-Ln-apUZxqmV0fbXRF9zInw==/base.apk!classes14.dex)
at com.burgstaller.okhttp.digest.DigestAuthenticator.authenticateWithState(DigestAuthenticator.java:233)
at com.burgstaller.okhttp.digest.DigestAuthenticator.authenticate(DigestAuthenticator.java:176)
at com.burgstaller.okhttp.DispatchingAuthenticator.authenticate(DispatchingAuthenticator.java:45)
at com.burgstaller.okhttp.CachingAuthenticatorDecorator.authenticate(CachingAuthenticatorDecorator.java:30)
at okhttp3.internal.http.RetryAndFollowUpInterceptor.followUpRequest(RetryAndFollowUpInterceptor.kt:213)
at okhttp3.internal.http.RetryAndFollowUpInterceptor.intercept(RetryAndFollowUpInterceptor.kt:102)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:112)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:87)
at com.burgstaller.okhttp.AuthenticationCacheInterceptor.intercept(AuthenticationCacheInterceptor.java:45)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:112)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:87)
at org.cryptomator.data.cloud.webdav.network.HttpLoggingInterceptor.intercept(HttpLoggingInterceptor.java:49)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:112)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:87)
at okhttp3.RealCall.getResponseWithInterceptorChain(RealCall.kt:184)
at okhttp3.RealCall.execute(RealCall.kt:66)
If I downgrade the OkHttp client to eg 3.14.1, everything works as expected.
Please add OkHttp 3.2.0 support. There are some api changes.
I tried the following in my maven pom.xml:
<dependency>
<groupId>com.burgstaller</groupId>
<artifactId>okhttp-digest</artifactId>
<version>1.12</version>
</dependency>
But I get "Could not find artifact com.burgstaller:okhttp-digest:jar:1.12 in central (https://repo.maven.apache.org/maven2)". When I search in mvnrepository.com I can't find anything under com.burgstaller. Is this deployed in maven central? If not, does gradle have another way to get it?
v0.7 is listed in the release notes but it does not have a git tag associated with it so it has not been pushed out to bintray for consumption.
Hey,
I was facing an issue with Digest Authentication flow if the url contains backslash in the URL.
Below is just an example:
Request dummyRequest = new Request.Builder()
.url("http://www.google.com?arg1=\&arg2=1892825586")
.get()
.build();
There seems to be some issue in encoding decoding the uri if the url contains backslash()
Clients may want to cache on subsites, or the public domain e.g. *.mycompany.com
Hello,
I've got an error 401 with following code:
https://stackoverflow.com/questions/59630599/android-java-do-digest-authentication-with-okhttp-and-okhttp-digest-error-co
Maybe i've done something wrong, but i have respected the exemple in main page. Thanks for your help.
My code works fine against Java 1.8. But when I test against Java 1.7 every request gets a 401 response and has to resend. Then, if the payload isn't retryable, the resend goes through blank. No error is thrown, it just silently fails to send the payload with the second request. I'll put together steps to reproduce, but I wanted to first ask if this library has been tested with Java 1.7.
Remove "missing realm in challenge" log entry from DigestAuthenticator. It overloads logs with this log entry if basic HTTP GET requests are coming all the time and caching used.
For example
Map<String, CachingAuthenticator> gAuthCache = new ConcurrentHashMap<>();
Credentials credentials = new Credentials(username, password);
final BasicAuthenticator basicAuthenticator = new BasicAuthenticator(credentials);
final DigestAuthenticator digestAuthenticator = new DigestAuthenticator(credentials);
DispatchingAuthenticator authenticator = new DispatchingAuthenticator.Builder()
.with("digest", digestAuthenticator)
.with("basic", basicAuthenticator)
.build();
OkHttpClient.Builder builder = new OkHttpClient.Builder();
OkHttpClient client = builder
.authenticator(new CachingAuthenticatorDecorator(authenticator, gAuthCache))
.addInterceptor(new AuthenticationCacheInterceptor(gAuthCache))
....
This happens because DispatchingAuthenticator will first call DigestAuthenticator.authenticateWithState()
and only then BasicAuthenticator.authenticateWithState()
.
I received this crash report and I couldn't figure where is the problem. Using version 1.18 with Okhttp 3.12.6.
Fatal Exception: java.util.ConcurrentModificationException
at java.util.HashMap$HashIterator.nextNode + 1441(HashMap.java:1441)
at java.util.HashMap$EntryIterator.next + 1475(HashMap.java:1475)
at java.util.HashMap$EntryIterator.next + 1473(HashMap.java:1473)
at java.util.Collections$UnmodifiableMap$UnmodifiableEntrySet$1.next + 1703(Collections.java:1703)
at java.util.Collections$UnmodifiableMap$UnmodifiableEntrySet$1.next + 1696(Collections.java:1696)
at java.util.HashMap.putMapEntries + 511(HashMap.java:511)
at java.util.HashMap. + 489(HashMap.java:489)
at zn.a + 26(DigestAuthenticator.java:26)
at wn.a + 2(DispatchingAuthenticator.java:2)
at sn.intercept + 6(AuthenticationCacheInterceptor.java:6)
at okhttp3.internal.http.RealInterceptorChain.proceed + 10(RealInterceptorChain.java:10)
at okhttp3.internal.http.RealInterceptorChain.proceed + 1(RealInterceptorChain.java:1)
at rd1$b$a.intercept + 9(HTTPClient.kt:9)
at okhttp3.internal.http.RealInterceptorChain.proceed + 10(RealInterceptorChain.java:10)
at okhttp3.internal.http.RealInterceptorChain.proceed + 1(RealInterceptorChain.java:1)
at okhttp3.RealCall.getResponseWithInterceptorChain + 13(RealCall.java:13)
at okhttp3.RealCall$AsyncCall.execute + 2(RealCall.java:2)
at okhttp3.internal.NamedRunnable.run + 3(NamedRunnable.java:3)
at java.util.concurrent.ThreadPoolExecutor.runWorker + 1167(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run + 641(ThreadPoolExecutor.java:641)
at java.lang.Thread.run + 764(Thread.java:764)
Method havePreviousDigestAuthorizationAndShouldAbort() looks only at "Authorization" header, and ignores "Proxy-Authorization" header completely. This means we ignore the fact that we could be interacting with a proxy rather than with a WWW server. If the user enters wrong credentials, then we'll constantly repeat our requests to the proxy. This loop never breaks. It's happening with my proxy now.
So, I suggest the following. Instead of
final String previousAuthorizationHeader = request.header("Authorization");
it should be something smarter, like this:
final String headerKey;
if (isProxy())
headerKey = PROXY_AUTH_RESP;
else
headerKey = WWW_AUTH_RESP;
final String previousAuthorizationHeader = request.header(headerKey);
As I'm getting started, it would help if you included the import statements. That way I don't have to go digging to see the package names in order to import the classes.
Hi,
I try to use okhhtp-digest for proxy authentication so I set proxy attribute to true.
But in findDigestHeader() method the "WWW-Authenticate" header is used instead of testing the proxy attribute and using WWW_AUTH or PROXY_AUTH constants.
Even after this change, the authentication fails :( The proxy-Authorization header is generated but refused by the proxy. And the mechanism to prevent infinite loops does not work also because of wrong header name used.
Here is the Proxy Authenticator I use :
public class ProxyDigestAuthenticator extends DigestAuthenticator {
public ProxyDigestAuthenticator(Credentials credentials) {
super(credentials);
setProxy(true);
}
@Override
public Request authenticate(Route route, Response response) throws IOException {
if(response.request().header(PROXY_AUTH_RESP) != null)
return null;
String header = findProxyDigestHeader(response);
parseChallenge(header, 7, header.length() - 7);
// first copy all request headers to our params array
copyHeaderMap(response.headers(), parameters);
return authenticateWithState(response.request());
}
private String findProxyDigestHeader(Response response) {
final List<String> headers = response.headers(PROXY_AUTH);
for (String header : headers) {
if (header.startsWith("Digest")) {
return header;
}
}
throw new IllegalArgumentException("unsupported auth scheme: " + headers);
}
private void copyHeaderMap(Headers headers, Map<String, String> dest) {
for (int i = 0; i < headers.size(); i++) {
dest.put(headers.name(i), headers.value(i));
}
}
}
And how it is used :
final ProxyDigestAuthenticator authenticator = new ProxyDigestAuthenticator(new Credentials("proxy_user", "proxy_pass"));
builder.proxyAuthenticator(authenticator);
final Map<String, CachingAuthenticator> authCache = new ConcurrentHashMap<>();
builder.addInterceptor(new AuthenticationCacheInterceptor(authCache));
builder.authenticator(new CachingAuthenticatorDecorator(authenticator, authCache));
OkHttpClient client = builder.build();
Request request = new Request.Builder()
.url("https://mySite.com")
.build();
Response response = client.newCall(request).execute();
Any idea why it fails with these changes ?
Thanks
I'm thinking you'll want to add a NOTICE file and prominent statement of modification in each of the classes from HttpClient for which you modified the package name. HttpClient is licensed with the Apache 2.0 license, correct? In the Apache 2.0 license it states you may redistribute the code if you do the following:
Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
Hi,
Am writing a similar feature in sttp project and it would be great if you can share what you used as a proxy for ProxyAuthenticationManualTest.
Now that we're using okhttp-digest, our users need to add the jcentral repository to their maven / gradle projects. Before they could get everything from maven central. Unfortunately, some users of our project are struggling with the requirement to add jcentral, just because it's one more step and something they haven't done before.
Is there any way okhttp-digest could also deploy to maven central? Official instructions are here. An automated option is here.
I'm trying to do Digest Authentication using okhttp-digest 2.2 and okhttp 4.4.0 to a server on the local network. After making the initial request without the Authentication header, the server replies with a Challenge. The initial request is resend with the generated Digest Authentication header, but I still get a 401 with an Authentication Challenge.
I'm not exactly sure what's the issue, but I compared the requests made on Android with okhttp-digest/okhttp with iOS (using Alamofire). It is working on iOS.
The generated Authorization
header on Android:
key | value |
---|---|
username | 85szSUFQF2jJzRAY_eQpsA== |
realm | XTV |
nonce | MTU4Mjk5MjkzMjUwMzphZDFhMWExZmE3Nzg5NmZkZjlhZTM4ODA2ZjkzNjUyOQ== |
uri | 192.168.1.6:1926 |
response | 42a83bebf69d0c1c16a8b643e44ecef6 |
qop | auth |
nc | 00000001 |
cnonce | 4f6a4ad6ee010e00 |
algorithm | MD5 |
And on iOS:
key | value |
---|---|
username | G5hXS1YdTIDMKc81Nr3YEQ== |
realm | XTV |
nonce | MTU4Mjk5Mjk3NTUzMjpiNzdiZTJiMTRmNWRjOTJmMDQ2OWQzNmE5NTliOWFkNg== |
uri | /6/channeldb/tv/channelLists/all |
response | 25d1976209a214d64b5033638bf9532f |
algorithm | MD5 |
cnonce | 7fc19177939316b80ff5b49a56b90dfc |
nc | 00000001 |
qop | auth |
A striking difference is the uri
.
platform | uri |
---|---|
Android | 192.168.1.6:1926 |
iOS | /6/channeldb/tv/channelLists/all |
While on Android the host with port is used, on iOS it's the path component. Could this be the issue?
I rewrote the DigestAuthenticator
(in Kotlin, but you can convert it to Java).
This does quite a few things:
Noted issues:
STALE=true
(a valid auth which is stale will cause 401)Have a look through, if you are happy I can move to Java and do a PR.
/**
* Created by chris on 08/06/2016.
* For project Owlr Android
*/
@Suppress("NOTHING_TO_INLINE")
class DigestAuthenticator() : CachingAuthenticator {
/**
* Previous cached auth headers <host:port>,<field,value>>
*/
private val cachedAuthFields = mutableMapOf<String, MutableMap<String, String>>()
private val charset = Charset.forName("ISO-8859-1")
override fun authenticate(route: Route?, response: Response): Request? {
val request = response.request()
// We see if this request has previously been authed by this nonce
request.url().let { "${it.host()}:${it.port()}" }.let { it to cachedAuthFields[it] }.apply {
val (key, fields) = this
val nonce: String = fields?.get("nonce") ?: return@apply
if (havePreviouslyAuthedWithSameNonce(request, nonce)) {
Timber.w("Previous Digest authentication failed, returning null")
// Clear the cachedAuthFields just in case the nonce is invalid
cachedAuthFields.remove(key)
return null
}
}
// If no Digest Request we don't auth.
val auth = findAuthenticationDigestHeader(response.headers()) ?: return null
val authFields = splitAuthFields(auth.substring(7))
return authFromRequest(authFields, response.request())
?.apply {
// We only cache the authFields if we manage to actually create a auth response
request.url().apply { cachedAuthFields.put("${host()}:${port()}", authFields) }
}
}
override fun authenticateWithState(request: Request): Request? {
// If we haven't authed this host previously this will be null, so we can't preemptively auth it
val cachedAuthFields = request.url().let { "${it.host()}:${it.port()}" }.let { cachedAuthFields[it] } ?: return null
cachedAuthFields["realm"] ?: return null
cachedAuthFields["nonce"] ?: return null
return authFromRequest(cachedAuthFields, request)
}
fun authFromRequest(authFields: MutableMap<String, String>, request: Request): Request? {
// Pull out username and password from request
val (username, password) = Credentials(request)
// We generate a cnonce and nc for the following situations
var cnonce = ""
var nc = ""
when {
authFields["algorithm"] == "MD5-sess"
|| authFields["qop"] == "auth"
|| authFields["qop"] == "auth-int" -> {
cnonce = getClientNonce()
nc = getNonceCount(authFields["nc"])
authFields["nc"] = nc
}
}
val md5: MessageDigest = try {
MessageDigest.getInstance("MD5")
} catch(e: NoSuchAlgorithmException) {
return null
}
val HA1: String? = try {
md5.reset()
val ha1str = arrayOf(username, authFields["realm"], password).joinToString(":")
md5.update(ha1str.toByteArray(charset))
md5.digest().toHexString().let {
if (authFields["algorithm"] != "MD5-sess") return@let it
val ha1Ha1Str = arrayOf(it, authFields["nonce"], cnonce).joinToString(":")
md5.apply {
reset(); update(ha1Ha1Str.toByteArray(charset))
}.digest().toHexString()
}
} catch(e: UnsupportedEncodingException) {
return null
}
val HA2 = try {
md5.reset()
val ha2str = arrayOf(request.method(), request.url().getUri()).joinToString(":")
md5.update(ha2str.toByteArray(charset))
md5.digest().toHexString()
} catch(e: UnsupportedEncodingException) {
return null
}
val HA3 = try {
md5.reset()
val ha3str = when (authFields["qop"]) {
"auth", "auth-int" -> arrayOf(HA1, authFields["nonce"], nc, cnonce, authFields["qop"], HA2).joinToString(":")
else -> arrayOf(HA1, authFields["nonce"], HA2).joinToString(":")
}
md5.update(ha3str.toByteArray(charset))
md5.digest().toHexString()
} catch(e: UnsupportedEncodingException) {
return null
}
val authHeader = buildString {
append("Digest ")
append("username").append("=\"").append(username).append("\", ")
append("realm").append("=\"").append(authFields["realm"]).append("\", ")
append("nonce").append("=\"").append(authFields["nonce"]).append("\", ")
append("uri").append("=\"").append(request.url().getUri()).append("\", ")
append("qop").append('=').append(authFields["qop"] ?: "").append(", ")
if (cnonce.isNotBlank()) {
append("nc").append('=').append(nc).append(", ")
append("cnonce").append("=\"").append(cnonce).append("\", ")
}
append("response").append("=\"").append(HA3).append("\", ")
append("opaque").append("=\"").append(authFields["opaque"] ?: "").append("\"")
}
return request.newBuilder()
.header("Authorization", authHeader)
.build()
}
companion object {
private val secureRandom by lazy { SecureRandom() }
private inline fun ByteArray.toHexString(): String = ByteString.of(*this).hex()
/**
* Generates a unique client nonce
*/
private inline fun getClientNonce(): String {
val cnonceByteArrray = ByteArray(16)
secureRandom.nextBytes(cnonceByteArrray)
return ByteString.of(*cnonceByteArrray).hex()
}
/**
* Will see if there was a previous nonceCount and increment the previous count if so.
*/
private inline fun getNonceCount(previousNC: String?): String {
return ((previousNC?.toInt() ?: 0) + 1).toString().padStart(8, '0')
}
private inline fun HttpUrl.getUri(): String {
return "${this.encodedPath()}${this.encodedQuery() ?: ""}"
}
fun splitAuthFields(authString: String): MutableMap<String, String> {
val fields = mutableMapOf<String, String>()
authString.splitToSequence(',').filter { it.isNotBlank() }
.forEach { keyPair ->
keyPair.splitToSequence('=')
.map { it.trim('"', '\t', ' ') }
.take(2)
.toList()
.apply {
fields.put(get(0), get(1))
}
}
return fields;
}
private fun havePreviouslyAuthedWithSameNonce(request: Request, nonce: String): Boolean {
// prevent infinite loops when the password is wrong
request.header("Authorization")?.apply {
if (startsWith("Digest")) {
// Safety
val requestNonce = splitAuthFields(this.substring(7)).let {
val nonce = it["nonce"]
Timber.d("Last Request nonce: $nonce")
nonce
} ?: return false
if (requestNonce == nonce) {
return true
}
}
}
return false
}
private fun findAuthenticationDigestHeader(headers: Headers): String? {
return headers.values("WWW-Authenticate").firstOrNull { it.startsWith("Digest ") }
}
}
}
The logger used throughout is "android.util.Log". This causes problems for programs that want to use okhttp-digest and are not on an Android device.
Tested with okhttp-digest 2.1 and okhttp 4.4.0
Using a squid as proxy on ubuntu-server (the docker image)
configured as in this howto: https://blog.mafr.de/2013/06/16/setting-up-a-web-proxy-with-squid/
Digest Authentication at the web proxy is working well, but after the remote server was sending an http-301 redirect, the client wants to call the new URL.
The session is keep-alive, so another authentication is not needed.
But the DigestAuthenticator still sees the 407 response code, and then looks for the digest header, and crashes because the value is "OkHttp-Preemptive" then.
The following is a helpful workaround:
`
DigestAuthenticator authenticator = new DigestAuthenticator(new Credentials(username, password));
Authenticator correctedAuthenticator = new Authenticator() {
@OverRide
public Request authenticate(Route route, Response response) throws IOException {
// workaround problem on HTTP-301 redirect
if ("OkHttp-Preemptive".equals(response.header("Proxy-Authenticate"))) {
return null;
}
return authenticator.authenticate(route, response);
}
};
builder = builder.proxyAuthenticator(correctedAuthenticator);`
When the request itself is not repeatable (let's say if the body's request is a stream), we should throw an exception when we get a 401 and not retry the request. For example see this request and response. The body is not repeatable.
PUT /v1/ext/test/evaltest.xqy HTTP/1.1
ML-Agent-ID: java
Content-Type: text/plain
Transfer-Encoding: chunked
Host: 192.168.0.39:8012
Connection: Keep-Alive
Accept-Encoding: gzip
User-Agent: okhttp/3.6.0
8d3
declare namespace test='http://marklogic.com/test';
declare variable $test:myString as xs:string external;
declare variable $myArray as json:array external;
declare variable $myObject as json:object external;
declare variable $myAnyUri as xs:anyURI external;
declare variable $myBinary as binary() external;
declare variable $myBase64Binary as xs:base64Binary external;
declare variable $myHexBinary as xs:hexBinary external;
declare variable $myDuration as xs:duration external;
declare variable $myQName as xs:QName external;
declare variable $myDocument as document-node() external;
declare variable $myComment as document-node() external;
(: declare variable $myComment as comment() external; :)
declare variable $myElement as element() external;
declare variable $myProcessingInstruction as document-node() external;
declare variable $myText as text() external;
declare variable $myBool as xs:boolean external;
declare variable $myInteger as xs:integer external;
declare variable $myBigInteger as xs:string external;
declare variable $myDecimal as xs:decimal external;
declare variable $myDouble as xs:double external;
declare variable $myFloat as xs:float external;
declare variable $myGDay as xs:gDay external;
declare variable $myGMonth as xs:gMonth external;
declare variable $myGMonthDay as xs:gMonthDay external;
declare variable $myGYear as xs:gYear external;
declare variable $myGYearMonth as xs:gYearMonth external;
declare variable $myDate as xs:date external;
declare variable $myDateTime as xs:dateTime external;
declare variable $myTime as xs:time external;
let $myAttribute := $myElement/@*
let $myComment := $myComment/comment()
let $myProcessingInstruction := $myProcessingInstruction/processing-instruction()
let $myCtsQuery := cts:word-query('a')
let $myFunction := xdmp:functions()[2]
return (
$test:myString, $myArray, $myObject, $myAnyUri,
$myBinary, $myBase64Binary, $myHexBinary, $myDuration, $myQName,
$myDocument, $myAttribute, $myComment, $myElement, $myProcessingInstruction, $myText,
$myBool, $myInteger, $myBigInteger, $myDecimal, $myDouble, $myFloat,
$myGDay, $myGMonth, $myGMonthDay, $myGYear, $myGYearMonth, $myDate, $myDateTime, $myTime,
$myCtsQuery, $myFunction
)
0
HTTP/1.1 401 Unauthorized
Server: MarkLogic
WWW-Authenticate: Digest realm="public", qop="auth", nonce="eaa1e15dd0e0d4e3760c8a82c799e2a2", opaque="7b5ca17f3c782ac1"
Content-Type: application/json; charset=utf-8
Content-Length: 104
Connection: Keep-Alive
Keep-Alive: timeout=5
{"errorResponse": {"statusCode":401,
"status":"Unauthorized",
"message":"401 Unauthorized"
}
}
PUT /v1/ext/test/evaltest.xqy HTTP/1.1
ML-Agent-ID: java
Authorization: Digest username="rest-admin", realm="public", nonce="eaa1e15dd0e0d4e3760c8a82c799e2a2", uri="/v1/ext/test/evaltest.xqy", response="18bad7d22af59c0e3ffe537014a71def", qop=auth, nc=00000001, cnonce="15af601b99993a21", algorithm=MD5, opaque="7b5ca17f3c782ac1"
Content-Type: text/plain
Transfer-Encoding: chunked
Host: 192.168.0.39:8012
Connection: Keep-Alive
Accept-Encoding: gzip
User-Agent: okhttp/3.6.0
0
HTTP/1.1 201 Created
Location:
Server: MarkLogic
Content-Length: 0
Connection: Keep-Alive
Keep-Alive: timeout=5
Please note that, in order to make this work, we have a ping as a first call to get the nonce and then make this request. However if something goes awry and we encounter a situation like this, we should get a 401 and not retry which would result in an empty body.
okhttp
does support providing different char-sets for authentication. Using the default of okhttp
which is ISO_8859_1
I've problems with some servers and chars e.g. "äöü" and want to switch to UTF-8
.
How can I archive this using basic and digest access authentication and this library?
If I provide the authentication directly to okhttp
using UTF-8
my Nextcloud accepts chars like "äöü" in the password.
.authenticator((route, response) -> {
String credential = okhttp3.Credentials.basic("test", "äöüäöüäöü", UTF_8);
return response.request().newBuilder().header("Authorization", credential).build();
})
ISSUE
If basic and digest authenticators specified (see code below), basic one (not secured one) is used always if web server supports both basic and digest authentications.
DispatchingAuthenticator authenticator = new DispatchingAuthenticator.Builder()
.with("digest", digestAuthenticator)
.with("basic", basicAuthenticator)
.build();
EXAMPLE
GET /Streaming/Channels/1/picture HTTP/1.1
Host: 192.168.1.4:1140
Connection: Keep-Alive
HTTP/1.1 401 Unauthorized
Date: Mon, 08 Aug 2016 17:28:03 GMT
Server: App-webs/
Content-Length: 275
Content-Type: text/html
Connection: close
WWW-Authenticate: Digest qop="auth", realm="DS-2CD2432F-IW", nonce="4d6a6c4551305a43515449364e6d497a4d6a63314e546b3d", stale="FALSE"
WWW-Authenticate: Basic realm="DS-2CD2432F-IW"
[data skipped]
GET /Streaming/Channels/1/picture HTTP/1.1
Authorization: Basic YWRtaW46dGVzdA==
Host: 192.168.1.4:1140
Connection: Keep-Alive
HTTP/1.1 200 OK
Content-Type: image/jpeg; charset="UTF-8"
Content-Length:131764
[data skipped]
The issue is that authenticate(...) method in DispatchingAuthenticator selects authenticator according to it's key value in hashmap authenticatorRegistry.
FIX
authenticatorRegistry in DispatchingAuthenticator class should be of type LinkedHashMap instead of HashMap.
client = new OkHttpClient();
final DigestAuthenticator authenticator = new DigestAuthenticator();
authenticator.setCredentials(new Credentials("username", "pass"));
this is your code, but authenticator don't have method "setCredentials",can you help me to solve it?
Attempting to use the example code in the readme for this project, but not able to find the above method in this project or the basic okhttp3 library. What is the correct usage?
I see that in 2.5.0 okhttp the method did exist, however if using 3.9.0 how would the code run?
Thanks a lot for making this project available. We're using it in Apache NiFi, but see that you have a dependency on com.squareup.okhttp:okhttp:2.6.0-SNAPSHOT. The most recent version of okhttp appears to be 2.7.0[1]... do you have plans to update the dependency? Thanks again.
[1] https://github.com/square/okhttp/blob/master/CHANGELOG.md
Brandon
When used in an application with many concurrent requests, I get the warning "previous digest authentication with same nonce failed, returning null" frequently right before I get a failed request. I'm wondering if somehow the shared "parameters" object in DigestAuthenticator is getting overwritten by multiple concurrent requests and thus accidentally getting marked as stale. I've been using DispatchingAuthenticator as directed in the README.md, but only using BasicAuthenticator or DigestAuthenticator because of #32.
Hi,
I am receiving several reports of this problem. See stack trace below. Is it my fault?
java.lang.IllegalArgumentException: missing nonce in challenge
at com.burgstaller.okhttp.digest.DigestAuthenticator.authenticateWithState(DigestAuthenticator.java:170)
at com.burgstaller.okhttp.digest.DigestAuthenticator.authenticate(DigestAuthenticator.java:148)
at com.burgstaller.okhttp.DispatchingAuthenticator.authenticate(DispatchingAuthenticator.java:45)
at com.burgstaller.okhttp.CachingAuthenticatorDecorator.authenticate(CachingAuthenticatorDecorator.java:30)
at okhttp3.internal.http.RetryAndFollowUpInterceptor.followUpRequest(RetryAndFollowUpInterceptor.java:283)
at okhttp3.internal.http.RetryAndFollowUpInterceptor.intercept(RetryAndFollowUpInterceptor.java:153)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:92)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:67)
at com.burgstaller.okhttp.AuthenticationCacheInterceptor.intercept(AuthenticationCacheInterceptor.java:37)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:92)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:67)
at com.xxxxxxx.xxxxxx.singleton.HTTPClient$1.intercept(HTTPClient.java:64)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:92)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:67)
at okhttp3.RealCall.getResponseWithInterceptorChain(RealCall.java:170)
at okhttp3.RealCall.access$100(RealCall.java:33)
at okhttp3.RealCall$AsyncCall.execute(RealCall.java:120)
at okhttp3.internal.NamedRunnable.run(NamedRunnable.java:32)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1113)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)
at java.lang.Thread.run(Thread.java:818)
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.