Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

We’re showing branches in this repository, but you can also compare across forks.

base fork: tekul/connect
base: d6259e020b
...
head fork: tekul/connect
compare: df9909177e
  • 3 commits
  • 22 files changed
  • 0 commit comments
  • 1 contributor
Commits on Feb 19, 2012
___ Move uf.oauth2 into connect codebase
The existing uf.oauth2 module doesn't
fit the multiple response_type use cases
so I need to hack it to bits which is
easier if it's part of the local codebase.
4555363
Commits on Feb 22, 2012
___ Minor renaming and comment updates. f014879
Commits on Feb 29, 2012
___ Update check_id endpoint and add tests.
The check_id endpoint now expects the id_token to
be passed as a bearer authentication token.

Also some refactoring of bearer auth extractors to
allow them to be used in the check_id implementation.
df99091
Showing with 957 additions and 56 deletions.
  1. +3 −5 client/src/main/scala/App.scala
  2. +13 −3 project/build.scala
  3. 0  project/{plugins → }/plugins.sbt
  4. +2 −2 server/src/main/scala/connect/Clients.scala
  5. +0 −4 server/src/main/scala/connect/ConnectServer.scala
  6. +2 −2 server/src/main/scala/connect/Oauth2Service.scala
  7. +1 −1  server/src/main/scala/connect/Templates.scala
  8. +1 −1  server/src/main/scala/connect/User.scala
  9. +32 −24 server/src/main/scala/connect/components.scala
  10. +153 −0 server/src/main/scala/connect/oauth2/AuthorizationEndpoint.scala
  11. +43 −0 server/src/main/scala/connect/oauth2/OAuthorization.scala
  12. +164 −0 server/src/main/scala/connect/oauth2/TokenEndpoint.scala
  13. +43 −0 server/src/main/scala/connect/oauth2/bearerAuth.scala
  14. +43 −0 server/src/main/scala/connect/oauth2/clients.scala
  15. +191 −0 server/src/main/scala/connect/oauth2/protections.scala
  16. +64 −0 server/src/main/scala/connect/oauth2/services.scala
  17. +59 −0 server/src/main/scala/connect/oauth2/stuff.scala
  18. +119 −0 server/src/main/scala/connect/oauth2/tokens.scala
  19. +8 −8 server/src/main/scala/connect/openid/OpenID.scala
  20. +1 −1  server/src/main/scala/connect/tokens/tokens.scala
  21. +1 −1  server/src/test/scala/connect/OpenIDProviderSpec.scala
  22. +14 −4 server/src/test/scala/connect/OpenIDServerSpec.scala
8 client/src/main/scala/App.scala
View
@@ -7,8 +7,6 @@ import dispatch._
import dispatch.liftjson.Js._
import net.liftweb.json._
-import unfiltered.oauth2.OAuthorization._
-
/**
*/
class App extends Templates with unfiltered.filter.Plan {
@@ -32,7 +30,7 @@ class App extends Templates with unfiltered.filter.Plan {
}
def intent = {
- // if we have an access token on hand, make a user info endpoint call
+ // if we have an access token on hand, make a user info endpoint call and parse the returned JSON
// if not, render the current list of tokens
case GET(Path("/") & AuthorizedToken(at)) =>
try {
@@ -57,8 +55,8 @@ class App extends Templates with unfiltered.filter.Plan {
code <- lookup("code") is
required("code is required") is nonempty("code can not be blank")
} yield {
- val postParams = Map(GrantType -> AuthorizationCode, Code -> code.get, ClientSecret -> "secret",
- ClientId -> client_id, RedirectURI -> redirect_uri)
+ val postParams = Map("grant_type" -> "authorization_code", "code" -> code.get, "client_secret" -> "secret",
+ "client_id" -> client_id, "redirect_uri" -> redirect_uri)
// Make an access token request and create a token from the returned JSON
val accessToken = Http(svc / "token" << postParams ># { AccessToken(_) })
println("Retrieved access token response: " + accessToken)
16 project/build.scala
View
@@ -33,6 +33,8 @@ object Dependencies {
val logbackVersion = "0.9.28"
val slf4jVersion = "1.6.1"
+// val servletApi = "javax.servlet" % "servlet-api" % "2.5" % "provided"
+
val scalaTest = "org.scalatest" %% "scalatest" % "1.6.1" % "test"
val mockito = "org.mockito" % "mockito-all" % "1.8.5" % "test"
val junit = "junit" % "junit" % "4.8.2" % "test"
@@ -50,9 +52,7 @@ object Dependencies {
val ufDeps = Seq(
"net.databinder" %% "unfiltered-filter" % ufversion,
"net.databinder" %% "unfiltered-json" % ufversion intransitive(),
- "net.databinder" %% "unfiltered-oauth2" % ufversion,
-
- "net.databinder" %% "unfiltered-jetty" % ufversion % "runtime",
+ "net.databinder" %% "unfiltered-jetty" % ufversion,
"net.databinder" %% "unfiltered-spec" % ufversion % "test"
)
@@ -101,4 +101,14 @@ object ConnectBuild extends Build {
)
) dependsOn(jwt)
+ {
+ val f : State => State = { (s: State) =>
+ println (s.remainingCommands)
+ s.copy(
+ remainingCommands = s.remainingCommands ++ Seq("project jwt")
+ )
+ }
+ onUnload in Global ~= (f compose _)
+ }
+
}
0  project/plugins/plugins.sbt → project/plugins.sbt
View
File renamed without changes
4 server/src/main/scala/connect/Clients.scala
View
@@ -1,10 +1,10 @@
package connect
-import unfiltered.oauth2.{Client, ClientStore}
+import connect.oauth2.{Client, ClientStore}
case class AppClient(id: String, secret: String, redirectUri: String) extends Client
-trait Clients extends ClientStore {
+class Clients extends ClientStore {
val clients = new java.util.HashMap[String, Client] {
put(
"exampleclient",
4 server/src/main/scala/connect/ConnectServer.scala
View
@@ -1,9 +1,5 @@
package connect
-import openid.OpenIDProvider
-import unfiltered.jetty.Server
-import unfiltered.request._
-import unfiltered.oauth2._
import connect.util.ReplPlan
import unfiltered.filter.Plan
import unfiltered.response.Pass
4 server/src/main/scala/connect/Oauth2Service.scala
View
@@ -4,12 +4,12 @@ import unfiltered.request._
import unfiltered.response._
import unfiltered.Cookie
-import unfiltered.oauth2.{Client, ResourceOwner, RequestBundle}
+import connect.oauth2.{Client, ResourceOwner, RequestBundle}
import Templates._
-trait OAuth2Service extends unfiltered.oauth2.Service {
+class OAuth2Service extends connect.oauth2.Service {
val ApproveKey = "Approve"
val DenyKey = "Deny"
2  server/src/main/scala/connect/Templates.scala
View
@@ -3,7 +3,7 @@ package connect
object Templates {
import unfiltered.response._
- import unfiltered.oauth2.{RequestBundle}
+ import connect.oauth2.{RequestBundle}
def page(body: scala.xml.NodeSeq) = Html(
<html>
2  server/src/main/scala/connect/User.scala
View
@@ -1,3 +1,3 @@
package connect
-case class User(id: String, password: Option[String]) extends unfiltered.oauth2.ResourceOwner
+case class User(id: String, password: Option[String]) extends connect.oauth2.ResourceOwner
56 server/src/main/scala/connect/components.scala
View
@@ -1,9 +1,11 @@
package connect
-import openid._
+import connect.oauth2._
+import connect.openid._
import unfiltered.filter.Plan
-import unfiltered.oauth2._
+
import unfiltered.request.HttpRequest
+
import net.liftweb.json._
import net.liftweb.json.Extraction._
import net.liftweb.json.Printer.compact
@@ -14,6 +16,7 @@ import crypto.sign.RsaSigner
import crypto.sign.RsaVerifier
import connect.tokens._
import connect.boot.ComponentRegistry
+import unfiltered.response.Pass
// Cake-pattern-(ish) application configuration.
@@ -33,7 +36,7 @@ trait TokenRepositoryComponent {
trait TokenStoreComponent { this: OpenIDProviderComponent with TokenRepositoryComponent =>
- trait TokenStoreDelegator extends TokenStore with Logger {
+ class TokenStoreDelegator extends TokenStore with Logger {
def refresh(other: Token): Token = tokenRepo.refresh(other)
def token(code: String) = tokenRepo.codeToken(code)
@@ -70,18 +73,14 @@ trait AccessTokenReaderComponent {
}
}
-trait OAuth2ServerComponent {
- def authorizationServer: AuthorizationServer
-}
-
/**
* Provides token authentication for the authorization filter by validating submitted access tokens
* against those in the token store.
*/
trait TokenAuthenticationComponent { this: AccessTokenReaderComponent with ClientStoreComponent =>
- def tokenAuthenticator: AuthSource
+ def tokenAuthenticator: AuthorizationSource
- class StdTokenAuthenticator extends AuthSource with Logger {
+ class StdTokenAuthenticator extends AuthorizationSource with Logger {
def authenticateToken[T](token: AccessToken, request: HttpRequest[T]): Either[String, (ResourceOwner, String, Seq[String])] = {
logger.debug("Authenticating token " + token)
token match {
@@ -118,6 +117,10 @@ trait TokenVerifierComponent {
trait OpenIDProviderComponent { this: TokenSignerComponent with TokenVerifierComponent =>
+ /**
+ * The name of the OpenID Provider, which will be used in the "iss" JWT field.
+ */
+ def providerName: String
def openIDProvider: OpenIDProvider
class StdOpenIDProvider extends OpenIDProvider with Logger {
@@ -129,7 +132,7 @@ trait OpenIDProviderComponent { this: TokenSignerComponent with TokenVerifierCom
def generateIdToken(owner: String, clientId: String, scopes: Seq[String]) = {
if (scopes.contains("openid")) {
val expiry = System.currentTimeMillis()/1000 + 600
- val claims = Map("iss" -> "CFID", "user_id" -> owner, "aud" -> clientId,"exp" -> expiry)
+ val claims = Map("iss" -> providerName, "user_id" -> owner, "aud" -> clientId,"exp" -> expiry)
val jwt = Jwt(compact(render(decompose(claims))), tokenSigner)
// Optionals iso29115, nonce, issued_to
logger.debug("id_token is " + jwt)
@@ -173,18 +176,13 @@ trait UserInfoServiceComponent {
def userInfoService: UserInfoService
}
-
// Web Plan Components
/**
* Provides the OAuth2 end points
*/
-trait OAuth2WebComponent { this: OAuth2ServerComponent =>
+trait OAuth2WebComponent {
def oauth2Plan: Plan
-
- class StdOAuth2Plan extends OAuthorization(authorizationServer) {
- require(authorizationServer != null);
- }
}
/**
@@ -198,9 +196,9 @@ trait AuthenticationWebComponent {
* Provides a resource protection layer for access-token validation
*/
trait TokenAuthorizationWebComponent { this: TokenAuthenticationComponent =>
- def tokenAuthorizationPlan: ProtectionLike
+ def tokenAuthorizationPlan: OAuth2Protection
- final class StdTokenAuthorizationPlan extends Protection(tokenAuthenticator)
+ final class StdTokenAuthorizationPlan extends OAuth2Protection(tokenAuthenticator)
}
/**
@@ -212,6 +210,13 @@ trait UserInfoComponent { this: UserInfoServiceComponent =>
class StdUserInfoPlan extends UserInfoPlan(userInfoService) with DefaultUserInfoEndPoint
}
+trait CheckIdComponent { this: OpenIDProviderComponent =>
+ def checkIdPlan: Plan
+
+ class StdCheckIdPlan extends CheckIdPlan(openIDProvider) with DefaultCheckIdEndPoint
+}
+
+
object ServerKey {
val n = BigInt("2524288480257888199556255116655542805598393034290055663431253555094618663541459985" +
"5793628180675586330233507173840470187695708486563026571606062758924232350260463016" +
@@ -237,8 +242,8 @@ object ServerKey {
class ConnectComponentRegistry extends OAuth2WebComponent with ComponentRegistry
with AuthenticationWebComponent
with UserInfoComponent
+ with CheckIdComponent
with TokenAuthorizationWebComponent
- with OAuth2ServerComponent
with UserInfoServiceComponent
with OpenIDProviderComponent
with TokenSignerComponent
@@ -249,17 +254,20 @@ class ConnectComponentRegistry extends OAuth2WebComponent with ComponentRegistry
with TokenStoreComponent
with TokenRepositoryComponent {
- override val oauth2Plan = new StdOAuth2Plan
+ override val providerName = "OpenIDConnectTest"
+ val authorizationEndpoint = new AuthorizationEndpoint(clientStore, new TokenStoreDelegator, new OAuth2Service)
+ val tokenEndpoint = new TokenEndpoint(clientStore, new TokenStoreDelegator)
+ override val oauth2Plan = new Plan {
+ def intent = Pass.onPass(authorizationEndpoint.intent, tokenEndpoint.intent)
+ }
override lazy val userInfoPlan = new StdUserInfoPlan
+ override lazy val checkIdPlan = new StdCheckIdPlan
override lazy val tokenAuthorizationPlan = new StdTokenAuthorizationPlan
override val authenticationPlan = new AuthenticationPlan
override lazy val openIDProvider = new StdOpenIDProvider
override val tokenSigner = RsaSigner(ServerKey.n, ServerKey.e)
override val tokenVerifier = RsaVerifier(ServerKey.n, 65537)
- lazy val authorizationServer = new AuthorizationServer with Clients with TokenStoreDelegator with OAuth2Service {
- val openidProvider = openIDProvider
- }
- override lazy val clientStore = authorizationServer
+ override lazy val clientStore = new Clients
override lazy val tokenAuthenticator = new StdTokenAuthenticator
override lazy val userInfoService = TestUsers
override val tokenRepo = new InMemoryTokenRepository
153 server/src/main/scala/connect/oauth2/AuthorizationEndpoint.scala
View
@@ -0,0 +1,153 @@
+package connect.oauth2
+
+import unfiltered.request._
+import unfiltered.response._
+import unfiltered.filter.request.ContextPath
+import unfiltered.request.QParams._
+import unfiltered.response.Redirect
+
+import OAuthorization._
+import Stuff._
+
+/**
+ *
+ * http://tools.ietf.org/html/draft-ietf-oauth-v2-23#section-3.1
+ */
+class AuthorizationEndpoint(clients: ClientStore, tokenStore: TokenStore, service: Service) extends unfiltered.filter.Plan with Formatting {
+
+ /** Some servers may wish to override this with custom redirect_url
+ * validation rules. We are being lenient here by checking the base
+ * of the registered redirect_uri. The spec recommends using the `state`
+ * param for per-request customization.
+ * @return true if valid, false otherwise
+ * see http://tools.ietf.org/html/draft-ietf-oauth-v2-22#section-3.1.2.2
+ *
+ * TODO: See TokenEndpoint
+ */
+ def validRedirectUri(provided: Option[String], client: Client): Boolean =
+ provided.isDefined && !provided.get.contains("#") && provided.get.startsWith(client.redirectUri)
+
+ def validScopes(scopes: Seq[String]) = true // TODO: Where should this go? Register scopes? OpenID ?
+
+ def errorResponse(redirectUri: String, error: String, desc: String,
+ euri: Option[String], state: Option[String], frag: Boolean): ResponseFunction[Any] = {
+ val params = Map(Error -> error, ErrorDescription -> desc) ++
+ euri.map(ErrorURI -> (_:String)) ++
+ state.map(State -> _)
+
+ if (frag) {
+ Redirect("%s#%s" format(redirectUri, qstr(params)))
+ } else {
+ Redirect(redirectUri ? qstr(params))
+ }
+ }
+
+ /**
+ */
+ def onAuthorizationRequest[T](req: HttpRequest[T], responseType: Seq[String], clientId: String,
+ redirectURI: Option[String], scope: Seq[String], state: Option[String]): ResponseFunction[Any] = {
+
+ // All responses are fragment encoded except "code"
+ val frag = !responseType.equals(Seq(Code))
+
+ clients.client(clientId, None) match {
+ case Some(client) =>
+ if(!validRedirectUri(redirectURI, client))
+ service.invalidRedirectUri(redirectURI, Some(client))
+ else if(!validScopes(scope)) {
+ errorResponse(redirectURI.get, InvalidScope, "invalid scope", service.errorUri(InvalidScope), state, frag)
+ } else {
+ service.resourceOwner(req) match {
+ case Some(owner) =>
+ if (service.denied(req))
+ errorResponse (redirectURI.get, AccessDenied, "user denied request", service.errorUri(AccessDenied), state, frag)
+ else if (service.accepted(req)) {
+ responseType match {
+ case Seq(Code) =>
+ // Authorization code flow
+ val code = tokenStore.generateAuthorizationCode(responseType, owner, client, scope, redirectURI.get)
+
+ Redirect(redirectURI.get ? "code=%s%s".format(code, state.map("&state=%s".format(_)).getOrElse("")))
+
+ case Seq(TokenKey) =>
+ // Implicit flow
+ val t = tokenStore.generateImplicitAccessToken(responseType, owner, client, scope, redirectURI.get)
+
+ // TODO: Get the different token types to return the response parameters here and combine them into
+ // a qstr
+
+ val fragment = qstr(
+ Map(AccessTokenKey -> t.value) ++
+ t.tokenType.map(TokenType -> _) ++
+ t.expiresIn.map(ExpiresIn -> (_:Int).toString) ++
+ (t.scopes match {
+ case Seq() => None
+ case xs => Some(spaceEncoder(xs))
+ }).map(Scope -> _) ++
+ state.map(State -> _) ++ t.extras
+ )
+ Redirect("%s#%s" format(redirectURI.get, fragment))
+
+ // OpenID Cases?
+// http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html
+ case Seq("code", "id_token") =>
+ ResponseString("'code id_token' not implemented yet")
+// When supplied as the value for the response_type parameter, a successful response MUST include both an Authorization Code as well as an id_token. Both success and error responses SHOULD be fragment-encoded.
+ case Seq("code", "token") =>
+// When supplied as the value for the response_type parameter, a successful response MUST include both an Access Token and an Authorization Code as defined in the OAuth 2.0 specification. Both successful and error responses SHOULD be fragment-encoded.
+ ResponseString("'code token' not implemented yet")
+ case Seq("id_token", "token") =>
+// When supplied as the value for the response_type parameter, a successful response MUST include both an Access Token as well as an id_token. Both success and error responses SHOULD be fragment-encoded.
+ ResponseString("'id_token token' not implemented yet")
+ case Seq("code", "id_token", "token") =>
+ ResponseString("'code id_token token' not implemented yet")
+// When supplied as the value for the response_type parameter, a successful response MUST include an Authorization Code, an id_token, and an Access Token. Both success and error responses SHOULD be fragment-encoded.
+ case unknown =>
+ errorResponse(redirectURI.get, UnsupportedResponseType, "unsupported response type(s) %s" format unknown,
+ service.errorUri(UnsupportedResponseType), state, frag)
+ }
+ } else {
+ service.requestAuthorization(
+ RequestBundle(req, responseType, client, Some(owner), redirectURI.get, scope, state)
+ )
+ }
+ case _ =>
+ service.login(RequestBundle(req, responseType, client, None, redirectURI.get, scope, state))
+ }
+ }
+ case _ => service.invalidClient
+ }
+ }
+
+ def intent = {
+ case req @ ContextPath(_, AuthorizePath) & Params(params) =>
+ val expected = for {
+ responseType <- lookup(ResponseType) is required(requiredMsg(ResponseType)) is
+ watch(_.map(spaceDecoder), e => "")
+ clientId <- lookup(ClientId) is required(requiredMsg(ClientId))
+ redirectURI <- lookup(RedirectURI) is required(requiredMsg(RedirectURI))
+ scope <- lookup(Scope) is watch(_.map(spaceDecoder), e => "")
+ state <- lookup(State) is optional[String, String]
+ } yield {
+ onAuthorizationRequest(req, responseType.get, clientId.get, redirectURI, scope.getOrElse(Nil), state.get)
+ }
+
+ expected(params) orFail { errs =>
+ params(RedirectURI) match {
+ case Seq(uri) =>
+ val qs = qstr(Map(
+ Error -> InvalidRequest,
+ ErrorDescription -> errs.map { _.error }.mkString(", ")
+ ))
+ params(ResponseType) match {
+ case Seq(Code) =>
+ Redirect(uri ? qs)
+ case _ =>
+ Redirect("%s#%s" format(uri, qs))
+ }
+ case _ =>
+ ResponseString("missing or invalid redirect_uri")
+ }
+ }
+ }
+}
43 server/src/main/scala/connect/oauth2/OAuthorization.scala
View
@@ -0,0 +1,43 @@
+package connect.oauth2
+
+object OAuthorization {
+
+ val RedirectURI = "redirect_uri"
+ val ClientId = "client_id"
+ val ClientSecret = "client_secret"
+
+ val Scope = "scope"
+ val State = "state"
+
+ val GrantType = "grant_type"
+ val AuthorizationCode = "authorization_code"
+ val PasswordType = "password"
+ val Password = "password"
+ val Username = "username"
+ val ClientCredentials = "client_credentials"
+ val OwnerCredentials = "password"
+ val RefreshToken = "refresh_token"
+
+ val ResponseType = "response_type"
+ val Code = "code"
+ val TokenKey = "token"
+
+ /**
+ * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-20#section-4.1.2.1
+ * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-20#section-4.2.2.1
+ */
+ val Error = "error"
+ val InvalidClient = "invalid_client"
+ val InvalidRequest = "invalid_request"
+ val UnauthorizedClient = "unauthorized_client"
+ val AccessDenied = "access_denied"
+ val UnsupportedResponseType = "unsupported_response_type"
+ val UnsupportedGrantType = "unsupported_grant_type"
+ val InvalidScope = "invalid_scope"
+ val ErrorDescription = "error_description"
+ val ErrorURI = "error_uri"
+
+ val AccessTokenKey = "access_token"
+ val TokenType = "token_type"
+ val ExpiresIn = "expires_in"
+}
164 server/src/main/scala/connect/oauth2/TokenEndpoint.scala
View
@@ -0,0 +1,164 @@
+package connect.oauth2
+
+import unfiltered.request._
+import unfiltered.response._
+import unfiltered.request.QParams._
+
+import unfiltered.filter.request.ContextPath
+
+import Stuff._
+import OAuthorization._
+
+/**
+ */
+class TokenEndpoint(clients: ClientStore, tokenStore: TokenStore) extends unfiltered.filter.Plan with Formatting {
+
+ val InvalidRedirectURIMsg = "invalid redirect_uri"
+ val UnknownClientMsg = "unknown client"
+
+ /** @return a function which builds a
+ * response for an accept token request */
+ protected def accessResponder(
+ accessToken: String,
+ tokenType: Option[String],
+ expiresIn: Option[Int],
+ refreshToken: Option[String],
+ scope:Seq[String],
+ extras: Iterable[(String, String)]) =
+ CacheControl("no-store") ~> Pragma("no-cache") ~>
+ Json(Map(AccessTokenKey -> accessToken) ++
+ tokenType.map(TokenType -> _) ++
+ expiresIn.map (ExpiresIn -> (_:Int).toString) ++
+ refreshToken.map (RefreshToken -> _) ++
+ (scope match {
+ case Seq() => None
+ case xs => Some(spaceEncoder(xs))
+ }).map (Scope -> _) ++ extras)
+
+ protected def errorResponder(
+ error: String, desc: String,
+ euri: Option[String], state: Option[String]) =
+ BadRequest ~> CacheControl("no-store") ~> Pragma("no-cache") ~>
+ Json(Map(Error -> error, ErrorDescription -> desc) ++
+ euri.map (ErrorURI -> (_: String)) ++
+ state.map (State -> _))
+
+
+ def errorUri(error: String) = None
+
+ // TODO: Extract this since it is used by both endpoints
+ def validRedirectUri(provided: Option[String], client: Client): Boolean = {
+ provided.isDefined && !provided.get.contains("#") && provided.get.startsWith(client.redirectUri)
+ }
+
+ def onGrantAuthCode(code: String, redirectUri: String, clientId: String, clientSecret: String) = {
+ clients.client(clientId, Some(clientSecret)) match {
+ case Some(client) =>
+ if(!validRedirectUri(Some(redirectUri), client)) errorResponder(
+ InvalidClient, "invalid redirect uri", None, None
+ )
+ else {
+ tokenStore.token(code) match {
+ case Some(token) =>
+ // tokens redirectUri must be exact match to the one provided
+ // in order further bind the access request to the auth request
+ if (token.clientId != client.id || token.redirectUri != redirectUri)
+ errorResponder(UnauthorizedClient, "client not authorized", errorUri(UnauthorizedClient), None)
+ else {
+ val t = tokenStore.exchangeAuthorizationCode(token)
+ accessResponder(t.value, t.tokenType, t.expiresIn, t.refresh, t.scopes, t.extras)
+ }
+ case _ => errorResponder(InvalidRequest, "unknown code", None, None)
+ }
+ }
+ case _ => errorResponder(InvalidRequest, UnknownClientMsg, errorUri(InvalidRequest), None)
+ }
+ }
+
+ def intent = {
+ case req @ POST(ContextPath(_, TokenPath)) & Params(params) =>
+ val expected = for {
+ grantType <- lookup(GrantType) is required(requiredMsg(GrantType))
+ code <- lookup(Code) is optional[String, String]
+ clientId <- lookup(ClientId) is required(requiredMsg(ClientId))
+ redirectURI <- lookup(RedirectURI) is optional[String, String]
+ // clientSecret is not recommended to be passed as a parameter but instead
+ // encoded in a basic auth header http://tools.ietf.org/html/draft-ietf-oauth-v2-16#section-3.1
+ clientSecret <- lookup(ClientSecret) is required(requiredMsg(ClientSecret))
+ refreshToken <- lookup(RefreshToken) is optional[String, String]
+ scope <- lookup(Scope) is watch(_.map(spaceDecoder), e => "")
+ userName <- lookup(Username) is optional[String, String]
+ password <- lookup(Password) is optional[String, String]
+ } yield {
+
+ grantType.get match {
+// case ClientCredentials =>
+// onClientCredentials(clientId.get, clientSecret.get, scope.getOrElse(Nil))
+//
+// case Password =>
+// (userName.get, password.get) match {
+// case (Some(u), Some(pw)) =>
+// onPassword(u, pw, clientId.get, clientSecret.get, scope.getOrElse(Nil))
+// case _ =>
+// errorResponder(
+// InvalidRequest,
+// (requiredMsg(Username) :: requiredMsg(Password) :: Nil).mkString(" and "),
+// auth.errUri(InvalidRequest), None
+// )
+// }
+//
+// case RefreshToken =>
+// refreshToken.get match {
+// case Some(rtoken) =>
+// onRefresh(rtoken, clientId.get, clientSecret.get, scope.getOrElse(Nil))
+// case _ => errorResponder(InvalidRequest, requiredMsg(RefreshToken), None, None)
+// }
+
+ case AuthorizationCode =>
+ (code.get, redirectURI.get) match {
+ case (Some(c), Some(r)) =>
+ onGrantAuthCode(c, r, clientId.get, clientSecret.get)
+ case _ =>
+ errorResponder(
+ InvalidRequest,
+ (requiredMsg(Code) :: requiredMsg(RedirectURI) :: Nil).mkString(" and "),
+ errorUri(InvalidRequest), None
+ )
+ }
+ case unsupported =>
+ // note the oauth2 spec does allow for extension grant types,
+ // this implementation currently does not
+ errorResponder(
+ UnsupportedGrantType, "%s is unsupported" format unsupported,
+ errorUri(UnsupportedGrantType), None)
+ }
+ }
+
+ // here, we are combining requests parameters with basic authentication headers
+ // the preferred way of providing client credentials is through
+ // basic auth but this is not required. The following folds basic auth data
+ // into the params ensuring there is no conflict in transports
+ val combinedParams = (
+ (Right(params): Either[String, Map[String, Seq[String]]]) /: BasicAuth.unapply(req)
+ )((a,e) => e match {
+ case (clientId, clientSecret) =>
+ val preferred = Right(
+ a.right.get ++ Map(ClientId -> Seq(clientId), ClientSecret-> Seq(clientSecret))
+ )
+ a.right.get(ClientId) match {
+ case Seq(id) =>
+ if(id == clientId) preferred else Left("client ids did not match")
+ case _ => preferred
+ }
+ case _ => a
+ })
+
+ combinedParams fold({ err =>
+ errorResponder(InvalidRequest, err, None, None)
+ }, { mixed =>
+ expected(mixed) orFail { errs =>
+ errorResponder(InvalidRequest, errs.map { _.error }.mkString(", "), None, None)
+ }
+ })
+ }
+}
43 server/src/main/scala/connect/oauth2/bearerAuth.scala
View
@@ -0,0 +1,43 @@
+package connect.oauth2
+
+import unfiltered.request.{Authorization, HttpRequest}
+
+
+/*
+ * Authentication using bearer tokens as defined in
+ *
+ * http://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-15
+ */
+
+/**
+ * Extractor for the bearer header HTTP authentication scheme
+ */
+object BearerAuth {
+
+ object BearerHeader {
+ val HeaderPattern = """Bearer ([\w\d!#$%&'\(\)\*+\-\.\/:<=>?@\[\]^_`{|}~\\,;]+)""".r
+
+ def unapply(hval: String) = hval match {
+ case HeaderPattern(token) => Some(token)
+ case _ => None
+ }
+ }
+
+ def unapply[T](r: HttpRequest[T]) = r match {
+ case Authorization(BearerHeader(token)) => Some(token)
+ case _ => None
+ }
+}
+
+/**
+ * Authentication using a form-encoded access_token parameter
+ * in the request body.
+ *
+ * http://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-14#section-2.2
+ */
+object ParamTokenAuth {
+ def unapply[T](r: HttpRequest[T]) = r.parameterValues(OAuthorization.AccessTokenKey) match {
+ case Seq(token) => Some(token)
+ case _ => None
+ }
+}
43 server/src/main/scala/connect/oauth2/clients.scala
View
@@ -0,0 +1,43 @@
+package connect.oauth2
+
+/**
+ * TODO: What about the designation of this client? WebApp, Native etc...
+ * these are mandated parts of client registration as the designtation
+ * infers the grant type.
+ *
+ * When registering a client, the client developer:
+ *
+ * - Specifies the client type as described in Section 2.1,
+ * - Provides its client redirection URIs as described in
+ * Section 3.1.2, and
+ * - Includes any other information required by the authorization
+ * server (e.g. application name, website, description, logo image,
+ * the acceptance of legal terms).
+ *
+ * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-20#section-2.2
+ */
+trait Client {
+ /**
+ * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-20#section-2.3
+ */
+ def id: String
+
+ /**
+ * TODO: Needs reference... isnt this generated?
+ */
+ def secret: String
+
+ /**
+ * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-20#section-2.2
+ */
+ def redirectUri: String
+}
+
+/**
+ * Locate a registered client. This could be from anywhere but assuming
+ * its a database or other persistance store then the clientId should
+ * be used as the key.
+ */
+trait ClientStore {
+ def client(clientId: String, secret: Option[String]): Option[Client]
+}
191 server/src/main/scala/connect/oauth2/protections.scala
View
@@ -0,0 +1,191 @@
+package connect.oauth2
+
+import unfiltered.response._
+import unfiltered.filter.Plan
+import unfiltered.request._
+
+/**
+ * Plan which can be used in front of an oauth protected resource
+ * to authenticate the access token supplied by the client.
+ *
+ * This will typically be a bearer token:
+ *
+ * GET /api/blah HTTP/1.1
+ * Host: www.example.com
+ * Authorization: Bearer vF9dft4qmT
+ */
+class OAuth2Protection(source: AuthorizationSource) extends Plan {
+ import javax.servlet.http.HttpServletRequest
+
+ /** Provides a list of schemes used for decoding access tokens in request */
+ val schemes = Seq(BearerAuthentication, ParamTokenAuthentication/*, MacAuth*/)
+
+ final def intent = ((schemes map { _.intent(this) }) :\ fallback) { _ orElse _ }
+
+ /** If no authentication token is provided at all, demand the first
+ * authentication scheme of all that are supported */
+ def fallback: Plan.Intent = {
+ case r =>
+ schemes.head.errorResponse(Unauthorized, "", r)
+ }
+
+ /** Returns access token response to client */
+ def authenticate[T <: HttpServletRequest](
+ token: AccessToken, request: HttpRequest[T])(errorResp: (String => ResponseFunction[Any])) =
+ source.authenticateToken(token, request) match {
+ case Left(msg) => errorResp(msg)
+ case Right((user, clientId, scopes)) =>
+ request.underlying.setAttribute(OAuth2.XAuthorizedIdentity, user.id)
+ request.underlying.setAttribute(OAuth2.XAuthorizedClientIdentity, clientId)
+ request.underlying.setAttribute(OAuth2.XAuthorizedScopes, scopes)
+ Pass
+ }
+}
+
+/**
+ * Represents the authorization source that issued the access token.
+ *
+ * Used by the resource server to authenticate an access token submitted by a client.
+ * This might involve the use of shared storage (where the resource and authorization servers
+ * are colocated), or the use of signed/encrypted tokens containing the required information.
+ */
+trait AuthorizationSource {
+ /**
+ * Given a deserialized access token and request, extract the resource owner, client id, and list of scopes
+ * associated with the request, if there is an error return it represented as a string message
+ * to return the the oauth client */
+ def authenticateToken[T](
+ token: AccessToken,
+ request: HttpRequest[T]): Either[String, (ResourceOwner, String, Seq[String])]
+}
+
+/** Represents the scheme used for decoding access tokens from a given requests. */
+trait AuthScheme {
+
+ def intent(protection: OAuth2Protection): Plan.Intent
+
+ def errorString(status: String, description: String) =
+ """error="%s" error_description="%s" """.trim format(status, description)
+
+ /**
+ * The WWW-Authenticate challege returned to the client in a 401 response for invalid requests */
+ val challenge: String
+
+ /**
+ * An error header, consisting of the challenge and possibly an error and error_description attribute
+ * (this depends on the authentication scheme).
+ */
+ def errorHeader(error: Option[String] = None, description: Option[String] = None) = {
+ val attrs = List("error" -> error, "error_description" -> description).collect { case (key, Some(value)) => key -> value }
+ attrs.tail.foldLeft(
+ attrs.headOption.foldLeft(challenge) { case (current, (key, value)) => """%s %s="%s"""".format(current, key, value) }
+ ) { case (current, (key, value)) => current + ",\n%s=\"%s\"".format(key, value) }
+ }
+
+ /**
+ * The response for failed authentication attempts. Intended to be overridden by authentication schemes that have
+ * differing requirements.
+ */
+ val failedAuthenticationResponse: (String => ResponseFunction[Any]) = { msg =>
+ Unauthorized ~> WWWAuthenticate(errorHeader(Some("invalid_token"), Some(msg))) ~>
+ ResponseString(errorString("invalid_token", msg))
+ }
+
+ /** Return a function representing an error response */
+ def errorResponse[T](status: Status, description: String,
+ request: HttpRequest[T]): ResponseFunction[Any] = (status, description) match {
+ case (Unauthorized, "") => Unauthorized ~> WWWAuthenticate(challenge) ~> ResponseString(challenge)
+ case (Unauthorized, _) => failedAuthenticationResponse(description)
+ case (BadRequest, _) => status ~> ResponseString(errorString("invalid_request", description))
+ case (Forbidden, _) => status ~> ResponseString(errorString("insufficient_scope", description))
+ case _ => status ~> ResponseString(errorString(status.toString, description))
+ }
+}
+
+sealed trait AccessToken
+
+case class BearerToken(value: String) extends AccessToken
+
+
+
+/**
+ */
+sealed trait BearerAuthentication extends AuthScheme {
+ val challenge = "Bearer"
+
+ def intent(protection: OAuth2Protection) = {
+ case BearerAuth(token) & request =>
+ protection.authenticate(BearerToken(token), request) { failedAuthenticationResponse }
+ }
+}
+
+object BearerAuthentication extends BearerAuthentication {}
+
+
+sealed trait ParamTokenAuthentication extends AuthScheme {
+ val challenge = "Bearer"
+
+ def intent(protection: OAuth2Protection) = {
+ case ParamTokenAuth(token) & request =>
+ protection.authenticate(BearerToken(token), request) { failedAuthenticationResponse }
+ }
+}
+
+object ParamTokenAuthentication extends ParamTokenAuthentication {}
+
+/** Represents MAC auth. */
+//trait MacAuth extends AuthScheme {
+// import unfiltered.mac.{ Mac, MacAuthorization }
+//
+// val challenge = "MAC"
+//
+// /** The algorigm used to sign the request */
+// def algorithm: String
+//
+// /** Given a token value, returns the associated token secret */
+// def tokenSecret(key: String): Option[String]
+//
+// def intent(protection: ProtectionLike) = {
+// case MacAuthorization(id, nonce, bodyhash, ext, mac) & req =>
+// try {
+// tokenSecret(id) match {
+// case Some(key) =>
+// // compare a signed request with the signature provided
+// Mac.sign(req, nonce, ext, bodyhash, key, algorithm).fold({ err =>
+// errorResponse(Unauthorized, err, req)
+// }, { sig =>
+// if(sig == mac) protection.authenticate(MacAuthToken(id, key, nonce, bodyhash, ext), req) {
+// failedAuthenticationResponse
+// }
+// else errorResponse(Unauthorized, "invalid MAC signature", req)
+// })
+// case _ =>
+// errorResponse(Unauthorized, "invalid token", req)
+// }
+// } catch {
+// case _ => errorResponse(Unauthorized, "invalid MAC header.", req)
+// }
+// }
+//
+// /**
+// * Whereas the Bearer token is supposed to return an error code in the error attribute and a human-readable
+// * error description in the error_description attribute of the WWW-Authenticate header, for the MAC
+// * authentication scheme, a human-readable error message may be supplied in the error attribute
+// * (see http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-00#section-4.1)
+// */
+// override val failedAuthenticationResponse: (String => ResponseFunction[Any]) = { msg =>
+// Unauthorized ~> WWWAuthenticate(errorHeader(Some(msg))) ~> ResponseString("""error="%s"""".format(msg))
+// }
+//}
+
+//object MacAuth extends MacAuth {
+// def algorithm = "hmac-sha-1"
+// def tokenSecret(key: String) = None
+//}
+//
+//case class MacAuthToken(id: String,
+// secret: String,
+// nonce: String,
+// bodyhash: Option[String],
+// ext: Option[String]
+// ) extends AccessToken
64 server/src/main/scala/connect/oauth2/services.scala
View
@@ -0,0 +1,64 @@
+package connect.oauth2
+
+import unfiltered.response._
+import unfiltered.request.{HttpRequest => Req}
+
+/** A ResourceOwner belongs to a Service */
+trait ResourceOwner {
+ def id: String
+ def password: Option[String]
+}
+
+/** Encapsulates information sent by a Client Authorization request that may
+ * need to be repeated after authentication, account creation, or other container
+ * behavior before an authorization request can be processed */
+case class RequestBundle[T](request: Req[T], responseTypes: Seq[String], client: Client,
+ owner: Option[ResourceOwner], redirectUri: String,
+ scope: Seq[String], state: Option[String])
+
+/** Request responses a Service must implement to complete OAuth flows */
+trait ServiceResponses {
+
+ /** @return a function that provides a means of logging a user in.
+ * the handling of a successfully login should post back to
+ * the authorization server's authorize endpoint */
+ def login[T](requestBundle: RequestBundle[T]): ResponseFunction[Any]
+
+ /** @return a function that provides a means of promting a resource ower
+ * for authorization. The handling of this response should post
+ * back to the authorization server's authorize endpoitn */
+ def requestAuthorization[T](requestBundle: RequestBundle[T]): ResponseFunction[Any]
+
+ /** @return a function that provides a user notification that a provided redirect
+ * uri was invalid or not present */
+ def invalidRedirectUri(uri: Option[String], client: Option[Client]): ResponseFunction[Any]
+
+
+
+ /** @return a function that provides a user notification that a request was made with an invalid client */
+ def invalidClient: ResponseFunction[Any]
+}
+
+trait Service extends ServiceResponses {
+
+ /** @return a uri for more information on a privded error code */
+ def errorUri(error: String): Option[String]
+
+ /** @return Some(resourceOwner) if one is authenticated, None otherwise. None will trigger a login request */
+ def resourceOwner[T](r: Req[T]): Option[ResourceOwner]
+
+ /** @return Some(resourceOwner) if they can be authenticated by the given password credentials, otherwise None */
+ def resourceOwner(userName: String, password: String): Option[ResourceOwner]
+
+ /** @return true if application-specific logic determines this request was accepted, false otherwise */
+ def accepted[T](r: Req[T]): Boolean
+
+ /** @return true if application-specific logic determines this request was denied, false otherwise */
+ def denied[T](r: Req[T]): Boolean
+
+ /** @return true if provides scopes are valid and not malformed */
+ def validScopes(scopes: Seq[String]): Boolean
+
+ /** @return true if the provided scopes are valid for a given client and resource owner */
+ def validScopes[T](resourceOwner: ResourceOwner, scopes: Seq[String], req: Req[T]): Boolean
+}
59 server/src/main/scala/connect/oauth2/stuff.scala
View
@@ -0,0 +1,59 @@
+package connect.oauth2
+
+/**
+ */
+private[oauth2] object Stuff {
+ val AuthorizePath = "/authorize"
+ val TokenPath = "/token"
+
+ implicit def s2qs(uri: String) = new {
+ def ?(qs: String) =
+ "%s%s%s" format(uri, if(uri.indexOf("?") > 0) "&" else "?", qs)
+ }
+
+ def spaceDecoder(raw: String) = raw.replace("""\s+"""," ").split(" ").sorted.toSeq
+ def spaceEncoder(scopes: Seq[String]) = scopes.mkString("+")
+ def requiredMsg(what: String) = "%s is required" format what
+
+}
+
+private[oauth2] trait Formatting {
+ import java.net.URLEncoder
+ def qstr(kvs: Iterable[(String, String)]) =
+ kvs map { _ match { case (k, v) => URLEncoder.encode(k, "utf-8") + "=" + URLEncoder.encode(v, "utf-8") } } mkString("&")
+
+ def Json(kvs: Iterable[(String, String)]) =
+ unfiltered.response.ResponseString(kvs map { _ match { case (k, v) => "\"%s\":\"%s\"".format(k,v) } } mkString(
+ "{",",","}"
+ )) ~> unfiltered.response.JsonContent
+}
+
+object OAuth2 {
+ val XAuthorizedIdentity = "X-Authorized-Identity"
+ val XAuthorizedClientIdentity = "X-Authorized-Client-Identity"
+ val XAuthorizedScopes = "X-Authorized-Scopes"
+}
+
+/** Extractor for a resource owner and the client they authorized, as well as the granted scope. */
+object OAuthIdentity {
+ import OAuth2._
+ import javax.servlet.http.HttpServletRequest
+ import unfiltered.request.HttpRequest
+
+ // TODO: how can we accomplish this and not tie ourselves to underlying request?
+ /**
+ * @return a 3-tuple of (resource-owner-id, client-id, scopes) as an Option, or None if any of these is not available
+ * in the request
+ */
+ def unapply[T <: HttpServletRequest](r: HttpRequest[T]): Option[(String, String, Seq[String])] =
+ r.underlying.getAttribute(XAuthorizedIdentity) match {
+ case null => None
+ case id: String => r.underlying.getAttribute(XAuthorizedClientIdentity) match {
+ case null => None
+ case clientId: String => r.underlying.getAttribute(XAuthorizedScopes) match {
+ case null => Some((id, clientId, Nil))
+ case scopes: Seq[String] => Some((id, clientId, scopes))
+ }
+ }
+ }
+}
119 server/src/main/scala/connect/oauth2/tokens.scala
View
@@ -0,0 +1,119 @@
+package connect.oauth2
+
+/**
+ * The access token provides an abstraction layer, replacing different
+ * authorization constructs (e.g. username and password) with a single
+ * token understood by the resource server. This abstraction enables
+ * issuing access tokens more restrictive than the authorization grant
+ * used to obtain them, as well as removing the resource server's need
+ * to understand a wide range of authentication methods.
+ *
+ * Access tokens can have different formats, structures, and methods of
+ * utilization (e.g. cryptographic properties) based on the resource
+ * server security requirements. Access token attributes and the
+ * methods used to access protected resources are beyond the scope of
+ * this specification and are defined by companion specifications.
+ *
+ * A hook for providing extention properties is provided as the `extras`
+ * method which defaults to an empty map
+ *
+ * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-20#section-1.3
+ */
+trait Token {
+ def value: String
+ def clientId: String
+ def redirectUri: String
+ def owner: String
+ def tokenType: Option[String]
+ def refresh: Option[String]
+ def expiresIn: Option[Int]
+ def scopes: Seq[String]
+ def extras: Iterable[(String, String)] = Map.empty[String, String]
+}
+
+/**
+ * The token store controls token-orientated operations. Specifically
+ * anything that needs to happen with a token is the responsibility
+ * of the incumbant TokenStore as typically it will require interacting
+ * with the some kind of storage
+ */
+trait TokenStore {
+ /**
+ *
+ * @see AuthorizationServer
+ * @see RefreshTokenRequest
+ * see also http://tools.ietf.org/html/draft-ietf-oauth-v2-20#section-6
+ * @return Gives a refreshed or new token given a valid access token
+ */
+ def refresh(other: Token): Token
+
+ /**
+ *
+ * query for Token by client
+ * @see AuthorizationServer
+ * @see RefreshTokenRequest
+ * see also http://tools.ietf.org/html/draft-ietf-oauth-v2-20#section-6
+ * @return Given the a refresh token gives a new access token
+ */
+ def refreshToken(refreshToken: String): Option[Token]
+
+ /**
+ *
+ * @see AuthorizationServer
+ * @see AccessTokenRequest
+ * see also http://tools.ietf.org/html/draft-ietf-oauth-v2-20#section-4.1.3
+ * @return Given a "code" return a resource access token
+ */
+ def token(code: String): Option[Token]
+
+ /**
+ *
+ * @see AuthorizationServer
+ * @see AccessTokenRequest
+ * see also http://tools.ietf.org/html/draft-ietf-oauth-v2-20#section-4.1.3
+ * @return Create an access token given a code token
+ */
+ def exchangeAuthorizationCode(codeToken: Token): Token
+
+ /**
+ * Not responseTypes is a seq to enable oauth extentions but for most cases, it can
+ * be expected to contain one element
+ * @see AuthorizationServer
+ * @see AuthorizationCodeRequest
+ * see also http://tools.ietf.org/html/draft-ietf-oauth-v2-20#section-4.1
+ * @return a short lived authorization code bound to a client
+ * and redirect uri for a given resource owner.
+ */
+ def generateAuthorizationCode(
+ responseTypes: Seq[String], owner: ResourceOwner, client: Client,
+ scope: Seq[String], redirectUri: String): String
+
+ /**
+ * Note responseTypes is a seq to enable oauth extentions but for most cases, it can
+ * be expected to contain one element
+ * @see AuthorizationServer
+ * @see ImplicitAuthorizationRequest
+ * see also http://tools.ietf.org/html/draft-ietf-oauth-v2-20#section-4.2
+ * @return an access token for an implicit client
+ */
+ def generateImplicitAccessToken(responseTypes: Seq[String], owner: ResourceOwner, client: Client,
+ scope: Seq[String], redirectUri: String): Token
+
+ /**
+ *
+ * @see AuthorizationServer
+ * @see ClientCredentialsRequest
+ * see also http://tools.ietf.org/html/draft-ietf-oauth-v2-20#section-4.4
+ * @return an access token for a given client, not tied to
+ * a given resource owner
+ */
+ def generateClientToken(client: Client, scope: Seq[String]): Token
+
+ /**
+ * @see AuthorizationServer
+ * @see PasswordRequest
+ * see also http://tools.ietf.org/html/draft-ietf-oauth-v2-21#section-4.3.3
+ * @return an access token for a client, given the resource owner's credentials
+ */
+ def generatePasswordToken(owner: ResourceOwner, client: Client, scope: Seq[String]): Token
+}
16 server/src/main/scala/connect/openid/OpenID.scala
View
@@ -2,13 +2,11 @@ package connect.openid
import connect.Logger
-import unfiltered.request._
-import unfiltered.response.Json._
import unfiltered.response._
import unfiltered.filter.Plan
import unfiltered.filter.request.ContextPath
-import unfiltered.oauth2.OAuthIdentity
import net.liftweb.json.JsonAST.JValue
+import connect.oauth2._
object OpenID {
/**
@@ -80,11 +78,13 @@ abstract class UserInfoPlan(userInfoService: UserInfoService) extends Plan with
abstract class CheckIdPlan(openIdProvider: OpenIDProvider) extends Plan with CheckIdEndPoint with Logger {
implicit val formats = Serialization.formats(NoTypeHints)
+ private def checkToken(idToken: String) = Json(openIdProvider.checkIdToken(idToken))
+
def intent = {
- case ContextPath(_, CheckIdPath) & Params(params) =>
- params(IdToken) match {
- case Seq(id_token) => Json(openIdProvider.checkIdToken(id_token))
- case Nil => Json(("error" -> "missing id_token"))
- }
+ case req @ ContextPath(_, CheckIdPath) => req match {
+ case BearerAuth(idToken) => checkToken(idToken)
+ case ParamTokenAuth(idToken) => checkToken(idToken)
+ case _ => Unauthorized ~> WWWAuthenticate("Bearer")
+ }
}
}
2  server/src/main/scala/connect/tokens/tokens.scala
View
@@ -1,6 +1,6 @@
package connect.tokens
-import unfiltered.oauth2._
import connect.Logger
+import connect.oauth2.{Client, ResourceOwner, Token}
case class AppToken(value: String, clientId: String, scopes: Seq[String],
2  server/src/test/scala/connect/OpenIDProviderSpec.scala
View
@@ -13,12 +13,12 @@ class OpenIDProviderSpec extends Specification {
"An OpenIDProvider" should {
"validate its own tokens" in {
val token = provider.generateIdToken("owner", "client", Seq("openid"))
-
val claims = provider.checkIdToken(token.get)
claims \ "error" must_== JNothing
}
}
+ // TODO: Write tests based on sections of the spec
}
18 server/src/test/scala/connect/OpenIDServerSpec.scala
View
@@ -12,6 +12,7 @@ class OpenIDServerSpec extends Specification with unfiltered.spec.jetty.Served {
object Config extends ConnectComponentRegistry
def setup = _.filter(Config.oauth2Plan)
+ .filter(Config.checkIdPlan)
.filter(Config.authenticationPlan)
.filter(Config.tokenAuthorizationPlan)
.filter(Config.userInfoPlan)
@@ -20,6 +21,7 @@ class OpenIDServerSpec extends Specification with unfiltered.spec.jetty.Served {
val authorize = host / "authorize"
val token = host / "token"
val userInfo = host / "userinfo"
+ val checkId = host / "check_id"
val cookies = new BasicCookieStore
override def http[T](handler: Handler[T]): T = {
@@ -97,16 +99,24 @@ class OpenIDServerSpec extends Specification with unfiltered.spec.jetty.Served {
})
// Decode the idToken claims
- val claims = parse(Jwt(idToken).claims).extract[Map[String,String]]
+ val claims = parse(Jwt(idToken).claims)
+ // claims.extract[Map[String,String]]
- // Client posts request to the user info endpoint with the access token
+ // Do the same using the check_id endpoint and check we get the same
+ // http://openid.net/specs/openid-connect-standard-1_0.html#check_id_ep
+ // id_token is submitted as the access_token
+
+ parse(http(checkId <:< bearerAuth(idToken) as_str)) must_== claims
- val uinfo = http(userInfo <:< Map("Authorization" -> ("Bearer " + accessToken)) as_str)
+ // Client posts request to the user info endpoint with the access token
+ val uinfo = http(userInfo <:< bearerAuth(accessToken) as_str)
// TODO: Check response header for content type json/jwt
parse(uinfo)
- println(uinfo)
+// println(uinfo)
}
}
+
+ def bearerAuth(token: String) = Map("Authorization" -> ("Bearer " + token))
}

No commit comments for this range

Something went wrong with that request. Please try again.