-
Notifications
You must be signed in to change notification settings - Fork 59
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add AWS request signing. Fixes #566 * Generalise OAuth signing to any pre-request signature. Fixes #562
- Loading branch information
Showing
14 changed files
with
415 additions
and
23 deletions.
There are no files selected for viewing
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 |
---|---|---|
|
@@ -38,6 +38,7 @@ Suggests: | |
jsonlite, | ||
knitr, | ||
later, | ||
paws.common, | ||
promises, | ||
rmarkdown, | ||
testthat (>= 3.1.8), | ||
|
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
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
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
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
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,217 @@ | ||
#' Sign a request with the AWS SigV4 signing protocol | ||
#' | ||
#' This is a custom auth protocol implemented by AWS. | ||
#' | ||
#' @inheritParams req_perform | ||
#' @param aws_access_key_id,aws_secret_access_key AWS key and secret. | ||
#' @param aws_session_token AWS session token, if required. | ||
#' @param aws_service,aws_region The AWS service and region to use for the | ||
#' request. If not supplied, will be automatically parsed from the URL | ||
#' hostname. | ||
#' @export | ||
#' @examplesIf httr2:::has_paws_credentials() | ||
#' creds <- paws.common::locate_credentials() | ||
#' model_id <- "anthropic.claude-3-5-sonnet-20240620-v1:0" | ||
#' req <- request("https://bedrock-runtime.us-east-1.amazonaws.com") | ||
#' # https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_Converse.html | ||
#' req <- req_url_path_append(req, "model", model_id, "converse") | ||
#' req <- req_body_json(req, list( | ||
#' messages = list(list( | ||
#' role = "user", | ||
#' content = list(list(text = "What's your name?")) | ||
#' )) | ||
#' )) | ||
#' req <- req_auth_aws_v4( | ||
#' req, | ||
#' aws_access_key_id = creds$access_key_id, | ||
#' aws_secret_access_key = creds$secret_access_key, | ||
#' aws_session_token = creds$session_token | ||
#' ) | ||
#' resp <- req_perform_connection(req) | ||
#' str(resp_body_json(resp)) | ||
req_auth_aws_v4 <- function(req, | ||
aws_access_key_id, | ||
aws_secret_access_key, | ||
aws_session_token = NULL, | ||
aws_service = NULL, | ||
aws_region = NULL) { | ||
|
||
check_request(req) | ||
check_string(aws_access_key_id) | ||
check_string(aws_secret_access_key) | ||
check_string(aws_session_token, allow_null = TRUE) | ||
check_string(aws_service, allow_null = TRUE) | ||
check_string(aws_region, allow_null = TRUE) | ||
|
||
req_auth_sign(req, | ||
fun = auth_aws_sign, | ||
params = list( | ||
aws_access_key_id = aws_access_key_id, | ||
aws_secret_access_key = aws_secret_access_key, | ||
aws_session_token = aws_session_token, | ||
aws_service = aws_service, | ||
aws_region = aws_region | ||
) | ||
) | ||
} | ||
|
||
auth_aws_sign <- function(req, | ||
aws_access_key_id, | ||
aws_secret_access_key, | ||
aws_session_token = NULL, | ||
aws_service = NULL, | ||
aws_region = NULL, | ||
reauth = FALSE) { | ||
|
||
current_time <- Sys.time() | ||
|
||
body_sha256 <- openssl::sha256(req_body_get(req)) | ||
|
||
# We begin by adding some necessary headers that must be added before | ||
# canoncalization even thought they aren't documented until later | ||
req <- req_aws_headers(req, | ||
current_time = current_time, | ||
aws_session_token = aws_session_token, | ||
body_sha256 = body_sha256 | ||
) | ||
|
||
signature <- aws_v4_signature( | ||
method = req_method_get(req), | ||
url = url_parse(req$url), | ||
headers = req$headers, | ||
body_sha256 = body_sha256, | ||
current_time = current_time, | ||
aws_service = aws_service, | ||
aws_region = aws_region, | ||
aws_access_key_id = aws_access_key_id, | ||
aws_secret_access_key = aws_secret_access_key | ||
) | ||
req_headers(req, Authorization = signature$Authorization) | ||
} | ||
|
||
|
||
req_aws_headers <- function(req, current_time, aws_session_token, body_sha256) { | ||
RequestDateTime <- format(current_time, "%Y%m%dT%H%M%SZ", tz = "UTC") | ||
|
||
req_headers( | ||
req, | ||
"x-amz-date" = RequestDateTime, | ||
"x-amz-security-token" = aws_session_token, | ||
.redact = "x-amz-security-token" | ||
) | ||
} | ||
|
||
# https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html | ||
aws_v4_signature <- function(method, | ||
url, | ||
headers, | ||
body_sha256, | ||
aws_access_key_id, | ||
aws_secret_access_key, | ||
current_time = Sys.time(), | ||
aws_service = NULL, | ||
aws_region = NULL) { | ||
|
||
if (is.null(aws_service) || is.null(aws_region)) { | ||
host <- strsplit(url$hostname, ".", fixed = TRUE)[[1]] | ||
aws_service <- aws_service %||% strsplit(host[[1]], "-", fixed = TRUE)[[1]][[1]] | ||
aws_region <- aws_region %||% host[[2]] | ||
} | ||
|
||
# 1. Create a canonical request | ||
# https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html#create-canonical-request | ||
HTTPMethod <- method | ||
CanonicalURI <- curl::curl_escape(url$path %||% "/") | ||
# AWS does not want / to be encoded here | ||
CanonicalURI <- gsub("%2F", "/", CanonicalURI, fixed = TRUE) | ||
|
||
if (is.null(url$query)) { | ||
CanonicalQueryString <- "" | ||
} else { | ||
sorted_query <- url$query[order(names(url$query))] | ||
CanonicalQueryString <- query_build(CanonicalQueryString) | ||
} | ||
|
||
headers$host <- url$hostname | ||
names(headers) <- tolower(names(headers)) | ||
headers <- headers[order(names(headers))] | ||
headers[] <- trimws(headers) | ||
headers[] <- gsub(" {2,}", " ", headers) | ||
CanonicalHeaders <- paste0(names(headers), ":", headers, "\n", collapse = "") | ||
SignedHeaders <- paste0(names(headers), collapse = ";") | ||
|
||
CanonicalRequest <- paste0( | ||
HTTPMethod, "\n", | ||
CanonicalURI, "\n", | ||
CanonicalQueryString, "\n", | ||
CanonicalHeaders, "\n", | ||
SignedHeaders, "\n", | ||
body_sha256 | ||
) | ||
# 2. Create the hash of the canonical request | ||
# https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html | ||
HashedCanonicalRequest <- openssl::sha256(CanonicalRequest) | ||
|
||
# 3. Create the string to sign | ||
# https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html#create-string-to-sign | ||
|
||
Algorithm <- "AWS4-HMAC-SHA256" | ||
RequestDateTime <- format(current_time, "%Y%m%dT%H%M%SZ", tz = "UTC") | ||
Date <- format(current_time, "%Y%m%d", tz = "UTC") | ||
CredentialScope <- file.path(Date, aws_region, aws_service, "aws4_request") | ||
|
||
string_to_sign <- paste0( | ||
Algorithm, "\n", | ||
RequestDateTime, "\n", | ||
CredentialScope, "\n", | ||
HashedCanonicalRequest | ||
) | ||
|
||
# 4. Derive a signing key | ||
# https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html#derive-signing-key | ||
|
||
DateKey <- hmac_sha256(paste0("AWS4", aws_secret_access_key), Date) | ||
DateRegionKey <- hmac_sha256(DateKey, aws_region) | ||
DateRegionServiceKey <- hmac_sha256(DateRegionKey, aws_service) | ||
SigningKey <- hmac_sha256(DateRegionServiceKey, "aws4_request") | ||
|
||
# 5. Calculate signature | ||
# https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html#calculate-signature | ||
|
||
signature <- hmac_sha256(SigningKey, string_to_sign) | ||
signature <- paste0(as.character(signature), collapse = "") | ||
|
||
# 6. Add the signature to the request | ||
# https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html#calculate-signature | ||
credential <- file.path(aws_access_key_id, CredentialScope) | ||
|
||
Authorization <- paste0( | ||
Algorithm, ",", | ||
"Credential=", credential, ",", | ||
"SignedHeaders=", SignedHeaders, ",", | ||
"Signature=", signature | ||
) | ||
|
||
list( | ||
CanonicalRequest = CanonicalRequest, | ||
string_to_sign = string_to_sign, | ||
SigningKey = SigningKey, | ||
Authorization = Authorization | ||
) | ||
} | ||
|
||
hmac_sha256 <- function(key, value) { | ||
openssl::sha256(charToRaw(value), key) | ||
} | ||
|
||
has_paws_credentials <- function() { | ||
tryCatch( | ||
{ | ||
paws.common::locate_credentials() | ||
TRUE | ||
}, | ||
error = function(e) { | ||
FALSE | ||
} | ||
) | ||
} |
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,20 @@ | ||
|
||
req_auth_sign <- function(req, fun, params) { | ||
req_policies(req, | ||
auth_sign = list( | ||
fun = fun, | ||
params = params | ||
) | ||
) | ||
} | ||
auth_sign <- function(req, reauth = FALSE) { | ||
if (!req_policy_exists(req, "auth_sign")) { | ||
return(req) | ||
} | ||
|
||
exec(req$policies$auth_sign$fun, | ||
req = req, | ||
reauth = reauth, | ||
!!!req$policies$auth_sign$params | ||
) | ||
} |
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
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
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
Oops, something went wrong.