forked from akka/alpakka
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Helper class to sign and populate Authorization header akka#3253
1. Populates required date and version header 2. Reads current request and create authorization token as per AWS requirements 3. Populates authorization header
- Loading branch information
Showing
1 changed file
with
102 additions
and
0 deletions.
There are no files selected for viewing
102 changes: 102 additions & 0 deletions
102
azure-storage/src/main/scala/akka/stream/alpakka/azure/storage/impl/auth/Signer.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
/* | ||
* Copyright (C) since 2016 Lightbend Inc. <https://www.lightbend.com> | ||
*/ | ||
|
||
package akka.stream.alpakka | ||
package azure | ||
package storage | ||
package impl.auth | ||
|
||
import akka.NotUsed | ||
import akka.http.scaladsl.model.{HttpRequest, Uri} | ||
import akka.http.scaladsl.model.headers._ | ||
import akka.stream.scaladsl.Source | ||
|
||
import java.util.Base64 | ||
import javax.crypto.Mac | ||
import javax.crypto.spec.SecretKeySpec | ||
|
||
/** Takes initial request and add signed `Authorization` header and essential XMS headers. | ||
* | ||
* @param initialRequest | ||
* - Initial HTTP request without authorization and XMS headers | ||
* @param settings | ||
* - Storage settings | ||
*/ | ||
final case class Signer(initialRequest: HttpRequest, settings: StorageSettings) { | ||
|
||
private val credential = settings.azureNameKeyCredential | ||
private val requestWithHeaders = | ||
initialRequest | ||
.addHeader(RawHeader(XmsDateHeaderKey, getFormattedDate)) | ||
.addHeader(RawHeader(XmsVersionHeaderKey, settings.apiVersion)) | ||
|
||
private val mac = { | ||
val mac = Mac.getInstance(settings.algorithm) | ||
mac.init(new SecretKeySpec(credential.accountKey, settings.algorithm)) | ||
mac | ||
} | ||
|
||
def signedRequest: Source[HttpRequest, NotUsed] = | ||
if (settings.authorizationType == "anon") Source.single(requestWithHeaders) | ||
else { | ||
val contentLengthValue = getHeaderValue(`Content-Length`.name, "0") | ||
val contentLength = if (contentLengthValue == "0") "" else contentLengthValue | ||
|
||
val headersToSign = Seq( | ||
requestWithHeaders.method.value.toUpperCase, | ||
getHeaderValue(`Content-Encoding`.name, ""), | ||
getHeaderValue("Content-Language", ""), | ||
contentLength, | ||
getHeaderValue("Content-MD5", ""), | ||
getHeaderValue(`Content-Type`.name, ""), | ||
"", // date, we are using `x-ms-date` instead | ||
getHeaderValue(`If-Modified-Since`.name, ""), | ||
getHeaderValue(`If-Match`.name, ""), | ||
getHeaderValue(`If-None-Match`.name, ""), | ||
getHeaderValue(`If-Unmodified-Since`.name, ""), | ||
getHeaderValue(Range.name, "") | ||
) ++ getAdditionalXmsHeaders ++ getCanonicalizedResource | ||
|
||
val signature = sign(headersToSign) | ||
|
||
Source.single( | ||
requestWithHeaders.addHeader( | ||
RawHeader(AuthorizationHeaderKey, s"${settings.authorizationType} ${credential.accountName}:$signature") | ||
) | ||
) | ||
} | ||
|
||
private def getHeaderOptionalValue(headerName: String) = | ||
requestWithHeaders.headers.collectFirst { | ||
case header if header.name() == headerName => header.value() | ||
} | ||
|
||
private def getHeaderValue(headerName: String, defaultValue: String) = | ||
getHeaderOptionalValue(headerName).getOrElse(defaultValue) | ||
|
||
private def getAdditionalXmsHeaders = | ||
requestWithHeaders.headers.filter(header => header.name().startsWith("x-ms-")).sortBy(_.name()).map { header => | ||
s"${header.name()}:${header.value()}" | ||
} | ||
|
||
private def getCanonicalizedResource = { | ||
val uri = requestWithHeaders.uri | ||
val resourcePath = s"/${credential.accountName}${uri.path.toString()}" | ||
|
||
val queries = | ||
uri.queryString() match { | ||
case Some(queryString) => | ||
Uri.Query(queryString).toMap.toSeq.sortBy(_._1).map { | ||
case (key, value) => s"$key:$value" | ||
} | ||
|
||
case None => Seq.empty[String] | ||
} | ||
|
||
Seq(resourcePath) ++ queries | ||
} | ||
|
||
private def sign(headersToSign: Seq[String]) = | ||
Base64.getEncoder.encodeToString(mac.doFinal(headersToSign.mkString(NewLine).getBytes)) | ||
} |