Skip to content

Commit

Permalink
Add authentication framework
Browse files Browse the repository at this point in the history
- Add the ability to place filters before the HTTP service, configurable
  at the request level
- Add the concept of an Authorizer, which is just a filter.
- Add some initial Authorizers - Bearer (for pre-shared Bearer tokens)
  and MAC (for pre-shared MAC tokens)

A future PR should contain a full OAuth2 flow implemented in an
Authorizer.
  • Loading branch information
Jeremy Smith committed Oct 4, 2016
1 parent bdecef0 commit c905297
Show file tree
Hide file tree
Showing 4 changed files with 237 additions and 60 deletions.
35 changes: 25 additions & 10 deletions featherbed-core/src/main/scala/featherbed/Client.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,27 @@ import java.nio.charset.{Charset, StandardCharsets}

import com.twitter.finagle._
import com.twitter.finagle.builder.ClientBuilder
import http.RequestBuilder
import featherbed.auth.Authorizer
import http.{Request, RequestBuilder, Response}
import shapeless.Coproduct

/**
* A REST client with a given base URL.
*/
class Client(
case class Client(
baseUrl: URL,
charset: Charset = StandardCharsets.UTF_8
charset: Charset = StandardCharsets.UTF_8,
filters: Filter[Request, Response, Request, Response] = Filter.identity[Request, Response]
) extends request.RequestTypes with request.RequestBuilding {

def addFilter(filter: Filter[Request, Response, Request, Response]): Client =
copy(filters = filter andThen filters)

def setFilter(filter: Filter[Request, Response, Request, Response]): Client =
copy(filters = filter)

def authorized(authorizer: Authorizer): Client = setFilter(filters andThen authorizer)

/**
* Specify a GET request to be performed against the given resource
* @param relativePath The path to the resource, relative to the baseUrl
Expand All @@ -25,7 +35,8 @@ class Client(
GetRequest[Coproduct.`"*/*"`.T](
baseUrl.toURI.resolve(relativePath).toURL,
List.empty,
charset
charset,
filters
)

/**
Expand All @@ -38,7 +49,8 @@ class Client(
baseUrl.toURI.resolve(relativePath).toURL,
None,
List.empty,
charset
charset,
filters
)

/**
Expand All @@ -51,7 +63,8 @@ class Client(
baseUrl.toURI.resolve(relativePath).toURL,
None,
List.empty,
charset
charset,
filters
)

/**
Expand All @@ -60,15 +73,15 @@ class Client(
* @return A [[HeadRequest]] object, which can further specify and send the request
*/
def head(relativePath: String): HeadRequest =
HeadRequest(baseUrl.toURI.resolve(relativePath).toURL, List.empty)
HeadRequest(baseUrl.toURI.resolve(relativePath).toURL, List.empty, charset, filters)

/**
* Specify a DELETE request to be performed against the given resource
* @param relativePath The path to the resource, relative to the baseUrl
* @return A [[DeleteRequest]] object, which can further specify and send the request
*/
def delete(relativePath: String): DeleteRequest[Coproduct.`"*/*"`.T] =
DeleteRequest[Coproduct.`"*/*"`.T](baseUrl.toURI.resolve(relativePath).toURL, List.empty)
DeleteRequest[Coproduct.`"*/*"`.T](baseUrl.toURI.resolve(relativePath).toURL, List.empty, charset, filters)

/**
* Close this client releasing allocated resources.
Expand All @@ -78,9 +91,11 @@ class Client(

protected def clientTransform(client: Http.Client): Http.Client = client

protected val client = clientTransform(Client.forUrl(baseUrl))
protected lazy val client =
clientTransform(Client.forUrl(baseUrl))

protected[featherbed] val httpClient = client.newService(Client.hostAndPort(baseUrl))
protected[featherbed] lazy val httpClient =
client.newService(Client.hostAndPort(baseUrl))
}

object Client {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package featherbed.auth

import com.twitter.finagle.Filter
import com.twitter.finagle.http.{Request, Response}

trait Authorizer extends Filter[Request, Response, Request, Response]
123 changes: 123 additions & 0 deletions featherbed-core/src/main/scala/featherbed/auth/OAuth2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package featherbed.auth

import java.nio.charset.{Charset, StandardCharsets}
import java.security.MessageDigest
import java.time.Instant
import java.util.{Base64, UUID}

import com.twitter.finagle.Service
import com.twitter.finagle.http.{Request, Response}
import com.twitter.util.Future
import javax.crypto.spec.SecretKeySpec

object OAuth2 {

/**
* RFC 6750 - OAuth2 Bearer Token
* https://tools.ietf.org/html/rfc6750
*
* @param token The OAuth2 Bearer Token
*/
case class Bearer(token: String) extends Authorizer {
def apply(
request: Request,
service: Service[Request, Response]
): Future[Response] = {
request.authorization = s"Bearer $token"
service(request)
}
}

/**
* IETF Draft for OAuth2 MAC Tokens
* https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-02
*
* @param keyIdentifier The MAC Key Identifier
* @param macKey The MAC Secret Key
* @param algorithm The MAC Algorithm (Mac.Sha1 or Mac.SHA256)
* @param ext A function which computes some "extension text" to be covered by the MAC signature
*/
case class Mac(
keyIdentifier: String,
macKey: String,
algorithm: Mac.Algorithm,
ext: Request => Option[String] = (req) => None
) extends Authorizer {

import Mac._

def apply(
request: Request,
service: Service[Request, Response]
): Future[Response] = {
val keyBytes = macKey.getBytes(requestCharset(request))
val timestamp = Instant.now()
val nonce = UUID.randomUUID().toString
val signature = sign(
keyBytes, algorithm, request, timestamp, nonce, ext
)
val authFields = List(
"id" -> keyIdentifier,
"timestamp" -> timestamp.getEpochSecond.toString,
"nonce" -> nonce,
"mac" -> Base64.getEncoder.encodeToString(signature)
) ++ List(ext(request).map("ext" -> _)).flatten

val auth = "MAC " + authFields.map {
case (key, value) => s""""$key"="$value""""
}.mkString(", ")
request.authorization = auth
service(request)
}
}

object Mac {
sealed trait Algorithm {
def name: String
}
case object Sha1 extends Algorithm { val name = "HmacSHA1" }
case object Sha256 extends Algorithm { val name = "HmacSHA256" }

private def requestCharset(request: Request) =
request.charset.map(Charset.forName).getOrElse(StandardCharsets.UTF_8)

private def sign(
key: Array[Byte],
algorithm: Mac.Algorithm,
request: Request,
timestamp: Instant,
nonce: String,
ext: Request => Option[String]
) = {
val stringToSign = normalizedRequestString(request, timestamp, nonce, ext)
val signingKey = new SecretKeySpec(key, algorithm.name)
val mac = javax.crypto.Mac.getInstance(algorithm.name)
mac.init(signingKey)
mac.doFinal(stringToSign.getBytes(requestCharset(request)))
}

private def normalizedRequestString(
request: Request,
timestamp: Instant,
nonce: String,
ext: Request => Option[String]
) = {
val hostAndPort = request.host.map(_.span(_ == ':')).map {
case (h, p) => h -> Option(p.stripPrefix(":")).filter(_.nonEmpty)
}
val host = hostAndPort.map(_._1)
val port = hostAndPort.flatMap(_._2)
Seq(
timestamp.getEpochSecond.toString,
nonce,
request.method.toString().toUpperCase,
request.uri,
host.getOrElse(""),
port.getOrElse(request.remotePort.toString),
ext(request).getOrElse(""),
""
).mkString("\n")
}
}

}
Loading

0 comments on commit c905297

Please sign in to comment.