diff --git a/DESCRIPTION b/DESCRIPTION index a1ff52b..466aa66 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,12 +1,13 @@ Package: auth0 Type: Package Title: Authentication in Shiny with Auth0 -Version: 0.2.4 +Version: 0.3.0 Authors@R: c( person("Julio", "Trecenti", email = "julio.trecenti@gmail.com", role = "cre"), person("Daniel", "Falbel", email = "dfalbel@gmail.com", role = "aut"), person("José", "Jesus", email = "jjesusfilho@gmail.com", role = "ctb"), person("Dean", "Attali", email = "daattali@gmail.com", role = "ctb"), + person("William", "Amorim", email = "TheWilliam89@gmail.com", role = "ctb"), person("C", "Lente", email = "c@lente.dev", role = "ctb")) Description: Uses Auth0 API (see for more information) to use a simple authentication system. It provides @@ -16,7 +17,7 @@ BugReports: https://github.com/curso-r/auth0/issues License: MIT + file LICENSE Encoding: UTF-8 Roxygen: list(markdown = TRUE) -RoxygenNote: 7.3.2 +RoxygenNote: 7.3.3 Imports: httr, shiny, yaml, diff --git a/NEWS.md b/NEWS.md index e9e7bd5..abb9420 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,9 @@ -# auth0 0.2.4 +# auth0 0.3.0 -- fix issue #101, which was caused by incorrect parenthesis in `gsub()` function (Thanks to @teofiln for the PR #102) +- Fix issue #101, which was caused by incorrect parenthesis in `gsub()` function (Fixed by @teofiln) +- Add `request_extra_params` option to `_auth0.yml` file, to allow passing extra parameters to the authorization endpoint (Issue #100). +- Custom URL params will no longer be removed after successful authentication. +- Add `remove_callback_params` param to `shinyAppAuth0()` function to remove `code` and `state` callback parameters after successful authentication. # auth0 0.2.3 diff --git a/R/auth0.R b/R/auth0.R index 084737c..13c8c13 100644 --- a/R/auth0.R +++ b/R/auth0.R @@ -69,10 +69,11 @@ auth0_info <- function(config) { api <- auth0_api(conf$api_url, conf$request, conf$access) audience <- conf$audience rurl <- config$remote_url + extra_params <- config$request_extra_params # backward compatibility if (is.null(rurl)) rurl <- config$shiny_config$remote_url - list(scope = scope, state = state, app = app, api = api, audience=audience, - remote_url = rurl) + list(scope = scope, state = state, app = app, api = api, audience = audience, + remote_url = rurl, extra_params = extra_params) } #' Parse `_auth0.yml` file. diff --git a/R/logout.R b/R/logout.R index 3cfd88c..de2a2fd 100644 --- a/R/logout.R +++ b/R/logout.R @@ -47,17 +47,14 @@ logoutButton <- function(label = "Log out", ..., id = "._auth0logout_") { #' #' @export logout_url <- function() { + config <- auth0_config() - # Use the base URL from the redirect_uri (without the current path) - base_url <- sub("(https?://[^/]+).*", "\\1", redirect_uri) - app_url_enc <- utils::URLencode(base_url, reserved = TRUE) - logout_url <- sprintf( - "%s/v2/logout?client_id=%s&returnTo=%s", - config$auth0_config$api_url, - config$auth0_config$credentials$key, - app_url_enc - ) + app_url_enc <- utils::URLencode(redirect_uri, reserved = TRUE) + logout_url <- sprintf("%s/v2/logout?client_id=%s&returnTo=%s", + config$auth0_config$api_url, + config$auth0_config$credentials$key, + app_url_enc) logout_url } @@ -74,14 +71,13 @@ logout_url <- function() { #' #' @export logout <- function() { + if (!requireNamespace("shinyjs", quietly = TRUE)) { stop("Package \"shinyjs\" required.", call. = FALSE) } shiny::insertUI( - selector = "head", - where = "beforeEnd", - immediate = TRUE, + selector = "head", where = "beforeEnd", immediate = TRUE, ui = shinyjs::useShinyjs() ) url <- logout_url() diff --git a/R/shiny.R b/R/shiny.R index fede422..081825e 100644 --- a/R/shiny.R +++ b/R/shiny.R @@ -14,6 +14,7 @@ #' # first, create the yml file using use_auth0() function #' #' if (interactive()) { +#' #' # ui.R file #' library(shiny) #' library(auth0) @@ -27,6 +28,7 @@ #' options(shiny.port = 8080) #' shiny::runApp() #' } +#' #' } #' @export auth0_ui <- function(ui, info) { @@ -36,53 +38,39 @@ auth0_ui <- function(ui, info) { } else { if (missing(info)) info <- auth0_info() function(req) { - verify <- has_auth_code( - shiny::parseQueryString(req$QUERY_STRING), - info$state - ) + verify <- has_auth_code(shiny::parseQueryString(req$QUERY_STRING), info$state) if (!verify) { if (grepl("error=unauthorized", req$QUERY_STRING)) { redirect <- sprintf("location.replace(\"%s\");", logout_url()) shiny::tags$script(shiny::HTML(redirect)) } else { + params <- shiny::parseQueryString(req$QUERY_STRING) params$code <- NULL params$state <- NULL - # Reconstruct the query string - query <- paste( + query_params <- paste( mapply(paste, names(params), params, MoreArgs = list(sep = "=")), - collapse = "&" - ) - query <- if (query != "") paste0("?", query) else "" - - # Preserve the original path (req$PATH_INFO) in the redirect URI - if (grepl("127.0.0.1", req$HTTP_HOST)) { - redirect_uri <- paste0( - "http://", - gsub("127.0.0.1", "localhost", req$HTTP_HOST), - req$PATH_INFO, - query - ) + collapse = "&") + query <- if (query_params != "") paste0("/?", query_params) else "/" + if (!is.null(info$remote_url) && info$remote_url != "" && !getOption("auth0_local")) { + redirect_uri <- paste0(info$remote_url, query) } else { - redirect_uri <- paste0( - "http://", - req$HTTP_HOST, - req$PATH_INFO, - query - ) + if (grepl("127.0.0.1", req$HTTP_HOST)) { + redirect_uri <- paste0("http://", gsub("127.0.0.1", "localhost", req$HTTP_HOST), query) + } else { + redirect_uri <- paste0("http://", req$HTTP_HOST, query) + } } redirect_uri <<- redirect_uri - - # Generate the Auth0 authorization URL - query_extra <- if (is.null(info$audience)) list() else - list(audience = info$audience) + query_extra <- if(!is.null(info$audience) || !is.null(info$extra_params)) { + c(info$extra_params, list(audience = info$audience)) + } else { + NULL + } url <- httr::oauth2.0_authorize_url( - info$api, - info$app(redirect_uri), - scope = info$scope, - state = info$state, - query_extra = query_extra + info$api, info$app(redirect_uri), scope = info$scope, state = info$state, + query_extra=query_extra ) redirect <- sprintf("location.replace(\"%s\");", url) shiny::tags$script(shiny::HTML(redirect)) @@ -103,21 +91,20 @@ auth0_ui <- function(ui, info) { #' @param server the shiny server function. #' @param info object returned from [auth0_info]. If not informed, #' will try to find the `_auth0.yml` and create it automatically. +#' @param remove_callback_params whether to remove the `code` and `state` query. #' #' @export -auth0_server <- function(server, info) { +auth0_server <- function(server, info, remove_callback_params = TRUE) { disable <- getOption("auth0_disable") if (!is.null(disable) && disable) { server } else { if (missing(info)) info <- auth0_info() function(input, output, session) { - shiny::isolate(auth0_server_verify( - session, - info$app, - info$api, - info$state - )) + shiny::isolate(auth0_server_verify(session, info$app, info$api, info$state)) + if (remove_callback_params) { + auth0_remove_callback_params(session) + } shiny::observeEvent(input[["._auth0logout_"]], logout()) server(input, output, session) } @@ -132,6 +119,8 @@ auth0_server <- function(server, info) { #' @param ui an ordinary UI object to create shiny apps. #' @param server an ordinary server object to create shiny apps. #' @param config_file path to YAML configuration file. +#' @param remove_callback_params whether to remove the `code` and `state` query +#' parameters from the URL after successful authentication. Defaults to `TRUE`. #' @param ... Other arguments passed on to [shiny::shinyApp()]. #' #' @details @@ -147,7 +136,8 @@ auth0_server <- function(server, info) { #' disable auth0 temporarily. #' #' @export -shinyAppAuth0 <- function(ui, server, config_file = NULL, ...) { +shinyAppAuth0 <- function(ui, server, config_file = NULL, remove_callback_params = TRUE, ...) { + disable <- getOption("auth0_disable") if (!is.null(disable) && disable) { shiny::shinyApp(ui, server, ...) @@ -156,7 +146,11 @@ shinyAppAuth0 <- function(ui, server, config_file = NULL, ...) { config_file <- auth0_find_config_file() } info <- auth0_info(config_file) - shiny::shinyApp(auth0_ui(ui, info), auth0_server(server, info), ...) + shiny::shinyApp( + auth0_ui(ui, info), + auth0_server(server, info, remove_callback_params), + ... + ) } } @@ -171,10 +165,8 @@ shinyAppAuth0 <- function(ui, server, config_file = NULL, ...) { #' #' @export shinyAuth0App <- function(ui, server, config_file = NULL) { - warning( - "`shinyAuth0App()` is soft-deprecated as of auth0 0.1.2.", - "Please use `shinyAppAuth0()` instead." - ) + warning("`shinyAuth0App()` is soft-deprecated as of auth0 0.1.2.", + "Please use `shinyAppAuth0()` instead.") shinyAppAuth0(ui, server, config_file) } @@ -221,12 +213,14 @@ shinyAuth0App <- function(ui, server, config_file = NULL) { #' } #' shinyAuth0App(ui, server, config_file) #' } +#' #' } #' +#' #' @export auth0_logout_url <- function(config_file = NULL, redirect_js = TRUE) { - stop( - "`auth0_logout_url()` is deprecated. ", - "See `?logoutButton()` to add a logout button in auth0 apps." - ) + + stop("`auth0_logout_url()` is deprecated. ", + "See `?logoutButton()` to add a logout button in auth0 apps.") + } diff --git a/R/utils.R b/R/utils.R index 8fe26bb..f244158 100644 --- a/R/utils.R +++ b/R/utils.R @@ -6,7 +6,7 @@ auth0_local = interactive() ) toset <- !(names(op.auth0) %in% names(op)) - if(any(toset)) options(op.auth0[toset]) + if (any(toset)) options(op.auth0[toset]) invisible() } @@ -24,7 +24,6 @@ #' #' @export auth0_find_config_file <- function() { - config_file <- getOption("auth0_config_file") if (is.null(config_file) || !file.exists(config_file)) { @@ -51,5 +50,44 @@ auth0_find_config_file <- function() { config_file } +auth0_remove_callback_params <- function(session) { + shiny::observeEvent(session[["clientData"]]$url_search, + { + params <- shiny::parseQueryString(session[["clientData"]]$url_search) + + # Remove only auth callback params and keep any other query params untouched. + if (!is.null(params$code) || !is.null(params$state)) { + params$code <- NULL + params$state <- NULL + + path <- session[["clientData"]]$url_pathname + hash <- session[["clientData"]]$url_hash + + query <- "" + if (length(params) > 0) { + encoded <- mapply( + function(nm, val) { + paste0( + utils::URLencode(nm, reserved = TRUE), + "=", + utils::URLencode(as.character(val), reserved = TRUE) + ) + }, + names(params), + params, + SIMPLIFY = TRUE, + USE.NAMES = FALSE + ) + query <- paste0("?", paste(encoded, collapse = "&")) + } + + shiny::updateQueryString(paste0(path, query, hash), mode = "replace", session = session) + } + }, + once = TRUE, + ignoreInit = FALSE + ) +} + # Get rid of NOTE globalVariables(c("redirect_uri")) diff --git a/man/auth0_logout_url.Rd b/man/auth0_logout_url.Rd index 2227d5b..73a9c00 100644 --- a/man/auth0_logout_url.Rd +++ b/man/auth0_logout_url.Rd @@ -50,6 +50,8 @@ if (interactive()) { } shinyAuth0App(ui, server, config_file) } + } + } diff --git a/man/shinyAppAuth0.Rd b/man/shinyAppAuth0.Rd index 80167fc..7aa9927 100644 --- a/man/shinyAppAuth0.Rd +++ b/man/shinyAppAuth0.Rd @@ -4,7 +4,13 @@ \alias{shinyAppAuth0} \title{Create a Shiny app object with Auth0 Authentication} \usage{ -shinyAppAuth0(ui, server, config_file = NULL, ...) +shinyAppAuth0( + ui, + server, + config_file = NULL, + remove_callback_params = TRUE, + ... +) } \arguments{ \item{ui}{an ordinary UI object to create shiny apps.} @@ -13,6 +19,9 @@ shinyAppAuth0(ui, server, config_file = NULL, ...) \item{config_file}{path to YAML configuration file.} +\item{remove_callback_params}{whether to remove the \code{code} and \code{state} query +parameters from the URL after successful authentication. Defaults to \code{TRUE}.} + \item{...}{Other arguments passed on to \code{\link[shiny:shinyApp]{shiny::shinyApp()}}.} } \description{ diff --git a/man/ui-server.Rd b/man/ui-server.Rd index ed8fae1..dc062f5 100644 --- a/man/ui-server.Rd +++ b/man/ui-server.Rd @@ -8,7 +8,7 @@ \usage{ auth0_ui(ui, info) -auth0_server(server, info) +auth0_server(server, info, remove_callback_params = TRUE) } \arguments{ \item{ui}{\code{shiny.tag.list} object to generate the user interface.} @@ -17,6 +17,8 @@ auth0_server(server, info) will try to find the \verb{_auth0.yml} and create it automatically.} \item{server}{the shiny server function.} + +\item{remove_callback_params}{whether to remove the \code{code} and \code{state} query.} } \description{ These functions can be used in a ui.R/server.R framework, modifying the @@ -27,6 +29,7 @@ shiny objects to authenticate using Auth0 service with no pain. # first, create the yml file using use_auth0() function if (interactive()) { + # ui.R file library(shiny) library(auth0) @@ -40,6 +43,7 @@ if (interactive()) { options(shiny.port = 8080) shiny::runApp() } + } } \seealso{