Skip to content

Commit 981db5f

Browse files
authored
Allow configurable OAuth expiry margins (#861)
Expose `expiry_margin` across request-based OAuth helpers and increase the default safety margin from 5 to 30 seconds. Preserve the normal refresh-token path while allowing clients to accommodate servers that reject near-expiry tokens. Fixes #860.
1 parent 93d6050 commit 981db5f

22 files changed

Lines changed: 220 additions & 32 deletions

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
* Fixed OAuth token cache pruning so that it actually matches the encrypted `.rds.enc` files written to disk; previously the pruning pattern only matched an unencrypted `.rds` file that was never created, so cached tokens were never automatically deleted regardless of age.
44
* httr2 now requires rlang >= 1.3.0, which changes the hash used to name files cached by `req_cache()` and on-disk OAuth token caches (e.g. from `req_oauth_auth_code(cache_disk = TRUE)`). Existing cached files won't match the new hash, so they'll be silently ignored (triggering a normal cache miss/re-authentication) and cleaned up over time by the usual pruning rules; you can also delete them manually.
5+
* `req_oauth_*()` gains an `expiry_margin` argument to control how early cached OAuth tokens are treated as expired; the default margin increases from 5 to 30 seconds (@zacdav-db, #860).
56

67
# httr2 1.2.3
78

R/oauth-flow-auth-code.R

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@
8383
#' Learn more in <https://httr2.r-lib.org/articles/oauth.html>.
8484
#' @param cache_key If you want to cache multiple tokens per app, use this
8585
#' key to disambiguate them.
86+
#' @param expiry_margin Number of seconds before a token's stated expiry that
87+
#' it should be treated as expired. Increase this for servers that reject
88+
#' tokens shortly before they expire. Defaults to 30 seconds.
8689
#' @returns `req_oauth_auth_code()` returns a modified HTTP [request] that will
8790
#' use OAuth; `oauth_flow_auth_code()` returns an [oauth_token].
8891
#' @examples
@@ -106,7 +109,8 @@ req_oauth_auth_code <- function(
106109
token_params = list(),
107110
redirect_uri = oauth_redirect_uri(),
108111
cache_disk = FALSE,
109-
cache_key = NULL
112+
cache_key = NULL,
113+
expiry_margin = 30
110114
) {
111115
auth_url <- oauth_flow_url(auth_url, client, "auth_url")
112116
redirect <- normalize_redirect_uri(redirect_uri = redirect_uri)
@@ -122,7 +126,13 @@ req_oauth_auth_code <- function(
122126
)
123127

124128
cache <- cache_choose(client, cache_disk, cache_key)
125-
req_oauth(req, "oauth_flow_auth_code", params, cache = cache)
129+
req_oauth(
130+
req,
131+
"oauth_flow_auth_code",
132+
params,
133+
cache = cache,
134+
expiry_margin = expiry_margin
135+
)
126136
}
127137

128138
#' @export

R/oauth-flow-client-credentials.R

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ req_oauth_client_credentials <- function(
2828
req,
2929
client,
3030
scope = NULL,
31-
token_params = list()
31+
token_params = list(),
32+
expiry_margin = 30
3233
) {
3334
params <- list(
3435
client = client,
@@ -37,7 +38,13 @@ req_oauth_client_credentials <- function(
3738
)
3839

3940
cache <- cache_mem(client, NULL)
40-
req_oauth(req, "oauth_flow_client_credentials", params, cache = cache)
41+
req_oauth(
42+
req,
43+
"oauth_flow_client_credentials",
44+
params,
45+
cache = cache,
46+
expiry_margin = expiry_margin
47+
)
4148
}
4249

4350
#' @export

R/oauth-flow-device.R

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ req_oauth_device <- function(
4141
auth_params = list(),
4242
token_params = list(),
4343
cache_disk = FALSE,
44-
cache_key = NULL
44+
cache_key = NULL,
45+
expiry_margin = 30
4546
) {
4647
auth_url <- oauth_flow_url(auth_url, client, "device_auth_url")
4748
params <- list(
@@ -54,7 +55,13 @@ req_oauth_device <- function(
5455
token_params = token_params
5556
)
5657
cache <- cache_choose(client, cache_disk, cache_key)
57-
req_oauth(req, "oauth_flow_device", params, cache = cache)
58+
req_oauth(
59+
req,
60+
"oauth_flow_device",
61+
params,
62+
cache = cache,
63+
expiry_margin = expiry_margin
64+
)
5865
}
5966

6067
#' @export

R/oauth-flow-jwt.R

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ req_oauth_bearer_jwt <- function(
4646
signature = "jwt_encode_sig",
4747
signature_params = list(),
4848
scope = NULL,
49-
token_params = list()
49+
token_params = list(),
50+
expiry_margin = 30
5051
) {
5152
params <- list(
5253
client = client,
@@ -58,7 +59,13 @@ req_oauth_bearer_jwt <- function(
5859
)
5960

6061
cache <- cache_mem(client, claim)
61-
req_oauth(req, "oauth_flow_bearer_jwt", params, cache = cache)
62+
req_oauth(
63+
req,
64+
"oauth_flow_bearer_jwt",
65+
params,
66+
cache = cache,
67+
expiry_margin = expiry_margin
68+
)
6269
}
6370

6471
#' @export

R/oauth-flow-password.R

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ req_oauth_password <- function(
3333
scope = NULL,
3434
token_params = list(),
3535
cache_disk = FALSE,
36-
cache_key = username
36+
cache_key = username,
37+
expiry_margin = 30
3738
) {
3839
params <- list(
3940
client = client,
@@ -43,7 +44,13 @@ req_oauth_password <- function(
4344
token_params = token_params
4445
)
4546
cache <- cache_choose(client, cache_disk = cache_disk, cache_key = cache_key)
46-
req_oauth(req, "oauth_flow_password", params, cache = cache)
47+
req_oauth(
48+
req,
49+
"oauth_flow_password",
50+
params,
51+
cache = cache,
52+
expiry_margin = expiry_margin
53+
)
4754
}
4855

4956
#' @export

R/oauth-flow-refresh.R

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ req_oauth_refresh <- function(
3636
client,
3737
refresh_token = Sys.getenv("HTTR2_REFRESH_TOKEN"),
3838
scope = NULL,
39-
token_params = list()
39+
token_params = list(),
40+
expiry_margin = 30
4041
) {
4142
params <- list(
4243
client = client,
@@ -46,7 +47,13 @@ req_oauth_refresh <- function(
4647
)
4748
cache <- cache_mem(client, refresh_token)
4849

49-
req_oauth(req, "oauth_flow_refresh", params, cache = cache)
50+
req_oauth(
51+
req,
52+
"oauth_flow_refresh",
53+
params,
54+
cache = cache,
55+
expiry_margin = expiry_margin
56+
)
5057
}
5158

5259
#' @export

R/oauth-flow-token-exchange.R

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ req_oauth_token_exchange <- function(
6565
requested_token_type = NULL,
6666
actor_token = NULL,
6767
actor_token_type = NULL,
68-
token_params = list()
68+
token_params = list(),
69+
expiry_margin = 30
6970
) {
7071
params <- list(
7172
client = client,
@@ -80,7 +81,13 @@ req_oauth_token_exchange <- function(
8081
token_params = token_params
8182
)
8283
cache <- cache_mem(client, NULL)
83-
req_oauth(req, "oauth_flow_token_exchange", params, cache = cache)
84+
req_oauth(
85+
req,
86+
"oauth_flow_token_exchange",
87+
params,
88+
cache = cache,
89+
expiry_margin = expiry_margin
90+
)
8491
}
8592

8693
#' @export

R/oauth-token.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ print.httr2_token <- function(x, ...) {
6666
invisible(x)
6767
}
6868

69-
token_has_expired <- function(token, delay = 5) {
69+
token_has_expired <- function(token, delay = 30) {
7070
if (is.null(token$expires_at)) {
7171
FALSE
7272
} else {

R/oauth.R

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,36 +15,57 @@
1515
#' @param flow An `oauth_flow_` function used to generate the access token.
1616
#' @param flow_params Parameters for the flow. This should be a named list
1717
#' whose names match the argument names of `flow`.
18+
#' @param expiry_margin Number of seconds before a token's stated expiry that
19+
#' it should be treated as expired. Increase this for servers that reject
20+
#' tokens shortly before they expire. Defaults to 30 seconds.
1821
#' @returns An [oauth_token].
1922
#' @keywords internal
2023
#' @export
21-
req_oauth <- function(req, flow, flow_params, cache) {
24+
req_oauth <- function(req, flow, flow_params, cache, expiry_margin = 30) {
25+
check_number_whole(expiry_margin, min = 0)
26+
2227
# Want req object to contain meaningful objects, not just a closure
2328
req <- req_auth_sign(
2429
req,
2530
fun = auth_oauth_sign,
26-
params = list(flow = flow, flow_params = flow_params),
31+
params = list(
32+
flow = flow,
33+
flow_params = flow_params,
34+
expiry_margin = expiry_margin
35+
),
2736
cache = cache
2837
)
2938
req <- req_policies(req, auth_oauth = TRUE)
3039
req
3140
}
3241

33-
auth_oauth_sign <- function(req, cache, flow, flow_params) {
42+
auth_oauth_sign <- function(
43+
req,
44+
cache,
45+
flow,
46+
flow_params,
47+
expiry_margin = 30
48+
) {
3449
token <- auth_oauth_token_get(
3550
cache = cache,
3651
flow = flow,
37-
flow_params = flow_params
52+
flow_params = flow_params,
53+
expiry_margin = expiry_margin
3854
)
3955
req_auth_bearer_token(req, token$access_token)
4056
}
4157

42-
auth_oauth_token_get <- function(cache, flow, flow_params = list()) {
58+
auth_oauth_token_get <- function(
59+
cache,
60+
flow,
61+
flow_params = list(),
62+
expiry_margin = 30
63+
) {
4364
token <- cache$get()
4465
if (is.null(token)) {
4566
token <- exec(flow, !!!flow_params)
4667
cache$set(token)
47-
} else if (token_has_expired(token)) {
68+
} else if (token_has_expired(token, delay = expiry_margin)) {
4869
cache$clear()
4970
if (is.null(token$refresh_token)) {
5071
token <- exec(flow, !!!flow_params)

0 commit comments

Comments
 (0)