@@ -56,9 +56,11 @@ req_oauth_auth_code <- function(req, client,
56
56
pkce = TRUE ,
57
57
auth_params = list (),
58
58
token_params = list (),
59
+ type = c(" desktop" , " web" ),
59
60
host_name = " localhost" ,
60
61
host_ip = " 127.0.0.1" ,
61
- port = httpuv :: randomPort()
62
+ port = httpuv :: randomPort(),
63
+ redirect_uri = " http://localhost"
62
64
) {
63
65
64
66
params <- list (
@@ -68,9 +70,11 @@ req_oauth_auth_code <- function(req, client,
68
70
pkce = pkce ,
69
71
auth_params = auth_params ,
70
72
token_params = token_params ,
73
+ type = type ,
71
74
host_name = host_name ,
72
75
host_ip = host_ip ,
73
- port = port
76
+ port = port ,
77
+ redirect_uri = redirect_uri
74
78
)
75
79
76
80
cache <- cache_choose(client , cache_disk , cache_key )
@@ -85,14 +89,20 @@ req_oauth_auth_code <- function(req, client,
85
89
# ' Section 4.1. This is the most commonly used OAuth flow where the user is
86
90
# ' opens a page in their browser, approves the access, and then returns to R.
87
91
# '
88
- # ' `oauth_flow_auth_code()` is a high-level wrapper that should
89
- # ' work with APIs that adhere relatively closely to the spec. The remaining
90
- # ' low-level functions can be used to assemble a custom flow for APIs that are
91
- # ' further from the spec:
92
+ # ' `oauth_flow_auth_code()` is a high-level wrapper that should work with APIs
93
+ # ' that adhere relatively closely to the spec. When possible, it redirects the
94
+ # ' browser back to a temporary local webserver to capture the authorization
95
+ # ' code. When this is not possible (e.g. when running on a hosted platform
96
+ # ' like RStudio Server) set `type = "web"` to instead prompts the user to enter
97
+ # ' the code manually instead.
92
98
# '
93
- # ' * `oauth_flow_auth_code_url()` generates the url where the user is sent.
94
- # ' * `oauth_flow_auth_code_listen()` starts an webserver that listens for
95
- # ' the response from the resource server.
99
+ # ' The remaining low-level functions can be used to assemble a custom flow for
100
+ # ' APIs that are further from the spec:
101
+ # '
102
+ # ' * `oauth_flow_auth_code_url()` generates the url that should be opened in a
103
+ # ' browser.
104
+ # ' * `oauth_flow_auth_code_listen()` starts a temporary local webserver that
105
+ # ' listens for the response from the resource server.
96
106
# ' * `oauth_flow_auth_code_parse()` parses the query parameters returned from
97
107
# ' the server redirect, verifying that the `state` is correct, and returning
98
108
# ' the authorisation code.
@@ -110,12 +120,20 @@ req_oauth_auth_code <- function(req, client,
110
120
# ' @param auth_params List containing additional parameters passed to `oauth_flow_auth_code_url()`
111
121
# ' @param token_params List containing additional parameters passed to the
112
122
# ' `token_url`.
113
- # ' @param host_name Host name used to generate `redirect_uri`
114
- # ' @param host_ip IP address web server will be bound to.
115
- # ' @param port Port to bind web server to. By default, this uses a random port.
116
- # ' You may need to set it to a fixed port if the API requires that the
123
+ # ' @param host_name `r lifecycle::badge("deprecated")` Use `redirect_uri`
124
+ # ' instead.
125
+ # ' @param host_ip IP address for the temporary webserver used to capture the
126
+ # ' authorization code.
127
+ # ' @param type Either `desktop` or `web`. Use desktop when running on the
128
+ # ' desktop in an environment where you can redirect the user to `localhost`.
129
+ # ' Use `web` when running in a hosted web environment.
130
+ # ' @param port Port to bind the temporary webserver to. Used only when
131
+ # ' `redirect_uri` is `"http(s)://localhost"`. By default, this uses a random
132
+ # ' port. You may need to set it to a fixed port if the API requires that the
117
133
# ' `redirect_uri` specified in the client exactly matches the `redirect_uri`
118
134
# ' generated by this function.
135
+ # ' @param redirect_uri URL to redirect back to after authorization is complete.
136
+ # ' Often this must be registered with the API in advance.
119
137
# ' @returns An [oauth_token].
120
138
# ' @export
121
139
# ' @keywords internal
@@ -139,12 +157,38 @@ oauth_flow_auth_code <- function(client,
139
157
pkce = TRUE ,
140
158
auth_params = list (),
141
159
token_params = list (),
142
- host_name = " localhost " ,
160
+ host_name = deprecated() ,
143
161
host_ip = " 127.0.0.1" ,
144
- port = httpuv :: randomPort()
162
+ type = c(" desktop" , " web" ),
163
+ port = httpuv :: randomPort(),
164
+ redirect_uri = " http://localhost"
145
165
) {
166
+
167
+ type <- arg_match(type )
168
+ if (type == " desktop" ) {
169
+ check_installed(" httpuv" , " desktop OAuth" )
170
+ if (is_hosted_session()) {
171
+ abort(" Only type='web' is supported in the current session" )
172
+ }
173
+ }
174
+
146
175
oauth_flow_check(" authorization code" , client , interactive = TRUE )
147
- check_installed(" httpuv" )
176
+
177
+ # For backwards compatibility, fall back to the original redirect URL
178
+ # construction.
179
+ if (lifecycle :: is_present(host_name )) {
180
+ lifecycle :: deprecate_warn(
181
+ when = " 0.3.0" ,
182
+ what = " oauth_flow_auth_code(host_name)" ,
183
+ with = " oauth_flow_auth_code(redirect_uri)"
184
+ )
185
+ redirect_uri <- paste0(" http://" , host_name , " :" , port , " /" )
186
+ }
187
+
188
+ # Only append a port if we have a bare HTTP(s) localhost redirect.
189
+ if (grepl(" https?://localhost$" , redirect_uri )) {
190
+ redirect_uri <- paste0(redirect_uri , " :" , port , " /" )
191
+ }
148
192
149
193
if (pkce ) {
150
194
code <- oauth_flow_auth_code_pkce()
@@ -154,26 +198,34 @@ oauth_flow_auth_code <- function(client,
154
198
}
155
199
156
200
state <- base64_url_rand(32 )
157
- redirect_url <- paste0(" http://" , host_name , " :" , port , " /" )
158
201
159
- # Redirect user to authorisation url, and listen for result
202
+ # Redirect user to authorisation url.
160
203
user_url <- oauth_flow_auth_code_url(client ,
161
204
auth_url = auth_url ,
162
- redirect_uri = redirect_url ,
205
+ redirect_uri = redirect_uri ,
163
206
scope = scope ,
164
207
state = state ,
165
208
auth_params = auth_params
166
209
)
167
210
utils :: browseURL(user_url )
168
- result <- oauth_flow_auth_code_listen(host_ip , port )
169
- code <- oauth_flow_auth_code_parse(result , state )
211
+
212
+ if (type == " desktop" ) {
213
+ # Listen on localhost for the result.
214
+ result <- oauth_flow_auth_code_listen(host_ip , port )
215
+ code <- oauth_flow_auth_code_parse(result , state )
216
+ } else {
217
+ # Allow the user to retrieve the token out of band manually and enter it
218
+ # into the console. This is what {gargle} terms the "pseudo out-of-band"
219
+ # flow.
220
+ code <- oauth_flow_auth_code_read(state )
221
+ }
170
222
171
223
# Get access/refresh token from authorisation code
172
224
# https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3
173
225
oauth_client_get_token(client ,
174
226
grant_type = " authorization_code" ,
175
227
code = code ,
176
- redirect_uri = redirect_url ,
228
+ redirect_uri = redirect_uri ,
177
229
!!! token_params
178
230
)
179
231
}
@@ -182,7 +234,6 @@ oauth_flow_auth_code <- function(client,
182
234
# https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
183
235
# ' @export
184
236
# ' @rdname oauth_flow_auth_code
185
- # ' @param redirect_uri URL to which user should be redirected.
186
237
# ' @param state Random state generated by `oauth_flow_auth_code()`. Used to
187
238
# ' verify that we're working with an authentication request that we created.
188
239
# ' (This is an unlikely threat for R packages since the webserver that
@@ -296,3 +347,47 @@ oauth_flow_auth_code_pkce <- function() {
296
347
challenge = base64_url_encode(openssl :: sha256(charToRaw(verifier )))
297
348
)
298
349
}
350
+
351
+ # Try to determine whether we can redirect the user's browser to a server on
352
+ # localhost, which isn't possible if we are running on a hosted platform.
353
+ #
354
+ # Currently this detects RStudio Server, Posit Workbench, and Google Colab. It
355
+ # is based on the strategy pioneered by the {gargle} package.
356
+ is_hosted_session <- function () {
357
+ if (nzchar(Sys.getenv(" COLAB_RELEASE_TAG" ))) {
358
+ return (TRUE )
359
+ }
360
+ # If RStudio Server or Posit Workbench is running locally (which is possible,
361
+ # though unusual), it's not acting as a hosted environment.
362
+ Sys.getenv(" RSTUDIO_PROGRAM_MODE" ) == " server" &&
363
+ ! grepl(" localhost" , Sys.getenv(" RSTUDIO_HTTP_REFERER" ), fixed = TRUE )
364
+ }
365
+
366
+ oauth_flow_auth_code_read <- function (state ) {
367
+ code <- trimws(read_line(" Enter authorization code: " ))
368
+ # We support two options here:
369
+ #
370
+ # 1) The original {gargle} style, where the user copy & pastes a
371
+ # base64-encoded JSON object with both the code and state. This is used on
372
+ # https://www.tidyverse.org/google-callback/; and
373
+ #
374
+ # 2) The full manual approach, where the code and state are entered
375
+ # independently.
376
+ result <- tryCatch(
377
+ jsonlite :: fromJSON(rawToChar(openssl :: base64_decode(code ))),
378
+ error = function (e ) {
379
+ list (
380
+ code = code ,
381
+ state = trimws(read_line(" Enter state parameter: " ))
382
+ )
383
+ })
384
+ if (! identical(result $ state , state )) {
385
+ abort(" Authentication failure: state does not match" )
386
+ }
387
+ result $ code
388
+ }
389
+
390
+ # base::readline() wrapper so we can mock user input during testing.
391
+ read_line <- function (prompt = " " ) {
392
+ readline(prompt )
393
+ }
0 commit comments