diff --git a/NAMESPACE b/NAMESPACE index dd1c1959..38719eee 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -15,7 +15,6 @@ S3method(str,httr2_obfuscated) export("%>%") export(curl_help) export(curl_translate) -export(default_redirect_uri) export(example_github_client) export(example_url) export(iterate_next_request) @@ -43,6 +42,7 @@ export(oauth_flow_client_credentials) export(oauth_flow_device) export(oauth_flow_password) export(oauth_flow_refresh) +export(oauth_redirect_uri) export(oauth_token) export(obfuscate) export(obfuscated) diff --git a/NEWS.md b/NEWS.md index 57e6e62e..5a444c7f 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,12 @@ # httr2 (development version) +* OAuth docs have been clarified to encourage the use of `req_oauth_*()`, + not `oauth_*()` (#330). This includes a new `vignette("oauth")` which + gives many more details about how OAuth works and how to use it with + httr2 (#234). + +* httr2 now informs the user when a token is cached. + * `req_perform_stream()` replaces `req_perform_stream()`. `req_perform_stream()` is now deprecated (#314). @@ -58,9 +65,6 @@ tokens. Additionally, you can now change the cache location by setting the `HTTR2_OAUTH_CACHE` env var. -* New `vignette("oauth")` makes the details of OAuth usage easier to find - (#234). - * New `req_cookie_preserve()` lets you use a file to share cookies across requests (#223). diff --git a/R/oauth-client.R b/R/oauth-client.R index 838204be..edd767c9 100644 --- a/R/oauth-client.R +++ b/R/oauth-client.R @@ -108,19 +108,15 @@ print.httr2_oauth_client <- function(x, ...) { #' There are three built-in strategies: #' #' * `oauth_client_req_body()` adds the client id and (optionally) the secret -#' to the request body, as described in -#' [rfc6749](https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1), -#' Section 2.3.1. +#' to the request body, as described in `r rfc(6749, "2.3.1")`. #' #' * `oauth_client_req_header()` adds the client id and secret using HTTP -#' basic authentication with the `Authorization` header, as described in -#' [rfc6749](https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1), -#' Section 2.3.1. +#' basic authentication with the `Authorization` header, as described +#' in `r rfc(6749, "2.3.1")`. #' #' * `oauth_client_jwt_rs256()` adds a client assertion to the body using a -#' JWT signed with `jwt_sign_rs256()` using a private key, as described in -#' [rfc7523](https://datatracker.ietf.org/doc/html/rfc7523#section-2.2), -#' Section 2.2. +#' JWT signed with `jwt_sign_rs256()` using a private key, as described +#' in `r rfc(7523, 2.2)`. #' #' You will generally not call these functions directly but will instead #' specify them through the `auth` argument to [oauth_client()]. The `req` and diff --git a/R/oauth-flow-auth-code.R b/R/oauth-flow-auth-code.R index 2fa3c7ea..a8eb985e 100644 --- a/R/oauth-flow-auth-code.R +++ b/R/oauth-flow-auth-code.R @@ -1,18 +1,22 @@ -#' OAuth authentication with authorization code +#' OAuth with authorization code #' #' @description -#' This uses [oauth_flow_auth_code()] to generate an access token, which is -#' then used to authentication the request with [req_auth_bearer_token()]. -#' The token is automatically cached (either in memory or on disk) to minimise -#' the number of times the flow is performed. +#' Authenticate using the OAuth **authorization code flow**, as defined +#' by `r rfc(6749, 4.1)`. #' -#' Learn more about the overall flow in `vignette("oauth")`. +#' This flow is the most commonly used OAuth flow where the user +#' opens a page in their browser, approves the access, and then returns to R. +#' When possible, it redirects the browser back to a temporary local webserver +#' to capture the authorization code. When this is not possible (e.g. when +#' running on a hosted platform like RStudio Server), provide a custom +#' `redirect_uri` and httr2 will prompt the user to enter the code manually. +#' +#' Learn more about the overall OAuth authentication flow in `vignette("oauth")`. #' #' # Security considerations #' #' The authorization code flow is used for both web applications and native -#' applications (which are equivalent to R packages). -#' [rfc8252](https://datatracker.ietf.org/doc/html/rfc8252) spells out +#' applications (which are equivalent to R packages). `r rfc(8252)` spells out #' important considerations for native apps. Most importantly there's no way #' for native apps to keep secrets from their users. This means that the #' server should either not require a `client_secret` (i.e. a public client @@ -28,16 +32,56 @@ #' create a new client than find your client secret. #' #' @export +#' @family OAuth flows +#' @seealso [oauth_flow_auth_code_url()] for the components necessary to +#' write your own auth code flow, if the API you are wrapping does not adhere +#' closely to the standard. #' @inheritParams req_perform +#' @param client An [oauth_client()]. +#' @param auth_url Authorization url; you'll need to discover this by reading +#' the documentation. +#' @param scope Scopes to be requested from the resource owner. +#' @param pkce Use "Proof Key for Code Exchange"? This adds an extra layer of +#' security and should always be used if supported by the server. +#' @param auth_params A list containing additional parameters passed to +#' [oauth_flow_auth_code_url()]. +#' @param token_params List containing additional parameters passed to the +#' `token_url`. +#' @param host_name,host_ip,port `r lifecycle::badge("deprecated")` +#' Now use `redirect_uri` instead. +#' @param redirect_uri URL to redirect back to after authorization is complete. +#' Often this must be registered with the API in advance. +#' +#' httr2 supports three forms of redirect. Firstly, you can use a `localhost` +#' url (the default), where httr2 will set up a temporary webserver to listen +#' for the OAuth redirect. In this case, httr2 will automatically append a +#' random port. If you need to set it to a fixed port because the API requires +#' it, then specify it with (e.g.) `"http://localhost:1011"`. This technique +#' works well when you are working on your own computer. +#' +#' Secondly, you can provide a URL to a website that uses Javascript to +#' give the user a code to copy and paste back into the R session (see +#' and +#' +#' for examples). This is less convenient (because it requires more +#' user interaction) but also works in hosted environments like RStudio +#' Server. +#' +#' Finally, hosted platforms might set the `HTTR2_OAUTH_REDIRECT_URL` and +#' `HTTR2_OAUTH_CODE_SOURCE_URL` environment variables. In this case, httr2 +#' will use `HTTR2_OAUTH_REDIRECT_URL` for redirects by default, and poll the +#' `HTTR2_OAUTH_CODE_SOURCE_URL` endpoint with the state parameter until it +#' receives a code in the response (or encounters an error). This delegates +#' completion of the authorization flow to the hosted platform. #' @param cache_disk Should the access token be cached on disk? This reduces #' the number of times that you need to re-authenticate at the cost of -#' storing access credentials on disk. Cached tokens are encrypted, -#' automatically deleted 30 days after creation, and stored in -#' [oauth_cache_path()]. +#' storing access credentials on disk. +#' +#' Learn more in `vignette("oauth")` #' @param cache_key If you want to cache multiple tokens per app, use this #' key to disambiguate them. -#' @returns A modified HTTP [request]. -#' @inheritParams oauth_flow_auth_code +#' @returns `req_oauth_auth_code()` returns a modified HTTP [request] that will +#' use OAuth; `oauth_flow_auth_code()` returns an [oauth_token]. #' @examples #' req_auth_github <- function(req) { #' req_oauth_auth_code( @@ -49,19 +93,19 @@ #' #' request("https://api.github.com/user") %>% #' req_auth_github() -req_oauth_auth_code <- function(req, client, +req_oauth_auth_code <- function(req, + client, auth_url, - cache_disk = FALSE, - cache_key = NULL, scope = NULL, pkce = TRUE, auth_params = list(), token_params = list(), - redirect_uri = default_redirect_uri(), + redirect_uri = oauth_redirect_uri(), + cache_disk = FALSE, + cache_key = NULL, host_name = deprecated(), host_ip = deprecated(), - port = deprecated() - ) { + port = deprecated()) { redirect <- normalize_redirect_uri( redirect_uri = redirect_uri, @@ -84,99 +128,15 @@ req_oauth_auth_code <- function(req, client, req_oauth(req, "oauth_flow_auth_code", params, cache = cache) } -#' OAuth flow: authorization code -#' -#' @description -#' These functions implement the OAuth authorization code flow, as defined -#' by [rfc6749](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1), -#' Section 4.1. This is the most commonly used OAuth flow where the user is -#' opens a page in their browser, approves the access, and then returns to R. -#' -#' `oauth_flow_auth_code()` is a high-level wrapper that should work with APIs -#' that adhere relatively closely to the spec. When possible, it redirects the -#' browser back to a temporary local webserver to capture the authorization -#' code. When this is not possible (e.g. when running on a hosted platform -#' like RStudio Server), provide a custom redirect URI and httr2 will prompt the -#' user to enter the code manually instead. -#' -#' `default_redirect_uri()` returns `http://localhost` but also respects the -#' `HTTR2_OAUTH_REDIRECT_URL` environment variable. -#' -#' The remaining low-level functions can be used to assemble a custom flow for -#' APIs that are further from the spec: -#' -#' * `oauth_flow_auth_code_url()` generates the url that should be opened in a -#' browser. -#' * `oauth_flow_auth_code_listen()` starts a temporary local webserver that -#' listens for the response from the resource server. -#' * `oauth_flow_auth_code_parse()` parses the query parameters returned from -#' the server redirect, verifying that the `state` is correct, and returning -#' the authorisation code. -#' * `oauth_flow_auth_code_pkce()` generates code verifier, method, and challenge -#' components as needed for PKCE, as defined in -#' [rfc7636](https://datatracker.ietf.org/doc/html/rfc7636). -#' -#' @family OAuth flows -#' @param client An [oauth_client()]. -#' @param auth_url Authorization url; you'll need to discover this by reading -#' the documentation. -#' @param scope Scopes to be requested from the resource owner. -#' @param pkce Use "Proof Key for Code Exchange"? This adds an extra layer of -#' security and should always be used if supported by the server. -#' @param auth_params List containing additional parameters passed to `oauth_flow_auth_code_url()` -#' @param token_params List containing additional parameters passed to the -#' `token_url`. -#' @param host_name,host_ip,port `r lifecycle::badge("deprecated")` -#' Now use `redirect_uri` instead. -#' @param redirect_uri URL to redirect back to after authorization is complete. -#' Often this must be registered with the API in advance. -#' -#' httr2 supports three forms of redirect. Firstly, you can use a `localhost` -#' url (the default), where httr2 will set up a temporary webserver to listen -#' for the OAuth redirect. In this case, httr2 will automatically append a -#' random port. If you need to set it to a fixed port because the API requires -#' it, then specify it with (e.g.) `"http://localhost:1011"`. This technique -#' works well when you are working on your own computer. -#' -#' Secondly, you can provide a URL to a website that uses Javascript to -#' give the user a code to copy and paste back into the R session (see -#' and -#' -#' for examples). This is less convenient (because it requires more -#' user interaction) but also works in hosted environments like RStudio -#' Server. -#' -#' Finally, hosted platforms might set the `HTTR2_OAUTH_REDIRECT_URL` and -#' `HTTR2_OAUTH_CODE_SOURCE_URL` environment variables. In this case, httr2 -#' will use `HTTR2_OAUTH_REDIRECT_URL` for redirects by default, and poll the -#' `HTTR2_OAUTH_CODE_SOURCE_URL` endpoint with the state parameter until it -#' receives a code in the response (or encounters an error). This delegates -#' completion of the authorization flow to the hosted platform. -#' -#' @returns An [oauth_token]. #' @export -#' @keywords internal -#' @examples -#' client <- oauth_client( -#' id = "28acfec0674bb3da9f38", -#' secret = obfuscated(paste0( -#' "J9iiGmyelHltyxqrHXW41ZZPZamyUNxSX1_uKnv", -#' "PeinhhxET_7FfUs2X0LLKotXY2bpgOMoHRCo" -#' )), -#' token_url = "https://github.com/login/oauth/access_token", -#' name = "hadley-oauth-test" -#' ) -#' if (interactive()) { -#' token <- oauth_flow_auth_code(client, auth_url = "https://github.com/login/oauth/authorize") -#' token -#' } +#' @rdname req_oauth_auth_code oauth_flow_auth_code <- function(client, auth_url, scope = NULL, pkce = TRUE, auth_params = list(), token_params = list(), - redirect_uri = default_redirect_uri(), + redirect_uri = oauth_redirect_uri(), host_name = deprecated(), host_ip = deprecated(), port = deprecated() @@ -293,16 +253,38 @@ normalize_redirect_uri <- function(redirect_uri, } + +#' Default redirect url for OAuth +#' +#' The default redirect uri used by [req_oauth_auth_code()]. Defaults to +#' `http://localhost` unless the `HTTR2_OAUTH_REDIRECT_URL` envvar is set. +#' #' @export -#' @rdname oauth_flow_auth_code -default_redirect_uri <- function() { +oauth_redirect_uri <- function() { Sys.getenv("HTTR2_OAUTH_REDIRECT_URL", "http://localhost") } # Authorisation request: make a url that the user navigates to # https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1 + +#' OAuth authorization code components +#' +#' @description +#' These low-level functions can be used to assemble a custom flow for +#' APIs that are further from the spec: +#' +#' * `oauth_flow_auth_code_url()` generates the url that should be opened in a +#' browser. +#' * `oauth_flow_auth_code_listen()` starts a temporary local webserver that +#' listens for the response from the resource server. +#' * `oauth_flow_auth_code_parse()` parses the query parameters returned from +#' the server redirect, verifying that the `state` is correct, and returning +#' the authorisation code. +#' * `oauth_flow_auth_code_pkce()` generates code verifier, method, and challenge +#' components as needed for PKCE, as defined in `r rfc(7636)`. +#' #' @export -#' @rdname oauth_flow_auth_code +#' @keywords internal #' @param state Random state generated by `oauth_flow_auth_code()`. Used to #' verify that we're working with an authentication request that we created. #' (This is an unlikely threat for R packages since the webserver that @@ -326,7 +308,7 @@ oauth_flow_auth_code_url <- function(client, } #' @export -#' @rdname oauth_flow_auth_code +#' @rdname oauth_flow_auth_code_url oauth_flow_auth_code_listen <- function(redirect_uri = "http://localhost:1410") { parsed <- url_parse(redirect_uri) port <- as.integer(parsed$port) @@ -387,7 +369,7 @@ parse_form_urlencoded <- function(query) { # Authorisation response: get query params back from redirect # https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2 #' @export -#' @rdname oauth_flow_auth_code +#' @rdname oauth_flow_auth_code_url #' @param query List of query parameters returned by `oauth_flow_auth_code_listen()`. oauth_flow_auth_code_parse <- function(query, state) { if (has_name(query, "error")) { @@ -404,7 +386,7 @@ oauth_flow_auth_code_parse <- function(query, state) { } #' @export -#' @rdname oauth_flow_auth_code +#' @rdname oauth_flow_auth_code_url oauth_flow_auth_code_pkce <- function() { # https://datatracker.ietf.org/doc/html/rfc7636#section-4.1 # diff --git a/R/oauth-flow-client-credentials.R b/R/oauth-flow-client-credentials.R index 914dcce3..6f512d4e 100644 --- a/R/oauth-flow-client-credentials.R +++ b/R/oauth-flow-client-credentials.R @@ -1,25 +1,32 @@ -#' OAuth authentication with client credentials +#' OAuth with client credentials #' #' @description -#' This uses [oauth_flow_client_credentials()] to generate an access token, -#' which is then used to authentication the request with [req_auth_bearer_token()]. -#' The token is cached in memory. +#' Authenticate using OAuth **client credentials flow**, as defined by +#' `r rfc(6749, 4.4)`. It is used to allow the client to access resources that +#' it controls directly, not on behalf of an user. #' -#' Learn more about the overall flow in `vignette("oauth")`. +#' Learn more about the overall OAuth authentication flow in `vignette("oauth")`. #' #' @export +#' @family OAuth flows #' @inheritParams req_perform -#' @inheritParams oauth_flow_client_credentials -#' @returns A modified HTTP [request]. +#' @inheritParams req_oauth_auth_code +#' @returns `req_oauth_client_credentials()` returns a modified HTTP [request] that will +#' use OAuth; `oauth_flow_client_credentials()` returns an [oauth_token]. #' @examples -#' client <- oauth_client("example", "https://example.com/get_token") -#' req <- request("https://example.com") +#' req_auth <- function(req) { +#' req_oauth_client_credentials( +#' req, +#' client = oauth_client("example", "https://example.com/get_token") +#' ) +#' } #' -#' req %>% req_oauth_client_credentials(client) -req_oauth_client_credentials <- function(req, client, +#' request("https://example.com") %>% +#' req_auth() +req_oauth_client_credentials <- function(req, + client, scope = NULL, - token_params = list() - ) { + token_params = list()) { params <- list( client = client, @@ -31,22 +38,11 @@ req_oauth_client_credentials <- function(req, client, req_oauth(req, "oauth_flow_client_credentials", params, cache = cache) } -#' OAuth flow: client credentials -#' -#' This function implements the OAuth client credentials flow, as defined -#' by [rfc6749](https://datatracker.ietf.org/doc/html/rfc6749#section-4.4), -#' Section 4.4. It is used to allow the client to access resources that it -#' controls directly, not on behalf of an user. -#' -#' @inheritParams oauth_flow_auth_code -#' @returns An [oauth_token]. #' @export -#' @family OAuth flows -#' @keywords internal +#' @rdname req_oauth_client_credentials oauth_flow_client_credentials <- function(client, scope = NULL, - token_params = list() - ) { + token_params = list()) { oauth_flow_check("client credentials", client, is_confidential = TRUE) oauth_client_get_token(client, diff --git a/R/oauth-flow-device.R b/R/oauth-flow-device.R index 5ea9a275..eef7c7fd 100644 --- a/R/oauth-flow-device.R +++ b/R/oauth-flow-device.R @@ -1,17 +1,18 @@ -#' OAuth authentication with device flow +#' OAuth with device flow #' #' @description -#' This uses [oauth_flow_device()] to generate an access token, which is -#' then used to authentication the request with [req_auth_bearer_token()]. -#' The token is automatically cached (either in memory or on disk) to minimise -#' the number of times the flow is performed. +#' Authenticate using the OAuth **device flow**, as defined by `r rfc(8626)`. +#' It's designed for devices that don't have access to a web browser (if you've +#' ever authenticated an app on your TV, this is probably the flow you've used), +#' but it also works well from within R. #' -#' Learn more about the overall flow in `vignette("oauth")`. +#' Learn more about the overall OAuth authentication flow in `vignette("oauth")`. #' #' @export #' @inheritParams oauth_flow_password #' @inheritParams req_oauth_auth_code -#' @returns A modified HTTP [request]. +#' @returns `req_oauth_device()` returns a modified HTTP [request] that will +#' use OAuth; `oauth_flow_device()` returns an [oauth_token]. #' @examples #' req_auth_github <- function(req) { #' req_oauth_device( @@ -23,12 +24,14 @@ #' #' request("https://api.github.com/user") %>% #' req_auth_github() -req_oauth_device <- function(req, client, auth_url, - cache_disk = FALSE, - cache_key = NULL, +req_oauth_device <- function(req, + client, + auth_url, scope = NULL, auth_params = list(), - token_params = list()) { + token_params = list(), + cache_disk = FALSE, + cache_key = NULL) { params <- list( client = client, @@ -41,26 +44,8 @@ req_oauth_device <- function(req, client, auth_url, req_oauth(req, "oauth_flow_device", params, cache = cache) } -#' OAuth flow: device -#' -#' @description -#' These functions implement the OAuth device flow, as defined -#' by [rfc8628](https://datatracker.ietf.org/doc/html/rfc8628). It's designed -#' for devices that don't have access to a web browser (if you've ever -#' authenticated an app on your TV, this is probably the flow you've used), -#' but it also works well from within R. -#' -#' This specification allows also some subspecifications: -#' * `oauth_flow_auth_code_pkce()` is also reused here to generate code -#' verifier, method, and challenge components as needed for PKCE, as -#' defined in [rfc7636](https://datatracker.ietf.org/doc/html/rfc7636). -#' -#' @inheritParams oauth_flow_auth_code -#' @returns An [oauth_token]. #' @export -#' @family OAuth flows -#' @keywords internal -#' @keywords internal +#' @rdname req_oauth_device oauth_flow_device <- function(client, auth_url, pkce = FALSE, diff --git a/R/oauth-flow-jwt.R b/R/oauth-flow-jwt.R index 28d4aa08..3e5962d2 100644 --- a/R/oauth-flow-jwt.R +++ b/R/oauth-flow-jwt.R @@ -1,22 +1,38 @@ -#' OAuth authentication with a bearer JWT (JSON web token) +#' OAuth with a bearer JWT (JSON web token) #' #' @description -#' This uses [oauth_flow_bearer_jwt()] to generate an access token which is then -#' used to authenticate the request with [req_auth_bearer_token()]. -#' The token is cached in memory. +#' Authenticate using a **Bearer JWT** (JSON web token) as an authorization +#' grant to get an access token, as defined by `r rfc(7523, 2.1)`. +#' It is often used for service accounts, accounts that are used primarily in +#' automated environments. #' -#' Learn more about the overall flow in `vignette("oauth")`. +#' Learn more about the overall OAuth authentication flow in `vignette("oauth")`. #' #' @export +#' @family OAuth flows #' @inheritParams req_perform -#' @inheritParams oauth_flow_bearer_jwt -#' @returns A modified HTTP [request]. +#' @inheritParams req_oauth_auth_code +#' @param claim A list of claims. If all elements of the claim set are static +#' apart from `iat`, `nbf`, `exp`, or `jti`, provide a list and +#' [jwt_claim()] will automatically fill in the dynamic components. +#' If other components need to vary, you can instead provide a zero-argument +#' callback function which should call `jwt_claim()`. +#' @param signature Function use to sign `claim`, e.g. [jwt_encode_sig()]. +#' @param signature_params Additional arguments passed to `signature`, e.g. +#' `size`, `header`. +#' @returns `req_oauth_bearer_jwt()` returns a modified HTTP [request] that will +#' use OAuth; `oauth_flow_bearer_jwt()` returns an [oauth_token]. #' @examples -#' client <- oauth_client("example", "https://example.com/get_token") -#' claim <- jwt_claim() -#' req <- request("https://example.com") +#' req_auth <- function(req) { +#' req_oauth_bearer_jwt( +#' req, +#' client = oauth_client("example", "https://example.com/get_token"), +#' claim = jwt_claim() +#' ) +#' } #' -#' req %>% req_oauth_bearer_jwt(client, claim) +#' request("https://example.com") %>% +#' req_auth() req_oauth_bearer_jwt <- function(req, client, claim, @@ -38,26 +54,8 @@ req_oauth_bearer_jwt <- function(req, req_oauth(req, "oauth_flow_bearer_jwt", params, cache = cache) } -#' OAuth flow: Bearer JWT -#' -#' This function uses a Bearer JWT (JSON web token) as an authorization grant to get an access -#' token, as defined by [rfc7523](https://datatracker.ietf.org/doc/html/rfc7523#section-2.1), -#' Section 2.1. It is often used for service accounts, accounts that are -#' used primarily in automated environments. -#' -#' @inheritParams oauth_flow_auth_code -#' @family OAuth flows -#' @param claim A list of claims. If all elements of the claim set are static -#' apart from `iat`, `nbf`, `exp`, or `jti`, provide a list and -#' [jwt_claim()] will automatically fill in the dynamic components. -#' If other components need to vary, you can instead provide a zero-argument -#' callback function which should call `jwt_claim()`. -#' @param signature Function use to sign `claim`, e.g. [jwt_encode_sig()]. -#' @param signature_params Additional arguments passed to `signature`, e.g. -#' `size`, `header`. -#' @returns An [oauth_token]. #' @export -#' @keywords internal +#' @rdname req_oauth_bearer_jwt oauth_flow_bearer_jwt <- function(client, claim, signature = "jwt_encode_sig", diff --git a/R/oauth-flow-password.R b/R/oauth-flow-password.R index 27e7e131..33a3c20b 100644 --- a/R/oauth-flow-password.R +++ b/R/oauth-flow-password.R @@ -1,31 +1,37 @@ -#' OAuth authentication with username and password +#' OAuth with username and password #' #' @description -#' This uses [oauth_flow_password()] to generate an access token, which is -#' then used to authentication the request with [req_auth_bearer_token()]. -#' The token, not the password is automatically cached (either in memory -#' or on disk); the password is used once to get the token and is then -#' discarded. +#' This function implements the OAuth **resource owner password flow**, as +#' defined by `r rfc(6749, 4.3)`. It allows the user to supply their password +#' once, exchanging it for an access token that can be cached locally. #' -#' Learn more about the overall flow in `vignette("oauth")`. +#' Learn more about the overall OAuth authentication flow in `vignette("oauth")`. #' #' @export -#' @inheritParams oauth_flow_password +#' @family OAuth flows #' @inheritParams req_oauth_auth_code -#' @returns A modified HTTP [request]. +#' @inheritParams req_auth_basic +#' @returns `req_oauth_password()` returns a modified HTTP [request] that will +#' use OAuth; `oauth_flow_password()` returns an [oauth_token]. #' @examples -#' client <- oauth_client("example", "https://example.com/get_token") -#' req <- request("https://example.com") -#' +#' req_auth <- function(req) { +#' req_oauth_password(req, +#' client = oauth_client("example", "https://example.com/get_token"), +#' username = "username" +#' ) +#' } #' if (interactive()) { -#' req %>% req_oauth_password(client, "username") +#' request("https://example.com") %>% +#' req_auth() #' } -req_oauth_password <- function(req, client, +req_oauth_password <- function(req, + client, username, password = NULL, - cache_disk = FALSE, scope = NULL, - token_params = list()) { + token_params = list(), + cache_disk = FALSE, + cache_key = username) { password <- check_password(password) params <- list( @@ -35,29 +41,17 @@ req_oauth_password <- function(req, client, scope = scope, token_params = token_params ) - cache <- cache_choose(client, cache_disk = cache_disk, cache_key = username) + cache <- cache_choose(client, cache_disk = cache_disk, cache_key = cache_key) req_oauth(req, "oauth_flow_password", params, cache = cache) } -#' OAuth flow: user password -#' -#' This function implements the OAuth resource owner password flow, as defined -#' by [rfc6749](https://datatracker.ietf.org/doc/html/rfc6749#section-4.3), -#' Section 4.3. It allows the user to supply their password once, exchanging -#' it for an access token that can be cached locally. -#' -#' @inheritParams oauth_flow_auth_code -#' @inheritParams req_auth_basic #' @export -#' @family OAuth flows -#' @returns An [oauth_token]. -#' @keywords internal +#' @rdname req_oauth_password oauth_flow_password <- function(client, username, password = NULL, scope = NULL, - token_params = list() -) { + token_params = list()) { oauth_flow_check("resource owner password credentials", client, interactive = is.null(password) ) diff --git a/R/oauth-flow-refresh.R b/R/oauth-flow-refresh.R index b5a5859f..bd4d9fca 100644 --- a/R/oauth-flow-refresh.R +++ b/R/oauth-flow-refresh.R @@ -1,28 +1,37 @@ -#' OAuth authentication with a refresh token +#' OAuth with a refresh token #' #' @description -#' This uses [oauth_flow_refresh()] to generate an access token, which is -#' then used to authenticate the request with [req_auth_bearer_token()]. -#' This is primarily useful for testing: you can manually execute another OAuth -#' flow (e.g. by calling [oauth_flow_auth_code()] or [oauth_flow_device()]), +#' Authenticate using a **refresh token**, following the process described in +#' `r rfc(6749, 6)`. +#' +#' This technique is primarily useful for testing: you can manually retrieve +#' a OAuth token using another OAuth flow (e.g. with [oauth_flow_auth_code()]), #' extract the refresh token from the result, and then save in an environment -#' variable for future use in automated tests. +#' variable for use in automated tests. #' #' When requesting an access token, the server may also return a new refresh -#' token. If this happens, `oauth_flow_refresh()` will warn, and you'll have to -#' update your stored refresh token. +#' token. If this happens, `oauth_flow_refresh()` will warn, and you'll have +#' retrieve a new update refresh token and update the stored value. If you find +#' this happening a lot, it's a sign that you should be using a different flow +#' in your automated tests. #' -#' Learn more about the overall flow in `vignette("oauth")`. +#' Learn more about the overall OAuth authentication flow in `vignette("oauth")`. #' +#' @inheritParams req_oauth_auth_code +#' @param refresh_token A refresh token. This is equivalent to a password +#' so shouldn't be typed into the console or stored in a script. Instead, +#' we recommend placing in an environment variable; the default behaviour +#' is to look in `HTTR2_REFRESH_TOKEN`. +#' @returns `req_oauth_refresh()` returns a modified HTTP [request] that will +#' use OAuth; `oauth_flow_refresh()` returns an [oauth_token]. +#' @family OAuth flows #' @export -#' @inheritParams req_perform -#' @inheritParams oauth_flow_refresh -#' @returns A modified HTTP [request]. #' @examples #' client <- oauth_client("example", "https://example.com/get_token") #' req <- request("https://example.com") #' req %>% req_oauth_refresh(client) -req_oauth_refresh <- function(req, client, +req_oauth_refresh <- function(req, + client, refresh_token = Sys.getenv("HTTR2_REFRESH_TOKEN"), scope = NULL, token_params = list()) { @@ -38,24 +47,8 @@ req_oauth_refresh <- function(req, client, req_oauth(req, "oauth_flow_refresh", params, cache = cache) } -#' OAuth flow: refresh token -#' -#' @description -#' This function generates an access token from a refresh token, following -#' the process described in -#' [rfc6749](https://datatracker.ietf.org/doc/html/rfc6749#section-6), -#' Section 6. Warns if the refresh returns a new refresh token, see -#' [req_oauth_refresh()] for details. -#' -#' @inheritParams oauth_flow_auth_code -#' @param refresh_token A refresh token. This is equivalent to a password -#' so shouldn't be typed into the console or stored in a script. Instead, -#' we recommend placing in an environment variable; the default behaviour -#' is to look in `HTTR2_REFRESH_TOKEN`. -#' @family OAuth flows -#' @returns An [oauth_token]. #' @export -#' @keywords internal +#' @rdname req_oauth_refresh oauth_flow_refresh <- function(client, refresh_token = Sys.getenv("HTTR2_REFRESH_TOKEN"), scope = NULL, diff --git a/R/oauth.R b/R/oauth.R index 2012f443..859aa6d2 100644 --- a/R/oauth.R +++ b/R/oauth.R @@ -103,7 +103,10 @@ cache_disk <- function(client, key) { path <- file.path(app_path, paste0(hash(key), "-token.rds.enc")) list( get = function() if (file.exists(path)) secret_read_rds(path, obfuscate_key()) else NULL, - set = function(token) secret_write_rds(token, path, obfuscate_key()), + set = function(token) { + cli::cli_inform("Caching httr2 token in {.path {path}}.") + secret_write_rds(token, path, obfuscate_key()) + }, clear = function() if (file.exists(path)) file.remove(path) ) } diff --git a/R/resp-headers.R b/R/resp-headers.R index a82f9c87..9f9daf38 100644 --- a/R/resp-headers.R +++ b/R/resp-headers.R @@ -148,8 +148,7 @@ resp_retry_after <- function(resp) { #' Parse link URL from a response #' -#' Parses URLs out of the the `Link` header as defined by -#' [rfc8288](https://datatracker.ietf.org/doc/html/rfc8288). +#' Parses URLs out of the the `Link` header as defined by `r rfc(8288)`. #' #' @export #' @inheritParams resp_headers diff --git a/R/roxygen2.R b/R/roxygen2.R new file mode 100644 index 00000000..fcf6c0ba --- /dev/null +++ b/R/roxygen2.R @@ -0,0 +1,10 @@ +rfc <- function(num, sec = NULL) { + paste0( + "[", + if (!is.null(sec)) paste0("Section ", sec, " of "), + "RFC ", num, "]", + "(https://datatracker.ietf.org/doc/html/rfc", num, + if (!is.null(sec)) paste0("#section-", sec), + ")" + ) +} diff --git a/R/url.R b/R/url.R index 4622653b..3825ce84 100644 --- a/R/url.R +++ b/R/url.R @@ -1,9 +1,8 @@ #' Parse and build URLs #' #' `url_parse()` parses a URL into its component pieces; `url_build()` does -#' the reverse, converting a list of pieces into a string URL. See -#' [rfc3986](https://www.rfc-editor.org/rfc/rfc3986) for details of parsing -#' algorithm. +#' the reverse, converting a list of pieces into a string URL. See `r rfc(3986)` +#' for the details of the parsing algorithm. #' #' @param url For `url_parse()` a string to parse into a URL; #' for `url_build()` a URL to turn back into a string. diff --git a/_pkgdown.yml b/_pkgdown.yml index 5eada2f8..330534f3 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -75,8 +75,9 @@ reference: desc: > These functions implement the low-level components of OAuth. contents: - - starts_with("oauth_") - starts_with("jwt_") + - starts_with("oauth_") + - -starts_with("req_oauth") articles: - title: Using httr2 diff --git a/man/oauth_client_req_auth.Rd b/man/oauth_client_req_auth.Rd index 6b7a4fab..e753bc53 100644 --- a/man/oauth_client_req_auth.Rd +++ b/man/oauth_client_req_auth.Rd @@ -40,17 +40,13 @@ to authenticate a request on behalf of a user. There are three built-in strategies: \itemize{ \item \code{oauth_client_req_body()} adds the client id and (optionally) the secret -to the request body, as described in -\href{https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1}{rfc6749}, -Section 2.3.1. +to the request body, as described in \href{https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1}{Section 2.3.1 of RFC 6749}. \item \code{oauth_client_req_header()} adds the client id and secret using HTTP -basic authentication with the \code{Authorization} header, as described in -\href{https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1}{rfc6749}, -Section 2.3.1. +basic authentication with the \code{Authorization} header, as described +in \href{https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1}{Section 2.3.1 of RFC 6749}. \item \code{oauth_client_jwt_rs256()} adds a client assertion to the body using a -JWT signed with \code{jwt_sign_rs256()} using a private key, as described in -\href{https://datatracker.ietf.org/doc/html/rfc7523#section-2.2}{rfc7523}, -Section 2.2. +JWT signed with \code{jwt_sign_rs256()} using a private key, as described +in \href{https://datatracker.ietf.org/doc/html/rfc7523#section-2.2}{Section 2.2 of RFC 7523}. } You will generally not call these functions directly but will instead diff --git a/man/oauth_flow_auth_code.Rd b/man/oauth_flow_auth_code.Rd deleted file mode 100644 index c7de3f1e..00000000 --- a/man/oauth_flow_auth_code.Rd +++ /dev/null @@ -1,151 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/oauth-flow-auth-code.R -\name{oauth_flow_auth_code} -\alias{oauth_flow_auth_code} -\alias{default_redirect_uri} -\alias{oauth_flow_auth_code_url} -\alias{oauth_flow_auth_code_listen} -\alias{oauth_flow_auth_code_parse} -\alias{oauth_flow_auth_code_pkce} -\title{OAuth flow: authorization code} -\usage{ -oauth_flow_auth_code( - client, - auth_url, - scope = NULL, - pkce = TRUE, - auth_params = list(), - token_params = list(), - redirect_uri = default_redirect_uri(), - host_name = deprecated(), - host_ip = deprecated(), - port = deprecated() -) - -default_redirect_uri() - -oauth_flow_auth_code_url( - client, - auth_url, - redirect_uri = NULL, - scope = NULL, - state = NULL, - auth_params = list() -) - -oauth_flow_auth_code_listen(redirect_uri = "http://localhost:1410") - -oauth_flow_auth_code_parse(query, state) - -oauth_flow_auth_code_pkce() -} -\arguments{ -\item{client}{An \code{\link[=oauth_client]{oauth_client()}}.} - -\item{auth_url}{Authorization url; you'll need to discover this by reading -the documentation.} - -\item{scope}{Scopes to be requested from the resource owner.} - -\item{pkce}{Use "Proof Key for Code Exchange"? This adds an extra layer of -security and should always be used if supported by the server.} - -\item{auth_params}{List containing additional parameters passed to \code{oauth_flow_auth_code_url()}} - -\item{token_params}{List containing additional parameters passed to the -\code{token_url}.} - -\item{redirect_uri}{URL to redirect back to after authorization is complete. -Often this must be registered with the API in advance. - -httr2 supports three forms of redirect. Firstly, you can use a \code{localhost} -url (the default), where httr2 will set up a temporary webserver to listen -for the OAuth redirect. In this case, httr2 will automatically append a -random port. If you need to set it to a fixed port because the API requires -it, then specify it with (e.g.) \code{"http://localhost:1011"}. This technique -works well when you are working on your own computer. - -Secondly, you can provide a URL to a website that uses Javascript to -give the user a code to copy and paste back into the R session (see -\url{https://www.tidyverse.org/google-callback/} and -\url{https://github.com/r-lib/gargle/blob/main/inst/pseudo-oob/google-callback/index.html} -for examples). This is less convenient (because it requires more -user interaction) but also works in hosted environments like RStudio -Server. - -Finally, hosted platforms might set the \code{HTTR2_OAUTH_REDIRECT_URL} and -\code{HTTR2_OAUTH_CODE_SOURCE_URL} environment variables. In this case, httr2 -will use \code{HTTR2_OAUTH_REDIRECT_URL} for redirects by default, and poll the -\code{HTTR2_OAUTH_CODE_SOURCE_URL} endpoint with the state parameter until it -receives a code in the response (or encounters an error). This delegates -completion of the authorization flow to the hosted platform.} - -\item{host_name, host_ip, port}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} -Now use \code{redirect_uri} instead.} - -\item{state}{Random state generated by \code{oauth_flow_auth_code()}. Used to -verify that we're working with an authentication request that we created. -(This is an unlikely threat for R packages since the webserver that -listens for authorization responses is transient.)} - -\item{query}{List of query parameters returned by \code{oauth_flow_auth_code_listen()}.} -} -\value{ -An \link{oauth_token}. -} -\description{ -These functions implement the OAuth authorization code flow, as defined -by \href{https://datatracker.ietf.org/doc/html/rfc6749#section-4.1}{rfc6749}, -Section 4.1. This is the most commonly used OAuth flow where the user is -opens a page in their browser, approves the access, and then returns to R. - -\code{oauth_flow_auth_code()} is a high-level wrapper that should work with APIs -that adhere relatively closely to the spec. When possible, it redirects the -browser back to a temporary local webserver to capture the authorization -code. When this is not possible (e.g. when running on a hosted platform -like RStudio Server), provide a custom redirect URI and httr2 will prompt the -user to enter the code manually instead. - -\code{default_redirect_uri()} returns \verb{http://localhost} but also respects the -\code{HTTR2_OAUTH_REDIRECT_URL} environment variable. - -The remaining low-level functions can be used to assemble a custom flow for -APIs that are further from the spec: -\itemize{ -\item \code{oauth_flow_auth_code_url()} generates the url that should be opened in a -browser. -\item \code{oauth_flow_auth_code_listen()} starts a temporary local webserver that -listens for the response from the resource server. -\item \code{oauth_flow_auth_code_parse()} parses the query parameters returned from -the server redirect, verifying that the \code{state} is correct, and returning -the authorisation code. -\item \code{oauth_flow_auth_code_pkce()} generates code verifier, method, and challenge -components as needed for PKCE, as defined in -\href{https://datatracker.ietf.org/doc/html/rfc7636}{rfc7636}. -} -} -\examples{ -client <- oauth_client( - id = "28acfec0674bb3da9f38", - secret = obfuscated(paste0( - "J9iiGmyelHltyxqrHXW41ZZPZamyUNxSX1_uKnv", - "PeinhhxET_7FfUs2X0LLKotXY2bpgOMoHRCo" - )), - token_url = "https://github.com/login/oauth/access_token", - name = "hadley-oauth-test" -) -if (interactive()) { - token <- oauth_flow_auth_code(client, auth_url = "https://github.com/login/oauth/authorize") - token -} -} -\seealso{ -Other OAuth flows: -\code{\link{oauth_flow_bearer_jwt}()}, -\code{\link{oauth_flow_client_credentials}()}, -\code{\link{oauth_flow_device}()}, -\code{\link{oauth_flow_password}()}, -\code{\link{oauth_flow_refresh}()} -} -\concept{OAuth flows} -\keyword{internal} diff --git a/man/oauth_flow_auth_code_url.Rd b/man/oauth_flow_auth_code_url.Rd new file mode 100644 index 00000000..f9827c00 --- /dev/null +++ b/man/oauth_flow_auth_code_url.Rd @@ -0,0 +1,48 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/oauth-flow-auth-code.R +\name{oauth_flow_auth_code_url} +\alias{oauth_flow_auth_code_url} +\alias{oauth_flow_auth_code_listen} +\alias{oauth_flow_auth_code_parse} +\alias{oauth_flow_auth_code_pkce} +\title{OAuth authorization code components} +\usage{ +oauth_flow_auth_code_url( + client, + auth_url, + redirect_uri = NULL, + scope = NULL, + state = NULL, + auth_params = list() +) + +oauth_flow_auth_code_listen(redirect_uri = "http://localhost:1410") + +oauth_flow_auth_code_parse(query, state) + +oauth_flow_auth_code_pkce() +} +\arguments{ +\item{state}{Random state generated by \code{oauth_flow_auth_code()}. Used to +verify that we're working with an authentication request that we created. +(This is an unlikely threat for R packages since the webserver that +listens for authorization responses is transient.)} + +\item{query}{List of query parameters returned by \code{oauth_flow_auth_code_listen()}.} +} +\description{ +These low-level functions can be used to assemble a custom flow for +APIs that are further from the spec: +\itemize{ +\item \code{oauth_flow_auth_code_url()} generates the url that should be opened in a +browser. +\item \code{oauth_flow_auth_code_listen()} starts a temporary local webserver that +listens for the response from the resource server. +\item \code{oauth_flow_auth_code_parse()} parses the query parameters returned from +the server redirect, verifying that the \code{state} is correct, and returning +the authorisation code. +\item \code{oauth_flow_auth_code_pkce()} generates code verifier, method, and challenge +components as needed for PKCE, as defined in \href{https://datatracker.ietf.org/doc/html/rfc7636}{RFC 7636}. +} +} +\keyword{internal} diff --git a/man/oauth_flow_bearer_jwt.Rd b/man/oauth_flow_bearer_jwt.Rd deleted file mode 100644 index 046d33cd..00000000 --- a/man/oauth_flow_bearer_jwt.Rd +++ /dev/null @@ -1,53 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/oauth-flow-jwt.R -\name{oauth_flow_bearer_jwt} -\alias{oauth_flow_bearer_jwt} -\title{OAuth flow: Bearer JWT} -\usage{ -oauth_flow_bearer_jwt( - client, - claim, - signature = "jwt_encode_sig", - signature_params = list(), - scope = NULL, - token_params = list() -) -} -\arguments{ -\item{client}{An \code{\link[=oauth_client]{oauth_client()}}.} - -\item{claim}{A list of claims. If all elements of the claim set are static -apart from \code{iat}, \code{nbf}, \code{exp}, or \code{jti}, provide a list and -\code{\link[=jwt_claim]{jwt_claim()}} will automatically fill in the dynamic components. -If other components need to vary, you can instead provide a zero-argument -callback function which should call \code{jwt_claim()}.} - -\item{signature}{Function use to sign \code{claim}, e.g. \code{\link[=jwt_encode_sig]{jwt_encode_sig()}}.} - -\item{signature_params}{Additional arguments passed to \code{signature}, e.g. -\code{size}, \code{header}.} - -\item{scope}{Scopes to be requested from the resource owner.} - -\item{token_params}{List containing additional parameters passed to the -\code{token_url}.} -} -\value{ -An \link{oauth_token}. -} -\description{ -This function uses a Bearer JWT (JSON web token) as an authorization grant to get an access -token, as defined by \href{https://datatracker.ietf.org/doc/html/rfc7523#section-2.1}{rfc7523}, -Section 2.1. It is often used for service accounts, accounts that are -used primarily in automated environments. -} -\seealso{ -Other OAuth flows: -\code{\link{oauth_flow_auth_code}()}, -\code{\link{oauth_flow_client_credentials}()}, -\code{\link{oauth_flow_device}()}, -\code{\link{oauth_flow_password}()}, -\code{\link{oauth_flow_refresh}()} -} -\concept{OAuth flows} -\keyword{internal} diff --git a/man/oauth_flow_client_credentials.Rd b/man/oauth_flow_client_credentials.Rd deleted file mode 100644 index f5435843..00000000 --- a/man/oauth_flow_client_credentials.Rd +++ /dev/null @@ -1,35 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/oauth-flow-client-credentials.R -\name{oauth_flow_client_credentials} -\alias{oauth_flow_client_credentials} -\title{OAuth flow: client credentials} -\usage{ -oauth_flow_client_credentials(client, scope = NULL, token_params = list()) -} -\arguments{ -\item{client}{An \code{\link[=oauth_client]{oauth_client()}}.} - -\item{scope}{Scopes to be requested from the resource owner.} - -\item{token_params}{List containing additional parameters passed to the -\code{token_url}.} -} -\value{ -An \link{oauth_token}. -} -\description{ -This function implements the OAuth client credentials flow, as defined -by \href{https://datatracker.ietf.org/doc/html/rfc6749#section-4.4}{rfc6749}, -Section 4.4. It is used to allow the client to access resources that it -controls directly, not on behalf of an user. -} -\seealso{ -Other OAuth flows: -\code{\link{oauth_flow_auth_code}()}, -\code{\link{oauth_flow_bearer_jwt}()}, -\code{\link{oauth_flow_device}()}, -\code{\link{oauth_flow_password}()}, -\code{\link{oauth_flow_refresh}()} -} -\concept{OAuth flows} -\keyword{internal} diff --git a/man/oauth_flow_device.Rd b/man/oauth_flow_device.Rd deleted file mode 100644 index 573005b6..00000000 --- a/man/oauth_flow_device.Rd +++ /dev/null @@ -1,58 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/oauth-flow-device.R -\name{oauth_flow_device} -\alias{oauth_flow_device} -\title{OAuth flow: device} -\usage{ -oauth_flow_device( - client, - auth_url, - pkce = FALSE, - scope = NULL, - auth_params = list(), - token_params = list() -) -} -\arguments{ -\item{client}{An \code{\link[=oauth_client]{oauth_client()}}.} - -\item{auth_url}{Authorization url; you'll need to discover this by reading -the documentation.} - -\item{pkce}{Use "Proof Key for Code Exchange"? This adds an extra layer of -security and should always be used if supported by the server.} - -\item{scope}{Scopes to be requested from the resource owner.} - -\item{auth_params}{List containing additional parameters passed to \code{oauth_flow_auth_code_url()}} - -\item{token_params}{List containing additional parameters passed to the -\code{token_url}.} -} -\value{ -An \link{oauth_token}. -} -\description{ -These functions implement the OAuth device flow, as defined -by \href{https://datatracker.ietf.org/doc/html/rfc8628}{rfc8628}. It's designed -for devices that don't have access to a web browser (if you've ever -authenticated an app on your TV, this is probably the flow you've used), -but it also works well from within R. - -This specification allows also some subspecifications: -\itemize{ -\item \code{oauth_flow_auth_code_pkce()} is also reused here to generate code -verifier, method, and challenge components as needed for PKCE, as -defined in \href{https://datatracker.ietf.org/doc/html/rfc7636}{rfc7636}. -} -} -\seealso{ -Other OAuth flows: -\code{\link{oauth_flow_auth_code}()}, -\code{\link{oauth_flow_bearer_jwt}()}, -\code{\link{oauth_flow_client_credentials}()}, -\code{\link{oauth_flow_password}()}, -\code{\link{oauth_flow_refresh}()} -} -\concept{OAuth flows} -\keyword{internal} diff --git a/man/oauth_flow_password.Rd b/man/oauth_flow_password.Rd deleted file mode 100644 index 336d4c55..00000000 --- a/man/oauth_flow_password.Rd +++ /dev/null @@ -1,48 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/oauth-flow-password.R -\name{oauth_flow_password} -\alias{oauth_flow_password} -\title{OAuth flow: user password} -\usage{ -oauth_flow_password( - client, - username, - password = NULL, - scope = NULL, - token_params = list() -) -} -\arguments{ -\item{client}{An \code{\link[=oauth_client]{oauth_client()}}.} - -\item{username}{User name.} - -\item{password}{Password. You avoid entering the password directly when -calling this function as it will be captured by \code{.Rhistory}. Instead, -leave it unset and the default behaviour will prompt you for it -interactively.} - -\item{scope}{Scopes to be requested from the resource owner.} - -\item{token_params}{List containing additional parameters passed to the -\code{token_url}.} -} -\value{ -An \link{oauth_token}. -} -\description{ -This function implements the OAuth resource owner password flow, as defined -by \href{https://datatracker.ietf.org/doc/html/rfc6749#section-4.3}{rfc6749}, -Section 4.3. It allows the user to supply their password once, exchanging -it for an access token that can be cached locally. -} -\seealso{ -Other OAuth flows: -\code{\link{oauth_flow_auth_code}()}, -\code{\link{oauth_flow_bearer_jwt}()}, -\code{\link{oauth_flow_client_credentials}()}, -\code{\link{oauth_flow_device}()}, -\code{\link{oauth_flow_refresh}()} -} -\concept{OAuth flows} -\keyword{internal} diff --git a/man/oauth_flow_refresh.Rd b/man/oauth_flow_refresh.Rd deleted file mode 100644 index 6f3a86c3..00000000 --- a/man/oauth_flow_refresh.Rd +++ /dev/null @@ -1,46 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/oauth-flow-refresh.R -\name{oauth_flow_refresh} -\alias{oauth_flow_refresh} -\title{OAuth flow: refresh token} -\usage{ -oauth_flow_refresh( - client, - refresh_token = Sys.getenv("HTTR2_REFRESH_TOKEN"), - scope = NULL, - token_params = list() -) -} -\arguments{ -\item{client}{An \code{\link[=oauth_client]{oauth_client()}}.} - -\item{refresh_token}{A refresh token. This is equivalent to a password -so shouldn't be typed into the console or stored in a script. Instead, -we recommend placing in an environment variable; the default behaviour -is to look in \code{HTTR2_REFRESH_TOKEN}.} - -\item{scope}{Scopes to be requested from the resource owner.} - -\item{token_params}{List containing additional parameters passed to the -\code{token_url}.} -} -\value{ -An \link{oauth_token}. -} -\description{ -This function generates an access token from a refresh token, following -the process described in -\href{https://datatracker.ietf.org/doc/html/rfc6749#section-6}{rfc6749}, -Section 6. Warns if the refresh returns a new refresh token, see -\code{\link[=req_oauth_refresh]{req_oauth_refresh()}} for details. -} -\seealso{ -Other OAuth flows: -\code{\link{oauth_flow_auth_code}()}, -\code{\link{oauth_flow_bearer_jwt}()}, -\code{\link{oauth_flow_client_credentials}()}, -\code{\link{oauth_flow_device}()}, -\code{\link{oauth_flow_password}()} -} -\concept{OAuth flows} -\keyword{internal} diff --git a/man/oauth_redirect_uri.Rd b/man/oauth_redirect_uri.Rd new file mode 100644 index 00000000..dabcf7f1 --- /dev/null +++ b/man/oauth_redirect_uri.Rd @@ -0,0 +1,12 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/oauth-flow-auth-code.R +\name{oauth_redirect_uri} +\alias{oauth_redirect_uri} +\title{Default redirect url for OAuth} +\usage{ +oauth_redirect_uri() +} +\description{ +The default redirect uri used by \code{\link[=req_oauth_auth_code]{req_oauth_auth_code()}}. Defaults to +\verb{http://localhost} unless the \code{HTTR2_OAUTH_REDIRECT_URL} envvar is set. +} diff --git a/man/req_oauth_auth_code.Rd b/man/req_oauth_auth_code.Rd index d4616915..4b2441d3 100644 --- a/man/req_oauth_auth_code.Rd +++ b/man/req_oauth_auth_code.Rd @@ -2,19 +2,33 @@ % Please edit documentation in R/oauth-flow-auth-code.R \name{req_oauth_auth_code} \alias{req_oauth_auth_code} -\title{OAuth authentication with authorization code} +\alias{oauth_flow_auth_code} +\title{OAuth with authorization code} \usage{ req_oauth_auth_code( req, client, auth_url, + scope = NULL, + pkce = TRUE, + auth_params = list(), + token_params = list(), + redirect_uri = oauth_redirect_uri(), cache_disk = FALSE, cache_key = NULL, + host_name = deprecated(), + host_ip = deprecated(), + port = deprecated() +) + +oauth_flow_auth_code( + client, + auth_url, scope = NULL, pkce = TRUE, auth_params = list(), token_params = list(), - redirect_uri = default_redirect_uri(), + redirect_uri = oauth_redirect_uri(), host_name = deprecated(), host_ip = deprecated(), port = deprecated() @@ -28,21 +42,13 @@ req_oauth_auth_code( \item{auth_url}{Authorization url; you'll need to discover this by reading the documentation.} -\item{cache_disk}{Should the access token be cached on disk? This reduces -the number of times that you need to re-authenticate at the cost of -storing access credentials on disk. Cached tokens are encrypted, -automatically deleted 30 days after creation, and stored in -\code{\link[=oauth_cache_path]{oauth_cache_path()}}.} - -\item{cache_key}{If you want to cache multiple tokens per app, use this -key to disambiguate them.} - \item{scope}{Scopes to be requested from the resource owner.} \item{pkce}{Use "Proof Key for Code Exchange"? This adds an extra layer of security and should always be used if supported by the server.} -\item{auth_params}{List containing additional parameters passed to \code{oauth_flow_auth_code_url()}} +\item{auth_params}{A list containing additional parameters passed to +\code{\link[=oauth_flow_auth_code_url]{oauth_flow_auth_code_url()}}.} \item{token_params}{List containing additional parameters passed to the \code{token_url}.} @@ -72,24 +78,38 @@ will use \code{HTTR2_OAUTH_REDIRECT_URL} for redirects by default, and poll the receives a code in the response (or encounters an error). This delegates completion of the authorization flow to the hosted platform.} +\item{cache_disk}{Should the access token be cached on disk? This reduces +the number of times that you need to re-authenticate at the cost of +storing access credentials on disk. + +Learn more in \code{vignette("oauth")}} + +\item{cache_key}{If you want to cache multiple tokens per app, use this +key to disambiguate them.} + \item{host_name, host_ip, port}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} Now use \code{redirect_uri} instead.} } \value{ -A modified HTTP \link{request}. +\code{req_oauth_auth_code()} returns a modified HTTP \link{request} that will +use OAuth; \code{oauth_flow_auth_code()} returns an \link{oauth_token}. } \description{ -This uses \code{\link[=oauth_flow_auth_code]{oauth_flow_auth_code()}} to generate an access token, which is -then used to authentication the request with \code{\link[=req_auth_bearer_token]{req_auth_bearer_token()}}. -The token is automatically cached (either in memory or on disk) to minimise -the number of times the flow is performed. +Authenticate using the OAuth \strong{authorization code flow}, as defined +by \href{https://datatracker.ietf.org/doc/html/rfc6749#section-4.1}{Section 4.1 of RFC 6749}. + +This flow is the most commonly used OAuth flow where the user +opens a page in their browser, approves the access, and then returns to R. +When possible, it redirects the browser back to a temporary local webserver +to capture the authorization code. When this is not possible (e.g. when +running on a hosted platform like RStudio Server), provide a custom +\code{redirect_uri} and httr2 will prompt the user to enter the code manually. -Learn more about the overall flow in \code{vignette("oauth")}. +Learn more about the overall OAuth authentication flow in \code{vignette("oauth")}. } \section{Security considerations}{ The authorization code flow is used for both web applications and native -applications (which are equivalent to R packages). -\href{https://datatracker.ietf.org/doc/html/rfc8252}{rfc8252} spells out +applications (which are equivalent to R packages). \href{https://datatracker.ietf.org/doc/html/rfc8252}{RFC 8252} spells out important considerations for native apps. Most importantly there's no way for native apps to keep secrets from their users. This means that the server should either not require a \code{client_secret} (i.e. a public client @@ -117,3 +137,15 @@ req_auth_github <- function(req) { request("https://api.github.com/user") \%>\% req_auth_github() } +\seealso{ +\code{\link[=oauth_flow_auth_code_url]{oauth_flow_auth_code_url()}} for the components necessary to +write your own auth code flow, if the API you are wrapping does not adhere +closely to the standard. + +Other OAuth flows: +\code{\link{req_oauth_bearer_jwt}()}, +\code{\link{req_oauth_client_credentials}()}, +\code{\link{req_oauth_password}()}, +\code{\link{req_oauth_refresh}()} +} +\concept{OAuth flows} diff --git a/man/req_oauth_bearer_jwt.Rd b/man/req_oauth_bearer_jwt.Rd index 2a20650d..b74fe239 100644 --- a/man/req_oauth_bearer_jwt.Rd +++ b/man/req_oauth_bearer_jwt.Rd @@ -2,7 +2,8 @@ % Please edit documentation in R/oauth-flow-jwt.R \name{req_oauth_bearer_jwt} \alias{req_oauth_bearer_jwt} -\title{OAuth authentication with a bearer JWT (JSON web token)} +\alias{oauth_flow_bearer_jwt} +\title{OAuth with a bearer JWT (JSON web token)} \usage{ req_oauth_bearer_jwt( req, @@ -13,6 +14,15 @@ req_oauth_bearer_jwt( scope = NULL, token_params = list() ) + +oauth_flow_bearer_jwt( + client, + claim, + signature = "jwt_encode_sig", + signature_params = list(), + scope = NULL, + token_params = list() +) } \arguments{ \item{req}{A \link{request}.} @@ -36,19 +46,34 @@ callback function which should call \code{jwt_claim()}.} \code{token_url}.} } \value{ -A modified HTTP \link{request}. +\code{req_oauth_bearer_jwt()} returns a modified HTTP \link{request} that will +use OAuth; \code{oauth_flow_bearer_jwt()} returns an \link{oauth_token}. } \description{ -This uses \code{\link[=oauth_flow_bearer_jwt]{oauth_flow_bearer_jwt()}} to generate an access token which is then -used to authenticate the request with \code{\link[=req_auth_bearer_token]{req_auth_bearer_token()}}. -The token is cached in memory. +Authenticate using a \strong{Bearer JWT} (JSON web token) as an authorization +grant to get an access token, as defined by \href{https://datatracker.ietf.org/doc/html/rfc7523#section-2.1}{Section 2.1 of RFC 7523}. +It is often used for service accounts, accounts that are used primarily in +automated environments. -Learn more about the overall flow in \code{vignette("oauth")}. +Learn more about the overall OAuth authentication flow in \code{vignette("oauth")}. } \examples{ -client <- oauth_client("example", "https://example.com/get_token") -claim <- jwt_claim() -req <- request("https://example.com") +req_auth <- function(req) { + req_oauth_bearer_jwt( + req, + client = oauth_client("example", "https://example.com/get_token"), + claim = jwt_claim() + ) +} -req \%>\% req_oauth_bearer_jwt(client, claim) +request("https://example.com") \%>\% + req_auth() +} +\seealso{ +Other OAuth flows: +\code{\link{req_oauth_auth_code}()}, +\code{\link{req_oauth_client_credentials}()}, +\code{\link{req_oauth_password}()}, +\code{\link{req_oauth_refresh}()} } +\concept{OAuth flows} diff --git a/man/req_oauth_client_credentials.Rd b/man/req_oauth_client_credentials.Rd index b6ff9159..a99d1d78 100644 --- a/man/req_oauth_client_credentials.Rd +++ b/man/req_oauth_client_credentials.Rd @@ -2,9 +2,12 @@ % Please edit documentation in R/oauth-flow-client-credentials.R \name{req_oauth_client_credentials} \alias{req_oauth_client_credentials} -\title{OAuth authentication with client credentials} +\alias{oauth_flow_client_credentials} +\title{OAuth with client credentials} \usage{ req_oauth_client_credentials(req, client, scope = NULL, token_params = list()) + +oauth_flow_client_credentials(client, scope = NULL, token_params = list()) } \arguments{ \item{req}{A \link{request}.} @@ -17,18 +20,32 @@ req_oauth_client_credentials(req, client, scope = NULL, token_params = list()) \code{token_url}.} } \value{ -A modified HTTP \link{request}. +\code{req_oauth_client_credentials()} returns a modified HTTP \link{request} that will +use OAuth; \code{oauth_flow_client_credentials()} returns an \link{oauth_token}. } \description{ -This uses \code{\link[=oauth_flow_client_credentials]{oauth_flow_client_credentials()}} to generate an access token, -which is then used to authentication the request with \code{\link[=req_auth_bearer_token]{req_auth_bearer_token()}}. -The token is cached in memory. +Authenticate using OAuth \strong{client credentials flow}, as defined by +\href{https://datatracker.ietf.org/doc/html/rfc6749#section-4.4}{Section 4.4 of RFC 6749}. It is used to allow the client to access resources that +it controls directly, not on behalf of an user. -Learn more about the overall flow in \code{vignette("oauth")}. +Learn more about the overall OAuth authentication flow in \code{vignette("oauth")}. } \examples{ -client <- oauth_client("example", "https://example.com/get_token") -req <- request("https://example.com") +req_auth <- function(req) { + req_oauth_client_credentials( + req, + client = oauth_client("example", "https://example.com/get_token") + ) +} -req \%>\% req_oauth_client_credentials(client) +request("https://example.com") \%>\% + req_auth() +} +\seealso{ +Other OAuth flows: +\code{\link{req_oauth_auth_code}()}, +\code{\link{req_oauth_bearer_jwt}()}, +\code{\link{req_oauth_password}()}, +\code{\link{req_oauth_refresh}()} } +\concept{OAuth flows} diff --git a/man/req_oauth_device.Rd b/man/req_oauth_device.Rd index 32aeafda..40af73cb 100644 --- a/man/req_oauth_device.Rd +++ b/man/req_oauth_device.Rd @@ -2,14 +2,24 @@ % Please edit documentation in R/oauth-flow-device.R \name{req_oauth_device} \alias{req_oauth_device} -\title{OAuth authentication with device flow} +\alias{oauth_flow_device} +\title{OAuth with device flow} \usage{ req_oauth_device( req, client, auth_url, + scope = NULL, + auth_params = list(), + token_params = list(), cache_disk = FALSE, - cache_key = NULL, + cache_key = NULL +) + +oauth_flow_device( + client, + auth_url, + pkce = FALSE, scope = NULL, auth_params = list(), token_params = list() @@ -23,32 +33,37 @@ req_oauth_device( \item{auth_url}{Authorization url; you'll need to discover this by reading the documentation.} +\item{scope}{Scopes to be requested from the resource owner.} + +\item{auth_params}{A list containing additional parameters passed to +\code{\link[=oauth_flow_auth_code_url]{oauth_flow_auth_code_url()}}.} + +\item{token_params}{List containing additional parameters passed to the +\code{token_url}.} + \item{cache_disk}{Should the access token be cached on disk? This reduces the number of times that you need to re-authenticate at the cost of -storing access credentials on disk. Cached tokens are encrypted, -automatically deleted 30 days after creation, and stored in -\code{\link[=oauth_cache_path]{oauth_cache_path()}}.} +storing access credentials on disk. + +Learn more in \code{vignette("oauth")}} \item{cache_key}{If you want to cache multiple tokens per app, use this key to disambiguate them.} -\item{scope}{Scopes to be requested from the resource owner.} - -\item{auth_params}{List containing additional parameters passed to \code{oauth_flow_auth_code_url()}} - -\item{token_params}{List containing additional parameters passed to the -\code{token_url}.} +\item{pkce}{Use "Proof Key for Code Exchange"? This adds an extra layer of +security and should always be used if supported by the server.} } \value{ -A modified HTTP \link{request}. +\code{req_oauth_device()} returns a modified HTTP \link{request} that will +use OAuth; \code{oauth_flow_device()} returns an \link{oauth_token}. } \description{ -This uses \code{\link[=oauth_flow_device]{oauth_flow_device()}} to generate an access token, which is -then used to authentication the request with \code{\link[=req_auth_bearer_token]{req_auth_bearer_token()}}. -The token is automatically cached (either in memory or on disk) to minimise -the number of times the flow is performed. +Authenticate using the OAuth \strong{device flow}, as defined by \href{https://datatracker.ietf.org/doc/html/rfc8626}{RFC 8626}. +It's designed for devices that don't have access to a web browser (if you've +ever authenticated an app on your TV, this is probably the flow you've used), +but it also works well from within R. -Learn more about the overall flow in \code{vignette("oauth")}. +Learn more about the overall OAuth authentication flow in \code{vignette("oauth")}. } \examples{ req_auth_github <- function(req) { diff --git a/man/req_oauth_password.Rd b/man/req_oauth_password.Rd index 388afb4c..803443b6 100644 --- a/man/req_oauth_password.Rd +++ b/man/req_oauth_password.Rd @@ -2,14 +2,24 @@ % Please edit documentation in R/oauth-flow-password.R \name{req_oauth_password} \alias{req_oauth_password} -\title{OAuth authentication with username and password} +\alias{oauth_flow_password} +\title{OAuth with username and password} \usage{ req_oauth_password( req, client, username, password = NULL, + scope = NULL, + token_params = list(), cache_disk = FALSE, + cache_key = username +) + +oauth_flow_password( + client, + username, + password = NULL, scope = NULL, token_params = list() ) @@ -26,34 +36,48 @@ calling this function as it will be captured by \code{.Rhistory}. Instead, leave it unset and the default behaviour will prompt you for it interactively.} -\item{cache_disk}{Should the access token be cached on disk? This reduces -the number of times that you need to re-authenticate at the cost of -storing access credentials on disk. Cached tokens are encrypted, -automatically deleted 30 days after creation, and stored in -\code{\link[=oauth_cache_path]{oauth_cache_path()}}.} - \item{scope}{Scopes to be requested from the resource owner.} \item{token_params}{List containing additional parameters passed to the \code{token_url}.} + +\item{cache_disk}{Should the access token be cached on disk? This reduces +the number of times that you need to re-authenticate at the cost of +storing access credentials on disk. + +Learn more in \code{vignette("oauth")}} + +\item{cache_key}{If you want to cache multiple tokens per app, use this +key to disambiguate them.} } \value{ -A modified HTTP \link{request}. +\code{req_oauth_password()} returns a modified HTTP \link{request} that will +use OAuth; \code{oauth_flow_password()} returns an \link{oauth_token}. } \description{ -This uses \code{\link[=oauth_flow_password]{oauth_flow_password()}} to generate an access token, which is -then used to authentication the request with \code{\link[=req_auth_bearer_token]{req_auth_bearer_token()}}. -The token, not the password is automatically cached (either in memory -or on disk); the password is used once to get the token and is then -discarded. +This function implements the OAuth \strong{resource owner password flow}, as +defined by \href{https://datatracker.ietf.org/doc/html/rfc6749#section-4.3}{Section 4.3 of RFC 6749}. It allows the user to supply their password +once, exchanging it for an access token that can be cached locally. -Learn more about the overall flow in \code{vignette("oauth")}. +Learn more about the overall OAuth authentication flow in \code{vignette("oauth")}. } \examples{ -client <- oauth_client("example", "https://example.com/get_token") -req <- request("https://example.com") - +req_auth <- function(req) { + req_oauth_password(req, + client = oauth_client("example", "https://example.com/get_token"), + username = "username" + ) +} if (interactive()) { - req \%>\% req_oauth_password(client, "username") + request("https://example.com") \%>\% + req_auth() +} } +\seealso{ +Other OAuth flows: +\code{\link{req_oauth_auth_code}()}, +\code{\link{req_oauth_bearer_jwt}()}, +\code{\link{req_oauth_client_credentials}()}, +\code{\link{req_oauth_refresh}()} } +\concept{OAuth flows} diff --git a/man/req_oauth_refresh.Rd b/man/req_oauth_refresh.Rd index 1f40a79d..fe84c40f 100644 --- a/man/req_oauth_refresh.Rd +++ b/man/req_oauth_refresh.Rd @@ -2,7 +2,8 @@ % Please edit documentation in R/oauth-flow-refresh.R \name{req_oauth_refresh} \alias{req_oauth_refresh} -\title{OAuth authentication with a refresh token} +\alias{oauth_flow_refresh} +\title{OAuth with a refresh token} \usage{ req_oauth_refresh( req, @@ -11,6 +12,13 @@ req_oauth_refresh( scope = NULL, token_params = list() ) + +oauth_flow_refresh( + client, + refresh_token = Sys.getenv("HTTR2_REFRESH_TOKEN"), + scope = NULL, + token_params = list() +) } \arguments{ \item{req}{A \link{request}.} @@ -28,24 +36,36 @@ is to look in \code{HTTR2_REFRESH_TOKEN}.} \code{token_url}.} } \value{ -A modified HTTP \link{request}. +\code{req_oauth_refresh()} returns a modified HTTP \link{request} that will +use OAuth; \code{oauth_flow_refresh()} returns an \link{oauth_token}. } \description{ -This uses \code{\link[=oauth_flow_refresh]{oauth_flow_refresh()}} to generate an access token, which is -then used to authenticate the request with \code{\link[=req_auth_bearer_token]{req_auth_bearer_token()}}. -This is primarily useful for testing: you can manually execute another OAuth -flow (e.g. by calling \code{\link[=oauth_flow_auth_code]{oauth_flow_auth_code()}} or \code{\link[=oauth_flow_device]{oauth_flow_device()}}), +Authenticate using a \strong{refresh token}, following the process described in +\href{https://datatracker.ietf.org/doc/html/rfc6749#section-6}{Section 6 of RFC 6749}. + +This technique is primarily useful for testing: you can manually retrieve +a OAuth token using another OAuth flow (e.g. with \code{\link[=oauth_flow_auth_code]{oauth_flow_auth_code()}}), extract the refresh token from the result, and then save in an environment -variable for future use in automated tests. +variable for use in automated tests. When requesting an access token, the server may also return a new refresh -token. If this happens, \code{oauth_flow_refresh()} will warn, and you'll have to -update your stored refresh token. +token. If this happens, \code{oauth_flow_refresh()} will warn, and you'll have +retrieve a new update refresh token and update the stored value. If you find +this happening a lot, it's a sign that you should be using a different flow +in your automated tests. -Learn more about the overall flow in \code{vignette("oauth")}. +Learn more about the overall OAuth authentication flow in \code{vignette("oauth")}. } \examples{ client <- oauth_client("example", "https://example.com/get_token") req <- request("https://example.com") req \%>\% req_oauth_refresh(client) } +\seealso{ +Other OAuth flows: +\code{\link{req_oauth_auth_code}()}, +\code{\link{req_oauth_bearer_jwt}()}, +\code{\link{req_oauth_client_credentials}()}, +\code{\link{req_oauth_password}()} +} +\concept{OAuth flows} diff --git a/man/resp_link_url.Rd b/man/resp_link_url.Rd index 9198c119..2b951503 100644 --- a/man/resp_link_url.Rd +++ b/man/resp_link_url.Rd @@ -16,8 +16,7 @@ Either a string providing a URL, if the specified \code{rel} exists, or \code{NULL} if not. } \description{ -Parses URLs out of the the \code{Link} header as defined by -\href{https://datatracker.ietf.org/doc/html/rfc8288}{rfc8288}. +Parses URLs out of the the \code{Link} header as defined by \href{https://datatracker.ietf.org/doc/html/rfc8288}{RFC 8288}. } \examples{ # Simulate response from GitHub code search diff --git a/man/url_parse.Rd b/man/url_parse.Rd index 026010e1..15a8d0aa 100644 --- a/man/url_parse.Rd +++ b/man/url_parse.Rd @@ -23,9 +23,8 @@ and elements \code{scheme}, \code{hostname}, \code{port}, \code{path}, \code{fra } \description{ \code{url_parse()} parses a URL into its component pieces; \code{url_build()} does -the reverse, converting a list of pieces into a string URL. See -\href{https://www.rfc-editor.org/rfc/rfc3986}{rfc3986} for details of parsing -algorithm. +the reverse, converting a list of pieces into a string URL. See \href{https://datatracker.ietf.org/doc/html/rfc3986}{RFC 3986} +for the details of the parsing algorithm. } \examples{ url_parse("http://google.com/") diff --git a/tests/testthat/_snaps/oauth.md b/tests/testthat/_snaps/oauth.md new file mode 100644 index 00000000..ec147c05 --- /dev/null +++ b/tests/testthat/_snaps/oauth.md @@ -0,0 +1,7 @@ +# can store on disk + + Code + cache$set(1) + Message + Caching httr2 token in '/httr2-test/ae743e0fbd718c21f2cca632e77bd180-token.rds.enc'. + diff --git a/tests/testthat/test-oauth.R b/tests/testthat/test-oauth.R index 99eebf5c..2aa473fd 100644 --- a/tests/testthat/test-oauth.R +++ b/tests/testthat/test-oauth.R @@ -42,7 +42,12 @@ test_that("can store on disk", { withr::defer(cache$clear()) expect_equal(cache$get(), NULL) - cache$set(1) + expect_snapshot( + cache$set(1), + transform = function(x) { + gsub(oauth_cache_path(), "", x, fixed = TRUE) + } + ) expect_equal(cache$get(), 1) cache$clear() expect_equal(cache$get(), NULL) diff --git a/vignettes/articles/oauth.Rmd b/vignettes/articles/oauth.Rmd index b9cb6c4f..8ea79ab8 100644 --- a/vignettes/articles/oauth.Rmd +++ b/vignettes/articles/oauth.Rmd @@ -13,39 +13,99 @@ knitr::opts_chunk$set( library(httr2) ``` -If the API provides access to a website where the user already has an account (think Twitter, Instagram, Facebook, Google, GitHub, etc), it's likely to use OAuth to allow you to authorise on behalf of the user. -OAuth[^1] is an authorisation framework that's designed so that you don't have to share your username and password with an app; instead the app asks for permission to use your account. -You've almost certainly used this before on the web; it's used in most cases where one website wants to use another website on your behalf. +OAuth[^1] is an authorization framework design for performing work on the behalf of a user. +You've probably used it a bunch without knowing what it's called: it's used when you login to a non-Google website using your Google account, when you give a phone access to your twitter account, or when you login to a streaming app on your smart TV. [^1]: Here I'll only talk about OAuth 2.0 which is the only version in common use today. OAuth 1.0 is largely only of historical interest. -OAuth is a broad framework that has many many many different variants which makes it hard to provide generalisable advice. -The following advice draws on my experience working with a number of OAuth using APIs, but don't be surprised if you need to do something slightly different for the API you're working with. +You'll notice none of these common scenarios quite match to using an R package to scrape data from a web API, and that's the pleasure and pain of OAuth. +OAuth gives you the incredible power to extract data from popular web services, but it's fundamentally designed for a different use case. +This means that while httr2 can make the interactive experience pretty seamless, unattended usage (i.e. on CI or in an automated script) is usually going to include some pain. + +This vignette builds on the techniques discussed in `vignette("wrapping-apis")`, so I recommend you read that vignette first. +I'll also assume that you're working in a package, since most people want to wrap up an API into a bunch of R functions, but most of the ideas will also apply if you're creating a one-off script. + +## OAuth basics + +OAuth is a broad framework that has many different variants, called **flows**, which makes it hard to provide sweeping generalisations, but the basic idea of OAuth is to create a hierarchy of increasingly more specific and shorter-lived credentials, so that the impact of a credential being lost is as small as possible. + +The longest lived and most powerful credential is typically a user name and password. +Most people don't change their passwords regularly, and often (against all advice) reuse the same password on multiple websites. +And if you have a user name and password, you have total control over that account; you can even use it to change the password so the actual user can't log in. +That means as a programmer you never want to touch user name-password pairs because if they're lost or stolen, they give very wide access. + +Avoiding that problem lead to the creation of OAuth. +The basic idea is that instead of your package asking the user to give you their user name and password, you instead ask them to log in and give your package permission to use the API on their behalf. +The API gives this permission in the form of an **access token** which is essentially a random string of numbers and letters, e.g. +`UfNlXaEog03hdRPTUPpEInEiIW01jI1WcjOB`. +An access token is very short lived, lasting maybe only days, and is bound to a specified **scope** of access. +The access token has some big advantages over a user name and password: + +- It's short-lived so if it's lost or stolen, there's only a limited amount of time where it can be abused. +- It has limited scope, so even if stolen, it can't be used to do something particularly nefarious like changing your password or contact details. +- It's bound to a specific application, so it can be invalidated (cancelled) without affecting any other uses. + +If you have an access token you can use it to authenticate an API by passing it as a **bearer token** in the `Authorization` header, which you can do with `req_auth_bearer_token()`. +However, in most cases you will want to let httr2 manage this by calling one of the `req_oauth_` functions we'll talk about shortly. + +One of the reasons that you want httr2 to manage your tokens is that because access tokens are so short lived, they're often accompanied by a **refresh token**. +A refresh token lasts for a longer amount of time and has one job: it allows you to get a new access token when the previous one expires. +You need to look after a refresh token a little more carefully than an access token. +In particular, you should never include a refresh token in a HTTP request as that's the job of the access token. + +Overall this leads to a hierarchy of credentials from weakest to strongest: + +- The access token usually only lasts a couple hours and needs to be submitted with every request. +- The refresh token lasts for days to weeks and it's designed to be stored locally so you can regenerate access tokens. (We'll talk about this more in Caching, below.) +- The user name + password gives access to everything so your code should never touch them! + +Now that you've got the basic idea of OAuth, lets talk about the details. ## Clients -The first step in working with any OAuth API is to create a client. -This involves you registering for a developer account on the API's website and creating a new OAuth app. -The process varies from API to API, but at the end of it you'll get a client id and in most cases a client secret. +The first step in working with any OAuth API is to create an **application** **client**. +It's called an application client because in the wider world OAuth is usually used with a web, phone, or tv app, but here your "app" is going to be your R package. +For this reason, httr2 calls the application client a **client**, but many APIs will call it an OAuth application. -(You'll definitely need this for testing your package, and you'll probably also baked it into your package for the convenience of your users. Bundling the app is user friendly, but not always possible, particularly if rate limits are enforced on a per-app rather than per-user basis. You should always provide some way for the user to provide their own app.) +To create a client, you'll need to first register for a developer account on the API's website. +In most cases this is easy, totally automated, and only takes a couple of minutes. +That will give you access to some sort of developer portal which you can then use to register a new OAuth app (aka a client). +This process varies from API to API (it's normal to spend some time hunting through the docs and settings), but at the end of it you'll get a client id (another random string of numbers and letters). +Sometimes the client id is all you need, and you can create an httr2 client with `oauth_client()`, e.g.: -If the API provides a way to authenticate your app without the client secret, you should leave it out of your package. -But in most cases, you'll need to include the secret in the package. -You can use `obfuscate()` to hide the secret; this is not bulletproof[^2], but in most cases it'll be easier to create a new client than try and steal yours. -Additionally, it's unusual for an OAuth client to be able to do anything in its own right, so even if someone does steal your secret there's not much harm they can do with it. +```{r} +client <- oauth_client( + id = "28acfec0674bb3da9f38", + token_url = "https://github.com/login/oauth/access_token", + name = "hadley-oauth-test-2" +) +``` -[^2]: It uses `secret_encrypt()` with a special encryption key that's bundled with httr2. +The call to `oauth_client()` also includes a `name` and a `token_url`. -To obfuscate a secret, call `obfuscate()`: +- The `name` is human-facing, and should typically be the same as your package (the thing that prompted you to create the client). I have a bunch of apps that I've used for testing, so here I've used the name `hadley-oauth-test-2` to remind me which app this client corresponds to. +- The `token_url` points to the URL that's used to obtain an access token. You'll need to find this from the documentation for the API you're wrapping; it will typically be found in the section that describes the OAuth process and will be an endpoint that returns an access token. Don't be surprised if this endpoint feels very different to the rest of the API; auth is often implemented by a third-party package with slightly different conventions to the rest of the API. + +### Client secret + +In most cases, however, the API will also require a client secret. +While this is called a secret, it's typically not that important to keep it a secret because of two reasons: + +- It's typically easy to create an new app on the developer website so stealing yours wouldn't save much time. +- It's unusual for an OAuth client to be able to do anything in its own right, so stealing your secret doesn't have much benefit. + +That means that unless you have paid for the app or given it private information while creating it, it's ok to embed the client in your package. +That said, httr2 provides some tooling to obfuscate your client secret so that the client secret isn't directly embedded in your source code, and hence vulnerable to scraping. + +To obfuscate a string, call `obfuscate()`, then copy and paste the result into your package. +For example, if your client secret was "secret", you'd call `obfuscate()` then you'd copy and paste `obfuscated("B4Evdd5x4wl0XTWvtTpuGaw7nM7GEg")` into your client specification. ```{r} obfuscate("secret") ``` -Then use the client id from the website along with the obfuscated secret to create a client. -The following code shows a GitHub OAuth app that I created specifically for this vignette: +Here's what a complete client specification for GitHub looks like, using a real app that I created specifically for this vignette: ```{r} client <- oauth_client( @@ -56,94 +116,179 @@ client <- oauth_client( ) ``` -You need to figure out the `token_url` from the [documentation](https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps). -I wish I could give good advice about how to find it 😞. +You can certainly uncover my client secret if you are an experienced R programmer and are willing to spend a bit of time experimenting, but I'm pretty sure it'd be easy for you to just create your own app on GitHub. + +### Packaging -Note that if you print the client the secret is automatically redacted: +I recommend wrapping client creation in a function in your package, e.g.: ```{r} -client +github_client <- function() { + oauth_client( + id = "28acfec0674bb3da9f38", + secret = obfuscated("J9iiGmyelHltyxqrHXW41ZZPZamyUNxSX1_uKnvPeinhhxET_7FfUs2X0LLKotXY2bpgOMoHRCo"), + token_url = "https://github.com/login/oauth/access_token", + name = "hadley-oauth-test" + ) +} ``` -## Flows +You'll need this in order to run tests for your package, but you'll probably also want to use it as the default client for your users. +In some cases, it will be necessary for each user to create their own app and matching client (e.g. if rate limits are applied by app, not by user), but this is much less user friendly so you should avoid it if possible. +That said, you should always provide a way for a user to supply their own client when you bundle a default. +You can see an example of this in [the googledrive package](https://googledrive.tidyverse.org/articles/bring-your-own-app.html). -Once you have a client you need to use it with a **flow** in order to get a token. -OAuth provides a number of different "flows", the most common is the "authorisation code" flow, which is implemented by `req_oauth_auth_code()`. -You can try it out by running this code: +## Authorization code flow -```{r, eval = FALSE} -token <- oauth_flow_auth_code(client, auth_url = "https://github.com/login/oauth/authorize") -``` +Once you have a client you need to use it with a flow in order to get a token. +You'll need to read the docs for your API to figure out which flows it supports, but the most common is the **authorization code**[^2] flow, which works something like this: -This flow can't be used inside a vignette because it's designed specifically for interactive use: it will open a webpage on GitHub that requires you to interactively confirm it's OK for this app to use your GitHub account. +[^2]: Confusingly, Github calls this the "web application flow", but this is not the official name. + This is a general problem with OAuth docs; often they use different terms for the same things. + In httr2, I've tried to stick as closely as possible to the terms used in the official RFC specifications. -Other flows provide different ways of getting the token: +1. httr2 opens a browser using the **authorization URL** provided by the API. The URL includes parameters that identify your app and what **scope** of access you're looking for (e.g. `tweet.read`, `userinfo.write`). +2. The user logs in using their user name and password (hopefully using a [password manager](https://www.nytimes.com/wirecutter/blog/why-you-need-a-password-manager-yes-you/)) and approves the request. +3. The API sends an **authorization code** back to httr2 using a callback URL that was supplied in the initial request. +4. httr2 sends the authorization code to the **token URL** to get an access token. -- `req_oauth_client_credentials()` is used to allow the client to perform actions on its own behalf (instead of on behalf of some other user). - This is typically need if you want to support **service accounts**, which are used in non-interactive environments. +In httr2 this flow is implemented by a pair of functions: `oauth_flow_auth_code()` and `req_oauth_auth_code()`. +Start with `oauth_flow_auth_code()` to check that you have all the parameters correctly specified, then use `req_oauth_auth_code()` to authenticate requests. +These two steps are described in the following sections. -- `req_oauth_device()` uses the "device" flow which is designed for devices like TVs that don't have an easy way to enter data. - It also works well from the console. +### Creating a token -- `req_oauth_bearer_jwt()` uses a JWT signed by a private key. +`oauth_flow_auth_code()` is the best way to verify you've correctly specified the parameters to the client and the `auth_url`, without depending on correctly understanding any other part of the API. +For example, you could get a token to access the GitHub API (using the client defined above) with this code: -- `req_oauth_password()` exchanges a user name and password for an access token. +```{r, eval = FALSE} +token <- oauth_flow_auth_code( + client = client, + auth_url = "https://github.com/login/oauth/authorize" +) +``` -- `req_oauth_refresh()` works directly with a refresh token that you already have. - It's useful for testing. +This flow can't be used inside a vignette because it's designed specifically for interactive use, but if you do run it and print out the `token`, you'll see something like this: + +```{r} +#| eval: false +token +#> +#> token_type: bearer +#> access_token: +#> scope: '' +``` -There's one historically important OAuth flow that httr2 doesn't support: the implicit grant flow. -This is now [mostly deprecated](https://developer.okta.com/blog/2019/05/01/is-the-oauth-implicit-flow-dead) and was never a particularly good fit for native applications because it relies on a technique for returning the access token that only works inside a web browser. +There's not much to see here because httr2 automatically redacts the access token (because that could be used to perform actions on behalf of the user). -When wrapping an API, you'll need to carefully read the documentation to figure out which flows are available. -Typically you'll want to use the auth code flow, but if it's not available you'll need to carefully consider the others. -An additional wrinkle is that many APIs don't implement the flow in exactly the same way as the spec. -If your initial attempt doesn't work, you're going to need to do some sleuthing. -This is going to be painful, but unfortunately there's no way around it. -I recommend using `with_verbosity()` so you can see exactly what httr2 is sending to the server. -You'll then need to carefully compare this to the API documentation and play "spot the difference". +If your call to `oauth_flow_auth_code()` succeeds then you've got everything set up correctly and you can proceed to the next step. +Otherwise, you'll get an HTTP error. +If you're very lucky, that error will be informative and will help you figure out want went wrong. +However, in most cases, you'll need to carefully double check that you've correctly copied and pasted the client id and secret, and check that you've supplied the correct authorization and token urls (`auth_url` and `token_url`). +If the docs have multiple candidates for each and you're unclear about which is which, you'll need to do some systematic experimentation. -## Tokens +### Authenticating a request -The point of a flow is to get a token. -You can use `req_auth_bearer_token()` to authorise a request with the access token stored inside the token object: +Initial configuration is the only time that you'll see an `httr2_token` object because you'll generally want to rely on httr2 to manage the tokens for you. +You'll do that with `req_oauth_auth_code()`. +To check that it's working correctly, I recommend finding the simplest possible API endpoint to test it with. +A good place to start is an endpoint that provides information about the "current" user, if your API provides one. -```{r, eval = FALSE} -request("https://api.github.com/user") %>% - req_auth_bearer_token(token$access_token) %>% - req_perform() %>% - resp_body_json() %>% - .$name -#> [1] "Hadley Wickham" +For example, the GitHub API provides a `GET` endpoint at `/user` that returns information about the current user. +If we make a request to this endpoint without authentication, we'll get an error: + +```{r} +#| error: true +req <- request("https://api.github.com/user") +req %>% + req_perform() ``` -However, in most cases you won't want to do this, but instead allow httr2 to manage the whole process, by switching from `oauth_flow_{name}` to `req_oauth_{name}`: +We can authenticate this request with `req_oauth_auth_code()`, using the same arguments as our previous call to `oauth_flow_auth_code()`: -```{r, eval = FALSE} -request("https://api.github.com/user") %>% - req_oauth_auth_code(client, auth_url = "https://github.com/login/oauth/authorize") %>% +```{r} +#| eval: false +req %>% + req_oauth_auth_code( + client = github_client(), + auth_url = "https://github.com/login/oauth/authorize" + ) %>% req_perform() %>% - resp_body_json() + resp_body_json() %>% + str() +``` + +When you run this code, you'll see something like this, but it will obviously contain information about you, not me. + +```{r} +#| eval: false +#> List of 32 +#> $ login : chr "hadley" +#> $ id : int 4196 +#> $ node_id : chr "MDQ6VXNlcjQxOTY=" +#> $ avatar_url : chr "https://avatars.githubusercontent.com/u/4196?v=4" +#> $ gravatar_id : chr "" +#> $ url : chr "https://api.github.com/users/hadley" +#> $ html_url : chr "https://github.com/hadley" +#> ... +#> $ type : chr "User" +#> $ site_admin : logi FALSE +#> $ name : chr "Hadley Wickham" +#> $ company : chr "@posit-pbc" +#> $ blog : chr "http://hadley.nz" +#> $ location : chr "Houston, TX" ``` -This is important because most APIs provide only a short-lived access token that needs to be regularly refreshed using a longer-lived refresh token. -httr2 will automatically refresh the token if it's expired (i.e. its expiry date is in the past) or if the request errors with a 401 and there's an `invalid_token` error in the `WWW-authenticate` header. +### Caching -## Caching +There are two big reasons to allow httr2 to manage tokens for you. +The first is that httr2 will automatically refresh the token if it's expired. +The second is cross-session caching, as described below. -By default, `req_oauth_auth_code()` and friends will cache the token in memory, so that multiple requests in the same session all use the same token. -In some cases, you may want to save the token so that it's automatically used across sessions. -This is easy to do (just set `cache_disk = TRUE` in `req_oauth_auth_code()`) but you need to carefully consider the consequences of saving the user's credentials on disk. +By default, the OAuth token will be cached in memory. +That means that you will only need to authenticate once in the current session, but you'll need to re-authenticate if you restart R. +In some cases, you may want to save the tokens (refresh and access) so that they can be used across sessions. +This is easy to do (just set `cache_disk = TRUE`) but you need to think through the consequences of saving the refresh token on disk. httr2 does the best it can to save these credentials securely. -They are stored in a local cache directory (`oauth_cache_path())` that should only be accessible to the current user, and are encrypted so they will be hard for any package other than httr2 to read. +They are stored in a local cache directory (`oauth_cache_path()`) that is accessible to the current user, and they are encrypted so they will be hard for any package other than httr2 to read. However, there's no way to prevent other R code from using httr2 to access them, so if you do choose to cache tokens, you should inform the user and give them the ability to opt-out. +httr2 automatically deletes any cached tokens that are older than 30 days whenever it's loaded. +This means that you'll need to re-auth at least once a month, but prevents tokens from hanging around on disk long after you've forgotten you created them. You can see which clients have cached tokens by looking in the cache directory used by httr2: ```{r} +#| eval: false dir(oauth_cache_path(), recursive = TRUE) ``` -httr2 automatically deletes any cached tokens that are older than 30 days whenever it's loaded. -This means that you'll need to re-auth at least once a month, but prevents tokens for hanging around on disk long after you've forgotten you created them. +Each client gets its own subdirectory named using the client `name`, so if you turn caching on, it's particularly important to give your client a good name so that the user can easily tell which package the tokens belong to. + +## Other flows + +When wrapping an API, you'll need to carefully read the documentation to figure out which flows it provides. +If possible you'll want to use the authorization code flow since it generally provides the best experience, but if it's not available you'll need to carefully consider the others. +Currently, httr2 supports the following flows: + +- `req_oauth_device()` uses the "device" flow which is designed for devices like TVs that don't have an easy way to enter data. + It also works well from the console. + +- `req_oauth_client_credentials()` and `req_oauth_bearer_jwt()` are often needed for **service accounts**, accounts that represent automated services, not people, and are often used in non-interactive environments. + +- `req_oauth_password()` exchanges a user name and password for an access token. + +- `req_oauth_refresh()` works directly with a refresh token that you already have. + It's useful for testing and automation. + +httr2 doesn't support the implicit grant flow. +This was historically important but is now [mostly deprecated](https://developer.okta.com/blog/2019/05/01/is-the-oauth-implicit-flow-dead) and was never a particularly good fit for R because it relies on a technique for returning the access token that only reliably works inside a web browser. + +Regardless of which flow you use, you'll need to follow the same process as the example above: first figure out how to get a token using the `oauth_flow_` function, then actually use oauth with a request, by calling the matching `req_oauth_` function. + +One additional wrinkle is that many APIs don't implement the flow in exactly the same way as the spec so httr2's built-in flows might not work at all. +If your initial attempt doesn't work, you're going to need to do some sleuthing. +This is going to be painful, but unfortunately there's no way around it. +I recommend wrapping your httr2 code in `with_verbosity()` so you can see exactly what httr2 is sending to the server. +You'll then need to carefully compare this to the API documentation and play "spot the difference". +You're very welcome to file an issue and I'll do my best to help you out.