Skip to content

Commit

Permalink
feat(auth): properly extend oauth scopes and credentials
Browse files Browse the repository at this point in the history
  • Loading branch information
imathews committed Feb 1, 2025
1 parent 3cce86b commit 6bd3feb
Show file tree
Hide file tree
Showing 9 changed files with 113 additions and 25 deletions.
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Package: redivis
Type: Package
Title: R interface for Redivis
Version: 0.8.4
Version: 0.8.5
Author: Redivis Inc.
Maintainer: Redivis <[email protected]>
Description: Supports working with Redivis API through R.
Expand Down
2 changes: 1 addition & 1 deletion R/Dataset.R
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ Dataset <- setRefClass("Dataset",
if (res$status == 404){
return(FALSE)
} else {
stop(res$message)
stop(str_interp("${res$error}: ${res$error_description}"))
}
} else {
return(TRUE)
Expand Down
2 changes: 1 addition & 1 deletion R/Table.R
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Table <- setRefClass("Table",
if (res$status == 404){
return(FALSE)
} else {
stop(res$error)
stop(str_interp("${res$error}: ${res$error_description}"))
}
} else {
return(TRUE)
Expand Down
2 changes: 1 addition & 1 deletion R/Upload.R
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Upload <- setRefClass("Upload",
if (res$status == 404){
return(FALSE)
} else {
stop(res$error)
stop(str_interp("${res$error}: ${res$error_description}"))
}
} else {
return(TRUE)
Expand Down
2 changes: 1 addition & 1 deletion R/Variable.R
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Variable <- setRefClass("Variable",
if (res$status == 404){
return(FALSE)
} else {
stop(res$error)
stop(str_interp("${res$error}: ${res$error_description}"))
}
} else {
return(TRUE)
Expand Down
2 changes: 1 addition & 1 deletion R/Workflow.R
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Workflow <- setRefClass("Workflow",
if (res$status == 404){
return(FALSE)
} else {
stop(res$message)
stop(str_interp("${res$error}: ${res$error_description}"))
}
} else {
return(TRUE)
Expand Down
56 changes: 50 additions & 6 deletions R/api_request.R
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,22 @@ make_request <- function(method='GET', query=NULL, payload = NULL, parse_respons
if (res_data$status_code >= 400){
if (res_data$status_code == 401 && is.na(Sys.getenv("REDIVIS_API_TOKEN", unset=NA)) && is.na(Sys.getenv("REDIVIS_NOTEBOOK_JOB_ID", unset=NA))){
refresh_credentials()
return(make_request(method, query, payload, parse_response, path, download_path, download_overwrite, as_stream, headers_callback, get_download_path_callback, stream_callback, stop_on_error))
return(
make_request(
method=method,
query=query,
payload=payload,
parse_response=parse_response,
path=path,
download_path=download_path,
download_overwrite=download_overwrite,
as_stream=as_stream,
headers_callback=headers_callback,
get_download_path_callback=get_download_path_callback,
stream_callback=stream_callback,
stop_on_error=stop_on_error
)
)
}
if (stop_on_error){
stop(str_interp("Received HTTP status ${res_data$status_code} for path ${url}"))
Expand Down Expand Up @@ -86,11 +101,6 @@ make_request <- function(method='GET', query=NULL, payload = NULL, parse_respons
warning(httr::headers(res)$'x-redivis-warning')
}

if (httr::status_code(res) == 401 && is.na(Sys.getenv("REDIVIS_API_TOKEN", unset=NA)) && is.na(Sys.getenv("REDIVIS_NOTEBOOK_JOB_ID", unset=NA))){
refresh_credentials()
return(make_request(method, query, payload, parse_response, path, download_path, download_overwrite, as_stream, get_download_path_callback, stream_callback, stop_on_error))
}

if (!parse_response && httr::status_code(res) < 400){
return(res)
}
Expand All @@ -111,6 +121,40 @@ make_request <- function(method='GET', query=NULL, payload = NULL, parse_respons
}
}

if (
(
httr::status_code(res) == 401
|| (httr::status_code(res) == 403 && response_content$error == 'insufficient_scope')
)
&& is.na(Sys.getenv("REDIVIS_API_TOKEN", unset=NA))
&& is.na(Sys.getenv("REDIVIS_NOTEBOOK_JOB_ID", unset=NA))
){
message(
str_interp("\n${response_content$error}: ${response_content$error_description}\n")
)
flush.console()
refresh_credentials(
scope=if(is.null(response_content$scope)) NULL else strsplit(response_content$scope, " "),
amr_values=response_content$amr_values
)
return(
make_request(
method=method,
query=query,
payload=payload,
parse_response=parse_response,
path=path,
download_path=download_path,
download_overwrite=download_overwrite,
as_stream=as_stream,
headers_callback=headers_callback,
get_download_path_callback=get_download_path_callback,
stream_callback=stream_callback,
stop_on_error=stop_on_error
)
)
}

if (httr::status_code(res) >= 400 && stop_on_error){
if (is_json){
stop(str_interp("${response_content$error}_error: ${response_content$error_description}"))
Expand Down
63 changes: 52 additions & 11 deletions R/auth.R
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@ auth_vars <- new.env(parent = emptyenv())
auth_vars$redivis_dir = file.path(Sys.getenv("HOME"), ".redivis")
auth_vars$cached_credentials = NULL
auth_vars$verify_ssl = !grepl("https://localhost", Sys.getenv("REDIVIS_API_ENDPOINT", "https://redivis.com"), fixed = TRUE)
auth_vars$credentials_file = file.path(file.path(Sys.getenv("HOME"), ".redivis"), "credentials")
auth_vars$scope = 'data.edit'
auth_vars$credentials_file = file.path(file.path(Sys.getenv("HOME"), ".redivis"), "r_credentials")
auth_vars$default_scope = list('data.edit')
auth_vars$client_id = 'Ah850nGnQg5mFWd25nkyk9Y3'
auth_vars$base_url = sub("(https?://.*?)(/|$).*", "\\1", Sys.getenv('REDIVIS_API_ENDPOINT', 'https://redivis.com'))

get_auth_token <- function() {
get_auth_token <- function(scope=NULL) {

if (is.null(scope)){
scope <- auth_vars$default_scope
}

if (!is.na(Sys.getenv("REDIVIS_API_TOKEN", unset=NA))) {
if (is.na(Sys.getenv("REDIVIS_NOTEBOOK_JOB_ID", unset=NA)) && interactive()) {
warning("Setting the REDIVIS_API_TOKEN for interactive sessions is deprecated and highly discouraged.
Expand All @@ -32,7 +37,14 @@ This environment variable should only ever be set in a non-interactive environme
})
}

if (!is.null(auth_vars$cached_credentials) && "expires_at" %in% names(auth_vars$cached_credentials) && "access_token" %in% names(auth_vars$cached_credentials)) {
missing_scope = setdiff(scope, get_current_credential_scope())

if (
!is.null(auth_vars$cached_credentials)
&& "expires_at" %in% names(auth_vars$cached_credentials)
&& "access_token" %in% names(auth_vars$cached_credentials)
&& length(missing_scope) == 0
) {
if (auth_vars$cached_credentials$expires_at < (as.numeric(Sys.time()) - 5 * 60)) {
refresh_credentials()
}
Expand All @@ -43,8 +55,7 @@ This environment variable should only ever be set in a non-interactive environme
dir.create(auth_vars$redivis_dir)
}

auth_vars$cached_credentials <- perform_oauth_login()
write(jsonlite::toJSON(auth_vars$cached_credentials, pretty = TRUE, auto_unbox=TRUE), auth_vars$credentials_file)
perform_oauth_login(scope=if(length(missing_scope)) missing_scope else scope, upgrade_credentials=length(missing_scope) > 0)
return(auth_vars$cached_credentials$access_token)
}
}
Expand All @@ -56,7 +67,7 @@ clear_cached_credentials <- function() {
}
}

perform_oauth_login <- function() {
perform_oauth_login <- function(scope, amr_values=NULL, upgrade_credentials=FALSE) {
pkce <- get_pkce()
challenge <- pkce$challenge
verifier <- pkce$verifier
Expand All @@ -66,7 +77,8 @@ perform_oauth_login <- function() {
httr::add_headers(`Content-Type` = "application/json"),
body = list(
client_id = auth_vars$client_id,
scope = auth_vars$scope,
scope = paste(scope, collapse=" "),
amr_values = amr_values,
code_challenge = challenge,
code_challenge_method = 'S256',
access_type = 'offline'
Expand All @@ -90,6 +102,11 @@ perform_oauth_login <- function() {
}
flush.console()

headers <- c()
if (upgrade_credentials && !is.null(auth_vars$cached_credentials)){
headers <- c('Authorization' = str_interp("Bearer ${auth_vars$cached_credentials$access_token}"))
}

started_polling_at <- Sys.time()
while (TRUE) {
if (difftime(Sys.time(), started_polling_at, units = "secs") > 60 * 10) {
Expand All @@ -100,6 +117,7 @@ perform_oauth_login <- function() {

res <- httr::POST(
url = paste0(auth_vars$base_url, "/oauth/token"),
httr::add_headers(headers),
body = list(
client_id = auth_vars$client_id,
grant_type = 'urn:ietf:params:oauth:grant-type:device_code',
Expand All @@ -125,11 +143,20 @@ perform_oauth_login <- function() {
}
}

return(httr::content(res, "parsed"))
auth_vars$cached_credentials <- httr::content(res, "parsed")
write(jsonlite::toJSON(auth_vars$cached_credentials, pretty = TRUE, auto_unbox=TRUE), auth_vars$credentials_file)

return(auth_vars$cached_credentials)
}

refresh_credentials <- function() {
if (!is.null(auth_vars$cached_credentials$refresh_token)) {
refresh_credentials <- function(scope=NULL, amr_values=NULL) {
if (!is.null(scope) || !is.null(amr_values)){
perform_oauth_login(
scope=if (is.null(scope)) get_current_credential_scope() else scope,
amr_values=amr_values,
upgrade_credentials=TRUE
)
}else if (!is.null(auth_vars$cached_credentials$refresh_token)) {
res <- httr::POST(
url = paste0(auth_vars$base_url, "/oauth/token"),
body = list(
Expand Down Expand Up @@ -157,6 +184,20 @@ refresh_credentials <- function() {
return(get_auth_token())
}

get_current_credential_scope <- function() {
tryCatch({
if (!is.null(auth_vars$cached_credentials)) {
token_payload = strsplit(auth_vars$cached_credentials$access_token, ".", fixed=TRUE)[[1]][2]
decoded_token <- jsonlite::fromJSON(rawToChar(base64enc::base64decode(token_payload)))
return(strsplit(decoded_token$scope, " ")[[1]])
}
}, error = function(e) {
# Ignore errors
})

return(auth_vars$default_scope)
}

get_pkce <- function() {
verifier <- safe_encode_base64_url(charToRaw(paste(sample(c(0:9, letters, LETTERS), 64, replace = TRUE), collapse = "")))
challenge <- safe_encode_base64_url(openssl::sha256(charToRaw(verifier)))
Expand Down
7 changes: 5 additions & 2 deletions R/index.R
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,14 @@ current_notebook <- function() {
#' @return void
#' @examples
#' redivis::authenticate(force_reauthentication=FALSE)
authenticate <- function(force_reauthentication=FALSE) {
authenticate <- function(scope=NULL, force_reauthentication=FALSE) {
if (force_reauthentication){
clear_cached_credentials()
}
get_auth_token()
if (is.character(scope)){
scope <- list(scope)
}
get_auth_token(scope=scope)
invisible(NULL)
}

Expand Down

0 comments on commit 6bd3feb

Please sign in to comment.