/
gargle_oauth_client.R
275 lines (243 loc) · 8.63 KB
/
gargle_oauth_client.R
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
#' Create an OAuth client for Google
#'
#' @description
#' A `gargle_oauth_client` consists of:
#' * A type. gargle only supports the "Desktop app" and "Web application" client
#' types. Different types are associated with different OAuth flows.
#' * A client ID and secret.
#' * Optionally, one or more redirect URIs.
#' * A name. This is really a human-facing label. Or, rather, it can be used
#' that way, but the default is just a hash. We recommend using the same name
#' here as the name used to label the client ID in the [Google Cloud Platform
#' Console](https://console.cloud.google.com).
#'
#' A `gargle_oauth_client` is an adaptation of httr's [oauth_app()] (currently)
#' and httr2's `oauth_client()` (which gargle will migrate to in the future).
#' @param path JSON downloaded from [Google Cloud
#' Console](https://console.cloud.google.com), containing a client id and
#' secret, in one of the forms supported for the `txt` argument of
#' [jsonlite::fromJSON()] (typically, a file path or JSON string).
#' @param name A label for this specific client, presumably the same name used
#' to label it in Google Cloud Console. Unfortunately there is no way to
#' make that true programmatically, i.e. the JSON representation does not
#' contain this information.
#' @param id Client ID
#' @param secret Client secret
#' @param redirect_uris Where your application listens for the response from
#' Google's authorization server. If you didn't configure this specifically
#' when creating the client (which is only possible for clients of the "web"
#' type), you can leave this unspecified.
#' @param type Specifies the type of OAuth client. The valid values are a subset
#' of possible Google client types and reflect the key used to describe the
#' client in its JSON representation:
#' * `"installed"` is associated with a "Desktop app"
#' * `"web"` is associated with a "Web application"
#' @return An OAuth client: An S3 list with class `gargle_oauth_client`. For
#' backwards compatibility reasons, this currently also inherits from the httr
#' S3 class `oauth_app`, but that is a temporary measure. An instance of
#' `gargle_oauth_client` stores more information than httr's `oauth_app`, such
#' as the OAuth client's type ("web" or "installed").
#'
#' There are some redundant fields in this object during the httr-to-httr2
#' transition period. The legacy fields `appname` and `key` repeat the
#' information in the future-facing fields `name` and (client) `id`. Prefer
#' `name` and `id` to `appname` and `key` in downstream code. Prefer the
#' constructors `gargle_oauth_client_from_json()` and `gargle_oauth_client()`
#' to [httr::oauth_app()] and [oauth_app_from_json()].
#' @export
#'
#' @examples
#' \dontrun{
#' gargle_oauth_client_from_json(
#' path = "/path/to/the/JSON/you/downloaded/from/gcp/console.json",
#' name = "my-nifty-oauth-client"
#' )
#' }
#'
#' gargle_oauth_client(
#' id = "some_long_id",
#' secret = "ssshhhhh_its_a_secret",
#' name = "my-nifty-oauth-client"
#' )
gargle_oauth_client_from_json <- function(path, name = NULL) {
check_string(path)
if (!is.null(name)) {
check_string(name)
}
json <- jsonlite::fromJSON(path, simplifyVector = FALSE)
if (length(json) != 1) {
gargle_abort(c(
"JSON has an unexpected form",
"i" = "Are you sure this is the JSON downloaded for an OAuth client?",
"i" = "It is easy to confuse the JSON for an OAuth client and a service account."
))
}
info <- json[[1]]
gargle_oauth_client(
id = info$client_id,
secret = info$client_secret,
redirect_uris = info$redirect_uris,
type = names(json),
name = name %||% glue("{info$project_id}_{hash(info$project_id)}")
)
}
#' @export
#' @rdname gargle_oauth_client_from_json
gargle_oauth_client <- function(id,
secret,
redirect_uris = NULL,
type = c("installed", "web"),
name = hash(id)) {
check_string(id)
check_string(secret)
check_string(name)
type <- arg_match(type)
if (!is.null(redirect_uris)) {
# httr appears to assume that an OAuth app can have exactly 1 redirect_uri
# (gargle has never used the `redirect_uri` field of httr::oauth_app)
# httr2 seems to think it can usually construct the redirect_uri?
# I think I have to accept multiple URIs, because that can be true in the
# downloaded JSON
# we'll just have to decide which one to use downstream, based on context
redirect_uris <- unlist(redirect_uris)
check_character(redirect_uris)
}
if (type == "web" && length(redirect_uris) == 0) {
gargle_abort('
A "web" type OAuth client must have one or more {.field redirect_uris}.')
}
structure(
list(
name = name,
id = id,
secret = secret,
type = type,
redirect_uris = redirect_uris,
# needed for backwards compatibility; I need this class to quack like a
# specialization of httr's oauth_app class, for now
appname = name,
key = id
),
class = c("gargle_oauth_client", "oauth_app")
# in the future, maybe:
# class = c("gargle_oauth_client", "httr2_oauth_client")
)
}
# adapted from httr2 ----
#' @export
print.gargle_oauth_client <- function(x, ...) {
# this print method needs work, but not a high priority atm
cli::cli_text(cli::style_bold("<gargle_oauth_client>"))
redacted <- list_redact(compact(x), "secret")
# quick fix for multiple URIs case
if (length(redacted$redirect_uris) > 0) {
redacted$redirect_uris <- commapse(redacted$redirect_uris)
}
# hide redundant fields that exist only for backwards compatibility with
# httr's oauth_app
redacted$appname <- redacted$key <- NULL
cli::cli_dl(redacted)
invisible(x)
}
list_redact <- function(x, names, case_sensitive = TRUE) {
if (case_sensitive) {
i <- match(names, names(x))
} else {
i <- match(tolower(names), tolower(names(x)))
}
x[i] <- cli::col_grey("<REDACTED>")
x
}
#' OAuth client for demonstration purposes
#'
#' @description
#' Invisibly returns an instance of
#' [`gargle_oauth_client`][gargle_oauth_client()] that can be used to test drive
#' gargle before obtaining your own client ID and secret. This OAuth client may
#' be deleted or rotated at any time. There are no guarantees about which APIs
#' are enabled. DO NOT USE THIS IN A PACKAGE or for anything other than
#' interactive, small-scale experimentation.
#'
#' You can get your own OAuth client ID and secret, without these limitations.
#' See the `vignette("get-api-credentials")` for more details.
#'
#' @inheritParams gargle_oauth_client_from_json
#'
#' @return An OAuth client, produced by [gargle_oauth_client()], invisibly.
#' @export
#' @keywords internal
#' @examples
#' \dontrun{
#' gargle_client()
#' }
gargle_client <- function(type = NULL) {
if (is.null(type) || is.na(type)) {
type <- gargle_oauth_client_type()
}
check_string(type)
type <- arg_match(type, values = c("installed", "web"))
switch(
type,
web = goc_web(),
installed = goc_installed()
)
}
#' @export
#' @keywords internal
#' @rdname internal-assets
tidyverse_client <- function(type = NULL) {
check_permitted_package(caller_env())
if (is.null(type) || is.na(type)) {
type <- gargle_oauth_client_type()
}
check_string(type)
type <- arg_match(type, values = c("installed", "web"))
switch(
type,
web = toc_web(),
installed = toc_installed()
)
}
# deprecated functions ----
#' Create an OAuth app from JSON
#'
#' @description
#' `r lifecycle::badge("deprecated")`
#'
#' `oauth_app_from_json()` is being replaced with
#' [`gargle_oauth_client_from_json()`], in light of the new
#' `gargle_oauth_client` class. Now `oauth_app_from_json()` potentially warns
#' about this deprecation and immediately passes its inputs through to
#' [`gargle_oauth_client_from_json()`].
#'
#' `gargle_app()` is being replaced with [gargle_client()].
#'
#' @inheritParams gargle_oauth_client
#' @inheritParams httr::oauth_app
#' @keywords internal
#' @export
oauth_app_from_json <- function(path,
appname = NULL) {
lifecycle::deprecate_soft(
"1.3.0", "oauth_app_from_json()", "gargle_oauth_client_from_json()"
)
gargle_oauth_client_from_json(path = path, name = appname)
}
#' @export
#' @keywords internal
#' @rdname internal-assets
tidyverse_app <- function() {
lifecycle::deprecate_soft(
"1.3.0", "tidyverse_app()", "tidyverse_client()"
)
tidyverse_client()
}
#' @export
#' @keywords internal
#' @rdname oauth_app_from_json
gargle_app <- function() {
lifecycle::deprecate_soft(
"1.3.0", "gargle_app()", "gargle_client()"
)
gargle_client()
}