Skip to content

Commit

Permalink
implemented oauth2.0 for stackoverflow
Browse files Browse the repository at this point in the history
  • Loading branch information
derabbink committed Nov 11, 2012
1 parent 56e9215 commit 0ad21ca
Show file tree
Hide file tree
Showing 11 changed files with 289 additions and 14 deletions.
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,8 @@ project/plugins/project/

# eclipse specific
bin/*
.cache
.cache

# sensitive information
stackoverflow.auth.properties
langpop-web/src/main/resources/application.conf
4 changes: 4 additions & 0 deletions langpop-aggregate/.classpath
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
<classpathentry kind="con" path="org.scala-ide.sdt.launching.SCALA_CONTAINER"></classpathentry>
<classpathentry path="C:\Users\maarten\.ivy2\cache\com.typesafe.akka\akka-actor\jars\akka-actor-2.0.3.jar" kind="lib"></classpathentry>
<classpathentry path="C:\Users\maarten\.ivy2\cache\com.typesafe\config\bundles\config-0.3.1.jar" kind="lib"></classpathentry>
<classpathentry path="C:\Users\maarten\.ivy2\cache\com.typesafe.akka\akka-slf4j\jars\akka-slf4j-2.0.3.jar" kind="lib"></classpathentry>
<classpathentry path="C:\Users\maarten\.ivy2\cache\ch.qos.logback\logback-classic\jars\logback-classic-1.0.7.jar" kind="lib"></classpathentry>
<classpathentry path="C:\Users\maarten\.ivy2\cache\ch.qos.logback\logback-core\jars\logback-core-1.0.7.jar" kind="lib"></classpathentry>
<classpathentry path="C:\Users\maarten\.ivy2\cache\org.slf4j\slf4j-api\jars\slf4j-api-1.6.6.jar" kind="lib"></classpathentry>
<classpathentry path="C:\Users\maarten\.ivy2\cache\org.scalatest\scalatest_2.9.2\jars\scalatest_2.9.2-1.8.jar" kind="lib"></classpathentry>
<classpathentry path="C:\Users\maarten\.ivy2\cache\com.typesafe.akka\akka-testkit\jars\akka-testkit-2.0.3.jar" kind="lib"></classpathentry>
<classpathentry exported="true" path="/langpop-query" kind="src" combineaccessrules="false"></classpathentry>
Expand Down
18 changes: 9 additions & 9 deletions langpop-web/.classpath
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,23 @@
<classpathentry output="target\scala-2.9.2\test-classes" path="src\test\java" kind="src"></classpathentry>
<classpathentry kind="con" path="org.scala-ide.sdt.launching.SCALA_CONTAINER"></classpathentry>
<classpathentry path="C:\Users\maarten\.ivy2\cache\com.typesafe.akka\akka-actor\jars\akka-actor-2.0.3.jar" kind="lib"></classpathentry>
<classpathentry path="C:\Users\maarten\.ivy2\cache\com.typesafe\config\bundles\config-0.3.1.jar" kind="lib"></classpathentry>
<classpathentry path="C:\Users\maarten\.ivy2\cache\com.typesafe\config\bundles\config-1.0.0.jar" kind="lib"></classpathentry>
<classpathentry path="C:\Users\maarten\.ivy2\cache\com.typesafe.akka\akka-slf4j\jars\akka-slf4j-2.0.3.jar" kind="lib"></classpathentry>
<classpathentry path="C:\Users\maarten\.ivy2\cache\ch.qos.logback\logback-classic\jars\logback-classic-1.0.7.jar" kind="lib"></classpathentry>
<classpathentry path="C:\Users\maarten\.ivy2\cache\ch.qos.logback\logback-core\jars\logback-core-1.0.7.jar" kind="lib"></classpathentry>
<classpathentry path="C:\Users\maarten\.ivy2\cache\org.slf4j\slf4j-api\jars\slf4j-api-1.6.6.jar" kind="lib"></classpathentry>
<classpathentry path="C:\Users\maarten\.ivy2\cache\org.scalatra\scalatra\jars\scalatra-2.1.1.jar" kind="lib"></classpathentry>
<classpathentry path="C:\Users\maarten\.ivy2\cache\org.clapper\grizzled-slf4j_2.9.2\jars\grizzled-slf4j_2.9.2-0.6.9.jar" kind="lib"></classpathentry>
<classpathentry path="C:\Users\maarten\.ivy2\cache\org.slf4j\slf4j-api\jars\slf4j-api-1.6.4.jar" kind="lib"></classpathentry>
<classpathentry path="C:\Users\maarten\.ivy2\cache\io.backchat.rl\rl_2.9.2\jars\rl_2.9.2-0.3.2.jar" kind="lib"></classpathentry>
<classpathentry path="C:\Users\maarten\.ivy2\cache\org.scalatra\scalatra-scalate\jars\scalatra-scalate-2.1.1.jar" kind="lib"></classpathentry>
<classpathentry path="C:\Users\maarten\.ivy2\cache\org.fusesource.scalate\scalate-core\bundles\scalate-core-1.5.3.jar" kind="lib"></classpathentry>
<classpathentry path="C:\Users\maarten\.ivy2\cache\org.fusesource.scalate\scalate-util\bundles\scalate-util-1.5.3.jar" kind="lib"></classpathentry>
<classpathentry kind="con" path="org.scala-ide.sdt.launching.SCALA_COMPILER_CONTAINER"></classpathentry>
<classpathentry path="C:\Users\maarten\.ivy2\cache\org.apache.httpcomponents\httpclient\jars\httpclient-4.2.2.jar" kind="lib"></classpathentry>
<classpathentry path="C:\Users\maarten\.ivy2\cache\org.apache.httpcomponents\httpcore\jars\httpcore-4.2.2.jar" kind="lib"></classpathentry>
<classpathentry path="C:\Users\maarten\.ivy2\cache\commons-logging\commons-logging\jars\commons-logging-1.1.1.jar" kind="lib"></classpathentry>
<classpathentry path="C:\Users\maarten\.ivy2\cache\commons-codec\commons-codec\jars\commons-codec-1.6.jar" kind="lib"></classpathentry>
<classpathentry path="C:\Users\maarten\.ivy2\cache\org.eclipse.jetty.orbit\javax.servlet\jars\javax.servlet-3.0.0.v201112011016.jar" kind="lib"></classpathentry>
<classpathentry path="C:\Users\maarten\.ivy2\cache\ch.qos.logback\logback-classic\jars\logback-classic-1.0.6.jar" kind="lib"></classpathentry>
<classpathentry path="C:\Users\maarten\.ivy2\cache\ch.qos.logback\logback-core\jars\logback-core-1.0.6.jar" kind="lib"></classpathentry>
<classpathentry path="C:\Users\maarten\.ivy2\cache\org.slf4j\slf4j-api\jars\slf4j-api-1.6.5.jar" kind="lib"></classpathentry>
<classpathentry path="C:\Users\maarten\.ivy2\cache\org.scalatra\scalatra-specs2\jars\scalatra-specs2-2.1.1.jar" kind="lib"></classpathentry>
<classpathentry path="C:\Users\maarten\.ivy2\cache\org.scalatra\scalatra-test\jars\scalatra-test-2.1.1.jar" kind="lib"></classpathentry>
<classpathentry path="C:\Users\maarten\.ivy2\cache\org.eclipse.jetty\test-jetty-servlet\jars\test-jetty-servlet-8.1.3.v20120416.jar" kind="lib"></classpathentry>
Expand All @@ -32,10 +36,6 @@
<classpathentry path="C:\Users\maarten\.ivy2\cache\org.eclipse.jetty\jetty-io\jars\jetty-io-8.1.3.v20120416.jar" kind="lib"></classpathentry>
<classpathentry path="C:\Users\maarten\.ivy2\cache\org.mockito\mockito-all\jars\mockito-all-1.8.5.jar" kind="lib"></classpathentry>
<classpathentry path="C:\Users\maarten\.ivy2\cache\org.apache.commons\commons-lang3\jars\commons-lang3-3.1.jar" kind="lib"></classpathentry>
<classpathentry path="C:\Users\maarten\.ivy2\cache\org.apache.httpcomponents\httpclient\jars\httpclient-4.2.jar" kind="lib"></classpathentry>
<classpathentry path="C:\Users\maarten\.ivy2\cache\org.apache.httpcomponents\httpcore\jars\httpcore-4.2.jar" kind="lib"></classpathentry>
<classpathentry path="C:\Users\maarten\.ivy2\cache\commons-logging\commons-logging\jars\commons-logging-1.1.1.jar" kind="lib"></classpathentry>
<classpathentry path="C:\Users\maarten\.ivy2\cache\commons-codec\commons-codec\jars\commons-codec-1.6.jar" kind="lib"></classpathentry>
<classpathentry path="C:\Users\maarten\.ivy2\cache\org.apache.httpcomponents\httpmime\jars\httpmime-4.2.jar" kind="lib"></classpathentry>
<classpathentry path="C:\Users\maarten\.ivy2\cache\org.specs2\specs2_2.9.2\jars\specs2_2.9.2-1.12.jar" kind="lib"></classpathentry>
<classpathentry path="C:\Users\maarten\.ivy2\cache\org.specs2\specs2-scalaz-core_2.9.2\jars\specs2-scalaz-core_2.9.2-6.0.1.jar" kind="lib"></classpathentry>
Expand Down
4 changes: 4 additions & 0 deletions langpop-web/build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ seq(webSettings :_*)

classpathTypes ~= (_ + "orbit")

resolvers += "Typesafe Repository" at "http://repo.typesafe.com/typesafe/releases/"

libraryDependencies ++= Seq(
"org.scalatra" % "scalatra" % "2.1.1",
"org.scalatra" % "scalatra-scalate" % "2.1.1",
"com.typesafe" % "config" % "1.0.0",
"org.apache.httpcomponents" % "httpclient" % "4.2.2",
"org.scalatra" % "scalatra-specs2" % "2.1.1" % "test",
"org.scalatra" % "scalatra-scalatest" % "2.1.1" % "test",
"ch.qos.logback" % "logback-classic" % "1.0.6" % "runtime",
Expand Down
18 changes: 18 additions & 0 deletions langpop-web/src/main/resources/application.conf.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# copy this file to application.conf
# and edit

langpop-web {
auth {
stackoverflow {
client_id = 123 # your app's id
client_secret = "your own app's super secret client secred"
key = "your app's not so secret key"
redirect_uri = "http://localhost:8080/auth/stackoverflow/redirect"

credentialsFile = "stackoverflow.auth.properties" # file will be created in execution path of server
}
github {
# nothing yet
}
}
}
2 changes: 2 additions & 0 deletions langpop-web/src/main/scala/Scalatra.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import org.scalatra.LifeCycle

import com.abbink.langpop.web.ComponentRegistry
import com.abbink.langpop.web.LangpopServlet
import com.abbink.langpop.web.AuthServlet

import javax.servlet.ServletContext

Expand All @@ -18,5 +19,6 @@ class Scalatra extends LifeCycle with ComponentRegistry {

// Mount one or more servlets
context.mount(new LangpopServlet, "/langpop/*")
context.mount(new AuthServlet, "/auth/*")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.abbink.langpop.web

import org.scalatra.scalate.ScalateSupport
import org.scalatra.ScalatraServlet
import java.net.URLDecoder

class AuthServlet extends ScalatraServlet with ScalateSupport with ComponentRegistry {

get("/") {
<html>
<body>
<h1>Login status</h1>
<ul>
<li>StackOverflow: {stackoverflowLogin()}</li>
<li>GitHub: {githubLogin}</li>
<li>Issue query <a href="/langpop">here</a></li>
</ul>
</body>
</html>
}

private def stackoverflowLogin() = {
if (stackOverflowAuth.isAuthenticated()) {
<strong>signed in</strong>
<a href="/auth/stackoverflow/logout">sign out</a>
}
else {
<strong>signed out</strong>
<a href="/auth/stackoverflow/login">sign in</a>
}
}

private def githubLogin() = {
<i>not implemented yet</i>
}

get("/stackoverflow/logout") {
if (stackOverflowAuth.isAuthenticated())
stackOverflowAuth.clearAuth()
redirect("/auth")
}

get("/stackoverflow/login") {
if (stackOverflowAuth.isAuthenticated())
redirect("/auth")
else
redirect(stackOverflowAuth.buildOAuthUrl())
}

get("/stackoverflow/redirect") {
val code = URLDecoder.decode(params("code"))
//val state = params.get("state") map URLDecoder.decode

stackOverflowAuth.finalizeAuth(code)
redirect("/auth")
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.abbink.langpop.web

import com.abbink.langpop.aggregate.{ComponentRegistry => AggregatorComponentRegistry}
import com.abbink.langpop.web.auth.StackOverflowAuthComponent

trait ComponentRegistry extends
AggregatorComponentRegistry
AggregatorComponentRegistry with
StackOverflowAuthComponent
{

val stackOverflowAuth = StackOverflowAuthImpl
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ class LangpopServlet extends ScalatraServlet with ScalateSupport with ComponentR
get("/") {
<html>
<body>
<h1>Hello, world!</h1>
Say <a href="hello-scalate">hello to Scalate</a>.
<h1>langpop</h1>
<ul>
<li>Issue query: e.g. <a href="/langpop/2012-11-10/scala">/langpop/2012-11-10/scala</a></li>
<li>check data source login status <a href="/auth">here</a></li>
</ul>
</body>
</html>
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package com.abbink.langpop.web.auth

import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.InputStream
import java.io.OutputStream
import java.util.ArrayList
import java.util.Date
import java.util.Properties
import org.apache.http.client.entity.UrlEncodedFormEntity
import org.apache.http.client.methods.HttpPost
import org.apache.http.client.utils.URIBuilder
import org.apache.http.client.HttpClient
import org.apache.http.impl.client.DefaultHttpClient
import org.apache.http.message.BasicNameValuePair
import org.apache.http.HttpResponse
import org.apache.http.NameValuePair
import com.typesafe.config.ConfigFactory
import org.apache.http.util.EntityUtils
import org.apache.http.client.utils.URLEncodedUtils
import java.net.URI
import java.nio.charset.Charset

trait StackOverflowAuth {

def isAuthenticated() : Boolean

def buildOAuthUrl() : String

def clearAuth() : Unit

def finalizeAuth(code : String) : Unit
}

trait StackOverflowAuthComponent {
val stackOverflowAuth:StackOverflowAuth

object StackOverflowAuthImpl extends StackOverflowAuth {

val config = ConfigFactory.load()
val mergedConfig = config.getConfig("langpop-web").withFallback(config)

val client_id = mergedConfig.getString("langpop-web.auth.stackoverflow.client_id")
val client_secret = mergedConfig.getString("langpop-web.auth.stackoverflow.client_secret")
val scope = "no_expiry"
val redirect_uri = mergedConfig.getString("langpop-web.auth.stackoverflow.redirect_uri")
val credentialsFileName = mergedConfig.getString("langpop-web.auth.stackoverflow.credentialsFile")

var access_token : Option[String] = None
var expires : Option[Date] = None
readAuth()

def isAuthenticated() = {
access_token match {
case None => false
case Some(t) => expires match {
case None => true
case Some(e) => e after new Date()
}
}
}

def buildOAuthUrl() = {
val uriBuilder = new URIBuilder();
uriBuilder.setScheme("https").setHost("stackexchange.com").setPath("/oauth")
.setParameter("client_id", client_id)
.setParameter("scope", scope)
.setParameter("redirect_uri", redirect_uri)
uriBuilder.build.toString()
}

/**
* reads access token and expiration date from properties file
*/
private def readAuth() : Unit = {
access_token = None
expires = None
try {
val fs : InputStream = new FileInputStream(credentialsFileName);
val props : Properties = new Properties()
props.load(fs);
fs.close()

val token = props.getProperty("access_token")
val exp = props.getProperty("expires")
access_token = token match {
case null => None
case x => Some(x)
}
expires = exp match {case null => None case x => Some(new Date(1000 * (x.toLong)))}
}
catch {
case e => //TODO
}
(access_token, expires)
}

/**
* clears all auth data (i.e. signs out)
*/
def clearAuth() = {
access_token = None
expires = None
try {
val props = new Properties()
val fs : OutputStream = new FileOutputStream(credentialsFileName)
props.store(fs, null)
fs.close()
}
catch {
case e => //TODO
}
}

def finalizeAuth(code : String) = {
val uriBuilder = new URIBuilder()
uriBuilder.setScheme("https").setHost("stackexchange.com").setPath("/oauth/access_token")
val client : HttpClient = new DefaultHttpClient()
val formparams : java.util.List[NameValuePair] = new ArrayList[NameValuePair]()
formparams.add(new BasicNameValuePair("client_id", client_id))
formparams.add(new BasicNameValuePair("client_secret", client_secret))
formparams.add(new BasicNameValuePair("code", code))
formparams.add(new BasicNameValuePair("redirect_uri", redirect_uri))
val entity : UrlEncodedFormEntity = new UrlEncodedFormEntity(formparams, "UTF-8")
val post = new HttpPost(uriBuilder.build())
post.setEntity(entity)
val response : HttpResponse = client.execute(post)

if (response.getStatusLine().getStatusCode() != 400) {
var access_token : Option[String] = None
var expires : Option[Date] = None

val entity = response.getEntity()
val entityContent = EntityUtils.toString(entity)
val entities : java.util.List[NameValuePair] = URLEncodedUtils.parse(
entityContent,
Charset.forName(entity.getContentEncoding() match {
case null => "ISO-8859-1" //default from EntityUtils
case x => x.getValue()
}))
val iter = entities.iterator()
while (iter.hasNext()) {
val pair : NameValuePair = iter.next()
pair.getName() match {
case "access_token" => access_token = Some(pair.getValue())
case "expires" => expires = Some(new Date(1000 * (pair.getValue().toLong)))
case _ => //ignore
}
}

writeAuth(access_token, expires)
}
}

private def writeAuth(accessToken : Option[String], expires : Option[Date]) = {
if (accessToken == None) {
clearAuth
}
else {
this.access_token = accessToken
this.expires = expires
try {
val props = new Properties()
props.setProperty("access_token", accessToken.get);
expires match {
case Some(date) => props.setProperty("expires", (date.getTime()/1000).toString())
case _ => //ignore
}
val fs : OutputStream = new FileOutputStream(credentialsFileName);
props.store(fs, null);
fs.close()
}
catch {
case e => //TODO
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ class DateLangRequestTests extends ScalatraSuite with FunSuite with TestingEnvir
// aggregator.system.shutdown()
// }



test("get date/language") {
println(":get date/language")
get("/langpop/2012-10-19/scala") {
Expand Down

0 comments on commit 0ad21ca

Please sign in to comment.