From 24018e92fd8c2e2727a559a34476117561874b41 Mon Sep 17 00:00:00 2001 From: hong-revo Date: Mon, 14 Jan 2019 08:28:26 +1100 Subject: [PATCH] fully implement token caching --- NAMESPACE | 5 ++ R/AzureToken.R | 152 +++++++++++++++++++++------------- R/az_login.R | 16 +++- R/is.R | 8 ++ R/normalize.R | 96 +++++++++++++++++++++ man/AzureToken.Rd | 2 +- man/get_azure_token.Rd | 57 ++++++++++++- man/guid.Rd | 61 ++++++++++++++ man/is.Rd | 5 +- man/normalize_tenant.Rd | 31 ------- tests/testthat/test00_token.R | 43 ++++++++++ 11 files changed, 377 insertions(+), 99 deletions(-) create mode 100644 R/normalize.R create mode 100644 man/guid.Rd delete mode 100644 man/normalize_tenant.Rd create mode 100644 tests/testthat/test00_token.R diff --git a/NAMESPACE b/NAMESPACE index 764bd5e..e9acb68 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -10,20 +10,25 @@ export(call_azure_rm) export(call_azure_url) export(create_azure_login) export(delete_azure_login) +export(delete_azure_token) export(format_auth_header) export(format_public_fields) export(format_public_methods) export(get_azure_login) export(get_azure_token) +export(is_azure_login) export(is_azure_token) export(is_empty) +export(is_guid) export(is_resource) export(is_resource_group) export(is_subscription) export(is_template) export(is_url) export(list_azure_logins) +export(list_azure_tokens) export(named_list) +export(normalize_guid) export(normalize_tenant) export(refresh_azure_logins) importFrom(utils,modifyList) diff --git a/R/AzureToken.R b/R/AzureToken.R index bc0c087..ee0e5e8 100644 --- a/R/AzureToken.R +++ b/R/AzureToken.R @@ -8,7 +8,7 @@ #' - `validate`: Checks if the token is still valid. For Azure tokens using client credentials, this just checks if the current time is less than the token's expiry time. #' #' @section Caching: -#' This class never caches its tokens, unlike httr::Token2.0. +#' Unlike httr::Token2.0, caching for Azure tokens is handled outside the class. Tokens are automatically cached by the `get_azure_token` function, and can be (manually) deleted with the `delete_azure_token` function. Calling `AzureToken$new()` directly will always acquire a new token from the server. #' #' @seealso #' [get_azure_token], [httr::Token] @@ -46,7 +46,7 @@ public=list( # overrides httr::Token method: caching done outside class hash=function() { - NULL + stop("Caching not handled by AzureToken class") }, # overrides httr::Token2.0 method @@ -142,9 +142,9 @@ private=list( )) -#' Generate an Azure OAuth token +#' Manage Azure Active Directory OAuth 2.0 tokens #' -#' This extends the OAuth functionality in httr for use with Azure Active Directory (AAD). +#' These functions extend the OAuth functionality in httr for use with Azure Active Directory (AAD). #' #' @param resource_host URL for your resource host. For Resource Manager in the public Azure cloud, this is `https://management.azure.com/`. #' @param tenant Your tenant. This can be a name ("myaadtenant"), a fully qualified domain name ("myaadtenant.onmicrosoft.com" or "mycompanyname.com"), or a GUID. @@ -155,17 +155,18 @@ private=list( #' @param aad_host URL for your AAD host. For the public Azure cloud, this is `https://login.microsoftonline.com/`. #' #' @details -#' This function does much the same thing as [httr::oauth2.0_token()], but customised for Azure. +#' `get_azure_token` does much the same thing as [httr::oauth2.0_token()], but customised for Azure. It obtains an OAuth token, first by checking if a cached value exists on disk, and if not, acquiring it from the AAD server. `delete_azure_token` deletes a cached token, and `list_azure_tokens` lists currently cached tokens. #' +#' @section Authentication methods: #' The OAuth authentication type can be one of four possible values: "authorization_code", "client_credentials", "device_code", or "resource_owner". The first two are provided by the [httr::Token2.0] token class, while the last two are provided by the AzureToken class which extends httr::Token2.0. Here is a short description of these methods. #' -#' - Using the authorization_code method is a 3-step process. First, `get_azure_token` contacts the AAD authorization endpoint to obtain a temporary access code. It then contacts the AAD access endpoint, passing it the code. The access endpoint sends back a login URL which `get_azure_token` opens in your browser, where you can enter your credentials. Once this is completed, the endpoint returns the OAuth token via a HTTP redirect URI. +#' 1. Using the authorization_code method is a 3-step process. First, `get_azure_token` contacts the AAD authorization endpoint to obtain a temporary access code. It then contacts the AAD access endpoint, passing it the code. The access endpoint sends back a login URL which `get_azure_token` opens in your browser, where you can enter your credentials. Once this is completed, the endpoint returns the OAuth token via a HTTP redirect URI. #' -#' - The device_code method is similar in concept to authorization_code, but is meant for situations where you are unable to browse the Internet -- for example if you don't have a browser installed or your computer has input constraints. First, `get_azure_token` contacts the AAD devicecode endpoint, which responds with a login URL and an access code. You then visit the URL and enter the code, possibly using a different computer. Meanwhile, `get_azure_token` polls the AAD access endpoint for a token, which is provided once you have successfully entered the code. +#' 2. The device_code method is similar in concept to authorization_code, but is meant for situations where you are unable to browse the Internet -- for example if you don't have a browser installed or your computer has input constraints. First, `get_azure_token` contacts the AAD devicecode endpoint, which responds with a login URL and an access code. You then visit the URL and enter the code, possibly using a different computer. Meanwhile, `get_azure_token` polls the AAD access endpoint for a token, which is provided once you have successfully entered the code. #' -#' - The client_credentials method is much simpler than the above methods, requiring only one step. `get_azure_token` contacts the access endpoint, passing it the app secret (which you supplied in the `password` argument). Assuming the secret is valid, the endpoint then returns the OAuth token. +#' 3. The client_credentials method is much simpler than the above methods, requiring only one step. `get_azure_token` contacts the access endpoint, passing it the app secret (which you supplied in the `password` argument). Assuming the secret is valid, the endpoint then returns the OAuth token. #' -#' - The resource_owner method also requires only one step. In this method, `get_azure_token` passes your (personal) username and password to the AAD access endpoint, which validates your credentials and returns the token. +#' 4. The resource_owner method also requires only one step. In this method, `get_azure_token` passes your (personal) username and password to the AAD access endpoint, which validates your credentials and returns the token. #' #' If the authentication method is not specified, it is chosen based on the presence or absence of the `password` and `username` arguments: #' @@ -177,6 +178,20 @@ private=list( #' The httpuv package must be installed to use the authorization_code method, as this requires a web server to listen on the (local) redirect URI. See [httr::oauth2.0_token] for more information; note that Azure does not support the `use_oob` feature of the httr OAuth 2.0 token class. #' #' Similarly, since the authorization_code method opens a browser to load the AAD authorization page, your machine must have an Internet browser installed that can be run from inside R. In particular, if you are using a Linux [Data Science Virtual Machine](https://azure.microsoft.com/en-us/services/virtual-machines/data-science-virtual-machines/) in Azure, you may run into difficulties; use one of the other methods instead. +#' +#' @section Caching: +#' AzureRMR differs from httr in its handling of token caching in a number of ways. +#' +#' - It moves caching of OAuth tokens out of the token class, and into the `get_azure_token` function. Caching is based on all the inputs to `get_azure_token` as listed above. Directly calling the AzureToken class constructor will always acquire a new token from the server. +#' +#' - It defines its own directory for caching tokens, using the rappdirs package. On recent Windows versions, this will usually be in the location `C:\\Users\\(username)\\AppData\\Local\\AzureR\\AzureRMR`. On Linux, it will be in `~/.config/AzureRMR`, and on MacOS, it will be in `~/Library/Application Support/AzureRMR`. Note that a single directory is used for all tokens, unlike httr, and the working directory is not touched (which lessens the risk of accidentally introducing cached tokens into source control). +#' +#' To list all cached tokens on disk, use `list_azure_tokens`. This returns a list of token objects, named according to their MD5 hashes. +#' +#' To delete a cached token, use `delete_azure_token`. This takes the same inputs as `get_azure_token`, or you can specify the MD5 hash directly in the `hash` argument. +#' +#' @section Value: +#' For `get_azure_token`, an object of class `AzureToken` representing the AAD token. For `list_azure_tokens`, a list of such objects retrieved from disk. #' #' @seealso #' [AzureToken], [httr::oauth2.0_token], [httr::Token], @@ -220,15 +235,32 @@ private=list( #' owner_token <- get_azure_token( #' resource_host="https://myresource/", #' tenant="myaadtenant", +#' app="app_id", #' username="user", #' password="abcdefg") #' +#' # list saved tokens +#' list_azure_tokens() +#' +#' # delete a saved token from disk +#' delete_azure_token( +#' resource_host="https://myresource/", +#' tenant="myaadtenant", +#' app="app_id", +#' username="user", +#' password="abcdefg") +#' +#' # delete a saved token by specifying its MD5 hash +#' delete_azure_token(hash="7ea491716e5b10a77a673106f3f53bfd") +#' #' } #' @export get_azure_token <- function(resource_host, tenant, app, password=NULL, username=NULL, auth_type=NULL, aad_host="https://login.microsoftonline.com/") { tenant <- normalize_tenant(tenant) + if(is_guid(app)) + app <- normalize_guid(app) base_url <- construct_path(aad_host, tenant) if(is.null(auth_type)) @@ -241,8 +273,10 @@ get_azure_token <- function(resource_host, tenant, app, password=NULL, username= # load saved token if available tokenfile <- file.path(config_dir(), token_hash(resource_host, tenant, app, password, username, auth_type, aad_host)) + if(file.exists(tokenfile)) { + message("Loading saved token") token <- readRDS(tokenfile) token$refresh() } @@ -324,6 +358,57 @@ select_auth_type <- function(password, username) } +#' @param hash The MD5 hash of this token, computed from the above inputs. Used by `delete_azure_token` for identification purposes. +#' @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_host, tenant, app, password=NULL, username=NULL, auth_type=NULL, + aad_host="https://login.microsoftonline.com/", + hash=NULL, + confirm=TRUE) +{ + if(is.null(hash)) + { + tenant <- normalize_tenant(tenant) + if(is_guid(app)) + app <- normalize_guid(app) + base_url <- construct_path(aad_host, tenant) + + if(is.null(auth_type)) + auth_type <- select_auth_type(password, username) + + hash <- token_hash(resource_host, tenant, app, password, username, auth_type, aad_host) + } + + if(confirm && interactive()) + { + yn <- readline( + paste0("Do you really want to delete this Azure Active Directory token? (y/N) ")) + if(tolower(substr(yn, 1, 1)) != "y") + return(invisible(NULL)) + } + file.remove(file.path(config_dir(), hash)) + invisible(NULL) +} + + +#' @rdname get_azure_token +#' @export +list_azure_tokens <- function() +{ + tokens <- dir(config_dir(), full.names=TRUE) + lst <- lapply(tokens, function(fname) + { + x <- readRDS(fname) + if(is_azure_token(x)) + x + else NULL + }) + names(lst) <- basename(tokens) + lst[!sapply(lst, is.null)] +} + + token_hash <- function(resource_host, tenant, app, password, username, auth_type, aad_host) { msg <- serialize(list(resource_host, tenant, app, password, username, auth_type, aad_host), NULL, version=2) @@ -331,52 +416,3 @@ token_hash <- function(resource_host, tenant, app, password, username, auth_type } -#' Normalizes a tenant -#' -#' @param tenant An Azure Active Directory tenant. This can be a name ("myaadtenant"), a fully qualified domain name ("myaadtenant.onmicrosoft.com" or "mycompanyname.com"), or a valid GUID. -#' -#' @details -#' This function is used by `get_azure_token` to recognise a tenant input. A tenant can be identified either by a GUID, or its name, or a fully-qualified domain name (FQDN). The rules for normalizing a tenant are: -#' 1. If `tenant` is a valid GUID, return its canonically formatted value -#' 2. Otherwise, if it is a FQDN, return it -#' 3. Otherwise, if it is not the string "common", append ".onmicrosoft.com" to it -#' 4. Otherwise, return the value of `tenant` -#' -#' @return -#' The normalized ID or name of the tenant. -#' @seealso -#' [get_azure_token], -#' -#' [Parsing rules for GUIDs in .NET](https://docs.microsoft.com/en-us/dotnet/api/system.guid.parse]). `normalize_tenant` recognises the "N", "D", "B" and "P" formats for GUIDs. -#' @export -normalize_tenant <- function(tenant) -{ - # see https://docs.microsoft.com/en-us/dotnet/api/system.guid.parse - # for possible input formats for GUIDs - is_guid <- function(x) - { - grepl("^[0-9a-f]{32}$", x) || - grepl("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", x) || - grepl("^\\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}}$", x) || - grepl("^\\([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\\)$", x) - } - - # check if supplied a guid; if not, check if a fqdn; - # if not, check if 'common'; if not, append '.onmicrosoft.com' - if(is_guid(tenant)) - { - tenant <- sub("^[({]?([-0-9a-f]+)[})]$", "\\1", tenant) - tenant <- gsub("-", "", tenant) - return(paste( - substr(tenant, 1, 8), - substr(tenant, 9, 12), - substr(tenant, 13, 16), - substr(tenant, 17, 20), - substr(tenant, 21, 32), sep="-")) - } - - if(!grepl("\\.", tenant) && tenant != "common") - tenant <- paste(tenant, "onmicrosoft.com", sep=".") - tenant -} - diff --git a/R/az_login.R b/R/az_login.R index f4b46de..5dc4033 100644 --- a/R/az_login.R +++ b/R/az_login.R @@ -55,8 +55,8 @@ config_dir <- function() #' @rdname azure_login #' @export create_azure_login <- function(tenant, app, password=NULL, username=NULL, auth_type=NULL, - host="https://management.azure.com/", aad_host="https://login.microsoftonline.com/", - config_file=NULL, ...) + host="https://management.azure.com/", aad_host="https://login.microsoftonline.com/", + config_file=NULL, ...) { if(!is.null(config_file)) { @@ -70,6 +70,8 @@ create_azure_login <- function(tenant, app, password=NULL, username=NULL, auth_t } tenant <- normalize_tenant(tenant) + if(is_guid(app)) + app <- normalize_guid(app) message("Creating Azure Resource Manager login for tenant '", tenant, "'") client <- az_rm$new(tenant, app, password, username, auth_type, host, aad_host, config_file, ...) save_client(client, tenant) @@ -123,9 +125,15 @@ delete_azure_login <- function(tenant, confirm=TRUE) list_azure_logins <- function() { tenants <- dir(config_dir(), full.names=TRUE) - lst <- lapply(tenants, readRDS) + lst <- lapply(tenants, function(fname) + { + x <- readRDS(fname) + if(is_azure_login(x)) + x + else NULL + }) names(lst) <- basename(tenants) - lst + lst[!sapply(lst, is.null)] } diff --git a/R/is.R b/R/is.R index 49a965c..47d1b14 100644 --- a/R/is.R +++ b/R/is.R @@ -6,6 +6,14 @@ #' #' @return #' A boolean. +#' @rdname is +#' @export +is_azure_login <- function(object) +{ + R6::is.R6(object) && inherits(object, "az_rm") +} + + #' @rdname is #' @export is_subscription <- function(object) diff --git a/R/normalize.R b/R/normalize.R new file mode 100644 index 0000000..ac6fdd2 --- /dev/null +++ b/R/normalize.R @@ -0,0 +1,96 @@ +#' Normalize GUID and tenant values +#' +#' These functions are used by `get_azure_token` to recognise and properly format tenant and app IDs. +#' +#' @param tenant For `normalize_tenant`, a string containing an Azure Active Directory tenant. This can be a name ("myaadtenant"), a fully qualified domain name ("myaadtenant.onmicrosoft.com" or "mycompanyname.com"), or a valid GUID. +#' @param x For `is_guid`, a character string; for `normalize_guid`, a string containing a _validly formatted_ GUID. +#' +#' @details +#' A tenant can be identified either by a GUID, or its name, or a fully-qualified domain name (FQDN). The rules for normalizing a tenant are: +#' 1. If `tenant` is recognised as a valid GUID, return its canonically formatted value +#' 2. Otherwise, if it is a FQDN, return it +#' 3. Otherwise, if it is not the string "common", append ".onmicrosoft.com" to it +#' 4. Otherwise, return the value of `tenant` +#' +#' See the link below for GUID formats recognised by these functions. +#' +#' @return +#' For `normalize_guid`, the canonically formatted GUID. Note that if `normalize_guid` is given an improperly formatted GUID, its output is undefined; you should always test a string with `is_guid` before passing it to `normalize_guid`. +#' +#' For `normalize_tenant`, the normalized ID or name of the tenant. +#' +#' @seealso +#' [get_azure_token] +#' +#' [Parsing rules for GUIDs in .NET](https://docs.microsoft.com/en-us/dotnet/api/system.guid.parse]). `is_guid` and `normalize_guid` recognise the "N", "D", "B" and "P" formats. +#' +#' @examples +#' +#' is_guid("72f988bf-86f1-41af-91ab-2d7cd011db47") # TRUE +#' is_guid("{72f988bf-86f1-41af-91ab-2d7cd011db47}") # TRUE +#' is_guid("72f988bf-86f1-41af-91ab-2d7cd011db47}") # FALSE (unmatched brace) +#' is_guid("microsoft") # FALSE +#' +#' # all of these return the same value +#' normalize_guid("72f988bf-86f1-41af-91ab-2d7cd011db47") +#' normalize_guid("{72f988bf-86f1-41af-91ab-2d7cd011db47}") +#' normalize_guid("(72f988bf-86f1-41af-91ab-2d7cd011db47)") +#' normalize_guid("72f988bf86f141af91ab2d7cd011db47") +#' +#' normalize_tenant("microsoft") # returns 'microsoft.onmicrosoft.com' +#' normalize_tenant("microsoft.com") # returns 'microsoft.com' +#' normalize_tenant("72f988bf-86f1-41af-91ab-2d7cd011db47") # returns the GUID +#' +#' @export +#' @rdname guid +normalize_tenant <- function(tenant) +{ + # see https://docs.microsoft.com/en-us/dotnet/api/system.guid.parse + # for possible input formats for GUIDs + is_guid <- function(x) + { + grepl("^[0-9a-f]{32}$", x) || + grepl("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", x) || + grepl("^\\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}}$", x) || + grepl("^\\([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\\)$", x) + } + + # check if supplied a guid; if not, check if a fqdn; + # if not, check if 'common'; if not, append '.onmicrosoft.com' + if(is_guid(tenant)) + { + tenant <- sub("^[({]?([-0-9a-f]+)[})]$", "\\1", tenant) + tenant <- gsub("-", "", tenant) + return(normalize_guid(tenant)) + } + + if(!grepl("\\.", tenant) && tenant != "common") + tenant <- paste(tenant, "onmicrosoft.com", sep=".") + tenant +} + + +#' @export +#' @rdname guid +normalize_guid <- function(x) +{ + x <- sub("^[({]?([-0-9a-f]+)[})]$", "\\1", x) + x <- gsub("-", "", x) + return(paste( + substr(x, 1, 8), + substr(x, 9, 12), + substr(x, 13, 16), + substr(x, 17, 20), + substr(x, 21, 32), sep="-")) +} + + +#' @export +#' @rdname guid +is_guid <- function(x) +{ + grepl("^[0-9a-f]{32}$", x) || + grepl("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", x) || + grepl("^\\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}}$", x) || + grepl("^\\([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\\)$", x) +} diff --git a/man/AzureToken.Rd b/man/AzureToken.Rd index 24ead42..2a493a5 100644 --- a/man/AzureToken.Rd +++ b/man/AzureToken.Rd @@ -21,7 +21,7 @@ Azure OAuth 2.0 token class, inheriting from the \link[httr:Token2.0]{Token2.0 c \section{Caching}{ -This class never caches its tokens, unlike httr::Token2.0. +Unlike httr::Token2.0, caching for Azure tokens is handled outside the class. Tokens are automatically cached by the \code{get_azure_token} function, and can be (manually) deleted with the \code{delete_azure_token} function. Calling \code{AzureToken$new()} directly will always acquire a new token from the server. } \seealso{ diff --git a/man/get_azure_token.Rd b/man/get_azure_token.Rd index 5785ee9..a9c6d93 100644 --- a/man/get_azure_token.Rd +++ b/man/get_azure_token.Rd @@ -2,11 +2,20 @@ % Please edit documentation in R/AzureToken.R \name{get_azure_token} \alias{get_azure_token} -\title{Generate an Azure OAuth token} +\alias{delete_azure_token} +\alias{list_azure_tokens} +\title{Manage Azure Active Directory OAuth 2.0 tokens} \usage{ get_azure_token(resource_host, tenant, app, password = NULL, username = NULL, auth_type = NULL, aad_host = "https://login.microsoftonline.com/") + +delete_azure_token(resource_host, tenant, app, password = NULL, + username = NULL, auth_type = NULL, + aad_host = "https://login.microsoftonline.com/", hash = NULL, + confirm = TRUE) + +list_azure_tokens() } \arguments{ \item{resource_host}{URL for your resource host. For Resource Manager in the public Azure cloud, this is \code{https://management.azure.com/}.} @@ -22,15 +31,21 @@ get_azure_token(resource_host, tenant, app, password = NULL, \item{auth_type}{The authentication type. See 'Details' below.} \item{aad_host}{URL for your AAD host. For the public Azure cloud, this is \code{https://login.microsoftonline.com/}.} + +\item{hash}{The MD5 hash of this token, computed from the above inputs. Used by \code{delete_azure_token} for identification purposes.} + +\item{confirm}{For \code{delete_azure_token}, whether to prompt for confirmation before deleting a token.} } \description{ -This extends the OAuth functionality in httr for use with Azure Active Directory (AAD). +These functions extend the OAuth functionality in httr for use with Azure Active Directory (AAD). } \details{ -This function does much the same thing as \code{\link[httr:oauth2.0_token]{httr::oauth2.0_token()}}, but customised for Azure. +\code{get_azure_token} does much the same thing as \code{\link[httr:oauth2.0_token]{httr::oauth2.0_token()}}, but customised for Azure. It obtains an OAuth token, first by checking if a cached value exists on disk, and if not, acquiring it from the AAD server. \code{delete_azure_token} deletes a cached token, and \code{list_azure_tokens} lists currently cached tokens. +} +\section{Authentication methods}{ The OAuth authentication type can be one of four possible values: "authorization_code", "client_credentials", "device_code", or "resource_owner". The first two are provided by the \link[httr:Token2.0]{httr::Token2.0} token class, while the last two are provided by the AzureToken class which extends httr::Token2.0. Here is a short description of these methods. -\itemize{ +\enumerate{ \item Using the authorization_code method is a 3-step process. First, \code{get_azure_token} contacts the AAD authorization endpoint to obtain a temporary access code. It then contacts the AAD access endpoint, passing it the code. The access endpoint sends back a login URL which \code{get_azure_token} opens in your browser, where you can enter your credentials. Once this is completed, the endpoint returns the OAuth token via a HTTP redirect URI. \item The device_code method is similar in concept to authorization_code, but is meant for situations where you are unable to browse the Internet -- for example if you don't have a browser installed or your computer has input constraints. First, \code{get_azure_token} contacts the AAD devicecode endpoint, which responds with a login URL and an access code. You then visit the URL and enter the code, possibly using a different computer. Meanwhile, \code{get_azure_token} polls the AAD access endpoint for a token, which is provided once you have successfully entered the code. \item The client_credentials method is much simpler than the above methods, requiring only one step. \code{get_azure_token} contacts the access endpoint, passing it the app secret (which you supplied in the \code{password} argument). Assuming the secret is valid, the endpoint then returns the OAuth token. @@ -49,6 +64,25 @@ The httpuv package must be installed to use the authorization_code method, as th Similarly, since the authorization_code method opens a browser to load the AAD authorization page, your machine must have an Internet browser installed that can be run from inside R. In particular, if you are using a Linux \href{https://azure.microsoft.com/en-us/services/virtual-machines/data-science-virtual-machines/}{Data Science Virtual Machine} in Azure, you may run into difficulties; use one of the other methods instead. } + +\section{Caching}{ + +AzureRMR differs from httr in its handling of token caching in a number of ways. +\itemize{ +\item It moves caching of OAuth tokens out of the token class, and into the \code{get_azure_token} function. Caching is based on all the inputs to \code{get_azure_token} as listed above. Directly calling the AzureToken class constructor will always acquire a new token from the server. +\item It defines its own directory for caching tokens, using the rappdirs package. On recent Windows versions, this will usually be in the location \code{C:\\Users\\(username)\\AppData\\Local\\AzureR\\AzureRMR}. On Linux, it will be in \code{~/.config/AzureRMR}, and on MacOS, it will be in \code{~/Library/Application Support/AzureRMR}. Note that a single directory is used for all tokens, unlike httr, and the working directory is not touched (which lessens the risk of accidentally introducing cached tokens into source control). +} + +To list all cached tokens on disk, use \code{list_azure_tokens}. This returns a list of token objects, named according to their MD5 hashes. + +To delete a cached token, use \code{delete_azure_token}. This takes the same inputs as \code{get_azure_token}, or you can specify the MD5 hash directly in the \code{hash} argument. +} + +\section{Value}{ + +For \code{get_azure_token}, an object of class \code{AzureToken} representing the AAD token. For \code{list_azure_tokens}, a list of such objects retrieved from disk. +} + \examples{ \dontrun{ @@ -84,9 +118,24 @@ storage_token <- get_azure_token( owner_token <- get_azure_token( resource_host="https://myresource/", tenant="myaadtenant", + app="app_id", username="user", password="abcdefg") +# list saved tokens +list_azure_tokens() + +# delete a saved token from disk +delete_azure_token( + resource_host="https://myresource/", + tenant="myaadtenant", + app="app_id", + username="user", + password="abcdefg") + +# delete a saved token by specifying its MD5 hash +delete_azure_token(hash="7ea491716e5b10a77a673106f3f53bfd") + } } \seealso{ diff --git a/man/guid.Rd b/man/guid.Rd new file mode 100644 index 0000000..70dd8d7 --- /dev/null +++ b/man/guid.Rd @@ -0,0 +1,61 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/normalize.R +\name{normalize_tenant} +\alias{normalize_tenant} +\alias{normalize_guid} +\alias{is_guid} +\title{Normalize GUID and tenant values} +\usage{ +normalize_tenant(tenant) + +normalize_guid(x) + +is_guid(x) +} +\arguments{ +\item{tenant}{For \code{normalize_tenant}, a string containing an Azure Active Directory tenant. This can be a name ("myaadtenant"), a fully qualified domain name ("myaadtenant.onmicrosoft.com" or "mycompanyname.com"), or a valid GUID.} + +\item{x}{For \code{is_guid}, a character string; for \code{normalize_guid}, a string containing a \emph{validly formatted} GUID.} +} +\value{ +For \code{normalize_guid}, the canonically formatted GUID. Note that if \code{normalize_guid} is given an improperly formatted GUID, its output is undefined; you should always test a string with \code{is_guid} before passing it to \code{normalize_guid}. + +For \code{normalize_tenant}, the normalized ID or name of the tenant. +} +\description{ +These functions are used by \code{get_azure_token} to recognise and properly format tenant and app IDs. +} +\details{ +A tenant can be identified either by a GUID, or its name, or a fully-qualified domain name (FQDN). The rules for normalizing a tenant are: +\enumerate{ +\item If \code{tenant} is recognised as a valid GUID, return its canonically formatted value +\item Otherwise, if it is a FQDN, return it +\item Otherwise, if it is not the string "common", append ".onmicrosoft.com" to it +\item Otherwise, return the value of \code{tenant} +} + +See the link below for GUID formats recognised by these functions. +} +\examples{ + +is_guid("72f988bf-86f1-41af-91ab-2d7cd011db47") # TRUE +is_guid("{72f988bf-86f1-41af-91ab-2d7cd011db47}") # TRUE +is_guid("72f988bf-86f1-41af-91ab-2d7cd011db47}") # FALSE (unmatched brace) +is_guid("microsoft") # FALSE + +# all of these return the same value +normalize_guid("72f988bf-86f1-41af-91ab-2d7cd011db47") +normalize_guid("{72f988bf-86f1-41af-91ab-2d7cd011db47}") +normalize_guid("(72f988bf-86f1-41af-91ab-2d7cd011db47)") +normalize_guid("72f988bf86f141af91ab2d7cd011db47") + +normalize_tenant("microsoft") # returns 'microsoft.onmicrosoft.com' +normalize_tenant("microsoft.com") # returns 'microsoft.com' +normalize_tenant("72f988bf-86f1-41af-91ab-2d7cd011db47") # returns the GUID + +} +\seealso{ +\link{get_azure_token} + +\href{https://docs.microsoft.com/en-us/dotnet/api/system.guid.parse]}{Parsing rules for GUIDs in .NET}. \code{is_guid} and \code{normalize_guid} recognise the "N", "D", "B" and "P" formats. +} diff --git a/man/is.Rd b/man/is.Rd index 6c4094d..be5015e 100644 --- a/man/is.Rd +++ b/man/is.Rd @@ -1,6 +1,7 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/is.R -\name{is_subscription} +\name{is_azure_login} +\alias{is_azure_login} \alias{is_subscription} \alias{is_resource_group} \alias{is_resource} @@ -8,6 +9,8 @@ \alias{is_azure_token} \title{Informational functions} \usage{ +is_azure_login(object) + is_subscription(object) is_resource_group(object) diff --git a/man/normalize_tenant.Rd b/man/normalize_tenant.Rd deleted file mode 100644 index d0354a9..0000000 --- a/man/normalize_tenant.Rd +++ /dev/null @@ -1,31 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/AzureToken.R -\name{normalize_tenant} -\alias{normalize_tenant} -\title{Normalizes a tenant} -\usage{ -normalize_tenant(tenant) -} -\arguments{ -\item{tenant}{An Azure Active Directory tenant. This can be a name ("myaadtenant"), a fully qualified domain name ("myaadtenant.onmicrosoft.com" or "mycompanyname.com"), or a valid GUID.} -} -\value{ -The normalized ID or name of the tenant. -} -\description{ -Normalizes a tenant -} -\details{ -This function is used by \code{get_azure_token} to recognise a tenant input. A tenant can be identified either by a GUID, or its name, or a fully-qualified domain name (FQDN). The rules for normalizing a tenant are: -\enumerate{ -\item If \code{tenant} is a valid GUID, return its canonically formatted value -\item Otherwise, if it is a FQDN, return it -\item Otherwise, if it is not the string "common", append ".onmicrosoft.com" to it -\item Otherwise, return the value of \code{tenant} -} -} -\seealso{ -\link{get_azure_token}, - -\href{https://docs.microsoft.com/en-us/dotnet/api/system.guid.parse]}{Parsing rules for GUIDs in .NET}. \code{normalize_tenant} recognises the "N", "D", "B" and "P" formats for GUIDs. -} diff --git a/tests/testthat/test00_token.R b/tests/testthat/test00_token.R new file mode 100644 index 0000000..36f7fd4 --- /dev/null +++ b/tests/testthat/test00_token.R @@ -0,0 +1,43 @@ +context("AzureToken") + +tenant <- Sys.getenv("AZ_TEST_TENANT_ID") +app <- Sys.getenv("AZ_TEST_APP_ID") +password <- Sys.getenv("AZ_TEST_PASSWORD") +subscription <- Sys.getenv("AZ_TEST_SUBSCRIPTION") + +if(tenant == "" || app == "" || password == "" || subscription == "") + skip("Authentication tests skipped: ARM credentials not set") + + +test_that("normalize_tenant, normalize_guid work", +{ + guid <- "abcdefab-1234-5678-9012-abcdefabcdef" + expect_identical(normalize_guid(guid), guid) + guid2 <- paste0("{", guid, "}") + expect_identical(normalize_guid(guid2), guid) + guid3 <- paste0("(", guid, ")") + expect_identical(normalize_guid(guid3), guid) + guid4 <- sub("-", "", guid, fixed=TRUE) + expect_identical(normalize_guid(guid4), guid) + + # improperly formatted GUID will be treated as a name + guid5 <- paste0("(", guid) + expect_identical(normalize_tenant(guid5), paste0(guid5, ".onmicrosoft.com")) + + expect_identical(normalize_tenant("common"), "common") + expect_identical(normalize_tenant("mytenant"), "mytenant.onmicrosoft.com") + expect_identical(normalize_tenant("mytenant.com"), "mytenant.com") +}) + +test_that("Authentication works", +{ + suppressWarnings(delete_azure_token("http://management.azure.com/", tenant, app, password, confirm=FALSE)) + + token <- get_azure_token("http://management.azure.com/", tenant, app, password) + expect_true(is_azure_token(token)) + + toklist <- list_azure_tokens() + expect_true(length(toklist) > 0) + + expect_null(delete_azure_token("http://management.azure.com/", tenant, app, password, confirm=FALSE)) +})