Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also compare across forks.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, 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
@tekul 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
@tekul Minor renaming and comment updates. f014879
Commits on Feb 29, 2012
@tekul 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
View
8 client/src/main/scala/App.scala
@@ -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)
View
16 project/build.scala
@@ -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 _)
+ }
+
}
View
0  project/plugins/plugins.sbt → project/plugins.sbt
File renamed without changes
View
4 server/src/main/scala/connect/Clients.scala
@@ -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",
View
4 server/src/main/scala/connect/ConnectServer.scala
@@ -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
View
4 server/src/main/scala/connect/Oauth2Service.scala
@@ -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"
View
2  server/src/main/scala/connect/Templates.scala
@@ -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>
View
2  server/src/main/scala/connect/User.scala
@@ -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
View
56 server/src/main/scala/connect/components.scala
@@ -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
View
153 server/src/main/scala/connect/oauth2/AuthorizationEndpoint.scala
@@ -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")
+ }
+ }
+ }
+}
View
43 server/src/main/scala/connect/oauth2/OAuthorization.scala
@@ -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"
+}
View
164 server/src/main/scala/connect/oauth2/TokenEndpoint.scala
@@ -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)
+ }
+ })
+ }
+}
View
43 server/src/main/scala/connect/oauth2/bearerAuth.scala
@@ -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
+ }
+}
View
43 server/src/main/scala/connect/oauth2/clients.scala
@@ -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]
+}
View
191 server/src/main/scala/connect/oauth2/protections.scala
@@ -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
View
64 server/src/main/scala/connect/oauth2/services.scala
@@ -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
+}
View
59 server/src/main/scala/connect/oauth2/stuff.scala
@@ -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))
+ }
+ }
+ }
+}
View
119 server/src/main/scala/connect/oauth2/tokens.scala
@@ -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
+}
View
16 server/src/main/scala/connect/openid/OpenID.scala
@@ -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")
+ }
}
}
View
2  server/src/main/scala/connect/tokens/tokens.scala
@@ -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],
View
2  server/src/test/scala/connect/OpenIDProviderSpec.scala
@@ -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
}
View
18 server/src/test/scala/connect/OpenIDServerSpec.scala
@@ -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.