Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Standalone build added.

  • Loading branch information...
commit 68eee16504f410b01839338fa39df65e3d9a466b 1 parent 4c7d187
@d6y d6y authored
View
20 README.md
@@ -0,0 +1,20 @@
+OpenID Lift Module
+==================
+
+This module provides integration with OpenID.
+
+---
+
+_Exploring Lift_, chapter 13 discusses [OpenID integration](http://exploring.liftweb.net/master/index-13.html).
+
+
+
+---
+
+Notes for module developers
+===========================
+
+* The [Jenkins build](https://liftmodules.ci.cloudbees.com/job/openid/) is triggered on a push to master.
+
+
+
View
78 build.sbt
@@ -0,0 +1,78 @@
+name := "openid"
+
+liftVersion <<= liftVersion ?? "2.4"
+
+version <<= liftVersion apply { _ + "-1.0-SNAPSHOT" }
+
+organization := "net.liftmodules"
+
+scalaVersion := "2.9.1"
+
+crossScalaVersions := Seq("2.8.1", "2.9.0-1", "2.9.1")
+
+resolvers += "Java.net Maven2 Repository" at "http://download.java.net/maven/2/"
+
+libraryDependencies <++= liftVersion { v =>
+ "net.liftweb" %% "lift-mapper" % v % "compile->default" ::
+ Nil
+}
+
+libraryDependencies <++= scalaVersion { sv =>
+ "org.openid4java" % "openid4java-consumer" % "0.9.5" ::
+ "org.scala-tools.testing" %% "specs" % (sv match {
+ case "2.8.0" => "1.6.5"
+ case "2.9.1" => "1.6.9"
+ case _ => "1.6.8"
+ }) % "test" ::
+ "org.scalacheck" %% "scalacheck" % (sv match {
+ case "2.8.0" => "1.7"
+ case "2.8.1" | "2.8.2" => "1.8"
+ case _ => "1.9"
+ }) % "test" ::
+ Nil
+}
+
+publishTo <<= version { _.endsWith("SNAPSHOT") match {
+ case true => Some("snapshots" at "https://oss.sonatype.org/content/repositories/snapshots")
+ case false => Some("releases" at "https://oss.sonatype.org/service/local/staging/deploy/maven2")
+ }
+ }
+
+
+// For local deployment:
+
+credentials += Credentials( file("sonatype.credentials") )
+
+// For the build server:
+
+credentials += Credentials( file("/private/liftmodules/sonatype.credentials") )
+
+publishMavenStyle := true
+
+publishArtifact in Test := false
+
+pomIncludeRepository := { _ => false }
+
+
+pomExtra := (
+ <url>https://github.com/liftmodules/openid</url>
+ <licenses>
+ <license>
+ <name>Apache 2.0 License</name>
+ <url>http://www.apache.org/licenses/LICENSE-2.0.html</url>
+ <distribution>repo</distribution>
+ </license>
+ </licenses>
+ <scm>
+ <url>git@github.com:liftmodules/openid.git</url>
+ <connection>scm:git:git@github.com:liftmodules/openid.git</connection>
+ </scm>
+ <developers>
+ <developer>
+ <id>liftmodules</id>
+ <name>Lift Team</name>
+ <url>http://www.liftmodules.net</url>
+ </developer>
+ </developers>
+ )
+
View
13 project/LiftModule.scala
@@ -0,0 +1,13 @@
+import sbt._
+import sbt.Keys._
+
+object LiftModuleBuild extends Build {
+
+val liftVersion = SettingKey[String]("liftVersion", "Version number of the Lift Web Framework")
+
+val project = Project("LiftModule", file("."))
+
+}
+
+
+
View
5 sonatype.credentials.template
@@ -0,0 +1,5 @@
+realm=Sonatype Nexus Repository Manager
+host=oss.sonatype.org
+user=your-jira-login
+password=you-jira-password
+
View
248 src/main/scala/net/liftweb/openid/Extensions.scala
@@ -0,0 +1,248 @@
+/*
+ * Copyright 2008-2010 WorldWide Conferencing, LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.liftweb {
+package openid {
+
+import net.liftweb.common._
+
+import _root_.org.openid4java.discovery.Identifier;
+import _root_.org.openid4java.discovery.DiscoveryInformation;
+import _root_.org.openid4java.message.ax.FetchRequest;
+import org.openid4java.message.sreg.SRegRequest
+import org.openid4java.message.sreg.{SRegResponse, SRegMessage}
+
+import _root_.org.openid4java.message.ax.FetchResponse;
+import _root_.org.openid4java.message.ax.AxMessage;
+import _root_.org.openid4java.message._
+import _root_.org.openid4java.OpenIDException;
+import _root_.org.openid4java.consumer._
+
+import scala.util.matching.Regex
+
+import Implicits._
+
+/**
+ * Convert from raw java.util.List to List[T]. Assumes list content is uniform T
+ */
+private object RawHelper {
+ implicit def rawJUL2List[T](l:java.util.List[_]):List[T] = {
+ //import scala.collection.jcl.Conversions._
+ //val lTyped: java.util.List[T] = l.asInstanceOf[java.util.List[T]]
+ //val s = convertList(lTyped)
+ //s.toList
+ val arr: Array[Object] = l.toArray()
+ arr.toList.map(_.asInstanceOf[T])
+ // (for {i <- 0 until l.size} yield l(i).asInstanceOf[T]).toList
+ }
+}
+
+/**
+ * Wrapper for org.openid4java.message.Message
+ */
+class RichMessage(underlying: Message) {
+ /**
+ * Return extension with the specified URI if available on the message
+ */
+ def extension(typeURI: String): Box[MessageExtension] = {
+ if (underlying.hasExtension(typeURI))
+ Box !! underlying.getExtension(typeURI)
+ else
+ Empty
+ }
+
+ /**
+ * Return the AxMessage.OPENID_NS_AX FetchResponse extension if available
+ */
+ def fetchResponse: Box[FetchResponse] = extension(AxMessage.OPENID_NS_AX).asA[FetchResponse]
+
+ /**
+ * Return the SRegMessage.OPENID_NS_SREG SRegResponse extentions if available
+ */
+ def sRegResponse: Box[SRegResponse] = extension(SRegMessage.OPENID_NS_SREG).asA[SRegResponse]
+}
+
+/**
+ * Wrapper for org.openid4java.message.ax.FetchResponse
+ */
+class RichFetchResponse(underlying: FetchResponse) {
+ import RawHelper._
+
+ /**
+ * Return the list of attribute aliases available in the reponse
+ */
+ def aliases: List[String] = underlying.getAttributeAliases
+
+ /**
+ * Get the first value with the specified alias
+ */
+ def value(alias:String) = Box !! underlying.getAttributeValue(alias)
+
+ /**
+ * Get all values with the specified alias
+ */
+ def values(alias:String): List[String] = underlying.getAttributeValues(alias) flatMap {v:String => Box !! v}
+}
+
+/**
+ * Wrapper for org.openid4java.message.sreg.SRegResponse
+ */
+class RichSRegResponse(underlying: SRegResponse) {
+ import RawHelper._
+
+ /**
+ * Return the list of attribute names available in the response
+ */
+ def names: List[String] = underlying.getAttributeNames
+
+ /**
+ * Get the value with the specified name
+ */
+ def value(name:String) = Box !! underlying.getAttributeValue(name)
+}
+
+object Implicits {
+ implicit def Message2Rich(msg: Message) = new RichMessage(msg)
+ implicit def FetchResponse2Rich(fr: FetchResponse) = new RichFetchResponse(fr)
+ implicit def SRegResponse2Rich(sr: SRegResponse) = new RichSRegResponse(sr)
+}
+
+/**
+ * Attribute defines an attribute retrieved using either Sreg or Ax
+ *
+ * name: SReg name of attribute
+ * uri: Ax uri of attribute
+ */
+case class Attribute(val name:String, val uri: String)
+
+/**
+ * Attributes that can retrieved using either Simple Registration or Attribute Exchange
+ * extensions
+ */
+object WellKnownAttributes {
+ val Nickname = Attribute("nickname", "http://axschema.org/namePerson/friendly")
+ val Email = Attribute("email", "http://axschema.org/contact/email")
+ val FullName = Attribute("fullname", "http://axschema.org/namePerson")
+ val Language = Attribute("language", "http://axschema.org/pref/language")
+ val TimeZone = Attribute("timezone", "http://axschema.org/pref/timezone")
+
+ // Ax supported only
+ val FirstName = Attribute("first", "http://axschema.org/namePerson/first")
+ val LastName = Attribute("last", "http://axschema.org/namePerson/last")
+
+ // All WellKnownAttributes
+ val attributes = List(Nickname, Email, FullName, Language, TimeZone, FirstName, LastName, Language)
+
+ // Locate attribute with the specified name
+ def withName(name: String) = attributes find {_.name == name}
+
+ /**
+ * Extract all WellKnownAttributes & their values from message
+ */
+ def attributeValues(msg: Message): Map[Attribute, String] = {
+ Map() ++
+ // Try Ax response
+ (for {response <- msg.fetchResponse.toList
+ alias <- response.aliases
+ attr <- withName(alias)
+ value <- response.value(alias)} yield (attr, value)) ++
+ // Try SReg response
+ (for {response <- msg.sRegResponse.toList
+ name <- response.names
+ attr <- withName(name)
+ value <- response.value(name)} yield (attr, value))
+ }
+}
+
+/**
+ * Endpoint as identifed from DiscoveredInformation
+ */
+case class DiscoveredEndpoint(val name:String, val uriRegex: String) {
+
+ /**
+ * Create a MessageExtension for the endpoint that fetches the requested attributes
+ */
+ def fetchRequestExtension(attributes: List[(Attribute, Boolean)]) = {
+ val fetch = FetchRequest.createFetchRequest()
+ attributes foreach {case (attr,required) => fetch.addAttribute(attr.name, attr.uri, required)}
+ fetch
+ }
+
+ /**
+ * Create a SReg Extension for the endpoint that fetches the requested attributes
+ */
+ def sRegRequestExtension(attributes: List[(Attribute, Boolean)]) = {
+ val sreg = SRegRequest.createFetchRequest()
+ attributes foreach {case (attr,required) => sreg.addAttribute(attr.name, required)}
+ sreg
+ }
+
+ /**
+ * Create a provider specific MessageExtension for retrieving
+ * the specified attributes using either Ax or SReg
+ */
+ def makeAttributeExtension(attributes: List[Attribute]): Box[MessageExtension] = Empty
+}
+
+/**
+ * WellKnownEndpoints know how to create an endpoint specific MessageExtension for retrieving
+ * the WellKnownAttributes
+ *
+ * Usefull for use in combination with the beforeAuth callback on OpenIDConsumer. The following example
+ * shows a method that can be passed to beforeAuth to add an extension that fetches the Email, FullName,
+ * FirstName & LastName attributes from the selected endpoint.
+ *
+ * <pre>
+ * def ext(di:DiscoveryInformation, authReq: AuthRequest): Unit = {
+ * import WellKnownAttributes._
+ * WellKnownEndpoints.findEndpoint(di) map {ep
+ * => ep.makeAttributeExtension(List(Email, FullName, FirstName, LastName)) foreach {ex => authReq.addExtension(ex)}}
+ * }
+ * </pre>
+ *
+ * See MetaOpenIDProtoUser for an example of how to extract the returned attribute values
+ */
+object WellKnownEndpoints {
+
+ val Google = new DiscoveredEndpoint("Google","https://www\\.google\\.com/accounts/o8/.+") {
+ override def makeAttributeExtension(attrs: List[Attribute]): Box[MessageExtension] =
+ Full(fetchRequestExtension(attrs.zipAll(Nil, null, true)))
+ }
+
+ val Yahoo = new DiscoveredEndpoint("Yahoo","https://open\\.login\\.yahooapis\\.com/openid/op/auth") {
+ override def makeAttributeExtension(attrs: List[Attribute]): Box[MessageExtension] =
+ Full(fetchRequestExtension(attrs.zipAll(Nil, null, true)))
+ }
+
+ val MyOpenId = new DiscoveredEndpoint("MyOpenId","http://www\\.myopenid\\.com/server") {
+ override def makeAttributeExtension(attrs: List[Attribute]): Box[MessageExtension] =
+ Full(sRegRequestExtension(attrs.zipAll(Nil, null, true)))
+ }
+
+ /**
+ * List of WellKnownEndpoints
+ */
+ val endpoints = List(Google, MyOpenId, Yahoo)
+
+ /**
+ * Try to identify a WellKnownEndpoint from DiscoveryInformation
+ */
+ def findEndpoint(di:DiscoveryInformation): Box[DiscoveredEndpoint] = {
+ endpoints find {v => v.uriRegex.r.findFirstIn(di.getOPEndpoint().toString).isDefined}
+ }
+}
+
+}
+}
View
316 src/main/scala/net/liftweb/openid/OpenID.scala
@@ -0,0 +1,316 @@
+/*
+ * Copyright 2008-2010 WorldWide Conferencing, LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.liftweb {
+package openid {
+
+import _root_.org.openid4java.discovery.Identifier;
+import _root_.org.openid4java.discovery.DiscoveryInformation;
+import _root_.org.openid4java.message.ax.FetchRequest;
+import _root_.org.openid4java.message.ax.FetchResponse;
+import _root_.org.openid4java.message.ax.AxMessage;
+import _root_.org.openid4java.message._
+import _root_.org.openid4java.OpenIDException;
+import _root_.org.openid4java.consumer._
+
+import _root_.java.util.List;
+import _root_.java.io.IOException;
+
+import _root_.net.liftweb._
+import http._
+import provider._
+import util._
+import common._
+
+import _root_.scala.xml.{NodeSeq, Text}
+
+trait OpenIDVendor {
+ type UserType
+
+ type ConsumerType <: OpenIDConsumer[UserType]
+
+ private object RedirectBackTo extends SessionVar[Box[String]](Empty)
+ lazy val PathRoot = "openid"
+
+ lazy val LoginPath = "login"
+
+ lazy val LogOutPath = "logout"
+
+ lazy val ResponsePath = "response"
+
+ def PostParamName = "openid_identifier" // "openIdUrl"
+
+ lazy val SnippetPrefix = "openId"
+
+ def postLogin(id: Box[Identifier],res: VerificationResult): Unit
+
+ def postUrl = "/"+ PathRoot + "/" + LoginPath
+
+ /**
+ * A session var that keeps track of the OpenID object through the request/response
+ */
+ object OpenIDObject extends SessionVar[ConsumerType](createAConsumer)
+
+ def createAConsumer: ConsumerType
+
+ def currentUser: Box[UserType]
+
+ def snippetPF: LiftRules.SnippetPF = NamedPF ("OpenID Default") {
+ case SnippetPrefix :: "ifLoggedIn" :: Nil => showIfLoggedIn
+ case SnippetPrefix :: "ifLoggedOut" :: Nil => showIfLoggedOut
+ case SnippetPrefix :: "userBox" :: Nil => showUserBox
+ }
+
+ def displayUser(id: UserType): NodeSeq
+
+ def logoutLink: NodeSeq = <xml:group> <a href={"/"+PathRoot+"/"+LogOutPath}>Log Out</a></xml:group>
+
+ def loginForm: NodeSeq = <form method="post" action={"/"+PathRoot+"/"+LoginPath}>
+ OpenID <input class="openidfield" name={PostParamName}/> <input type='submit' value="Log In"/>
+ </form>
+
+ def showUserBox(ignore: NodeSeq): NodeSeq = <div class="openidbox">{
+ currentUser match {
+ case Full(user) => displayUser(user) ++ logoutLink
+ case _ => loginForm
+ }
+ }</div>
+
+ def showIfLoggedIn(in: NodeSeq): NodeSeq = currentUser match {
+ case Full(_) => in
+ case _ => Text("")
+ }
+
+ def showIfLoggedOut(in: NodeSeq): NodeSeq = currentUser match {
+ case Full(_) => Text("")
+ case _ => in
+ }
+
+ def logUserOut(): Unit
+
+ /**
+ * Try to log a user into the system with a given openId
+ */
+ def loginAndRedirect(openId: String, onComplete: (Box[Identifier], Box[VerificationResult], Box[Exception]) => LiftResponse) {
+ val oid = OpenIDObject.is
+ oid.onComplete = Full(onComplete)
+
+ throw ResponseShortcutException.shortcutResponse(try {
+ oid.authRequest(openId, "/"+PathRoot+"/"+ResponsePath)
+ } catch {
+ case e: Exception => onComplete(Empty, Empty, Full(e))
+ })
+ }
+
+ /**
+ * Based on an exception, generate an error message
+ */
+ protected def generateOpenIDFailureMessage(exception: Exception): String =
+ (S ? "OpenID Failure") + ": " + exception.getMessage
+
+ def dispatchPF: LiftRules.DispatchPF = NamedPF("Login default") {
+ case Req(PathRoot :: LogOutPath :: _, "", _) =>
+ () => {
+ logUserOut()
+ Full(RedirectResponse(S.referer openOr "/", S responseCookies :_*))
+ }
+
+ case r @ Req(PathRoot :: LoginPath :: _, "", PostRequest)
+ if r.param(PostParamName).isDefined =>
+ () => {
+ try {
+ RedirectBackTo(S.referer)
+ Full(OpenIDObject.is.authRequest(r.param(PostParamName).get, "/"+PathRoot+"/"+ResponsePath))
+ } catch {
+ case e: Exception => {
+ S.error(generateOpenIDFailureMessage(e))
+ // FIXME -- log the name and the error
+ Full(RedirectResponse(S.referer openOr "/", S responseCookies :_*))
+ }
+ }
+ }
+
+ case r @ Req(PathRoot :: ResponsePath :: _, "", _) =>
+ () => {
+ for (req <- S.request;
+ ret <- {
+ val (id, res) = OpenIDObject.is.verifyResponse(req.request)
+
+ OpenIDObject.onComplete match {
+ case Full(f) => Full(f(id, Full(res), Empty))
+
+ case _ => postLogin(id, res)
+ val rb = RedirectBackTo.is
+ Full(RedirectResponse(rb openOr "/", S responseCookies :_*))
+ }
+ }) yield ret
+
+
+ }
+ }
+}
+
+trait SimpleOpenIDVendor extends OpenIDVendor {
+ type UserType = Identifier
+ type ConsumerType = OpenIDConsumer[UserType]
+
+ def currentUser = OpenIDUser.is
+
+ /**
+ * Generate a welcome message for the OpenID identifier
+ */
+ protected def generateWelcomeMessage(id: Identifier): String =
+ (S ? "Welcome")+ ": "+ id
+
+ /**
+ * If verification failed, generate a polite message to that
+ * effect.
+ */
+ protected def generateAuthenticationFailure(res: VerificationResult): String =
+ S ? "Failed to authenticate"
+
+ def postLogin(id: Box[Identifier],res: VerificationResult): Unit = {
+ id match {
+ case Full(id) => S.notice(generateWelcomeMessage(id))
+
+ case _ => S.error(generateAuthenticationFailure(res))
+ }
+
+ OpenIDUser(id)
+ }
+
+ def logUserOut() {
+ OpenIDUser.remove
+ }
+
+ /**
+ * Generate a welcome message.
+ */
+ def displayUser(in: UserType): NodeSeq = Text("Welcome "+in)
+
+ def createAConsumer = new AnyRef with OpenIDConsumer[UserType]
+}
+
+object SimpleOpenIDVendor extends SimpleOpenIDVendor
+
+
+object OpenIDUser extends SessionVar[Box[Identifier]](Empty)
+
+/** * Sample Consumer (Relying Party) implementation. */
+trait OpenIDConsumer[UserType] extends Logger {
+ val manager = new ConsumerManager
+
+ var onComplete: Box[(Box[Identifier], Box[VerificationResult], Box[Exception]) => LiftResponse] = Empty
+
+ /**
+ * Set this to a function that can modify (eg add extensions) to the auth request before send
+ */
+ var beforeAuth: Box[(DiscoveryInformation,AuthRequest) => Unit] = Empty
+
+ // --- placing the authentication request ---
+ def authRequest(userSuppliedString: String, targetUrl: String): LiftResponse =
+ {
+ // configure the return_to URL where your application will receive
+ // the authentication responses from the OpenID provider
+ val returnToUrl = S.encodeURL(S.hostAndPath + targetUrl)
+
+ info("Creating openId auth request. returnToUrl: "+returnToUrl)
+
+ // perform discovery on the user-supplied identifier
+ val discoveries = manager.discover(userSuppliedString)
+
+ // attempt to associate with the OpenID provider
+ // and retrieve one service endpoint for authentication
+ val discovered = manager.associate(discoveries)
+
+ S.containerSession.foreach(_.setAttribute("openid-disc", discovered))
+
+ // obtain a AuthRequest message to be sent to the OpenID provider
+ val authReq = manager.authenticate(discovered, returnToUrl)
+
+ beforeAuth foreach {f => f(discovered, authReq)}
+
+ if (! discovered.isVersion2() )
+ {
+ // Option 1: GET HTTP-redirect to the OpenID Provider endpoint
+ // The only method supported in OpenID 1.x
+ // redirect-URL usually limited ~2048 bytes
+ RedirectResponse(authReq.getDestinationUrl(true))
+ }
+ else
+ {
+ // Option 2: HTML FORM Redirection (Allows payloads >2048 bytes)
+ val pm = authReq.getParameterMap()
+ val info: Seq[(String, String)] = pm.keySet.toArray.
+ map(k => (k.toString, pm.get(k).toString))
+
+ XhtmlResponse(
+ <html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>OpenID HTML FORM Redirection</title>
+ </head>
+ <body onload="document.forms['openid-form-redirection'].submit();">
+ <form name="openid-form-redirection" action={authReq.getDestinationUrl(false)} method="post" accept-charset="utf-8">
+ {
+ info.map{ case(key, value) =>
+ <input type="hidden" name={key} value={value}/>
+ }
+ }
+ <button type="submit">Continue...</button>
+ </form>
+ </body>
+ </html>, Empty, Nil, Nil, 200, true)
+ }
+ }
+
+ // --- processing the authentication response ---
+ def verifyResponse(httpReq: HTTPRequest): (Box[Identifier], VerificationResult) =
+ {
+ // extract the parameters from the authentication response
+ // (which comes in as a HTTP request from the OpenID provider)
+ val paramMap = new java.util.HashMap[String, String]
+ httpReq.params.foreach(e => paramMap.put(e.name, e.values.headOption getOrElse null))
+ val response = new ParameterList(paramMap);
+
+ // retrieve the previously stored discovery information
+ val discovered = httpReq.session.attribute("openid-disc") match {
+ case d: DiscoveryInformation => d
+ case _ => throw ResponseShortcutException.redirect("/")
+ }
+
+ // extract the receiving URL from the HTTP request
+ var receivingURL = httpReq.url
+ val queryString = httpReq.queryString openOr ""
+ if (queryString != null && queryString.length() > 0) {
+ receivingURL += "?" + queryString;
+ }
+
+
+ // verify the response; ConsumerManager needs to be the same
+ // (static) instance used to place the authentication request
+ val verification = manager.verify(receivingURL.toString(),
+ response, discovered)
+
+ // examine the verification result and extract the verified identifier
+
+ val verified = verification.getVerifiedId();
+
+ (Box.legacyNullTest(verified), verification)
+ }
+}
+
+}
+}
View
263 src/main/scala/net/liftweb/openid/OpenIDProtoUser.scala
@@ -0,0 +1,263 @@
+/*
+ * Copyright 2008-2010 WorldWide Conferencing, LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.liftweb {
+package openid {
+
+import _root_.net.liftweb._
+import mapper._
+import http._
+import js._
+import JsCmds._
+import sitemap.{Loc, Menu}
+import common._
+import util._
+import Helpers._
+
+import _root_.org.openid4java.consumer._
+import _root_.org.openid4java.discovery.Identifier;
+
+
+trait MetaOpenIDProtoUser[ModelType <: OpenIDProtoUser[ModelType]] extends MetaMegaProtoUser[ModelType] {
+ self: ModelType =>
+
+ private val logger = Logger(classOf[MetaOpenIDProtoUser[ModelType]])
+ import logger._
+
+ override def signupFields: List[FieldPointerType] =
+ List(nickname, firstName, lastName, locale, timezone)
+
+ override def fieldOrder: List[FieldPointerType] =
+ List(nickname, firstName, lastName, locale, timezone)
+
+ /**
+ * Create a new user with the specified openId
+ * The default implementation tries to extract attributes from
+ * the VerificationReuslt, but not all providers supply this information (most notably Yahoo)
+ *
+ * Override this method to change how a new user should be created
+ */
+ def createNewUser(openId: String, result: Box[VerificationResult]): ModelType = {
+ logger.debug("Creating new user for openId: %s".format(openId))
+
+ val u = self.create.openId(openId)
+
+ // Set default values
+ u.nickname("change"+Helpers.randomInt(1000000000)).firstName("Unknown").
+ lastName("Unknown").password(Helpers.randomString(15)).
+ email(Helpers.randomInt(100000000)+"unknown@unknown.com")
+
+ // Try to extract parameters from response
+ result foreach {res =>
+ import WellKnownAttributes._
+
+ val attrs = WellKnownAttributes.attributeValues(res.getAuthResponse)
+
+ attrs.get(Email) map {e => u.email(trace("Extracted email",e))}
+
+ self.findAll(By(email, u.email.is)) map {existing =>
+ info("Cannot register new user, email %s already exists with openId ".format(existing.email.is,existing.openId.is))
+ S.error(duplicateEmailErrorMessage(u.email))
+ S.redirectTo(homePage)
+ }
+
+ attrs.get(Nickname) map {nick => u.nickname(trace("Extracted nickname",nick))}
+
+ // Try to construct first/last from fullname
+ val (first, last) = attrs.get(FullName) map {full =>
+ trace("Calculated first/lastname",full.trim.lastIndexOf(' ') match {
+ case -1 => (Some(full), None)
+ case n => (Some(full.substring(0,n)), Some(full.substring(n+1)))
+ })
+ } getOrElse ((None, None))
+
+ attrs.get(FirstName) orElse first map {f => u.firstName(trace("Extracted firstName",f)) }
+ attrs.get(LastName) orElse last map {l => u.lastName(trace("Extracted lastName",l)) }
+ attrs.get(TimeZone) map {tz => u.timezone(trace("Extracted timeZone",tz)) }
+ }
+ u
+ }
+
+ /**
+ * Called on successfull login
+ */
+ def findOrCreate(openId: String, result: Box[VerificationResult]): ModelType = find(By(this.openId, openId)) match {
+ case Full(u) =>
+ logger.debug("Found existing user for openId:%s, user:%s".format(openId,u))
+ u
+ case _ =>
+ val u = createNewUser(openId, result)
+ logger.debug("Saving new user for openId:%s, user:%s".format(openId,u))
+ u.saveMe
+ }
+
+ // no need for these menu items with OpenID
+ /**
+ * The menu item for creating the user/sign up (make this "Empty" to disable)
+ */
+ override def createUserMenuLoc: Box[Menu] = Empty
+
+ /**
+ * The menu item for lost password (make this "Empty" to disable)
+ */
+ override def lostPasswordMenuLoc: Box[Menu] = Empty
+
+ /**
+ * The menu item for resetting the password (make this "Empty" to disable)
+ */
+ override def resetPasswordMenuLoc: Box[Menu] = Empty
+
+ /**
+ * The menu item for changing password (make this "Empty" to disable)
+ */
+ override def changePasswordMenuLoc: Box[Menu] = Empty
+
+ /**
+ * The menu item for validating a user (make this "Empty" to disable)
+ */
+ override def validateUserMenuLoc: Box[Menu] = Empty
+
+ override def loginXhtml =
+ <form method="post" action={S.uri}>
+ <table>
+ <tr>
+ <td colspan="2">{S.??("log.in")}</td>
+ </tr>
+ <tr>
+ <td>OpenID</td><td><user:openid /></td>
+ </tr>
+ <tr>
+ <td>&nbsp;</td><td><user:submit /></td>
+ </tr>
+ </table>
+ </form>
+
+ def openIDVendor: OpenIDVendor
+
+ /**
+ * Generate an error message on duplicate email addresses.
+ */
+ protected def duplicateEmailErrorMessage(email: String): String =
+ "A user with email %s already exists with a different OpenID.".format(email)
+
+ /**
+ * Generate an error message based on an exception
+ */
+ protected def errorMessageFromException(exception: Exception): String =
+ (S ? "Got an exception") + ": " + exception.getMessage
+
+ /**
+ * Generate an error message based on a bad verification result
+ */
+ protected def unableToLogInErrorMessage(verificationResult: Box[VerificationResult]): String =
+ (S ? "Unable to log you in") +": "+verificationResult
+
+ def performLogUserIn(openid: Box[Identifier], fo: Box[VerificationResult], exp: Box[Exception]): LiftResponse = {
+ (openid, exp) match {
+ case (Full(id), _) =>
+ val user = self.findOrCreate(id.getIdentifier, fo)
+ logUserIn(user)
+ S.notice(S.??("Welcome ")+user.niceName)
+
+ case (_, Full(exp)) =>
+ warn("Got an exception", exp)
+ S.error(errorMessageFromException(exp))
+
+
+ case _ =>
+ warn("Unable to login using OpenID: "+fo)
+ S.error(unableToLogInErrorMessage(fo))
+ }
+
+ val redir = loginRedirect.is match {
+ case Full(url) =>
+ loginRedirect(Empty)
+ url
+ case _ =>
+ homePage
+ }
+
+ info("OpenID login complete, redirecting to: %s".format(redir))
+ RedirectResponse(redir)
+ }
+
+ override def login = {
+ if (S.post_?) {
+ debug("Processing OpenID login request:"+S.request)
+ S.param(openIDVendor.PostParamName).
+ foreach(username => {
+ logger.debug("Trying to do OpenID login for user:"+username)
+ openIDVendor.loginAndRedirect(username, performLogUserIn)
+ })
+ }
+
+
+ Helpers.bind("user", loginXhtml,
+ "openid" -> (FocusOnLoad(<input type="text" name={openIDVendor.PostParamName}/>)),
+ "submit" -> (<input type="submit" value={S.??("log.in")}/>))
+ }
+
+ private[openid] def findByNickname(str: String): List[ModelType] = findAll(By(nickname, str))
+}
+
+object ValidNickName {
+ val is = """^[a-z][a-z0-9_]*$""".r
+ def apply(in: String): Boolean = is.findFirstIn(in).isDefined
+}
+/**
+ * An OpenID friendly extension to ProtoUser
+ */
+trait OpenIDProtoUser[T <: OpenIDProtoUser[T]] extends MegaProtoUser[T] {
+ self: T =>
+
+ override def getSingleton: MetaOpenIDProtoUser[T]
+
+ object openId extends MappedString(this, 512) {
+ override def dbIndexed_? = true
+ }
+
+ object nickname extends MappedPoliteString(this, 64) {
+ override def dbIndexed_? = true
+
+ def deDupUnderscore(in: String): String = in.indexOf("__") match {
+ case -1 => in
+ case pos => deDupUnderscore(in.substring(0, pos)+in.substring(pos + 1))
+ }
+
+ override def setFilter = notNull _ :: toLower _ :: trim _ ::
+ deDupUnderscore _ :: super.setFilter
+
+ private def validateNickname(str: String): List[FieldError] = {
+ val others = getSingleton.findByNickname(str).
+ filter(_.id.is != fieldOwner.id.is)
+ others.map(u => FieldError(this, <xml:group>Duplicate nickname: {str}</xml:group>))
+ }
+
+ private def validText(str: String): List[FieldError] =
+ if (ValidNickName(str)) Nil
+ else List(FieldError(this,
+ <xml:group>Invalid nickname. Must start with
+ a letter and contain only letters,
+ numbers or "_"</xml:group>))
+
+ override def validations = validText _ :: validateNickname _ :: super.validations
+ }
+
+ override def niceName: String = nickname
+}
+
+}
+}
View
36 src/test/scala/net/liftweb/openid/RawHelperSpec.scala
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2010-2011 WorldWide Conferencing, LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.liftweb
+package openid
+
+import java.util.{List, ArrayList}
+
+import org.specs.Specification
+
+
+object RawHelperSpec extends Specification {
+ "RawUtils" should {
+ "Convert a java.util.List" in {
+ val org: List[Object] = new ArrayList[Object]()
+
+ org.add("Hello")
+ org.add("Woof")
+
+ RawHelper.rawJUL2List[String](org) must_== scala.List("Hello", "Woof")
+ }
+ }
+}
Please sign in to comment.
Something went wrong with that request. Please try again.