From d6fa4e873f48b225f5680539e689dd4eb7645daa Mon Sep 17 00:00:00 2001 From: andres Date: Tue, 27 Aug 2024 11:42:02 -0500 Subject: [PATCH] chore!: Finalizes migration to tower --- DESCRIPTION | 1 + Makefile | 6 ++ NAMESPACE | 4 +- R/auth0.R | 117 +++++++++++++++++---------------------- R/entra_id.R | 123 ++++++++++++++++++----------------------- R/futures.R | 1 - R/google.R | 119 +++++++++++++++++---------------------- R/shiny.R | 28 +++------- example/app.R | 6 +- man/sso_shiny_app.Rd | 26 --------- man/tapLock-package.Rd | 7 +-- 11 files changed, 180 insertions(+), 258 deletions(-) create mode 100644 Makefile delete mode 100644 man/sso_shiny_app.Rd diff --git a/DESCRIPTION b/DESCRIPTION index c5f3795..6d7f598 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -28,6 +28,7 @@ Suggests: knitr, rmarkdown, shiny, + tower, testthat (>= 3.0.0) VignetteBuilder: knitr diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2d93efd --- /dev/null +++ b/Makefile @@ -0,0 +1,6 @@ +.PHONY: install + +install: + Rscript -e "devtools::install()" + + diff --git a/NAMESPACE b/NAMESPACE index 98a4bb7..91b4f9a 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,6 +1,9 @@ # Generated by roxygen2: do not edit by hand +S3method(internal_add_auth_layers,entra_id_config) +S3method(internal_add_auth_layers,google_config) S3method(print,access_token) +export(add_auth_layers) export(expires_at) export(expires_in) export(get_token_field) @@ -10,6 +13,5 @@ export(new_auth0_config) export(new_entra_id_config) export(new_google_config) export(new_openid_config) -export(sso_shiny_app) export(token) export(use_futures) diff --git a/R/auth0.R b/R/auth0.R index f5fd547..ffd94a5 100644 --- a/R/auth0.R +++ b/R/auth0.R @@ -105,14 +105,9 @@ get_client_id.auth0_config <- function(config) { #' @keywords internal #' @noRd -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") { +internal_add_auth_layers.auth0_config <- function(config, tower) { + tower |> + tower::add_get_route("/login", \(req) { query <- shiny::parseQueryString(req$QUERY_STRING) token <- promises::future_promise({ request_token(config, query[["code"]]) @@ -140,9 +135,8 @@ shiny_app.auth0_config <- function(config, app) { } ) ) - } - - if (req$PATH_INFO == "/logout") { + }) |> + tower::add_get_route("/logout", \(req) { return( shiny::httpResponse( status = 302, @@ -152,72 +146,61 @@ shiny_app.auth0_config <- function(config, app) { ) ) ) - } - - # 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 == "/") { + }) |> + tower::add_http_layer(\(req) { + # Get the HTTP cookies from the request + cookies <- parse_cookies(req$HTTP_COOKIE) + req$PARSED_COOKIES <- cookies + + # 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) + ) + ) + ) + } + } + }) |> + tower::add_http_layer(function(req) { + # 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)), + expr = access_token( + config, + remove_bearer(req$PARSED_COOKIES$access_token) + ), error = function(e) { return(NULL) } ) + if (is.null(token)) { return( shiny::httpResponse( - status = 302, - headers = list( - Location = get_login_url(config) - ) + status = 403, + content_type = "text/plain", + content = "Forbidden" ) ) } - } - - # 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) + # 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) + }) } diff --git a/R/entra_id.R b/R/entra_id.R index 3fe0076..2e68d18 100644 --- a/R/entra_id.R +++ b/R/entra_id.R @@ -86,6 +86,9 @@ decode_token.entra_id_config <- function(config, token) { jose::jwt_decode_sig(token, jwk), error = function(e) { NULL + }, + warning = function(w) { + NULL } ) }) |> @@ -102,15 +105,10 @@ get_client_id.entra_id_config <- function(config) { config$client_id } -#' @keywords internal -shiny_app.entra_id_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$REQUEST_METHOD == "POST" && req$PATH_INFO == "/login") { +#' @export +internal_add_auth_layers.entra_id_config <- function(config, tower) { + tower |> + tower::add_post_route("/login", function(req) { form <- shiny::parseQueryString(req[["rook.input"]]$read_lines()) token <- promises::future_promise({ request_token(config, form[["code"]]) @@ -138,9 +136,8 @@ shiny_app.entra_id_config <- function(config, app) { } ) ) - } - - if (req$PATH_INFO == "/logout") { + }) |> + tower::add_get_route("/logout", function(req) { return( shiny::httpResponse( status = 302, @@ -150,72 +147,62 @@ shiny_app.entra_id_config <- function(config, app) { ) ) ) - } - - # 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 == "/") { + }) |> + tower::add_http_layer(function(req) { + # Get the HTTP cookies from the request + cookies <- parse_cookies(req$HTTP_COOKIE) + req$PARSED_COOKIES <- cookies + + # 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) + } + ) + print(token) + if (is.null(token)) { + return( + shiny::httpResponse( + status = 302, + headers = list( + Location = get_login_url(config) + ) + ) + ) + } + } + }) |> + tower::add_http_layer(function(req) { + # 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)), + expr = access_token( + config, + remove_bearer(req$PARSED_COOKIES$access_token) + ), error = function(e) { return(NULL) } ) + if (is.null(token)) { return( shiny::httpResponse( - status = 302, - headers = list( - Location = get_login_url(config) - ) + status = 403, + content_type = "text/plain", + content = "Forbidden" ) ) } - } - - # 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) + # 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) + }) } diff --git a/R/futures.R b/R/futures.R index 31cbac2..ebabf40 100644 --- a/R/futures.R +++ b/R/futures.R @@ -1,4 +1,3 @@ - #' @title Use futures for asynchronous computations #' @description Enable a future plan for asynchronous computations. #' Since tapLock needs to do calls to external APIs, it can be a good idea diff --git a/R/google.R b/R/google.R index b13a6d2..4c6f835 100644 --- a/R/google.R +++ b/R/google.R @@ -97,15 +97,10 @@ get_client_id.google_config <- function(config) { config$client_id } -#' @keywords internal -shiny_app.google_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") { +#' @export +internal_add_auth_layers.google_config <- function(config, tower) { + tower |> + tower::add_get_route("/login", function(req) { query <- shiny::parseQueryString(req$QUERY_STRING) token <- promises::future_promise({ request_token(config, query[["code"]]) @@ -133,9 +128,8 @@ shiny_app.google_config <- function(config, app) { } ) ) - } - - if (req$PATH_INFO == "/logout") { + }) |> + tower::add_get_route("/logout", function(req) { return( shiny::httpResponse( status = 302, @@ -145,72 +139,61 @@ shiny_app.google_config <- function(config, app) { ) ) ) - } - - # 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 == "/") { + }) |> + tower::add_http_layer(function(req) { + # Get the HTTP cookies from the request + cookies <- parse_cookies(req$HTTP_COOKIE) + req$PARSED_COOKIES <- cookies + + # 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) + ) + ) + ) + } + } + }) |> + tower::add_http_layer(function(req) { + # 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)), + expr = access_token( + config, + remove_bearer(req$PARSED_COOKIES$access_token) + ), error = function(e) { return(NULL) } ) + if (is.null(token)) { return( shiny::httpResponse( - status = 302, - headers = list( - Location = get_login_url(config) - ) + status = 403, + content_type = "text/plain", + content = "Forbidden" ) ) } - } - - # 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) + # 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) + }) } diff --git a/R/shiny.R b/R/shiny.R index 9fe6c64..e145687 100644 --- a/R/shiny.R +++ b/R/shiny.R @@ -19,9 +19,13 @@ rsso_server <- function(config, server_func) { } } -#' @keywords internal -shiny_app <- function(config, app) { - UseMethod("shiny_app") +internal_add_auth_layers <- function(config, tower) { + UseMethod("internal_add_auth_layers") +} + +#' @export +add_auth_layers <- function(tower, config) { + internal_add_auth_layers(config, tower) } #' @title Get the access token @@ -36,21 +40,3 @@ shiny_app <- function(config, app) { token <- function(session = shiny::getDefaultReactiveDomain()) { session$userData$token } - -#' @title Create a Shiny app with SSO -#' -#' @description Creates a Shiny app with SSO (single sign-on) -#' based on the given configuration. -#' -#' @param config An openid_config object -#' @param ui A Shiny UI function -#' @param server A Shiny server function. This function requires -#' all three arguments: `input`, `output`, and `session`. -#' -#' @seealso [tapLock::new_openid_config()] -#' @return A Shiny app (Compatible with [`shinyApp()`][shiny::shinyApp]) -#' @export -sso_shiny_app <- function(config, ui, server) { - app <- shiny::shinyApp(ui = ui, server = rsso_server(config, server)) - shiny_app(config, app) -} diff --git a/example/app.R b/example/app.R index 6707044..6ad8758 100644 --- a/example/app.R +++ b/example/app.R @@ -28,5 +28,7 @@ server <- function(input, output, session) { }) } - -sso_shiny_app(auth_config, ui, server) +shinyApp(ui, server) |> + tower::create_tower() |> + tapLock::add_auth_layers(auth_config) |> + tower::build_tower() diff --git a/man/sso_shiny_app.Rd b/man/sso_shiny_app.Rd deleted file mode 100644 index 29d6ee8..0000000 --- a/man/sso_shiny_app.Rd +++ /dev/null @@ -1,26 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/shiny.R -\name{sso_shiny_app} -\alias{sso_shiny_app} -\title{Create a Shiny app with SSO} -\usage{ -sso_shiny_app(config, ui, server) -} -\arguments{ -\item{config}{An openid_config object} - -\item{ui}{A Shiny UI function} - -\item{server}{A Shiny server function. This function requires -all three arguments: \code{input}, \code{output}, and \code{session}.} -} -\value{ -A Shiny app (Compatible with \code{\link[shiny:shinyApp]{shinyApp()}}) -} -\description{ -Creates a Shiny app with SSO (single sign-on) -based on the given configuration. -} -\seealso{ -\code{\link[=new_openid_config]{new_openid_config()}} -} diff --git a/man/tapLock-package.Rd b/man/tapLock-package.Rd index a806164..15ae7f7 100644 --- a/man/tapLock-package.Rd +++ b/man/tapLock-package.Rd @@ -11,14 +11,13 @@ Swift and seamless Single Sign-On (SSO) integration. Designed for effortless com \seealso{ Useful links: \itemize{ - \item \url{https://github.com/maurolepore/tapLock} - \item \url{https://maurolepore.github.io/tapLock/} - \item Report bugs at \url{https://github.com/maurolepore/tapLock/issues} + \item \url{https://github.com/ixpantia/tapLock} + \item Report bugs at \url{https://github.com/ixpantia/tapLock/issues} } } \author{ -\strong{Maintainer}: +\strong{Maintainer}: Andres Quintero \email{andres@ixpantia.com} Other contributors: \itemize{