{"action":"opened","issue":{"url":"https://api.github.com/repos/rburgst/okhttp-digest/issues/14","repository_url":"https://api.github.com/repos/rburgst/okhttp-digest","labels_url":"https://api.github.com/repos/rburgst/okhttp-digest/issues/14/labels{/name}","comments_url":"https://api.github.com/repos/rburgst/okhttp-digest/issues/14/comments","events_url":"https://api.github.com/repos/rburgst/okhttp-digest/issues/14/events","html_url":"https://github.com/rburgst/okhttp-digest/issues/14","id":164564226,"number":14,"title":"Digest Auth Without Using httpclient","user":{"login":"chrisjenx","id":1167793,"avatar_url":"https://avatars.githubusercontent.com/u/1167793?v=3","gravatar_id":"","url":"https://api.github.com/users/chrisjenx","html_url":"https://github.com/chrisjenx","followers_url":"https://api.github.com/users/chrisjenx/followers","following_url":"https://api.github.com/users/chrisjenx/following{/other_user}","gists_url":"https://api.github.com/users/chrisjenx/gists{/gist_id}","starred_url":"https://api.github.com/users/chrisjenx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/chrisjenx/subscriptions","organizations_url":"https://api.github.com/users/chrisjenx/orgs","repos_url":"https://api.github.com/users/chrisjenx/repos","events_url":"https://api.github.com/users/chrisjenx/events{/privacy}","received_events_url":"https://api.github.com/users/chrisjenx/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"assignees":[],"milestone":null,"comments":0,"created_at":"2016-07-08T16:10:11Z","updated_at":"2016-07-08T16:10:11Z","closed_at":null,"body":"I rewrote the `DigestAuthenticator` (in Kotlin, but you can convert it to Java).\r\n\r\nThis does quite a few things:\r\n- Handles MD5-sess\r\n- Handles qop=auth (needs a little more work for auth-int)\r\n- Has a significantly smaller foot print (one class now, it could be split up a little bit)\r\n- Handles caching correctly (remembers nonce and nonceCount)\r\n- Will fail on incorrect user/pass.\r\n\r\nNoted issues:\r\n- Does not retry on `STALE=true` (a valid auth which is stale will cause 401)\r\n\r\nHave a look through, if you are happy I can move to Java and do a PR.\r\n\r\n```\r\n/**\r\n * Created by chris on 08/06/2016.\r\n * For project Owlr Android\r\n */\r\n@Suppress(\"NOTHING_TO_INLINE\")\r\nclass DigestAuthenticator() : CachingAuthenticator {\r\n\r\n    /**\r\n     * Previous cached auth headers <host:port>,<field,value>>\r\n     */\r\n    private val cachedAuthFields = mutableMapOf<String, MutableMap<String, String>>()\r\n    private val charset = Charset.forName(\"ISO-8859-1\")\r\n\r\n    override fun authenticate(route: Route?, response: Response): Request? {\r\n        val request = response.request()\r\n\r\n        // We see if this request has previously been authed by this nonce\r\n        request.url().let { \"${it.host()}:${it.port()}\" }.let { it to cachedAuthFields[it] }.apply {\r\n            val (key, fields) = this\r\n            val nonce: String = fields?.get(\"nonce\") ?: return@apply\r\n            if (havePreviouslyAuthedWithSameNonce(request, nonce)) {\r\n                Timber.w(\"Previous Digest authentication failed, returning null\")\r\n                // Clear the cachedAuthFields just in case the nonce is invalid\r\n                cachedAuthFields.remove(key)\r\n                return null\r\n            }\r\n        }\r\n\r\n        // If no Digest Request we don't auth.\r\n        val auth = findAuthenticationDigestHeader(response.headers()) ?: return null\r\n        val authFields = splitAuthFields(auth.substring(7))\r\n\r\n        return authFromRequest(authFields, response.request())\r\n                ?.apply {\r\n                    // We only cache the authFields if we manage to actually create a auth response\r\n                    request.url().apply { cachedAuthFields.put(\"${host()}:${port()}\", authFields) }\r\n                }\r\n    }\r\n\r\n    override fun authenticateWithState(request: Request): Request? {\r\n        // If we haven't authed this host previously this will be null, so we can't preemptively auth it\r\n        val cachedAuthFields = request.url().let { \"${it.host()}:${it.port()}\" }.let { cachedAuthFields[it] } ?: return null\r\n        cachedAuthFields[\"realm\"] ?: return null\r\n        cachedAuthFields[\"nonce\"] ?: return null\r\n        return authFromRequest(cachedAuthFields, request)\r\n    }\r\n\r\n    fun authFromRequest(authFields: MutableMap<String, String>, request: Request): Request? {\r\n        // Pull out username and password from request\r\n        val (username, password) = Credentials(request)\r\n\r\n        // We generate a cnonce and nc for the following situations\r\n        var cnonce = \"\"\r\n        var nc = \"\"\r\n        when {\r\n            authFields[\"algorithm\"] == \"MD5-sess\"\r\n                    || authFields[\"qop\"] == \"auth\"\r\n                    || authFields[\"qop\"] == \"auth-int\" -> {\r\n                cnonce = getClientNonce()\r\n                nc = getNonceCount(authFields[\"nc\"])\r\n                authFields[\"nc\"] = nc\r\n            }\r\n        }\r\n        val md5: MessageDigest = try {\r\n            MessageDigest.getInstance(\"MD5\")\r\n        } catch(e: NoSuchAlgorithmException) {\r\n            return null\r\n        }\r\n\r\n        val HA1: String? = try {\r\n            md5.reset()\r\n            val ha1str = arrayOf(username, authFields[\"realm\"], password).joinToString(\":\")\r\n            md5.update(ha1str.toByteArray(charset))\r\n            md5.digest().toHexString().let {\r\n                if (authFields[\"algorithm\"] != \"MD5-sess\") return@let it\r\n                val ha1Ha1Str = arrayOf(it, authFields[\"nonce\"], cnonce).joinToString(\":\")\r\n                md5.apply {\r\n                    reset(); update(ha1Ha1Str.toByteArray(charset))\r\n                }.digest().toHexString()\r\n            }\r\n        } catch(e: UnsupportedEncodingException) {\r\n            return null\r\n        }\r\n\r\n        val HA2 = try {\r\n            md5.reset()\r\n            val ha2str = arrayOf(request.method(), request.url().getUri()).joinToString(\":\")\r\n            md5.update(ha2str.toByteArray(charset))\r\n            md5.digest().toHexString()\r\n        } catch(e: UnsupportedEncodingException) {\r\n            return null\r\n        }\r\n\r\n        val HA3 = try {\r\n            md5.reset()\r\n            val ha3str = when (authFields[\"qop\"]) {\r\n                \"auth\", \"auth-int\" -> arrayOf(HA1, authFields[\"nonce\"], nc, cnonce, authFields[\"qop\"], HA2).joinToString(\":\")\r\n                else -> arrayOf(HA1, authFields[\"nonce\"], HA2).joinToString(\":\")\r\n            }\r\n            md5.update(ha3str.toByteArray(charset))\r\n            md5.digest().toHexString()\r\n        } catch(e: UnsupportedEncodingException) {\r\n            return null\r\n        }\r\n\r\n        val authHeader = buildString {\r\n            append(\"Digest \")\r\n            append(\"username\").append(\"=\\\"\").append(username).append(\"\\\", \")\r\n            append(\"realm\").append(\"=\\\"\").append(authFields[\"realm\"]).append(\"\\\", \")\r\n            append(\"nonce\").append(\"=\\\"\").append(authFields[\"nonce\"]).append(\"\\\", \")\r\n            append(\"uri\").append(\"=\\\"\").append(request.url().getUri()).append(\"\\\", \")\r\n            append(\"qop\").append('=').append(authFields[\"qop\"] ?: \"\").append(\", \")\r\n            if (cnonce.isNotBlank()) {\r\n                append(\"nc\").append('=').append(nc).append(\", \")\r\n                append(\"cnonce\").append(\"=\\\"\").append(cnonce).append(\"\\\", \")\r\n            }\r\n            append(\"response\").append(\"=\\\"\").append(HA3).append(\"\\\", \")\r\n            append(\"opaque\").append(\"=\\\"\").append(authFields[\"opaque\"] ?: \"\").append(\"\\\"\")\r\n        }\r\n\r\n        return request.newBuilder()\r\n                .header(\"Authorization\", authHeader)\r\n                .build()\r\n    }\r\n\r\n    companion object {\r\n\r\n        private val secureRandom by lazy { SecureRandom() }\r\n\r\n        private inline fun ByteArray.toHexString(): String = ByteString.of(*this).hex()\r\n\r\n        /**\r\n         * Generates a unique client nonce\r\n         */\r\n        private inline fun getClientNonce(): String {\r\n            val cnonceByteArrray = ByteArray(16)\r\n            secureRandom.nextBytes(cnonceByteArrray)\r\n            return ByteString.of(*cnonceByteArrray).hex()\r\n        }\r\n\r\n        /**\r\n         * Will see if there was a previous nonceCount and increment the previous count if so.\r\n         */\r\n        private inline fun getNonceCount(previousNC: String?): String {\r\n            return ((previousNC?.toInt() ?: 0) + 1).toString().padStart(8, '0')\r\n        }\r\n\r\n        private inline fun HttpUrl.getUri(): String {\r\n            return \"${this.encodedPath()}${this.encodedQuery() ?: \"\"}\"\r\n        }\r\n\r\n        fun splitAuthFields(authString: String): MutableMap<String, String> {\r\n            val fields = mutableMapOf<String, String>()\r\n            authString.splitToSequence(',').filter { it.isNotBlank() }\r\n                    .forEach { keyPair ->\r\n                        keyPair.splitToSequence('=')\r\n                                .map { it.trim('\"', '\\t', ' ') }\r\n                                .take(2)\r\n                                .toList()\r\n                                .apply {\r\n                                    fields.put(get(0), get(1))\r\n                                }\r\n                    }\r\n            return fields;\r\n        }\r\n\r\n        private fun havePreviouslyAuthedWithSameNonce(request: Request, nonce: String): Boolean {\r\n            // prevent infinite loops when the password is wrong\r\n            request.header(\"Authorization\")?.apply {\r\n                if (startsWith(\"Digest\")) {\r\n                    // Safety\r\n                    val requestNonce = splitAuthFields(this.substring(7)).let {\r\n                        val nonce = it[\"nonce\"]\r\n                        Timber.d(\"Last Request nonce: $nonce\")\r\n                        nonce\r\n                    } ?: return false\r\n                    if (requestNonce == nonce) {\r\n                        return true\r\n                    }\r\n                }\r\n            }\r\n            return false\r\n        }\r\n\r\n        private fun findAuthenticationDigestHeader(headers: Headers): String? {\r\n            return headers.values(\"WWW-Authenticate\").firstOrNull { it.startsWith(\"Digest \") }\r\n        }\r\n    }\r\n}\r\n```\r\n"}}
{"actor":{"display_login":"chrisjenx"}}