All Downloads are FREE. Search and download functionalities are using the official Maven repository.

scalaj.http.DigestAuth.scala Maven / Gradle / Ivy

The newest version!
package scalaj.http

import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import java.util.Locale

import scala.collection.immutable.VectorBuilder
import scala.util.Random


case class WwwAuthenticate(authType: String, params: Map[String, String])
object DigestAuth {

  def trimQuotes(str: String): String = {
    if (str.length >= 2 && str.charAt(0) == '"' && str.charAt(str.length - 1) == '"') {
      str.substring(1, str.length - 1)
    } else {
      str
    }
  }

  // need to parse one char at a time rather than split on comma because values can be
  // quoted comma separated strings
  def splitParams(params: String): IndexedSeq[String] = {
    val builder = new VectorBuilder[String]()
    var start = 0
    var i = 0
    var quotes = 0
    while (i < params.length) {
      params.charAt(i) match {
        case '\\' => i += 1
        case '"' => quotes += 1
        case ',' =>
          if (quotes % 2 == 0) {
            val item = params.substring(start, i).trim()
            if (item.length > 0) {
              builder += item
            }
            start = i + 1
          }
        case _ => // nada
      }
      i += 1
    }
    builder += params.substring(start).trim()
    builder.result()
  }

  def getAuthDetails(headerValue: String): Option[WwwAuthenticate] = {
    headerValue.indexOf(' ') match {
      case indexOfSpace if indexOfSpace > 0 =>
        val authType = headerValue.substring(0, indexOfSpace)
        val params: Map[String, String] = splitParams(headerValue.substring(indexOfSpace + 1)).flatMap(param => {
          param.split("=", 2) match {
            case Array(key, value) => Some(key.trim.toLowerCase(Locale.ENGLISH) -> trimQuotes(value.trim))
            case _ => None
          }
        }).toMap
        Some(WwwAuthenticate(authType, params))
      case _ => None
    }
  }

  val HexArray = "0123456789abcdef".toCharArray()

  def hex(bytes: Array[Byte]): String = {
    val hexChars = new Array[Char](bytes.length * 2)
    var j = 0
    while (j < bytes.length) {
      val v = bytes(j) & 0xFF
      hexChars(j * 2) = HexArray(v >>> 4)
      hexChars(j * 2 + 1) = HexArray(v & 0x0F)
      j += 1
    }
    new String(hexChars)
  }

  val DigestPrefix = "Digest"

  def createHeaderValue
  (
    username: String,
    password: String,
    method: String,
    uri: String,
    content: Array[Byte],
    serverParams: Map[String, String],
    testClientNonce: Option[String] = None
  ): Option[String] = {
    val algorithm = serverParams.getOrElse("algorithm", "MD5")
    val digester = Option(MessageDigest.getInstance(algorithm)).getOrElse(
      throw new Exception("unsupported digest algorithm" + algorithm)
    )
    def hexDigest(str: String): String = hex(digester.digest(str.getBytes(StandardCharsets.ISO_8859_1)))
    for {
      realm <- serverParams.get("realm")
      nonce <- serverParams.get("nonce")
    } yield {
      val qopOpt: Option[String] = serverParams.get("qop").flatMap(serverQop => {
        val serverQopValues = serverQop.split(',').map(_.trim)
        if(serverQopValues.contains("auth")) Some("auth")
        else if (serverQopValues.contains("auth-int")) Some("auth-int")
        else None
      })
      val a1 = username + ":" + realm + ":" + password
      val hashA1: String = hexDigest(a1)
      val a2 = method + ":" + uri + {
        if (qopOpt.exists(_ == "auth-int")) ":" + hex(digester.digest(content)) else ""
      }
      val hashA2: String = hexDigest(a2)

      val (nonceCountOpt, clientNonceOpt, a3) = qopOpt match {
        case Some(qop) =>
          val nc = "00000001"
          val clientNonce = testClientNonce.getOrElse({
            val bytes = new Array[Byte](16)
            Random.nextBytes(bytes)
            hex(bytes)
          })
          val a3 = hashA1 + ":" + nonce + ":" + nc + ":" + clientNonce + ":" + qop + ":" + hashA2
          (Some(nc), Some(clientNonce), a3)
        case _ =>
          (None, None, hashA1 + ":" + nonce + ":" + hashA2)
      }
      val hashA3: String = hexDigest(a3)
      val sb = new StringBuilder(DigestPrefix).append(" ")
      def appendQuoted(key: String, value: String): StringBuilder = {
        sb.append(key + "=\"").append(value).append("\"")
      }
      appendQuoted("username", username).append(", ")
      appendQuoted("realm", realm).append(", ")
      appendQuoted("nonce", nonce).append(", ")
      serverParams.get("opaque").foreach(opaque => {
        appendQuoted("opaque", opaque).append(", ")
      })
      appendQuoted("algorithm", algorithm).append(", ")
      appendQuoted("uri", uri).append(", ")
      for {
        qop <- qopOpt
        nonceCount <- nonceCountOpt
        clientNonce <- clientNonceOpt
      } {
        appendQuoted("qop", qop).append(", ")
        appendQuoted("nc", nonceCount).append(", ")
        appendQuoted("cnonce", clientNonce).append(", ")
      }
      appendQuoted("response", hashA3)
      sb.toString()
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy