Created
December 11, 2012 13:21
-
-
Save takumakei/4258500 to your computer and use it in GitHub Desktop.
HTTP Digest Authorization for Play 2.0
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(":")) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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