|
Play Framework/Scala example source code file (OpenID.scala)
The OpenID.scala Play Framework example source code
/*
* Copyright (C) 2009-2013 Typesafe Inc. <http://www.typesafe.com>
*/
package play.api.libs.openid
import scala.concurrent.Future
import scala.util.control.Exception._
import scala.util.matching.Regex
import play.api.http.HeaderNames
import play.api.libs.ws._
import java.net._
import play.api.mvc.Request
import xml.Node
//TODO do not use Play's internal execution context in libs
import play.core.Execution.Implicits.internalContext
case class OpenIDServer(url: String, delegate: Option[String])
case class UserInfo(id: String, attributes: Map[String, String] = Map.empty)
/**
* provides user information for a verified user
*/
object UserInfo {
def apply(queryString: Map[String, Seq[String]]): UserInfo = {
val extractor = new UserInfoExtractor(queryString)
val id = extractor.id getOrElse (throw Errors.BAD_RESPONSE)
new UserInfo(id, extractor.axAttributes)
}
/**
* Extract the values required to create an instance of the UserInfo
*
* The UserInfoExtractor ensures that attributes returned via OpenId attribute exchange are signed
* (i.e. listed in the openid.signed field) and verified in the check_authentication step.
*/
private[openid] class UserInfoExtractor(params: Map[String, Seq[String]]) {
val AxAttribute = """^openid\.([^.]+\.value\.([^.]+(\.\d+)?))$""".r
val extractAxAttribute: PartialFunction[String, (String, String)] = {
case AxAttribute(fullKey, key, num) => (fullKey, key) // fullKey e.g. 'ext1.value.email', shortKey e.g. 'email' or 'fav_movie.2'
}
private lazy val signedFields = params.get("openid.signed") flatMap { _.headOption map { _.split(",") } } getOrElse (Array())
def id = params.get("openid.claimed_id").flatMap(_.headOption).orElse(params.get("openid.identity").flatMap(_.headOption))
def axAttributes = params.foldLeft(Map[String, String]()) {
case (result, (key, values)) => extractAxAttribute.lift(key) flatMap {
case (fullKey, shortKey) if signedFields.contains(fullKey) => values.headOption map { value => Map(shortKey -> value) }
case _ => None
} map (result ++ _) getOrElse result
}
}
}
/**
* provides OpenID support
*/
object OpenID extends OpenIDClient(WS.client(play.api.Play.current))
private[openid] class OpenIDClient(ws: WSClient) {
val discovery = new Discovery(ws)
/**
* Retrieve the URL where the user should be redirected to start the OpenID authentication process
*/
def redirectURL(openID: String,
callbackURL: String,
axRequired: Seq[(String, String)] = Seq.empty,
axOptional: Seq[(String, String)] = Seq.empty,
realm: Option[String] = None): Future[String] = {
val claimedId = discovery.normalizeIdentifier(openID)
discovery.discoverServer(openID).map({ server =>
val parameters = Seq(
"openid.ns" -> "http://specs.openid.net/auth/2.0",
"openid.mode" -> "checkid_setup",
"openid.claimed_id" -> claimedId,
"openid.identity" -> server.delegate.getOrElse(claimedId),
"openid.return_to" -> callbackURL
) ++ axParameters(axRequired, axOptional) ++ realm.map("openid.realm" -> _).toList
val separator = if (server.url.contains("?")) "&" else "?"
server.url + separator + parameters.map(pair => pair._1 + "=" + URLEncoder.encode(pair._2, "UTF-8")).mkString("&")
})
}
/**
* From a request corresponding to the callback from the OpenID server, check the identity of the current user
*/
def verifiedId(implicit request: Request[_]): Future[UserInfo] = verifiedId(request.queryString)
/**
* For internal use
*/
def verifiedId(queryString: java.util.Map[String, Array[String]]): Future[UserInfo] = {
import scala.collection.JavaConversions._
verifiedId(queryString.toMap.mapValues(_.toSeq))
}
private def verifiedId(queryString: Map[String, Seq[String]]): Future[UserInfo] = {
(queryString.get("openid.mode").flatMap(_.headOption),
queryString.get("openid.claimed_id").flatMap(_.headOption)) match { // The Claimed Identifier. "openid.claimed_id" and "openid.identity" SHALL be either both present or both absent.
case (Some("id_res"), Some(id)) => {
// MUST perform discovery on the claimedId to resolve the op_endpoint.
val server: Future[OpenIDServer] = discovery.discoverServer(id)
server.flatMap(directVerification(queryString))
}
case (Some("cancel"), _) => Future.failed(Errors.AUTH_CANCEL)
case _ => Future.failed(Errors.BAD_RESPONSE)
}
}
/**
* Perform direct verification (see 11.4.2. Verifying Directly with the OpenID Provider)
*/
private def directVerification(queryString: Map[String, Seq[String]])(server: OpenIDServer) = {
import play.api.Play.current
val fields = (queryString - "openid.mode" + ("openid.mode" -> Seq("check_authentication")))
ws.url(server.url).post(fields).map(response => {
if (response.status == 200 && response.body.contains("is_valid:true")) {
UserInfo(queryString)
} else throw Errors.AUTH_ERROR
})
}
private def axParameters(axRequired: Seq[(String, String)],
axOptional: Seq[(String, String)]): Seq[(String, String)] = {
if (axRequired.isEmpty && axOptional.isEmpty)
Nil
else {
val axRequiredParams = if (axRequired.isEmpty) Nil
else Seq("openid.ax.required" -> axRequired.map(_._1).mkString(","))
val axOptionalParams = if (axOptional.isEmpty) Nil
else Seq("openid.ax.if_available" -> axOptional.map(_._1).mkString(","))
val definitions = (axRequired ++ axOptional).map(attribute => ("openid.ax.type." + attribute._1 -> attribute._2))
Seq("openid.ns.ax" -> "http://openid.net/srv/ax/1.0", "openid.ax.mode" -> "fetch_request") ++ axRequiredParams ++ axOptionalParams ++ definitions
}
}
}
/**
* Resolve the OpenID identifier to the location of the user's OpenID service provider.
*
* Known limitations:
*
* * The Discovery doesn't support XRIs at the moment
*/
private[openid] class Discovery(ws: WSClient) {
import Discovery._
case class UrlIdentifier(url: String) {
def normalize = catching(classOf[MalformedURLException], classOf[URISyntaxException]) opt {
def port(p: Int) = p match {
case 80 | 443 => -1
case port => port
}
def schemeForPort(p: Int) = p match {
case 443 => "https"
case _ => "http"
}
def scheme(uri: URI) = Option(uri.getScheme) getOrElse schemeForPort(uri.getPort)
def path(path: String) = if (null == path || path.isEmpty) "/" else path
val uri = (if (url.matches("^(http|HTTP)(s|S)?:.*")) new URI(url) else new URI("http://" + url)).normalize()
new URI(scheme(uri), uri.getUserInfo, uri.getHost.toLowerCase, port(uri.getPort), path(uri.getPath), uri.getQuery, null).toURL.toExternalForm
}
}
def normalizeIdentifier(openID: String) = {
val trimmed = openID.trim
UrlIdentifier(trimmed).normalize getOrElse trimmed
}
/**
* Resolve the OpenID server from the user's OpenID
*/
def discoverServer(openID: String): Future[OpenIDServer] = {
val discoveryUrl = normalizeIdentifier(openID)
ws.url(discoveryUrl).get().map(response => {
val maybeOpenIdServer = new XrdsResolver().resolve(response) orElse new HtmlResolver().resolve(response)
maybeOpenIdServer.getOrElse(throw Errors.NETWORK_ERROR)
})
}
}
private[openid] object Discovery {
trait Resolver {
def resolve(response: WSResponse): Option[OpenIDServer]
}
// TODO: Verify schema, namespace and support verification of XML signatures
class XrdsResolver extends Resolver {
// http://openid.net/specs/openid-authentication-2_0.html#service_elements and
// OpenID 1 compatibility: http://openid.net/specs/openid-authentication-2_0.html#anchor38
private val serviceTypeId = Seq("http://specs.openid.net/auth/2.0/server", "http://specs.openid.net/auth/2.0/signon", "http://openid.net/server/1.0", "http://openid.net/server/1.1")
def resolve(response: WSResponse) = for {
_ <- response.header(HeaderNames.CONTENT_TYPE).filter(_.contains("application/xrds+xml"))
findInXml = findUriWithType(response.xml) _
uri <- serviceTypeId.flatMap(findInXml(_)).headOption
} yield OpenIDServer(uri, None)
private def findUriWithType(xml: Node)(typeId: String) = (xml \ "XRD" \ "Service" find (node => (node \ "Type").find(inner => inner.text == typeId).isDefined)).map {
node =>
(node \ "URI").text.trim
}
}
class HtmlResolver extends Resolver {
private val providerRegex = new Regex("""<link[^>]+openid2[.]provider[^>]+>""")
private val serverRegex = new Regex("""<link[^>]+openid[.]server[^>]+>""")
private val localidRegex = new Regex("""<link[^>]+openid2[.]local_id[^>]+>""")
private val delegateRegex = new Regex("""<link[^>]+openid[.]delegate[^>]+>""")
def resolve(response: WSResponse) = {
val serverUrl: Option[String] = providerRegex.findFirstIn(response.body)
.orElse(serverRegex.findFirstIn(response.body))
.flatMap(extractHref(_))
serverUrl.map(url => {
val delegate: Option[String] = localidRegex.findFirstIn(response.body)
.orElse(delegateRegex.findFirstIn(response.body)).flatMap(extractHref(_))
OpenIDServer(url, delegate)
})
}
private def extractHref(link: String): Option[String] =
new Regex("""href="([^"]*)"""").findFirstMatchIn(link).map(_.group(1).trim).
orElse(new Regex("""href='([^']*)'""").findFirstMatchIn(link).map(_.group(1).trim))
}
}
Other Play Framework source code examplesHere is a short list of links related to this Play Framework OpenID.scala source code file: |
| ... this post is sponsored by my books ... | |
#1 New Release! |
FP Best Seller |
Copyright 1998-2024 Alvin Alexander, alvinalexander.com
All Rights Reserved.
A percentage of advertising revenue from
pages under the /java/jwarehouse
URI on this website is
paid back to open source projects.