From 68f9455d612b58db74d0dc5e21c22996dccefe84 Mon Sep 17 00:00:00 2001 From: Daniel Lockau Date: Tue, 5 Jan 2016 11:06:07 +0100 Subject: [PATCH 1/6] This commit works with my local example code, using my Azure credentials. Rd files have not yet been created and tests have not been run. --- R/oauth-endpoint.r | 7 ++++++- R/oauth-init.R | 23 ++++++++++++++--------- R/oauth-refresh.R | 16 +++++++++------- R/oauth-token.r | 15 ++++++++------- 4 files changed, 37 insertions(+), 24 deletions(-) diff --git a/R/oauth-endpoint.r b/R/oauth-endpoint.r index dbf6e54b..629bf490 100644 --- a/R/oauth-endpoint.r +++ b/R/oauth-endpoint.r @@ -60,7 +60,7 @@ print.oauth_endpoint <- function(x, ...) { #' Provides some common OAuth endpoints. #' #' @param name One of the following endpoints: linkedin, twitter, -#' vimeo, google, facebook, github. +#' vimeo, google, facebook, github, azure. #' @export #' @examples #' oauth_endpoints("twitter") @@ -112,6 +112,11 @@ oauth_endpoints <- function(name) { authorize = "authorize", access = "access_token" ), + azure = oauth_endpoint( + base_url = "https://login.windows.net/common/oauth2", + authorize = "authorize", + access = "token" + ), stop("Unknown endpoint", call. = FALSE) ) } diff --git a/R/oauth-init.R b/R/oauth-init.R index aa28faa4..96973922 100644 --- a/R/oauth-init.R +++ b/R/oauth-init.R @@ -46,6 +46,8 @@ init_oauth1.0 <- function(endpoint, app, permission = NULL, #' @inheritParams init_oauth1.0 #' @param type content type used to override incorrect server response #' @param scope a character vector of scopes to request. +#' @param user_params List of named values holding endpoint specific parameters to pass to +#' the server when posting the request for obtaining or refreshing the access token. #' @param use_oob if FALSE, use a local webserver for the OAuth dance. #' Otherwise, provide a URL to the user and prompt for a validation #' code. Defaults to the of the \code{"httr_oob_default"} default, @@ -53,8 +55,8 @@ init_oauth1.0 <- function(endpoint, app, permission = NULL, #' @param is_interactive Is the current environment interactive? #' @export #' @keywords internal -init_oauth2.0 <- function(endpoint, app, scope = NULL, type = NULL, - use_oob = getOption("httr_oob_default"), +init_oauth2.0 <- function(endpoint, app, scope = NULL, user_params = NULL, + type = NULL, use_oob = getOption("httr_oob_default"), is_interactive = interactive()) { if (!use_oob && !is_installed("httpuv")) { message("httpuv not installed, defaulting to out-of-band authentication") @@ -85,13 +87,16 @@ init_oauth2.0 <- function(endpoint, app, scope = NULL, type = NULL, } # Use authorisation code to get (temporary) access token - req <- POST(endpoint$access, encode = "form", - body = list( - client_id = app$key, - client_secret = app$secret, - redirect_uri = redirect_uri, - grant_type = "authorization_code", - code = code)) + req_params <- c( + list( + client_id = app$key, + client_secret = app$secret, + redirect_uri = redirect_uri, + grant_type = "authorization_code", + code = code + ), + user_params) + req <- POST(endpoint$access, encode = "form", body=req_params) stop_for_status(req) content(req, type = type) diff --git a/R/oauth-refresh.R b/R/oauth-refresh.R index 805b6348..bb1a31b2 100644 --- a/R/oauth-refresh.R +++ b/R/oauth-refresh.R @@ -3,18 +3,20 @@ # Refreshes the given token, and returns a new credential with a # valid access_token. Based on: # https://developers.google.com/accounts/docs/OAuth2InstalledApp#refresh -refresh_oauth2.0 <- function(endpoint, app, credentials) { +refresh_oauth2.0 <- function(endpoint, app, credentials, user_params) { if (is.null(credentials$refresh_token)) { stop("Refresh token not available", call. = FALSE) } refresh_url <- endpoint$access - body <- list( - refresh_token = credentials$refresh_token, - client_id = app$key, - client_secret = app$secret, - grant_type = "refresh_token" - ) + body <- c( + list( + refresh_token = credentials$refresh_token, + client_id = app$key, + client_secret = app$secret, + grant_type = "refresh_token" + ), + user_params) response <- POST(refresh_url, body = body, encode = "form") stop_for_status(response) diff --git a/R/oauth-token.r b/R/oauth-token.r index 62c3fcba..d1c53259 100644 --- a/R/oauth-token.r +++ b/R/oauth-token.r @@ -187,12 +187,12 @@ Token1.0 <- R6::R6Class("Token1.0", inherit = Token, list( #' @return A \code{Token2.0} reference class (RC) object. #' @family OAuth #' @export -oauth2.0_token <- function(endpoint, app, scope = NULL, type = NULL, - use_oob = getOption("httr_oob_default"), +oauth2.0_token <- function(endpoint, app, scope = NULL, user_params = NULL, + type = NULL, use_oob = getOption("httr_oob_default"), as_header = TRUE, cache = getOption("httr_oauth_cache")) { - params <- list(scope = scope, type = type, use_oob = use_oob, - as_header = as_header) + params <- list(scope = scope, user_params = user_params, type = type, + use_oob = use_oob, as_header = as_header) Token2.0$new(app = app, endpoint = endpoint, params = params, cache_path = cache) } @@ -202,14 +202,15 @@ oauth2.0_token <- function(endpoint, app, scope = NULL, type = NULL, Token2.0 <- R6::R6Class("Token2.0", inherit = Token, list( init_credentials = function() { self$credentials <- init_oauth2.0(self$endpoint, self$app, - scope = self$params$scope, type = self$params$type, - use_oob = self$params$use_oob) + scope = self$params$scope, user_params = self$params$user_params, + type = self$params$type, use_oob = self$params$use_oob) }, can_refresh = function() { !is.null(self$credentials$refresh_token) }, refresh = function() { - self$credentials <- refresh_oauth2.0(self$endpoint, self$app, self$credentials) + self$credentials <- refresh_oauth2.0(self$endpoint, self$app, + self$credentials, self$params$user_params) self$cache() self }, From 7a9a8d467c0810fa6a7a7a2d9fd309bc4af83852 Mon Sep 17 00:00:00 2001 From: Daniel Lockau Date: Tue, 5 Jan 2016 11:08:29 +0100 Subject: [PATCH 2/6] Add non-working demo as code example for Azure AD interface. --- demo/00Index | 1 + demo/oauth2-azure.r | 52 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 demo/oauth2-azure.r diff --git a/demo/00Index b/demo/00Index index 528fc2f3..04c3aaf5 100644 --- a/demo/00Index +++ b/demo/00Index @@ -7,3 +7,4 @@ oauth2-facebook Using the facebook api with OAuth 2.0 oauth2-github Using the github api with OAuth 2.0 oauth2-google Using the google api with OAuth 2.0 oauth2-linkedin Using linkedin api with OAuth 1.0 +oauth2-azure Using Azure apis with OAuth 2.0 diff --git a/demo/oauth2-azure.r b/demo/oauth2-azure.r new file mode 100644 index 00000000..db9b350d --- /dev/null +++ b/demo/oauth2-azure.r @@ -0,0 +1,52 @@ +# !!! The special redirect URI "urn:ietf:wg:oauth:2.0:oob used +# !!! by httr in case httuv is not installed is currently not +# !!! supported by Azure Active Directory (AAD). +# !!! Therefore it is required to install httpuv to make this work. + +# 1. Register an app app in AAD, e.g. as a "Native app", with +# redirect URI . +# 2. Insert the App name: +app_name <- 'myapp' # not important for authorization grant flow +# 3. Insert the created apps client ID which was issued after app creation: +client_id <- 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' +# In case your app was registered as a web app instead of a native app, +# you might have to add your secret key string here: +client_secret <- NULL +# API resource ID to request access for, e.g. Power BI: +resource_uri <- 'https://analysis.windows.net/powerbi/api' + +# Obtain OAuth2 endpoint settings for azure: +# This uses the "common" endpoint. +# To use a tenant url, create an +# oauth_endpoint(authorize = "https://login.windows.net//oauth2/authorize", +# access = "https://login.windows.net//oauth2/token") +# with replaced by your endpoint ID. +azure_endpoint <- oauth_endpoints('azure') + +# Create the app instance. +myapp <- oauth_app(appname = app_name, + key = client_id, + secret = client_secret) + +# Step through the authorization chain: +# 1. You will be redirected to you authorization endpoint via web browser. +# 2. Once you responded to the request, the endpoint will redirect you to +# the local address specified by httr. +# 3. httr will acquire the authorization code (or error) from the data +# posted to the redirect URI. +# 4. If a code was acquired, httr will contact your authorized token access +# endpoint to obtain the token. +mytoken <- oauth2.0_token(azure_endpoint, myapp, + user_params = list(resource = resource_uri), + use_oob = FALSE) +if (('error' %in% names(mytoken$credentials)) && (nchar(mytoken$credentials$error) > 0)) { + errorMsg <- paste('Error while acquiring token.', + paste('Error message:', mytoken$credentials$error), + paste('Error description:', mytoken$credentials$error_description), + paste('Error code:', mytoken$credentials$error_codes), + sep = '\n') + stop(errorMsg) +} + +# Resource API can be accessed through "mytoken" at this point. + From 5cfdb15f8a3fcd286e865a4403384457d11d59a5 Mon Sep 17 00:00:00 2001 From: Daniel Lockau Date: Tue, 5 Jan 2016 11:47:29 +0100 Subject: [PATCH 3/6] Add optional "user_params" parameter to auth2.0_token. This allows to pass additional parameters during token access or refresh as required by endpoints like Azure AD. --- DESCRIPTION | 2 +- man/init_oauth2.0.Rd | 8 ++++++-- man/oauth2.0_token.Rd | 7 +++++-- man/oauth_endpoints.Rd | 2 +- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 0f4d89d7..52a4649f 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -27,4 +27,4 @@ Suggests: VignetteBuilder: knitr License: MIT + file LICENSE URL: https://github.com/hadley/httr -RoxygenNote: 5.0.0 +RoxygenNote: 5.0.1 diff --git a/man/init_oauth2.0.Rd b/man/init_oauth2.0.Rd index 6395911e..3509b658 100644 --- a/man/init_oauth2.0.Rd +++ b/man/init_oauth2.0.Rd @@ -4,8 +4,9 @@ \alias{init_oauth2.0} \title{Retrieve OAuth 2.0 access token.} \usage{ -init_oauth2.0(endpoint, app, scope = NULL, type = NULL, - use_oob = getOption("httr_oob_default"), is_interactive = interactive()) +init_oauth2.0(endpoint, app, scope = NULL, user_params = NULL, + type = NULL, use_oob = getOption("httr_oob_default"), + is_interactive = interactive()) } \arguments{ \item{endpoint}{An OAuth endpoint, created by \code{\link{oauth_endpoint}}} @@ -15,6 +16,9 @@ init_oauth2.0(endpoint, app, scope = NULL, type = NULL, \item{scope}{a character vector of scopes to request.} +\item{user_params}{List of named values holding endpoint specific parameters to pass to +the server when posting the request for obtaining or refreshing the access token.} + \item{type}{content type used to override incorrect server response} \item{use_oob}{if FALSE, use a local webserver for the OAuth dance. diff --git a/man/oauth2.0_token.Rd b/man/oauth2.0_token.Rd index 1f818797..1b2ce655 100644 --- a/man/oauth2.0_token.Rd +++ b/man/oauth2.0_token.Rd @@ -4,8 +4,8 @@ \alias{oauth2.0_token} \title{Generate an oauth2.0 token.} \usage{ -oauth2.0_token(endpoint, app, scope = NULL, type = NULL, - use_oob = getOption("httr_oob_default"), as_header = TRUE, +oauth2.0_token(endpoint, app, scope = NULL, user_params = NULL, + type = NULL, use_oob = getOption("httr_oob_default"), as_header = TRUE, cache = getOption("httr_oauth_cache")) } \arguments{ @@ -16,6 +16,9 @@ oauth2.0_token(endpoint, app, scope = NULL, type = NULL, \item{scope}{a character vector of scopes to request.} +\item{user_params}{List of named values holding endpoint specific parameters to pass to +the server when posting the request for obtaining or refreshing the access token.} + \item{type}{content type used to override incorrect server response} \item{use_oob}{if FALSE, use a local webserver for the OAuth dance. diff --git a/man/oauth_endpoints.Rd b/man/oauth_endpoints.Rd index de095c87..00c4a209 100644 --- a/man/oauth_endpoints.Rd +++ b/man/oauth_endpoints.Rd @@ -8,7 +8,7 @@ oauth_endpoints(name) } \arguments{ \item{name}{One of the following endpoints: linkedin, twitter, -vimeo, google, facebook, github.} +vimeo, google, facebook, github, azure.} } \description{ Provides some common OAuth endpoints. From bcb72e53a78185823c56cf1501793a3113860599 Mon Sep 17 00:00:00 2001 From: Daniel Lockau Date: Tue, 5 Jan 2016 12:08:28 +0100 Subject: [PATCH 4/6] Make user_params an optional parameter in refresh_oauth2.0. --- R/oauth-refresh.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/oauth-refresh.R b/R/oauth-refresh.R index bb1a31b2..7a5586cb 100644 --- a/R/oauth-refresh.R +++ b/R/oauth-refresh.R @@ -3,7 +3,7 @@ # Refreshes the given token, and returns a new credential with a # valid access_token. Based on: # https://developers.google.com/accounts/docs/OAuth2InstalledApp#refresh -refresh_oauth2.0 <- function(endpoint, app, credentials, user_params) { +refresh_oauth2.0 <- function(endpoint, app, credentials, user_params = NULL) { if (is.null(credentials$refresh_token)) { stop("Refresh token not available", call. = FALSE) } From 98e5fcfa8a17a65b0b6ff9a236d719cb01ab0ebb Mon Sep 17 00:00:00 2001 From: Daniel Lockau Date: Wed, 6 Jan 2016 09:23:20 +0100 Subject: [PATCH 5/6] Add news bullet for new parameter user_params to oauth2.0_token. --- NEWS.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/NEWS.md b/NEWS.md index b50575d4..d4298984 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,9 @@ # httr 1.0.0.9000 +* `oauth2.0_token()` accepts the optional named list parameter `user_params` + which can be used to pass additional parameters to the token access endpoint + when acquiring or refreshing a token, if required by the endpoint protocol. + * `oauth_service_token()` checks that its arguments are the correct types (#282). From 1600fa8489bd835d27d6841eb5f50dbbeda7b3e4 Mon Sep 17 00:00:00 2001 From: Daniel Lockau Date: Wed, 6 Jan 2016 09:58:39 +0100 Subject: [PATCH 6/6] Use modifyList to merge user_params into request parameters of init_oauth2.0 and refresh_oauth2.0. --- R/oauth-init.R | 25 +++++++++++++------------ R/oauth-refresh.R | 16 ++++++++-------- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/R/oauth-init.R b/R/oauth-init.R index 96973922..a5c89bd5 100644 --- a/R/oauth-init.R +++ b/R/oauth-init.R @@ -46,8 +46,9 @@ init_oauth1.0 <- function(endpoint, app, permission = NULL, #' @inheritParams init_oauth1.0 #' @param type content type used to override incorrect server response #' @param scope a character vector of scopes to request. -#' @param user_params List of named values holding endpoint specific parameters to pass to -#' the server when posting the request for obtaining or refreshing the access token. +#' @param user_params Named list holding endpoint specific parameters to pass to +#' the server when posting the request for obtaining or refreshing the +#' access token. #' @param use_oob if FALSE, use a local webserver for the OAuth dance. #' Otherwise, provide a URL to the user and prompt for a validation #' code. Defaults to the of the \code{"httr_oob_default"} default, @@ -87,16 +88,16 @@ init_oauth2.0 <- function(endpoint, app, scope = NULL, user_params = NULL, } # Use authorisation code to get (temporary) access token - req_params <- c( - list( - client_id = app$key, - client_secret = app$secret, - redirect_uri = redirect_uri, - grant_type = "authorization_code", - code = code - ), - user_params) - req <- POST(endpoint$access, encode = "form", body=req_params) + req_params <- list( + client_id = app$key, + client_secret = app$secret, + redirect_uri = redirect_uri, + grant_type = "authorization_code", + code = code) + if (! is.null(user_params)) { + req_params <- modifyList(user_params, req_params); + } + req <- POST(endpoint$access, encode = "form", body = req_params) stop_for_status(req) content(req, type = type) diff --git a/R/oauth-refresh.R b/R/oauth-refresh.R index 7a5586cb..842580b9 100644 --- a/R/oauth-refresh.R +++ b/R/oauth-refresh.R @@ -9,14 +9,14 @@ refresh_oauth2.0 <- function(endpoint, app, credentials, user_params = NULL) { } refresh_url <- endpoint$access - body <- c( - list( - refresh_token = credentials$refresh_token, - client_id = app$key, - client_secret = app$secret, - grant_type = "refresh_token" - ), - user_params) + body <- list( + refresh_token = credentials$refresh_token, + client_id = app$key, + client_secret = app$secret, + grant_type = "refresh_token") + if (! is.null(user_params)) { + body <- modifyList(user_params, body); + } response <- POST(refresh_url, body = body, encode = "form") stop_for_status(response)