Skip to content

Commit

Permalink
Add support for RFC 8693 token exchange requests. (#581)
Browse files Browse the repository at this point in the history
This commit adds a new OAuth "flow" to perform the token exchange
protocol described in RFC 8693 [0]. This is a pretty obscure and
advanced OAuth feature, but I thought it would be nice to have some
helpers to support it in `httr2`, anyway.

I'm not aware of *that* many implementations of this RFC, though there
are a few to note:

- GCP uses it for a couple of identity federation features [1].

- Okta uses it for some advanced delegation features [2].

- Some open-source auth tools like Curity seem to support it [3], as do
  various commercial identity management platforms like Asgardeo [4].

- Posit Connect uses it to power its OAuth integration feature [5].

Closes #460.

[0]: https://datatracker.ietf.org/doc/html/rfc8693
[1]: https://cloud.google.com/iam/docs/workforce-obtaining-short-lived-credentials
[2]: https://developer.okta.com/docs/guides/set-up-token-exchange/main/
[3]: https://curity.io/resources/learn/token-exchange-flow/
[4]: https://wso2.com/asgardeo/docs/guides/authentication/configure-token-exchange/
[5]: https://docs.posit.co/connect/admin/integrations/oauth-integrations/

Signed-off-by: Aaron Jacobs <[email protected]>
  • Loading branch information
atheriel authored Nov 14, 2024
1 parent 84bb22d commit 9271f32
Show file tree
Hide file tree
Showing 9 changed files with 234 additions and 5 deletions.
2 changes: 2 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export(oauth_flow_client_credentials)
export(oauth_flow_device)
export(oauth_flow_password)
export(oauth_flow_refresh)
export(oauth_flow_token_exchange)
export(oauth_redirect_uri)
export(oauth_token)
export(oauth_token_cached)
Expand Down Expand Up @@ -74,6 +75,7 @@ export(req_oauth_client_credentials)
export(req_oauth_device)
export(req_oauth_password)
export(req_oauth_refresh)
export(req_oauth_token_exchange)
export(req_options)
export(req_perform)
export(req_perform_connection)
Expand Down
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# httr2 (development version)

* `req_perform_promise()` upgraded to use event-driven async based on waiting efficiently on curl socket activity (#579).
* New `req_oauth_token_exchange()` and `oauth_flow_token_exchange()` functions implement the OAuth token exchange protocol from RFC 8693 (@atheriel, #460).

# httr2 1.0.6

Expand Down
109 changes: 109 additions & 0 deletions R/oauth-flow-token-exchange.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#' OAuth token exchange
#'
#' @description
#' Authenticate by exchanging one security token for another, as defined by
#' `r rfc(8693, 2)`. It is typically used for advanced authorization flows that
#' involve "delegation" or "impersonation" semantics, such as when a client
#' accesses a resource on behalf of another party, or when a client's identity
#' is federated from another provider.
#'
#' Learn more about the overall OAuth authentication flow in
#' <https://httr2.r-lib.org/articles/oauth.html>.
#'
#' @export
#' @family OAuth flows
#' @inheritParams req_perform
#' @inheritParams req_oauth_auth_code
#' @param subject_token The security token to exchange. This is usually an
#' OpenID Connect ID token or a SAML2 assertion.
#' @param subject_token_type A URI that describes the type of the security
#' token. Usually one of the options in `r rfc(8693, 3)`.
#' @param resource The URI that identifies the resource that the client is
#' trying to access, if applicable.
#' @param audience The logical name that identifies the resource that the client
#' is trying to access, if applicable. Usually one of `resource` or `audience`
#' must be supplied.
#' @param requested_token_type An optional URI that describes the type of the
#' security token being requested. Usually one of the options in
#' `r rfc(8693, 3)`.
#' @param actor_token An optional security token that represents the client,
#' rather than the identity behind the subject token.
#' @param actor_token_type When `actor_token` is not `NULL`, this must be the
#' URI that describes the type of the security token being requested. Usually
#' one of the options in `r rfc(8693, 3)`.
#' @returns `req_oauth_token_exchange()` returns a modified HTTP [request] that
#' will exchange one security token for another; `oauth_flow_token_exchange()`
#' returns the resulting [oauth_token] directly.
#'
#' @examples
#' # List Google Cloud storage buckets using an OIDC token obtained
#' # from e.g. Microsoft Entra ID or Okta and federated to Google. (A real
#' # project ID and workforce pool would be required for this in practice.)
#' #
#' # See: https://cloud.google.com/iam/docs/workforce-obtaining-short-lived-credentials
#' oidc_token <- "an ID token from Okta"
#' request("https://storage.googleapis.com/storage/v1/b?project=123456") |>
#' req_oauth_token_exchange(
#' client = oauth_client("gcp", "https://sts.googleapis.com/v1/token"),
#' subject_token = oidc_token,
#' subject_token_type = "urn:ietf:params:oauth:token-type:id_token",
#' scope = "https://www.googleapis.com/auth/cloud-platform",
#' requested_token_type = "urn:ietf:params:oauth:token-type:access_token",
#' audience = "//iam.googleapis.com/locations/global/workforcePools/123/providers/456",
#' token_params = list(
#' options = '{"userProject":"123456"}'
#' )
#' )
req_oauth_token_exchange <- function(req,
client,
subject_token,
subject_token_type,
resource = NULL,
audience = NULL,
scope = NULL,
requested_token_type = NULL,
actor_token = NULL,
actor_token_type = NULL,
token_params = list()) {
params <- list(
client = client,
subject_token = subject_token,
subject_token_type = subject_token_type,
resource = resource,
audience = audience,
scope = scope,
requested_token_type = requested_token_type,
actor_token = actor_token,
actor_token_type = actor_token_type,
token_params = token_params
)
cache <- cache_mem(client, NULL)
req_oauth(req, "oauth_flow_token_exchange", params, cache = cache)
}

#' @export
#' @rdname req_oauth_token_exchange
oauth_flow_token_exchange <- function(client,
subject_token,
subject_token_type,
resource = NULL,
audience = NULL,
scope = NULL,
requested_token_type = NULL,
actor_token = NULL,
actor_token_type = NULL,
token_params = list()) {
oauth_client_get_token(
client,
grant_type = "urn:ietf:params:oauth:grant-type:token-exchange",
subject_token = subject_token,
subject_token_type = subject_token_type,
resource = resource,
audience = audience,
scope = scope,
requested_token_type = requested_token_type,
actor_token = actor_token,
actor_token_type = actor_token_type,
!!!token_params
)
}
3 changes: 2 additions & 1 deletion man/req_oauth_auth_code.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion man/req_oauth_bearer_jwt.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion man/req_oauth_client_credentials.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion man/req_oauth_password.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion man/req_oauth_refresh.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

112 changes: 112 additions & 0 deletions man/req_oauth_token_exchange.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 9271f32

Please sign in to comment.