Skip to content

Instantly share code, notes, and snippets.

@takumakei
Created December 11, 2012 13:21
Show Gist options
  • Save takumakei/4258500 to your computer and use it in GitHub Desktop.
Save takumakei/4258500 to your computer and use it in GitHub Desktop.
HTTP Digest Authorization for Play 2.0
import play.api._
import play.api.mvc._
import java.security.{ MessageDigest, SecureRandom }
import scala.util.matching.Regex.Match
import org.apache.commons.codec.binary.Hex.encodeHex
import org.apache.commons.codec.binary.Base64.encodeBase64
trait DigestRealm {
def createChallenge(): Challenge
/**
* DigestRealm companion object will use this value to verify the request-digest from a client.
* @return A1 (RFC2617 #3.2.2.2) if credentials was valid. otherwise None.
*/
def authorize(credentials: Map[String, String]): Option[String]
}
case class Challenge(
realm: String,
nonce: String,
domain: Option[String] = None,
opaque: Option[String] = None,
stale: Option[Boolean] = None,
algorithm: Option[String] = None,
qop: Option[String] = None
) {
def toHeaderValue = (
keyValueQuoted("realm", realm) ::
simpleKeyValue("nonce", nonce) ::
(
domain.map(keyValueQuoted("domain", _)) ::
opaque.map(simpleKeyValue("opaque", _)) ::
stale.map(simpleBoolean("stale", _)) ::
algorithm.map(simpleKeyValue("algorithm", _)) ::
qop.map(keyValueQuoted("qop", _)) ::
Nil
).flatten
).mkString("Digest ", ", ", "")
private def simpleKeyValue(a: String, b: String) = Seq(a, b).mkString("=")
private def simpleBoolean(a: String, b: Boolean) = simpleKeyValue(a, if (b) "true" else "false")
private def keyValueQuoted(a: String, b: String) = Seq(a, b).mkString("", "=\"", "\"")
}
trait RandomChallenge {
def createChallenge() = Challenge(
realm = realm,
nonce = nonce(),
opaque = opaque(),
algorithm = algorithm,
qop = qop
)
val realm = "Secured"
def nonce() = new String(encodeBase64(random(24)))
def opaque() = Some(new String(encodeBase64(random(24))))
val algorithm = Some("MD5")
val qop = Some("auth")
val rng = SecureRandom.getInstance("SHA1PRNG")
def random(size: Int) = {
val a = new Array[Byte](size)
rng.nextBytes(a)
a
}
}
object DigestRealm extends Controller {
def apply[A](digestRealm: DigestRealm)(action: Action[A]) = Action(action.parser) { request =>
request.headers.get(AUTHORIZATION).flatMap { authorization =>
digestResponseParser(authorization).flatMap { digestResponse =>
for {
nc <- digestResponse.get("nc")
uri <- digestResponse.get("uri")
qop <- digestResponse.get("qop")
nonce <- digestResponse.get("nonce")
cnonce <- digestResponse.get("cnonce")
response <- digestResponse.get("response")
ha1 <- digestRealm.authorize(digestResponse) if {
val ha2 = md5(List(request.method, uri).mkString(":"))
val requestDigest = md5(List(ha1, nonce, nc, cnonce, qop, ha2).mkString(":"))
requestDigest == response
}
} yield action(request)
}
}.getOrElse {
Unauthorized.withHeaders(WWW_AUTHENTICATE -> digestRealm.createChallenge.toHeaderValue)
}
}
private val credentials = """Digest\s+(.*)""".r
private val kv0 = """\s*([^\s=]+)\s*=\s*"([^"]+)"\s*""".r
private val kv1 = """\s*([^\s=]+)\s*=\s*(.*)""".r
private val messageDigest = MessageDigest.getInstance("MD5")
private def digestResponseParser(authorization: String) = {
credentials.findPrefixMatchOf(authorization).map { m =>
m.group(1).split(",").flatMap {
case kv0(k, v) => Some(k.toLowerCase -> v)
case kv1(k, v) => Some(k.toLowerCase -> v)
case _ => None
}.toMap
}
}
def md5(s: String): String = new String(encodeHex(messageDigest.digest(s.getBytes)))
def mkDigest(realm: String, username: String, password: String) =
md5(List(username, realm, password).mkString(":"))
}
val realm = new DigestRealm with RandomChallenge {
override val realm = "Digest Realm"
def authorize(credentials: Map[String, String]) = for {
username <- credentials.get("username")
digest <- digests.get(username)
} yield digest
val digests = Map(mkDigestEntry("admin", "1234secret"))
def mkDigestEntry(username: String, password: String) =
username -> DigestRealm.mkDigest(realm, username, password)
}
def myAction = DigestRealm(realm) {
Action { request =>
Ok
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment