Skip to content

Commit 09dfe33

Browse files
Merge pull request #104 from The-Strategy-Unit/francisbarton/issue94
🧹 Refactor, simplify and redocument `get_auth_token()`
2 parents b42d629 + 90f3b93 commit 09dfe33

7 files changed

Lines changed: 101 additions & 151 deletions

File tree

R/get_auth_token.R

Lines changed: 57 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -2,44 +2,42 @@
22
#'
33
#' This function retrieves an Azure token for a specified resource.
44
#'
5-
#' If the environment variables `AZ_TENANT_ID`, `AZ_CLIENT_ID` and
6-
#' `AZ_APP_SECRET` are all set, it will try to use these to return a token.
5+
#' It will try to get a managed token when used within a managed resource such
6+
#' as Azure VM or Azure App Service.
77
#'
8-
#' Otherwise it will try to get a managed token from a managed resource such as
9-
#' Azure VM or Azure App Service.
10-
#'
11-
#' If neither of these approaches has returned a token, it will try to retrieve
12-
#' a user token using the provided parameters, requiring the user to have
13-
#' authenticated using their device. If `force_refresh` is set to `TRUE`, a
14-
#' fresh web authentication process should be launched. Otherwise it will
15-
#' attempt to use a cached token matching the given `resource`, `tenant` and
16-
#' `aad_version`.
8+
#' If this method does not return a token, it will try to retrieve a user token
9+
#' using the provided parameters, requiring the user to have authenticated
10+
#' using their device. If `force_refresh` is set to `TRUE`, a fresh web
11+
#' authentication process should be launched. Otherwise it will attempt to use
12+
#' a cached token matching the given `resource`, `tenant` and `aad_version`.
1713
#'
1814
#' @param resource For v1, a simple URL such as `"https://storage.azure.com/"`
19-
#' should be supplied.For v2, a vector specifying the URL of the Azure resource
20-
#' for which the token is requested as well as any desired scopes. See
21-
#' [AzureAuth::get_azure_token] for details. Use [generate_resource]
22-
#' to help provide an appropriate string or vector. The values default to
23-
#' `c("https://storage.azure.com/.default", "openid", "offline_access")`.
15+
#' should be supplied. For v2, a vector specifying the URL of the Azure
16+
#' resource for which the token is requested as well as any desired scopes.
17+
#' See [AzureAuth::get_azure_token] for details. Use [generate_resource]
18+
#' to help provide an appropriate string or vector.
2419
#' If setting version to 2, ensure that the `aad_version` argument is also set
2520
#' to 2. Both are set to use AAD version 1 by default.
2621
#' @param tenant A string specifying the Azure tenant. Defaults to
2722
#' `"common"`. See [AzureAuth::get_azure_token] for other values.
28-
#' @param client_id A string specifying the application ID (client ID). If
23+
#' @param client_id A string specifying the application ID (aka client ID). If
2924
#' `NULL`, (the default) the function attempts to obtain the client ID from the
3025
#' Azure Resource Manager token, or prompts the user to log in to obtain it.
3126
#' @param auth_method A string specifying the authentication method. Defaults to
32-
#' `"authorization_code"`. See [AzureAuth::get_azure_token] for other values.
27+
#' `"authorization_code"`. To use a secret, pass `"client_credentials"` instead
28+
#' and provide the secret using the `password` argument in `...`. See
29+
#' [AzureAuth::get_azure_token] for more information.
3330
#' @param aad_version Numeric. The AAD version, either 1 or 2 (1 by default)
34-
#' @param force_refresh Boolean: whether to use a stored token if available
31+
#' @param force_refresh logical. Whether to use a stored token if available
3532
#' (`FALSE`, the default), or try to obtain a new one from Azure (`TRUE`).
3633
#' This may be useful if you wish to generate a new token with the same
3734
#' `resource` value as an existing token, but a different `tenant` or
38-
#' `auth_method`. Note that you can also try using [refresh_token] which will
35+
#' `auth_method`. Note that you can also try [refresh_token], which should
3936
#' cause an existing token to refresh itself, without obtaining a new token
4037
#' from Azure via online reauthentication
41-
#' @param ... Optional arguments (`token_args` or `use_cache`) to be passed on
42-
#' to [AzureAuth::get_managed_token] or [AzureAuth::get_azure_token].
38+
#' @param ... Optional arguments (eg `token_args` or `use_cache`) to be passed
39+
#' on to [AzureAuth::get_managed_token] or [AzureAuth::get_azure_token], for
40+
#' example to overwrite any opf their default values or to supply a `password`
4341
#'
4442
#' @returns An Azure token object
4543
#' @examples
@@ -58,6 +56,14 @@
5856
#'
5957
#' # Get a token using a specific app ID
6058
#' token <- get_auth_token(client_id = "my-app-id")
59+
#'
60+
#' # Use a secret
61+
#' token <- get_auth_token(
62+
#' tenant = "my-tenant-id",
63+
#' client_id = "my-app-id",
64+
#' auth_method = "client_credentials",
65+
#' password = "123459878&%^"
66+
#' )
6167
#' }
6268
#' @export
6369
get_auth_token <- function(
@@ -73,52 +79,49 @@ get_auth_token <- function(
7379
aad_version <- check_that(aad_version, \(x) x %in% seq(2), aad_msg)
7480

7581
safely_get_token <- \(...) purrr::safely(AzureAuth::get_azure_token)(...)
76-
get_azure_token <- purrr::partial(
77-
safely_get_token,
78-
resource = resource,
79-
version = aad_version
80-
)
81-
possibly_get_mtk <- \(...) purrr::possibly(AzureAuth::get_managed_token)(...)
82+
possibly_get_mgdt <- \(...) purrr::possibly(AzureAuth::get_managed_token)(...)
8283

8384
dots <- rlang::list2(...)
8485
# If the user specifies force_refresh = TRUE we turn off `use_cache`,
85-
# otherwise we leave `use_cache` as it is (or as `NULL`, its default value)
86-
use_cached <- !force_refresh && (dots[["use_cache"]] %||% TRUE)
87-
dots <- rlang::dots_list(!!!dots, use_cache = use_cached, .homonyms = "last")
88-
89-
# We have 4 approaches to get a token, depending on the context
90-
# 1. Use environment variables if all three are set
91-
token_resp <- rlang::inject(try_token_from_vars(get_azure_token, !!!dots))
92-
token <- token_resp[["result"]]
93-
token_error <- token_resp[["error"]]
86+
# otherwise we leave `use_cache` as it is
87+
if (force_refresh) {
88+
use_cached <- FALSE
89+
} else {
90+
use_cached <- dots[["use_cache"]] # NULL by default if not supplied by user
91+
}
92+
dots <- rlang::dots_list(..., use_cache = use_cached, .homonyms = "last")
9493

95-
# 2. Try to get a managed token (for example on Azure VM, App Service)
96-
if (is.null(token) && imds_available()) {
97-
token <- rlang::inject(possibly_get_mtk(resource, !!!dots))
94+
# We have 3 approaches to get a token, depending on the context
95+
# 1. Try to get a managed token (for example on Azure VM, App Service)
96+
if (imds_available()) {
97+
token <- rlang::inject(possibly_get_mgdt(resource, !!!dots))
98+
} else {
99+
token <- NULL
98100
}
99101

100-
# 3. If neither of those has worked, try to get an already stored user token
101-
# (unless `force_refresh` is on, in which case skip to option 4)
102+
# 2. If that hasn't worked, try to get an already stored user token
103+
# (unless `force_refresh` is on, in which case skip to option 3)
102104
if (is.null(token) && use_cached) {
103105
token <- match_cached_token(resource, tenant, aad_version)
104106
}
105107

106-
# 4. If we still don't have a token, try to get a new one via reauthentication
108+
# 3. If we still don't have a token, try to get a new one via reauthentication
107109
if (is.null(token)) {
108110
if (!force_refresh) {
109111
cli::cli_alert_info("No matching cached token found: fetching new token")
110112
}
111-
client_id <- client_id %||% get_client_id()
112113
token_resp <- rlang::inject(
113-
get_azure_token(
114+
safely_get_token(
115+
resource = resource,
114116
tenant = tenant,
115-
app = client_id,
117+
app = client_id %||% get_client_id(),
116118
auth_type = auth_method,
119+
version = aad_version,
117120
!!!dots
118121
)
119122
)
120123
token <- token_resp[["result"]]
121-
token_error <- token_error %||% token_resp[["error"]]
124+
token_error <- token_resp[["error"]]
122125
}
123126

124127
# Give some helpful feedback if the steps above have not succeeded
@@ -141,33 +144,8 @@ get_auth_token <- function(
141144
}
142145

143146

144-
#' Get token via app and secret environment variables
145-
#' Sub-routine for `get_auth_token()`
146-
#' @keywords internal
147-
#' @returns A list with elements `result` and `error`. If this method is
148-
#' successful, the `result` element will contain a token.
149-
try_token_from_vars <- function(get_token_fun, ...) {
150-
tenant_id_env <- Sys.getenv("AZ_TENANT_ID")
151-
client_id_env <- Sys.getenv("AZ_CLIENT_ID")
152-
client_secret <- Sys.getenv("AZ_APP_SECRET")
153-
154-
if (all(nzchar(c(tenant_id_env, client_id_env, client_secret)))) {
155-
rlang::inject(
156-
get_token_fun(
157-
tenant = tenant_id_env,
158-
app = client_id_env,
159-
password = client_secret,
160-
...
161-
)
162-
)
163-
} else {
164-
list(result = NULL, error = NULL)
165-
}
166-
}
167-
168-
169147
#' Find an already cached token that matches desired parameters
170-
#' Sub-routine for `get_auth_token()`
148+
#' Sub-routine for [get_auth_token]
171149
#' @keywords internal
172150
#' @returns A token from local cache, or NULL if none matches
173151
match_cached_token <- function(resource, tenant, aad_version) {
@@ -193,7 +171,7 @@ match_cached_token <- function(resource, tenant, aad_version) {
193171
}
194172

195173

196-
#' Sub-routine for `get_auth_token()`
174+
#' Sub-routine for [get_auth_token]
197175
#'
198176
#' Pulled out mainly to tidy up the main function code a bit
199177
#' @keywords internal
@@ -264,12 +242,12 @@ generate_resource <- function(
264242
#' Use a token's internal `refresh()` method to refresh it
265243
#'
266244
#' This method avoids the need to refresh by re-authenticating online. It seems
267-
#' like this only works with v1 tokens. v2 tokens always seem to refresh by
268-
#' re-authenticating with Azure online. But v2 tokens _ought_ to refresh
269-
#' automatically and not need manual refreshing. To instead generate a
270-
#' completely fresh token, pass `use_cache = FALSE` or `force_refresh = TRUE`
271-
#' to [get_auth_token].
245+
#' that this only works with v1 tokens. (v2 tokens always seem to refresh via
246+
#' online re-authentication, but they _ought_ to refresh automatically.)
247+
#' To instead generate a completely fresh token, set `force_refresh = TRUE` in
248+
#' [get_auth_token]
272249
#' @param token An Azure authentication token
250+
#' @rdname get_auth_token
273251
#' @returns An Azure authentication token
274252
#' @export
275253
refresh_token <- \(token) token$refresh()

man/get_auth_token.Rd

Lines changed: 41 additions & 22 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/get_client_id.Rd

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/match_cached_token.Rd

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/refresh_token.Rd

Lines changed: 0 additions & 22 deletions
This file was deleted.

man/try_token_from_vars.Rd

Lines changed: 0 additions & 18 deletions
This file was deleted.

0 commit comments

Comments
 (0)