Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add authentication framework #44

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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