From 28b57c9d6f99d9b883b4cc4c419d68f1230c092f Mon Sep 17 00:00:00 2001 From: hong-revo Date: Thu, 10 Jan 2019 18:51:48 +1100 Subject: [PATCH 01/12] new get_azure_token signature --- R/AzureToken.R | 45 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/R/AzureToken.R b/R/AzureToken.R index d1fcf40..2941c68 100644 --- a/R/AzureToken.R +++ b/R/AzureToken.R @@ -20,12 +20,12 @@ AzureToken <- R6::R6Class("AzureToken", inherit=httr::Token2.0, public=list( # need to do hacky init to support explicit re-authentication instead of using a refresh token - initialize=function(endpoint, app, user_params, use_device=FALSE) + initialize=function(endpoint, app, user_params, use_device=FALSE, client_credentials=TRUE) { private$az_use_device <- use_device params <- list(scope=NULL, user_params=user_params, type=NULL, use_oob=FALSE, as_header=TRUE, - use_basic_auth=use_device, config_init=list(), client_credentials=TRUE) + use_basic_auth=use_device, config_init=list(), client_credentials=client_credentials) super$initialize(app=app, endpoint=endpoint, params=params, credentials=NULL, cache_path=FALSE) @@ -142,16 +142,21 @@ private=list( #' #' } #' @export -get_azure_token=function(resource_host, tenant, app, password=NULL, - auth_type=if(is.null(password)) "device_code" else "client_credentials", - aad_host="https://login.microsoftonline.com/") +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) - base_url <- construct_path(aad_host, tenant) - if(auth_type == "client_credentials") - auth_with_creds(base_url, app, password, resource_host) - else auth_with_device(base_url, app, resource_host) + + if(is.null(auth_type)) + auth_type <- select_auth_type(password, username) + + switch(auth_type, + client_credentials=auth_with_creds(base_url, app, password, resource_host), + device_code=auth_with_device(base_url, app, resource_host), + password=auth_with_password(base_url, app, password, username, resource_host), + authorization_code=auth_with_code(base_url, app, resource_host), + stop("Invalid auth_type argument", call.=FALSE)) } @@ -172,3 +177,25 @@ auth_with_device <- function(base_url, app, resource) AzureToken$new(endp, app, user_params=list(resource=resource), use_device=TRUE) } + +# select authentication method based on input arguments and presence of httpuv +select_auth_type <- function(password, username) +{ + got_pwd <- !is.null(password) + got_user <- !is.null(username) + + if(got_pwd && got_user) + "password" + else if(!got_pwd && !got_user) + { + if(system.file(package="httpuv") == "") + { + message("httpuv package not found, defaulting to device code authentication") + "device_code" + } + else "authorization_code" + } + else if(got_pwd && !got_user) + "client_credentials" + else stop("Can't select authentication method", call.=FALSE) +} From 89cfb707d592cf10da48a959ba30fb1371dc82d1 Mon Sep 17 00:00:00 2001 From: hong-revo Date: Thu, 10 Jan 2019 20:05:55 +1100 Subject: [PATCH 02/12] new auth method stubs --- R/AzureToken.R | 80 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 62 insertions(+), 18 deletions(-) diff --git a/R/AzureToken.R b/R/AzureToken.R index 2941c68..95bffe6 100644 --- a/R/AzureToken.R +++ b/R/AzureToken.R @@ -27,7 +27,12 @@ public=list( params <- list(scope=NULL, user_params=user_params, type=NULL, use_oob=FALSE, as_header=TRUE, use_basic_auth=use_device, config_init=list(), client_credentials=client_credentials) - super$initialize(app=app, endpoint=endpoint, params=params, credentials=NULL, cache_path=FALSE) + if(!is.null(params$username)) + super$initialize(app=app, endpoint=endpoint, params=params, credentials=NULL, cache_path=FALSE) + else + { + # handle username/password authentication + } # if auth is via device, token now contains initial server response; call devicecode handler to get actual token if(use_device) @@ -117,28 +122,44 @@ private=list( #' #' @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 ID. -#' @param app Your client/app ID which you registered in AAD. -#' @param password Your password. Required for `auth_type == "client_credentials"`, ignored for `auth_type == "device_code"`. -#' @param auth_type The authentication type, either `"client_credentials"` or `"device_code"`. Defaults to the latter if no password is provided, otherwise the former. -#' @param aad_host URL for your Azure Active Directory host. For the public Azure cloud, this is `https://login.microsoftonline.com/`. +#' @param app The client/app ID to use to authenticate with Azure Active Directory (AAD). +#' @param password The password, either for the app, or your username if supplied. See 'Details' below. +#' @param username Your AAD username, if using the resource owner grant. See 'Details' below. +#' @param auth_type The authentication type. See 'Details' below. +#' @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 with support for device authentication and with unnecessary options removed. Device authentication removes the need to save a password on your machine. Instead, the server provides you with a code, along with a URL. You then visit the URL in your browser and enter the code, which completes the authentication process. +#' +#' The OAuth authentication type can be one of 4 possible values: "authorization_code", "device_code", "client_credentials" or "resource_owner". If this is not specified, the value is chosen based on the presence or absence of the `password` and `username` arguments: +#' +#' - Password and username present: "resource_owner" +#' - Password and username absent: "authorization_code" if the httpuv package is installed, "device_code" otherwise +#' - Password present, username absent: "client_credentials" +#' - Password absent, username present: error +#' +#' 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. #' #' @seealso #' [AzureToken], [httr::oauth2.0_token], [httr::Token], +#' #' [OAuth authentication for Azure Active Directory](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-protocols-oauth-code), -#' [Device code flow on OAuth.com](https://www.oauth.com/oauth2-servers/device-flow/token-request/) +#' [Device code flow on OAuth.com](https://www.oauth.com/oauth2-servers/device-flow/token-request/), +#' [OAuth 2.0 RFC](https://tools.ietf.org/html/rfc6749) for the gory details on how OAuth works #' #' @examples #' \dontrun{ #' -#' token <- get_azure_token( -#' aad_host="https://login.microsoftonline.com/", +#' arm_token <- get_azure_token( +#' resource_host="https://management.azure.com/", # authenticate with Azure Resource Manager #' tenant="myaadtenant.onmicrosoft.com", #' app="app_id", -#' password="password", -#' resource_host="https://management.azure.com/") +#' password="password") +#' +#' storage_token <- get_azure_token( +#' resource_host="https://storage.azure.com/", # authenticate with Azure storage +#' tenant="myaadtenant.onmicrosoft.com", +#' app="app_id") #' #' } #' @export @@ -152,20 +173,24 @@ get_azure_token <- function(resource_host, tenant, app, password=NULL, username= auth_type <- select_auth_type(password, username) switch(auth_type, - client_credentials=auth_with_creds(base_url, app, password, resource_host), - device_code=auth_with_device(base_url, app, resource_host), - password=auth_with_password(base_url, app, password, username, resource_host), - authorization_code=auth_with_code(base_url, app, resource_host), + client_credentials= + auth_with_client_creds(base_url, app, password, resource_host), + device_code= + auth_with_device(base_url, app, resource_host), + authorization_code= + auth_with_code(base_url, app, resource_host), + resource_owner= + auth_with_username(base_url, app, password, username, resource_host), stop("Invalid auth_type argument", call.=FALSE)) } -auth_with_creds <- function(base_url, app, password, resource) +auth_with_client_creds <- function(base_url, app, password, resource) { endp <- httr::oauth_endpoint(base_url=base_url, authorize="oauth2/authorize", access="oauth2/token") app <- httr::oauth_app("azure", key=app, secret=password) - AzureToken$new(endp, app, user_params=list(resource=resource)) + AzureToken$new(endp, app, user_params=list(resource=resource), use_device=FALSE, client_credentials=TRUE) } @@ -174,7 +199,26 @@ auth_with_device <- function(base_url, app, resource) endp <- httr::oauth_endpoint(base_url=base_url, authorize="oauth2/authorize", access="oauth2/devicecode") app <- httr::oauth_app("azure", key=app, secret="") - AzureToken$new(endp, app, user_params=list(resource=resource), use_device=TRUE) + AzureToken$new(endp, app, user_params=list(resource=resource), use_device=TRUE, client_credentials=FALSE) +} + + +auth_with_code <- function(base_url, app, resource) +{ + endp <- httr::oauth_endpoint(base_url=base_url, authorize="oauth2/authorize", access="oauth2/token") + app <- httr::oauth_app("azure", key=app, secret=NULL) + + AzureToken$new(endp, app, user_params=list(resource=resource), use_device=FALSE, client_credentials=FALSE) +} + + +auth_with_username <- function(base_url, app, password, username, resource) +{ + endp <- httr::oauth_endpoint(base_url=base_url, authorize="oauth2/authorize", access="oauth2/token") + app <- httr::oauth_app("azure", key=app, secret=NULL) + + AzureToken$new(endp, app, user_params=list(resource=resource, username=username), + use_device=TRUE, client_credentials=FALSE) } @@ -185,7 +229,7 @@ select_auth_type <- function(password, username) got_user <- !is.null(username) if(got_pwd && got_user) - "password" + "resource_owner" else if(!got_pwd && !got_user) { if(system.file(package="httpuv") == "") From 1aa6e8a36f5a63088a84dd481d4f7613f1a4717d Mon Sep 17 00:00:00 2001 From: hong-revo Date: Thu, 10 Jan 2019 20:44:07 +1100 Subject: [PATCH 03/12] fill out token class --- R/AzureToken.R | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/R/AzureToken.R b/R/AzureToken.R index 95bffe6..c1e2309 100644 --- a/R/AzureToken.R +++ b/R/AzureToken.R @@ -27,11 +27,17 @@ public=list( params <- list(scope=NULL, user_params=user_params, type=NULL, use_oob=FALSE, as_header=TRUE, use_basic_auth=use_device, config_init=list(), client_credentials=client_credentials) - if(!is.null(params$username)) + if(is.null(user_params$username)) super$initialize(app=app, endpoint=endpoint, params=params, credentials=NULL, cache_path=FALSE) else { + self$app <- app + self$endpoint <- endpoint + self$params <- params + self$cache_path <- NULL + self$private_key <- NULL # handle username/password authentication + private$init_with_username(app=app, endpoint=endpoint, user_params) } # if auth is via device, token now contains initial server response; call devicecode handler to get actual token @@ -67,7 +73,8 @@ public=list( return(super$refresh()) # re-authenticate if no refresh token - self$initialize(self$endpoint, self$app, self$params$user_params, use_device=private$az_use_device) + self$initialize(self$endpoint, self$app, self$params$user_params, use_device=private$az_use_device, + client_credentials=self$params$client_credentials) NULL } ), @@ -83,7 +90,7 @@ private=list( req_params <- list(client_id=app$key, grant_type="device_code", code=self$credentials$device_code) req_params <- utils::modifyList(user_params, req_params) - endpoint$access <- sub("devicecode", "token", endpoint$access) + endpoint$access <- sub("devicecode$", "token", endpoint$access) interval <- as.numeric(self$credentials$interval) ntries <- as.numeric(self$credentials$expires_in) %/% interval @@ -112,6 +119,22 @@ private=list( self$endpoint <- endpoint self$credentials <- cont NULL + }, + + # resource owner authentication: send username/password + init_with_username=function(endpoint, app, user_params) + { + body <- list( + client_id=app$key, + grant_type="password", + username=user_params$username, + password=user_params$password) + + res <- httr::POST(endpoint$access, httr::add_headers(`Cache-Control`="no-cache"), encode="form", + body=body) + + httr::stop_for_status(res, task="get an access token") + httr::content(res) } )) @@ -153,13 +176,13 @@ private=list( #' arm_token <- get_azure_token( #' resource_host="https://management.azure.com/", # authenticate with Azure Resource Manager #' tenant="myaadtenant.onmicrosoft.com", -#' app="app_id", -#' password="password") +#' app="app_id") #' #' storage_token <- get_azure_token( #' resource_host="https://storage.azure.com/", # authenticate with Azure storage #' tenant="myaadtenant.onmicrosoft.com", -#' app="app_id") +#' app="app_id", +#' password="password") #' #' } #' @export @@ -217,7 +240,7 @@ auth_with_username <- function(base_url, app, password, username, resource) endp <- httr::oauth_endpoint(base_url=base_url, authorize="oauth2/authorize", access="oauth2/token") app <- httr::oauth_app("azure", key=app, secret=NULL) - AzureToken$new(endp, app, user_params=list(resource=resource, username=username), + AzureToken$new(endp, app, user_params=list(resource=resource, username=username, password=password), use_device=TRUE, client_credentials=FALSE) } From 98eac52f5f99c5c9f883facd94d85f09be2040cb Mon Sep 17 00:00:00 2001 From: hong-revo Date: Thu, 10 Jan 2019 21:09:19 +1100 Subject: [PATCH 04/12] quick fix for absent httpuv --- R/AzureToken.R | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/R/AzureToken.R b/R/AzureToken.R index c1e2309..7aa7319 100644 --- a/R/AzureToken.R +++ b/R/AzureToken.R @@ -222,7 +222,8 @@ auth_with_device <- function(base_url, app, resource) endp <- httr::oauth_endpoint(base_url=base_url, authorize="oauth2/authorize", access="oauth2/devicecode") app <- httr::oauth_app("azure", key=app, secret="") - AzureToken$new(endp, app, user_params=list(resource=resource), use_device=TRUE, client_credentials=FALSE) + # client_credentials=TRUE to tell httr::init_oauth2.0 not to use authorization code flow + AzureToken$new(endp, app, user_params=list(resource=resource), use_device=TRUE, client_credentials=TRUE) } @@ -257,7 +258,7 @@ select_auth_type <- function(password, username) { if(system.file(package="httpuv") == "") { - message("httpuv package not found, defaulting to device code authentication") + message("httpuv not installed, defaulting to device code authentication") "device_code" } else "authorization_code" From 23f6f2d6a3a6fa6853dcc7249b32030e1e238ee6 Mon Sep 17 00:00:00 2001 From: hong-revo Date: Thu, 10 Jan 2019 21:34:21 +1100 Subject: [PATCH 05/12] forgot resource --- R/AzureToken.R | 1 + 1 file changed, 1 insertion(+) diff --git a/R/AzureToken.R b/R/AzureToken.R index 7aa7319..f4131fc 100644 --- a/R/AzureToken.R +++ b/R/AzureToken.R @@ -125,6 +125,7 @@ private=list( init_with_username=function(endpoint, app, user_params) { body <- list( + resource=user_params$resource, client_id=app$key, grant_type="password", username=user_params$username, From b09ac31314ef06f37a4b12dae208544f3188dd11 Mon Sep 17 00:00:00 2001 From: hong-revo Date: Fri, 11 Jan 2019 00:49:46 +1100 Subject: [PATCH 06/12] get methods working --- R/AzureToken.R | 66 +++++++++++++++++++++++--------------------------- 1 file changed, 30 insertions(+), 36 deletions(-) diff --git a/R/AzureToken.R b/R/AzureToken.R index f4131fc..3851ce9 100644 --- a/R/AzureToken.R +++ b/R/AzureToken.R @@ -25,28 +25,22 @@ public=list( private$az_use_device <- use_device params <- list(scope=NULL, user_params=user_params, type=NULL, use_oob=FALSE, as_header=TRUE, - use_basic_auth=use_device, config_init=list(), client_credentials=client_credentials) + use_basic_auth=FALSE, config_init=list(), client_credentials=client_credentials) - if(is.null(user_params$username)) - super$initialize(app=app, endpoint=endpoint, params=params, credentials=NULL, cache_path=FALSE) - else - { - self$app <- app - self$endpoint <- endpoint - self$params <- params - self$cache_path <- NULL - self$private_key <- NULL - # handle username/password authentication - private$init_with_username(app=app, endpoint=endpoint, user_params) - } + # use httr initialize for authorization_code, client_credentials methods + if(!use_device && is.null(user_params$username)) + return(super$initialize(app=app, endpoint=endpoint, params=params, cache_path=FALSE)) - # if auth is via device, token now contains initial server response; call devicecode handler to get actual token + self$app <- app + self$endpoint <- endpoint + self$params <- params + self$cache_path <- NULL + self$private_key <- NULL + + # use our own init functions for device_code, resource_owner methods if(use_device) - private$init_with_device(endpoint, app, user_params) - - # ensure password is never NULL (important for renewing) - if(is_empty(self$app$secret)) - self$app$secret <- "" + private$init_with_device(user_params) + else private$init_with_username(user_params) }, # overrides httr::Token2.0 method @@ -75,7 +69,7 @@ public=list( # re-authenticate if no refresh token self$initialize(self$endpoint, self$app, self$params$user_params, use_device=private$az_use_device, client_credentials=self$params$client_credentials) - NULL + self } ), @@ -84,21 +78,24 @@ private=list( # device code authentication: after sending initial request, loop until server indicates code has been received # after init_oauth2.0, oauth2.0_access_token - init_with_device=function(endpoint, app, user_params) + init_with_device=function(user_params) { - cat(self$credentials$message, "\n") # tell user to enter the code + creds <- httr::oauth2.0_access_token(self$endpoint, self$app, code=NULL, user_params=user_params, + redirect_uri=NULL) - req_params <- list(client_id=app$key, grant_type="device_code", code=self$credentials$device_code) + cat(creds$message, "\n") # tell user to enter the code + + req_params <- list(client_id=self$app$key, grant_type="device_code", code=creds$device_code) req_params <- utils::modifyList(user_params, req_params) - endpoint$access <- sub("devicecode$", "token", endpoint$access) + self$endpoint$access <- sub("devicecode$", "token", self$endpoint$access) - interval <- as.numeric(self$credentials$interval) - ntries <- as.numeric(self$credentials$expires_in) %/% interval + interval <- as.numeric(creds$interval) + ntries <- as.numeric(creds$expires_in) %/% interval for(i in seq_len(ntries)) { Sys.sleep(interval) - res <- httr::POST(endpoint$access, httr::add_headers(`Cache-Control`="no-cache"), encode="form", + res <- httr::POST(self$endpoint$access, httr::add_headers(`Cache-Control`="no-cache"), encode="form", body=req_params) status <- httr::status_code(res) @@ -115,23 +112,21 @@ private=list( if(status >= 300) stop("Unable to authenticate") - # replace original fields with authenticated fields - self$endpoint <- endpoint self$credentials <- cont NULL }, # resource owner authentication: send username/password - init_with_username=function(endpoint, app, user_params) + init_with_username=function(user_params) { body <- list( resource=user_params$resource, - client_id=app$key, + client_id=self$app$key, grant_type="password", username=user_params$username, password=user_params$password) - res <- httr::POST(endpoint$access, httr::add_headers(`Cache-Control`="no-cache"), encode="form", + res <- httr::POST(self$endpoint$access, httr::add_headers(`Cache-Control`="no-cache"), encode="form", body=body) httr::stop_for_status(res, task="get an access token") @@ -221,10 +216,9 @@ auth_with_client_creds <- function(base_url, app, password, resource) auth_with_device <- function(base_url, app, resource) { endp <- httr::oauth_endpoint(base_url=base_url, authorize="oauth2/authorize", access="oauth2/devicecode") - app <- httr::oauth_app("azure", key=app, secret="") + app <- httr::oauth_app("azure", key=app, secret=NULL) - # client_credentials=TRUE to tell httr::init_oauth2.0 not to use authorization code flow - AzureToken$new(endp, app, user_params=list(resource=resource), use_device=TRUE, client_credentials=TRUE) + AzureToken$new(endp, app, user_params=list(resource=resource), use_device=TRUE, client_credentials=FALSE) } @@ -243,7 +237,7 @@ auth_with_username <- function(base_url, app, password, username, resource) app <- httr::oauth_app("azure", key=app, secret=NULL) AzureToken$new(endp, app, user_params=list(resource=resource, username=username, password=password), - use_device=TRUE, client_credentials=FALSE) + use_device=FALSE, client_credentials=FALSE) } From 6fd38a017154cda314056554b5c4cae68200e0f4 Mon Sep 17 00:00:00 2001 From: hong-revo Date: Fri, 11 Jan 2019 00:58:30 +1100 Subject: [PATCH 07/12] update news --- NEWS.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/NEWS.md b/NEWS.md index bc90ef9..b091da4 100644 --- a/NEWS.md +++ b/NEWS.md @@ -3,15 +3,14 @@ ## Significant interface changes * New `create_azure_login`, `get_azure_login` and `delete_azure_login` functions to handle ARM authentication. These will persist the login object across sessions, removing the need to re-authenticate each time. While directly calling `az_rm$new()` will still work, it's recommended to use `create_azure_login` and `get_azure_login` going forward. +* `get_azure_token` revamped, now supports the resource owner authentication method for obtaining an AAD token with a username and password. ## Other changes * Don't print empty fields for ARM objects. * Add optional `etag` field to resource object definition. -* Fix `AzureToken` object to never have a `NULL` password field (important to allow devicecode refreshing). * Add `location` argument to `az_resource_group$create_resource` method, rather than hardcoding it to the resgroup location. * Add `wait` argument when creating a new resource, similar to deploying a template, since some resources will return before provisioning is complete. Defaults to `FALSE` for backward compatibility. -* Initialise AzureToken objects with an empty string as password instead of `NULL` when using device code flow; required by httr 1.4.0's stricter input checking. * Export `is_azure_token`. # AzureRMR 1.0.0 From 6d4d623f3370f59a96691d75f31da17bf48d1d05 Mon Sep 17 00:00:00 2001 From: hong-revo Date: Fri, 11 Jan 2019 02:01:06 +1100 Subject: [PATCH 08/12] fix username auth --- R/AzureToken.R | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/R/AzureToken.R b/R/AzureToken.R index 3851ce9..4ba11d1 100644 --- a/R/AzureToken.R +++ b/R/AzureToken.R @@ -130,7 +130,8 @@ private=list( body=body) httr::stop_for_status(res, task="get an access token") - httr::content(res) + self$credentials <- httr::content(res) + NULL } )) From fc26b2bacf7a5c6a1a508b8e8aaa43ea259f9634 Mon Sep 17 00:00:00 2001 From: hong-revo Date: Fri, 11 Jan 2019 05:12:58 +1100 Subject: [PATCH 09/12] fix tests, run document() --- R/az_auth.R | 12 +++++------ R/az_login.R | 12 +++++------ man/az_rm.Rd | 7 ++++--- man/azure_login.Rd | 13 ++++++------ man/get_azure_token.Rd | 40 ++++++++++++++++++++++++++---------- tests/testthat/test01_auth.R | 2 +- 6 files changed, 53 insertions(+), 33 deletions(-) diff --git a/R/az_auth.R b/R/az_auth.R index 5ccb5ae..9886cfb 100644 --- a/R/az_auth.R +++ b/R/az_auth.R @@ -15,9 +15,10 @@ #' #' To authenticate with the `az_rm` class directly, provide the following arguments to the `new` method: #' - `tenant`: Your tenant ID. -#' - `app`: Your client/app ID which you registered in Azure Active Directory. -#' - `password`: if `auth_type == "client_credentials"`, your password. -#' - `auth_type`: Either `"client_credentials"` or `"device_code"`. Defaults to the latter if no password is provided, otherwise the former. +#' - `app`: The client/app ID to use to authenticate with Azure Active Directory. +#' - `password`: if `auth_type == "client_credentials"`, the app secret; if `auth_type == "resource_owner"`, your account password. +#' - `username`: if `auth_type == "resource_owner"`, your username. +#' - `auth_type`: The OAuth authentication method to use, one of "client_credentials", "authorization_code", "device_code" or "resource_owner". See [get_azure_token] for how the default method is chosen. #' - `host`: your ARM host. Defaults to `https://management.azure.com/`. Change this if you are using a government or private cloud. #' - `aad_host`: Azure Active Directory host for authentication. Defaults to `https://login.microsoftonline.com/`. Change this if you are using a government or private cloud. #' - `config_file`: Optionally, a JSON file containing any of the arguments listed above. Arguments supplied in this file take priority over those supplied on the command line. You can also use the output from the Azure CLI `az ad sp create-for-rbac` command. @@ -57,8 +58,7 @@ public=list( token=NULL, # authenticate and get subscriptions - initialize=function(tenant, app, password=NULL, - auth_type=if(is.null(password)) "device_code" else "client_credentials", + initialize=function(tenant, app, password=NULL, username=NULL, auth_type=NULL, host="https://management.azure.com/", aad_host="https://login.microsoftonline.com/", config_file=NULL, token=NULL) { @@ -82,7 +82,7 @@ public=list( } self$host <- host self$tenant <- normalize_tenant(tenant) - self$token <- get_azure_token(self$host, self$tenant, app, password, auth_type, aad_host) + self$token <- get_azure_token(self$host, self$tenant, app, password, username, auth_type, aad_host) NULL }, diff --git a/R/az_login.R b/R/az_login.R index 529d81a..381853d 100644 --- a/R/az_login.R +++ b/R/az_login.R @@ -7,9 +7,10 @@ config_dir <- function() #' Functions to login to Azure Resource Manager #' #' @param tenant The Azure Active Directory tenant for which to obtain a login client. Can be a name ("myaadtenant"), a fully qualified domain name ("myaadtenant.onmicrosoft.com" or "mycompanyname.com"), or a GUID. -#' @param app The app ID to authenticate with. -#' @param password If `auth_type == "client_credentials"`, your password. -#' @param auth_type The type of authentication to use, either "device_code" or "client_credentials". Defaults to the latter if no password is provided, otherwise the former. +#' @param app The client/app ID to use to authenticate with Azure Active Directory. +#' @param password If `auth_type == "client_credentials"`, the app secret; if `auth_type == "resource_owner"`, your account password. +#' @param username If `auth_type == "resource_owner"`, your username. +#' @param auth_type The OAuth authentication method to use, one of "client_credentials", "authorization_code", "device_code" or "resource_owner". See [get_azure_token] for how the default method is chosen. #' @param host Your ARM host. Defaults to `https://management.azure.com/`. Change this if you are using a government or private cloud. #' @param aad_host Azure Active Directory host for authentication. Defaults to `https://login.microsoftonline.com/`. Change this if you are using a government or private cloud. #' @param config_file Optionally, a JSON file containing any of the arguments listed above. Arguments supplied in this file take priority over those supplied on the command line. You can also use the output from the Azure CLI `az ad sp create-for-rbac` command. @@ -53,8 +54,7 @@ config_dir <- function() #' } #' @rdname azure_login #' @export -create_azure_login <- function(tenant, app, password=NULL, - auth_type=if(is.null(password)) "device_code" else "client_credentials", +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, ...) { @@ -71,7 +71,7 @@ create_azure_login <- function(tenant, app, password=NULL, tenant <- normalize_tenant(tenant) message("Creating Azure Active Directory login for tenant '", tenant, "'") - client <- az_rm$new(tenant, app, password, auth_type, host, aad_host, config_file, ...) + client <- az_rm$new(tenant, app, password, username, auth_type, host, aad_host, config_file, ...) save_client(client, tenant) client } diff --git a/man/az_rm.Rd b/man/az_rm.Rd index 7664ac8..26cabf1 100644 --- a/man/az_rm.Rd +++ b/man/az_rm.Rd @@ -27,9 +27,10 @@ The best way to authenticate with ARM is probably via the \link{create_azure_log To authenticate with the \code{az_rm} class directly, provide the following arguments to the \code{new} method: \itemize{ \item \code{tenant}: Your tenant ID. -\item \code{app}: Your client/app ID which you registered in Azure Active Directory. -\item \code{password}: if \code{auth_type == "client_credentials"}, your password. -\item \code{auth_type}: Either \code{"client_credentials"} or \code{"device_code"}. Defaults to the latter if no password is provided, otherwise the former. +\item \code{app}: The client/app ID to use to authenticate with Azure Active Directory. +\item \code{password}: if \code{auth_type == "client_credentials"}, the app secret; if \code{auth_type == "resource_owner"}, your account password. +\item \code{username}: if \code{auth_type == "resource_owner"}, your username. +\item \code{auth_type}: The OAuth authentication method to use, one of "client_credentials", "authorization_code", "device_code" or "resource_owner". See \link{get_azure_token} for how the default method is chosen. \item \code{host}: your ARM host. Defaults to \code{https://management.azure.com/}. Change this if you are using a government or private cloud. \item \code{aad_host}: Azure Active Directory host for authentication. Defaults to \code{https://login.microsoftonline.com/}. Change this if you are using a government or private cloud. \item \code{config_file}: Optionally, a JSON file containing any of the arguments listed above. Arguments supplied in this file take priority over those supplied on the command line. You can also use the output from the Azure CLI \code{az ad sp create-for-rbac} command. diff --git a/man/azure_login.Rd b/man/azure_login.Rd index 8399908..a21756e 100644 --- a/man/azure_login.Rd +++ b/man/azure_login.Rd @@ -8,9 +8,8 @@ \alias{refresh_azure_logins} \title{Functions to login to Azure Resource Manager} \usage{ -create_azure_login(tenant, app, password = NULL, auth_type = if - (is.null(password)) "device_code" else "client_credentials", - host = "https://management.azure.com/", +create_azure_login(tenant, app, password = NULL, username = NULL, + auth_type = NULL, host = "https://management.azure.com/", aad_host = "https://login.microsoftonline.com/", config_file = NULL, ...) @@ -25,11 +24,13 @@ refresh_azure_logins() \arguments{ \item{tenant}{The Azure Active Directory tenant for which to obtain a login client. Can be a name ("myaadtenant"), a fully qualified domain name ("myaadtenant.onmicrosoft.com" or "mycompanyname.com"), or a GUID.} -\item{app}{The app ID to authenticate with.} +\item{app}{The client/app ID to use to authenticate with Azure Active Directory.} -\item{password}{If \code{auth_type == "client_credentials"}, your password.} +\item{password}{If \code{auth_type == "client_credentials"}, the app secret; if \code{auth_type == "resource_owner"}, your account password.} -\item{auth_type}{The type of authentication to use, either "device_code" or "client_credentials". Defaults to the latter if no password is provided, otherwise the former.} +\item{username}{If \code{auth_type == "resource_owner"}, your username.} + +\item{auth_type}{The OAuth authentication method to use, one of "client_credentials", "authorization_code", "device_code" or "resource_owner". See \link{get_azure_token} for how the default method is chosen.} \item{host}{Your ARM host. Defaults to \code{https://management.azure.com/}. Change this if you are using a government or private cloud.} diff --git a/man/get_azure_token.Rd b/man/get_azure_token.Rd index 6323278..0a7cbe5 100644 --- a/man/get_azure_token.Rd +++ b/man/get_azure_token.Rd @@ -5,42 +5,60 @@ \title{Generate an Azure OAuth token} \usage{ get_azure_token(resource_host, tenant, app, password = NULL, - auth_type = if (is.null(password)) "device_code" else - "client_credentials", aad_host = "https://login.microsoftonline.com/") + username = NULL, auth_type = NULL, + aad_host = "https://login.microsoftonline.com/") } \arguments{ \item{resource_host}{URL for your resource host. For Resource Manager in the public Azure cloud, this is \code{https://management.azure.com/}.} \item{tenant}{Your tenant ID.} -\item{app}{Your client/app ID which you registered in AAD.} +\item{app}{The client/app ID to use to authenticate with Azure Active Directory (AAD).} -\item{password}{Your password. Required for \code{auth_type == "client_credentials"}, ignored for \code{auth_type == "device_code"}.} +\item{password}{The password, either for the app, or your username if supplied. See 'Details' below.} -\item{auth_type}{The authentication type, either \code{"client_credentials"} or \code{"device_code"}. Defaults to the latter if no password is provided, otherwise the former.} +\item{username}{Your AAD username, if using the resource owner grant. See 'Details' below.} -\item{aad_host}{URL for your Azure Active Directory host. For the public Azure cloud, this is \code{https://login.microsoftonline.com/}.} +\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/}.} } \description{ This extends the OAuth functionality in httr to allow for device code authentication. } \details{ This function does much the same thing as \code{\link[httr:oauth2.0_token]{httr::oauth2.0_token()}}, but with support for device authentication and with unnecessary options removed. Device authentication removes the need to save a password on your machine. Instead, the server provides you with a code, along with a URL. You then visit the URL in your browser and enter the code, which completes the authentication process. + +The OAuth authentication type can be one of 4 possible values: "authorization_code", "device_code", "client_credentials" or "resource_owner". If this is not specified, the value is chosen based on the presence or absence of the \code{password} and \code{username} arguments: +\itemize{ +\item Password and username present: "resource_owner" +\item Password and username absent: "authorization_code" if the httpuv package is installed, "device_code" otherwise +\item Password present, username absent: "client_credentials" +\item Password absent, username present: error +} + +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 \link[httr:oauth2.0_token]{httr::oauth2.0_token} for more information; note that Azure does not support the \code{use_oob} feature of the httr OAuth 2.0 token class. } \examples{ \dontrun{ -token <- get_azure_token( - aad_host="https://login.microsoftonline.com/", +arm_token <- get_azure_token( + resource_host="https://management.azure.com/", # authenticate with Azure Resource Manager + tenant="myaadtenant.onmicrosoft.com", + app="app_id") + +storage_token <- get_azure_token( + resource_host="https://storage.azure.com/", # authenticate with Azure storage tenant="myaadtenant.onmicrosoft.com", app="app_id", - password="password", - resource_host="https://management.azure.com/") + password="password") } } \seealso{ \link{AzureToken}, \link[httr:oauth2.0_token]{httr::oauth2.0_token}, \link[httr:Token]{httr::Token}, + \href{https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-protocols-oauth-code}{OAuth authentication for Azure Active Directory}, -\href{https://www.oauth.com/oauth2-servers/device-flow/token-request/}{Device code flow on OAuth.com} +\href{https://www.oauth.com/oauth2-servers/device-flow/token-request/}{Device code flow on OAuth.com}, +\href{https://tools.ietf.org/html/rfc6749}{OAuth 2.0 RFC} for the gory details on how OAuth works } diff --git a/tests/testthat/test01_auth.R b/tests/testthat/test01_auth.R index ca4097a..fc71c03 100644 --- a/tests/testthat/test01_auth.R +++ b/tests/testthat/test01_auth.R @@ -32,7 +32,7 @@ test_that("Authentication works", test_that("Persistent authentication works", { - expect_true(is.null(delete_azure_login(tenant, confirm=FALSE))) + expect_true(suppressWarnings(is.null(delete_azure_login(tenant, confirm=FALSE)))) expect_true(all(names(list_azure_logins()) != tenant)) login_dirs <- rappdirs::user_config_dir("AzureRMR", "AzureR", roaming=FALSE) From 9d2b9b2ee55224801035e211dfc09d58dd59a55c Mon Sep 17 00:00:00 2001 From: hong-revo Date: Fri, 11 Jan 2019 05:19:44 +1100 Subject: [PATCH 10/12] force fail if httpuv required but unavailable --- R/AzureToken.R | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/R/AzureToken.R b/R/AzureToken.R index 4ba11d1..bf917ae 100644 --- a/R/AzureToken.R +++ b/R/AzureToken.R @@ -192,6 +192,10 @@ get_azure_token <- function(resource_host, tenant, app, password=NULL, username= if(is.null(auth_type)) auth_type <- select_auth_type(password, username) + # fail if authorization_code selected but httpuv not available + if(auth_type == "authorization_code" && system.file(package="httpuv") == "") + stop("httpuv package must be installed to use authorization_code method", call.=FALSE) + switch(auth_type, client_credentials= auth_with_client_creds(base_url, app, password, resource_host), From 9c00505c91b63b80b1b21df75fb1cc9e133af67c Mon Sep 17 00:00:00 2001 From: hong-revo Date: Fri, 11 Jan 2019 06:28:08 +1100 Subject: [PATCH 11/12] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b8009cd..28e297e 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ![Downloads](https://cranlogs.r-pkg.org/badges/AzureRMR) [![Travis Build Status](https://travis-ci.org/cloudyr/AzureRMR.png?branch=master)](https://travis-ci.org/cloudyr/AzureRMR) -AzureRMR is a package for interacting with Azure Resource Manager: authenticate, list subscriptions, manage resource groups, deploy and delete templates and resources. It calls the Resource Manager [REST API](https://docs.microsoft.com/en-us/rest/api/resources) directly, so you don't need to have PowerShell or Python installed. +AzureRMR is a package for interacting with Azure Active Directory and Azure Resource Manager: obtain AAD authentication tokens, list subscriptions, manage resource groups, deploy and delete templates and resources. It calls the Resource Manager [REST API](https://docs.microsoft.com/en-us/rest/api/resources) directly, so you don't need to have PowerShell or Python installed. You can install the development version from GitHub, via `devtools::install_github("cloudyr/AzureRMR")`. From 03bda29635abfb4a73d57002ba29ee628a142c0f Mon Sep 17 00:00:00 2001 From: hong-revo Date: Fri, 11 Jan 2019 07:11:24 +1100 Subject: [PATCH 12/12] update docs --- NEWS.md | 6 +++++- R/AzureToken.R | 10 +++++++--- R/az_auth.R | 2 +- man/az_rm.Rd | 2 +- man/get_azure_token.Rd | 10 +++++++--- 5 files changed, 21 insertions(+), 9 deletions(-) diff --git a/NEWS.md b/NEWS.md index b091da4..6545519 100644 --- a/NEWS.md +++ b/NEWS.md @@ -3,7 +3,11 @@ ## Significant interface changes * New `create_azure_login`, `get_azure_login` and `delete_azure_login` functions to handle ARM authentication. These will persist the login object across sessions, removing the need to re-authenticate each time. While directly calling `az_rm$new()` will still work, it's recommended to use `create_azure_login` and `get_azure_login` going forward. -* `get_azure_token` revamped, now supports the resource owner authentication method for obtaining an AAD token with a username and password. +* `get_azure_token` revamped, now supports four authentication methods for obtaining AAD tokens: + - Client credentials (what you would use with a "web app" registered service principal) + - Authorization code (for a "native" service principal) + - Device code + - With a username and password (resource owner grant) ## Other changes diff --git a/R/AzureToken.R b/R/AzureToken.R index bf917ae..75e8bb3 100644 --- a/R/AzureToken.R +++ b/R/AzureToken.R @@ -149,16 +149,20 @@ 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 with support for device authentication and with unnecessary options removed. Device authentication removes the need to save a password on your machine. Instead, the server provides you with a code, along with a URL. You then visit the URL in your browser and enter the code, which completes the authentication process. +#' This function does much the same thing as [httr::oauth2.0_token()], but customised for Azure. #' -#' The OAuth authentication type can be one of 4 possible values: "authorization_code", "device_code", "client_credentials" or "resource_owner". If this is not specified, the value is chosen based on the presence or absence of the `password` and `username` arguments: +#' 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. #' -#' - Password and username present: "resource_owner" +#' If the authentication method is not specified, the value is chosen based on the presence or absence of the `password` and `username` arguments: +#' +#' - Password and username present: "resource_owner". In this #' - Password and username absent: "authorization_code" if the httpuv package is installed, "device_code" otherwise #' - Password present, username absent: "client_credentials" #' - Password absent, username present: error #' #' 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 requires you to browse to a URL, your machine should 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. #' #' @seealso #' [AzureToken], [httr::oauth2.0_token], [httr::Token], diff --git a/R/az_auth.R b/R/az_auth.R index 9886cfb..76d381a 100644 --- a/R/az_auth.R +++ b/R/az_auth.R @@ -18,7 +18,7 @@ #' - `app`: The client/app ID to use to authenticate with Azure Active Directory. #' - `password`: if `auth_type == "client_credentials"`, the app secret; if `auth_type == "resource_owner"`, your account password. #' - `username`: if `auth_type == "resource_owner"`, your username. -#' - `auth_type`: The OAuth authentication method to use, one of "client_credentials", "authorization_code", "device_code" or "resource_owner". See [get_azure_token] for how the default method is chosen. +#' - `auth_type`: The OAuth authentication method to use, one of "client_credentials", "authorization_code", "device_code" or "resource_owner". See [get_azure_token] for how the default method is chosen, along with some caveats. #' - `host`: your ARM host. Defaults to `https://management.azure.com/`. Change this if you are using a government or private cloud. #' - `aad_host`: Azure Active Directory host for authentication. Defaults to `https://login.microsoftonline.com/`. Change this if you are using a government or private cloud. #' - `config_file`: Optionally, a JSON file containing any of the arguments listed above. Arguments supplied in this file take priority over those supplied on the command line. You can also use the output from the Azure CLI `az ad sp create-for-rbac` command. diff --git a/man/az_rm.Rd b/man/az_rm.Rd index 26cabf1..fb8f8a6 100644 --- a/man/az_rm.Rd +++ b/man/az_rm.Rd @@ -30,7 +30,7 @@ To authenticate with the \code{az_rm} class directly, provide the following argu \item \code{app}: The client/app ID to use to authenticate with Azure Active Directory. \item \code{password}: if \code{auth_type == "client_credentials"}, the app secret; if \code{auth_type == "resource_owner"}, your account password. \item \code{username}: if \code{auth_type == "resource_owner"}, your username. -\item \code{auth_type}: The OAuth authentication method to use, one of "client_credentials", "authorization_code", "device_code" or "resource_owner". See \link{get_azure_token} for how the default method is chosen. +\item \code{auth_type}: The OAuth authentication method to use, one of "client_credentials", "authorization_code", "device_code" or "resource_owner". See \link{get_azure_token} for how the default method is chosen, along with some caveats. \item \code{host}: your ARM host. Defaults to \code{https://management.azure.com/}. Change this if you are using a government or private cloud. \item \code{aad_host}: Azure Active Directory host for authentication. Defaults to \code{https://login.microsoftonline.com/}. Change this if you are using a government or private cloud. \item \code{config_file}: Optionally, a JSON file containing any of the arguments listed above. Arguments supplied in this file take priority over those supplied on the command line. You can also use the output from the Azure CLI \code{az ad sp create-for-rbac} command. diff --git a/man/get_azure_token.Rd b/man/get_azure_token.Rd index 0a7cbe5..349cace 100644 --- a/man/get_azure_token.Rd +++ b/man/get_azure_token.Rd @@ -27,17 +27,21 @@ get_azure_token(resource_host, tenant, app, password = NULL, This extends the OAuth functionality in httr to allow for device code authentication. } \details{ -This function does much the same thing as \code{\link[httr:oauth2.0_token]{httr::oauth2.0_token()}}, but with support for device authentication and with unnecessary options removed. Device authentication removes the need to save a password on your machine. Instead, the server provides you with a code, along with a URL. You then visit the URL in your browser and enter the code, which completes the authentication process. +This function does much the same thing as \code{\link[httr:oauth2.0_token]{httr::oauth2.0_token()}}, but customised for Azure. -The OAuth authentication type can be one of 4 possible values: "authorization_code", "device_code", "client_credentials" or "resource_owner". If this is not specified, the value is chosen based on the presence or absence of the \code{password} and \code{username} arguments: +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. + +If the authentication method is not specified, the value is chosen based on the presence or absence of the \code{password} and \code{username} arguments: \itemize{ -\item Password and username present: "resource_owner" +\item Password and username present: "resource_owner". In this \item Password and username absent: "authorization_code" if the httpuv package is installed, "device_code" otherwise \item Password present, username absent: "client_credentials" \item Password absent, username present: error } 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 \link[httr:oauth2.0_token]{httr::oauth2.0_token} for more information; note that Azure does not support the \code{use_oob} feature of the httr OAuth 2.0 token class. + +Similarly, since the "authorization_code" method requires you to browse to a URL, your machine should 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. } \examples{ \dontrun{