-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #10 from andyquinterom/T7
Adds support for auth0 and fixes bugs
- Loading branch information
Showing
14 changed files
with
405 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,216 @@ | ||
#' @keywords internal | ||
build_auth0_login_url <- function(auth_url, client_id, redirect_uri) { | ||
url <- httr2::url_parse(auth_url) | ||
url$query <- list( | ||
client_id = client_id, | ||
redirect_uri = redirect_uri, | ||
response_type = "code", | ||
scope = "openid email profile" | ||
) | ||
httr2::url_build(url) | ||
} | ||
|
||
#' @title Create a new auth0_config object | ||
#' @description Creates a new auth0_config object | ||
#' | ||
#' @param client_id The client ID for the app | ||
#' @param client_secret The client secret for the app | ||
#' @param auth0_domain The domain for the Auth0 tenant | ||
#' @param app_url The URL for the app | ||
#' | ||
#' @return An auth0_config object | ||
#' @export | ||
new_auth0_config <- function(client_id, client_secret, auth0_domain, app_url) { | ||
app_url <- add_trailing_slash(app_url) | ||
auth_url <- glue::glue("https://{auth0_domain}/authorize") | ||
token_url <- glue::glue("https://{auth0_domain}/oauth/token") | ||
jwks_url <- glue::glue("https://{auth0_domain}/.well-known/jwks.json") | ||
redirect_uri <- build_redirect_uri(app_url) | ||
login_url <- build_auth0_login_url(auth_url, client_id, redirect_uri) | ||
structure( | ||
list( | ||
app_url = app_url, | ||
client_id = client_id, | ||
client_secret = client_secret, | ||
redirect_uri = redirect_uri, | ||
auth_url = auth_url, | ||
token_url = token_url, | ||
jwks_url = jwks_url, | ||
login_url = login_url, | ||
jwks = fetch_jwks(jwks_url) | ||
), | ||
class = c("auth0_config", "openid_config") | ||
) | ||
} | ||
|
||
#' @keywords internal | ||
get_login_url.auth0_config <- function(config) { | ||
config$login_url | ||
} | ||
|
||
#' @keywords internal | ||
get_logout_url.auth0_config <- function(config) { | ||
stop("Not implemented") | ||
} | ||
|
||
#' @keywords internal | ||
request_token.auth0_config <- function(config, authorization_code) { | ||
res <- httr2::request(config$token_url) |> | ||
httr2::req_method("POST") |> | ||
httr2::req_body_form( | ||
code = authorization_code, | ||
client_id = config$client_id, | ||
client_secret = config$client_secret, | ||
grant_type = "authorization_code", | ||
redirect_uri = config$redirect_uri | ||
) |> | ||
httr2::req_perform() | ||
resp_status <- httr2::resp_status(res) | ||
if (resp_status != 200) { | ||
stop(httr2::resp_body_string(res)) | ||
} | ||
resp_body <- httr2::resp_body_json(res) | ||
access_token(config, resp_body$id_token) | ||
} | ||
|
||
#' @keywords internal | ||
decode_token.auth0_config <- function(config, token) { | ||
decoded <- config$jwks |> | ||
purrr::map(function(jwk) { | ||
tryCatch( | ||
jose::jwt_decode_sig(token, jwk), | ||
error = function(e) { | ||
NULL | ||
} | ||
) | ||
}) |> | ||
purrr::discard(is.null) |> | ||
purrr::pluck(1, .default = NULL) | ||
if (is.null(decoded)) { | ||
stop("Unable to decode token") | ||
} | ||
return(decoded) | ||
} | ||
|
||
#' @keywords internal | ||
get_client_id.auth0_config <- function(config) { | ||
config$client_id | ||
} | ||
|
||
#' @keywords internal | ||
shiny_app.auth0_config <- function(config, app) { | ||
app_handler <- app$httpHandler | ||
login_handler <- function(req) { | ||
|
||
# If the user sends a POST request to /login, we'll get a code | ||
# and exchange it for an access token. We'll then redirect the | ||
# user to the root path, setting a cookie with the access token. | ||
if (req$PATH_INFO == "/login") { | ||
query <- shiny::parseQueryString(req$QUERY_STRING) | ||
token <- promises::future_promise({ | ||
request_token(config, query[["code"]]) | ||
}) | ||
return( | ||
promises::then( | ||
token, | ||
onFulfilled = function(token) { | ||
shiny::httpResponse( | ||
status = 302, | ||
headers = list( | ||
Location = config$app_url, | ||
"Set-Cookie" = build_cookie("access_token", get_bearer(token)) | ||
) | ||
) | ||
}, | ||
onRejected = function(e) { | ||
shiny::httpResponse( | ||
status = 302, | ||
headers = list( | ||
Location = config$app_url, | ||
"Set-Cookie" = build_cookie("access_token", "") | ||
) | ||
) | ||
} | ||
) | ||
) | ||
} | ||
|
||
if (req$PATH_INFO == "/logout") { | ||
return( | ||
shiny::httpResponse( | ||
status = 302, | ||
headers = list( | ||
Location = config$app_url, | ||
"Set-Cookie" = build_cookie("access_token", "") | ||
) | ||
) | ||
) | ||
} | ||
|
||
# Get eh HTTP cookies from the request | ||
cookies <- parse_cookies(req$HTTP_COOKIE) | ||
|
||
# If the user requests the root path, we'll check if they have | ||
# an access token. If they don't, we'll redirect them to the | ||
# login page. | ||
if (req$PATH_INFO == "/") { | ||
token <- tryCatch( | ||
expr = access_token(config, remove_bearer(cookies$access_token)), | ||
error = function(e) { | ||
return(NULL) | ||
} | ||
) | ||
if (is.null(token)) { | ||
return( | ||
shiny::httpResponse( | ||
status = 302, | ||
headers = list( | ||
Location = get_login_url(config) | ||
) | ||
) | ||
) | ||
} | ||
} | ||
|
||
# If the user requests any other path, we'll check if they have | ||
# an access token. If they don't, we'll return a 403 Forbidden | ||
# response. | ||
token <- tryCatch( | ||
expr = access_token(config, remove_bearer(cookies$access_token)), | ||
error = function(e) { | ||
return(NULL) | ||
} | ||
) | ||
|
||
if (is.null(token)) { | ||
return( | ||
shiny::httpResponse( | ||
status = 403, | ||
content_type = "text/plain", | ||
content = "Forbidden" | ||
) | ||
) | ||
} | ||
|
||
# If we have reached this point, the user has a valid access | ||
# token and therefore we can return NULL, which will cause the | ||
# app handler to be called. | ||
return(NULL) | ||
} | ||
|
||
handlers <- list( | ||
login_handler, | ||
app_handler | ||
) | ||
|
||
app$httpHandler <- function(req) { | ||
for (handler in handlers) { | ||
response <- handler(req) | ||
if (!is.null(response)) { | ||
return(response) | ||
} | ||
} | ||
} | ||
|
||
return(app) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,27 @@ | ||
url: ~ | ||
template: | ||
bootstrap: 5 | ||
reference: | ||
- title: Provider configurations | ||
desc: Setup an authentication provider | ||
- contents: | ||
- new_openid_config | ||
- new_google_config | ||
- new_entra_id_config | ||
- new_auth0_config | ||
- title: Work with token | ||
desc: Interact with the authentication token | ||
- contents: | ||
- get_token_field | ||
- expires_at | ||
- expires_in | ||
- is_expired | ||
- is_valid | ||
- title: Shiny | ||
desc: Functions to use inside Shiny | ||
- contents: | ||
- sso_shiny_app | ||
- token | ||
- title: S3 methods | ||
- contents: | ||
- print.access_token |
Oops, something went wrong.