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 Azure CLI token authentication option #71

Draft
wants to merge 17 commits into
base: master
Choose a base branch
from
Draft
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -263,3 +263,6 @@ __pycache__/
.RHistory
misc/
.Rproj.user
.vscode/

.Renviron
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package: AzureAuth
Title: Authentication Services for Azure Active Directory
Version: 1.3.3
Version: 1.4.0
Authors@R: c(
person("Hong", "Ooi", , "[email protected]", role = c("aut", "cre")),
person("Tyler", "Littlefield", role="ctb"),
Expand Down
5 changes: 5 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# AzureAuth 1.4.0

- Add new CLI auth type, `get_azure_token(auth_type="cli")` to use the Azure CLI
to retrieve a user token.

# AzureAuth 1.3.3

- Documentation update only:
Expand Down
9 changes: 7 additions & 2 deletions R/AzureToken.R
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public=list(
if(is.null(self$credentials))
{
res <- private$initfunc(auth_info)
self$credentials <- process_aad_response(res)
self$credentials <- private$process_response(res)
}
private$set_expiry_time(request_time)

Expand Down Expand Up @@ -126,7 +126,7 @@ public=list(
}
else private$initfunc() # reauthenticate if no refresh token (cannot reuse any supplied creds)

creds <- try(process_aad_response(res))
creds <- try(private$process_response(res))
if(inherits(creds, "try-error"))
{
delete_azure_token(hash=self$hash(), confirm=FALSE)
Expand Down Expand Up @@ -216,6 +216,11 @@ private=list(
list(resource=self$resource)
else list(scope=paste_v2_scopes(self$scope))
)
},

process_response = function(res)
{
process_aad_response(res)
}
))

51 changes: 51 additions & 0 deletions R/classes.R
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,57 @@ private=list(
))


#' @rdname AzureToken
#' @export
AzureTokenCLI <- R6::R6Class("AzureTokenCLI",
inherit = AzureToken,
public = list(
initialize = function(common_args)
{
self$auth_type <- "cli"
do.call(super$initialize, common_args)
}
),
private = list(
initfunc = function(init_args)
{
tryCatch(
{
cmd <- build_az_token_cmd(
resource = self$resource,
tenant = self$tenant
)
result <- execute_cmd(cmd)
# result is a multi-line JSON string, concatenate together
paste0(result)
},
warning = function(cond)
{
not_found <- grepl("not found", cond, fixed = TRUE)
not_loggedin <- grepl("az login", cond, fixed = TRUE) |
grepl("az account set", cond, fixed = TRUE)
bad_resource <- grepl(
"was not found in the tenant",
cond,
fixed = TRUE
)
if (not_found)
message("Azure CLI not found on path.")
else if (not_loggedin)
message("Please run 'az login' to set up account.")
else
message("Failed to invoke the Azure CLI.")
}
)
},
process_response = function(res)
{
process_cli_response(res, self$resource)
}
)
)


norenew_alert <- function(version)
{
if(version == 1)
Expand Down
27 changes: 24 additions & 3 deletions R/token.R
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@
#'
#' }
#' @export
get_azure_token <- function(resource, tenant, app, password=NULL, username=NULL, certificate=NULL, auth_type=NULL,
get_azure_token <- function(resource=NULL, tenant=NULL, app=NULL, password=NULL, username=NULL, certificate=NULL, auth_type=NULL,
aad_host="https://login.microsoftonline.com/", version=1,
authorize_args=list(), token_args=list(),
use_cache=NULL, on_behalf_of=NULL, auth_code=NULL, device_creds=NULL)
Expand Down Expand Up @@ -271,6 +271,8 @@ get_azure_token <- function(resource, tenant, app, password=NULL, username=NULL,
AzureTokenOnBehalfOf$new(common_args, on_behalf_of),
resource_owner=
AzureTokenResOwner$new(common_args),
cli=
AzureTokenCLI$new(common_args),
stop("Unknown authentication method ", auth_type, call.=FALSE))
}

Expand All @@ -279,7 +281,7 @@ get_azure_token <- function(resource, tenant, app, password=NULL, username=NULL,
#' @param confirm For `delete_azure_token`, whether to prompt for confirmation before deleting a token.
#' @rdname get_azure_token
#' @export
delete_azure_token <- function(resource, tenant, app, password=NULL, username=NULL, certificate=NULL, auth_type=NULL,
delete_azure_token <- function(resource=NULL, tenant=NULL, app=NULL, password=NULL, username=NULL, certificate=NULL, auth_type=NULL,
aad_host="https://login.microsoftonline.com/", version=1,
authorize_args=list(), token_args=list(), on_behalf_of=NULL,
hash=NULL, confirm=TRUE)
Expand Down Expand Up @@ -344,7 +346,7 @@ list_azure_tokens <- function()

#' @rdname get_azure_token
#' @export
token_hash <- function(resource, tenant, app, password=NULL, username=NULL, certificate=NULL, auth_type=NULL,
token_hash <- function(resource=NULL, tenant=NULL, app=NULL, password=NULL, username=NULL, certificate=NULL, auth_type=NULL,
aad_host="https://login.microsoftonline.com/", version=1,
authorize_args=list(), token_args=list(), on_behalf_of=NULL)
{
Expand Down Expand Up @@ -411,3 +413,22 @@ is_azure_v2_token <- function(object)
{
is_azure_token(object) && object$version == 2
}

#' @rdname az_login
#' @export
az_login <- function(...)
{
args <- list(...)
cmdargs <- list(command = "az", args = c("login"))
for (arg in names(args))
{
argval <- args[[arg]]
# CLI expects dashes, not underscores
argkey <- gsub("_", "-", arg)
if (is.logical(argval))
cmdargs$args <- c(cmdargs$args, paste0("--", argkey))
else
cmdargs$args <- c(cmdargs$args, paste0("--", argkey, " ", argval))
}
execute_cmd(cmdargs)
}
77 changes: 75 additions & 2 deletions R/utils.R
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ select_auth_type <- function(password, username, certificate, auth_type, on_beha
if(!is.null(auth_type))
{
if(!auth_type %in%
c("authorization_code", "device_code", "client_credentials", "resource_owner", "on_behalf_of",
"managed"))
c("authorization_code", "device_code", "client_credentials",
"resource_owner", "on_behalf_of", "managed", "cli"))
stop("Invalid authentication method")
return(auth_type)
}
Expand Down Expand Up @@ -59,6 +59,20 @@ process_aad_response <- function(res)
else httr::content(res)
}

process_cli_response <- function(res, resource)
{
# Parse the JSON from the CLI and fix the names to snake_case
ret <- jsonlite::parse_json(res)
tok_data <- list(
token_type = ret$tokenType,
access_token = ret$accessToken,
expires_on = as.numeric(as.POSIXct(ret$expiresOn))
)
# CLI doesn't return resource identifier so we need to pass it through
if (!missing(resource)) tok_data$resource <- resource
return(tok_data)
}


# need to capture bad scopes before requesting auth code
# v2.0 endpoint will show error page rather than redirecting, causing get_azure_token to wait forever
Expand Down Expand Up @@ -147,3 +161,62 @@ in_shiny <- function()
{
("shiny" %in% loadedNamespaces()) && shiny::isRunning()
}

build_az_token_cmd <- function(command = "az", resource, tenant)
{
args <- c("account", "get-access-token", "--output json")
if (!missing(resource)) args <- c(args, paste("--resource", resource))
if (!missing(tenant)) args <- c(args, paste("--tenant", tenant))
list(command = command, args = args)
}

handle_az_cmd_errors <- function(cond)
{
not_loggedin <- grepl("az login", cond, fixed = TRUE) |
grepl("az account set", cond, fixed = TRUE)
not_found <- grepl("not found", cond, fixed = TRUE)
error_in <- grepl("error in running", cond, fixed = TRUE)

if (not_found | error_in)
{
msg <- paste("az is not installed or not in PATH.\n",
"Please see: ",
"https://learn.microsoft.com/en-us/cli/azure/install-azure-cli\n",
"for installation instructions."
)
stop(msg)
}
else if (not_loggedin)
{
stop("You are not logged into the Azure CLI.
Please call AzureAuth::az_login()
or run 'az login' from your shell and try again.")
}
else
{
# Other misc errors, pass through the CLI error message
message("Failed to invoke the Azure CLI.")
stop(cond)
}
}

execute_cmd <- function(cmd)
{
tryCatch(
{
cat(cmd$command, paste(cmd$args), "\n")
result <- do.call(system2, append(cmd, list(stdout = TRUE)))
# result is a multi-line JSON string, concatenate together
paste0(result)
},
warning = function()
{
# if an error case, catch it, pass the error string and handle it
handle_az_cmd_errors(result)
},
error = function(cond)
{
handle_az_cmd_errors(cond$message)
}
)
}
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,15 @@ tok2 <- get_azure_token("resource2", "mytenant," "serviceapp_id",
password="serviceapp_secret", auth_type="on_behalf_of", on_behalf_of=tok0)
```

6. The **cli** method uses the
[Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli)
command `az account get-access-token` to retrieve an auth token. It is mostly
useful for interactive programming.

```r
get_azure_token(auth_type="cli")
```

If you don't specify the method, `get_azure_token` makes a best guess based on the presence or absence of the other authentication arguments, and whether httpuv is installed.

### Managed identities
Expand Down
Loading