From 56e097d70327846faff6e92162738fdf85ef6e80 Mon Sep 17 00:00:00 2001 From: Thomas Hansen Date: Thu, 29 Aug 2024 14:34:44 +0200 Subject: [PATCH] When require_auth: Don't listen to non-auth login endpoints if user is not authenticated + minor doc fixes --- R/oauth-shiny-app.R | 13 +++--- R/oauth-shiny-http-handlers.R | 42 +++++++++++++++---- man/handle_oauth_client_login.Rd | 8 +++- man/handle_oauth_client_login_redirect.Rd | 15 +++++-- man/handle_oauth_client_logout.Rd | 8 +++- ...ndle_oauth_client_logout_delete_cookies.Rd | 14 ++++++- man/oauth_shiny_infer_app_url.Rd | 3 +- 7 files changed, 80 insertions(+), 23 deletions(-) diff --git a/R/oauth-shiny-app.R b/R/oauth-shiny-app.R index 3f33bfb6..2838f1e1 100644 --- a/R/oauth-shiny-app.R +++ b/R/oauth-shiny-app.R @@ -92,7 +92,8 @@ oauth_shiny_app <- function( # This function takes the app object and transforms/decorates it to create a # new app object. The new app object will wrap the original ui/server with # authentication logic, so that the original ui/server is not invoked unless - # and until the user has a valid Google token. + # and until the user has an app token from an auth provider if `require_auth` + # is `TRUE`. check_installed("jose") check_installed("sodium") @@ -119,12 +120,12 @@ oauth_shiny_app <- function( # request, and either HTML tag objects or a shiny::httpResponse if it # decided to handle it. resp <- - # The logout_path revokes all app and client tokens and deletes cookies + # The logout_path revokes all app and access tokens and deletes cookies handle_oauth_app_logout(req, client_config, logout_path, cookie_name, logout_ui) %||% - # The client logout_path revokes a single client token and deletes cookies - handle_oauth_client_logout(req, client_config) %||% + # The client logout_path revokes a single access token and deletes cookies + handle_oauth_client_logout(req, client_config, require_auth, cookie_name, key) %||% # The client login_path handles redirection to the specific client - handle_oauth_client_login(req, client_config) %||% + handle_oauth_client_login(req, client_config, require_auth, cookie_name, key) %||% # Handles callback from oauth client (after login) handle_oauth_client_callback(req, client_config, require_auth, cookie_name, key, token_validity) %||% # Handles requests that have good cookies or does not require auth @@ -178,7 +179,7 @@ oauth_shiny_app <- function( #' #' @description Inferring the correct app url on the server requires some work. #' This function attempts to guess the correct server url, but may fail outside -#' of tested hosts (´127.0.0.1` and `shinyapps.io`). To be sure, set the +#' of tested hosts (`127.0.0.1` and `shinyapps.io`). To be sure, set the #' environment variable `HTTR2_OAUTH_APP_URL` explicitly. Logic inspired by #' [https://github.com/r4ds/shinyslack](r4ds/shinyslack). #' @param req A request object. diff --git a/R/oauth-shiny-http-handlers.R b/R/oauth-shiny-http-handlers.R index 6c912036..ff9adef1 100644 --- a/R/oauth-shiny-http-handlers.R +++ b/R/oauth-shiny-http-handlers.R @@ -117,12 +117,15 @@ handle_oauth_app_logout <- function(req, client_config, logout_path, cookie, log #' #' @param req A `shiny` request object. #' @param client_config A list of client configurations used for OAuth. +#' @param require_auth Logical, whether authentication is required. +#' @param cookie The name of the cookie where the app's token is stored. +#' @param key A secret key used to encrypt and decrypt tokens. #' #' @return An HTTP response object or `NULL` if no client is matched. #' @keywords internal -handle_oauth_client_login <- function(req, client_config) { +handle_oauth_client_login <- function(req, client_config, require_auth, cookie, key) { for (client in client_config) { - resp <- handle_oauth_client_login_redirect(req, client) + resp <- handle_oauth_client_login_redirect(req, client, require_auth, cookie, key) if (!is.null(resp)) { return(resp) } @@ -131,21 +134,31 @@ handle_oauth_client_login <- function(req, client_config) { #' Handle OAuth Client Login Redirect #' -#' This function handles redirection to the OAuth authorization URL for a -#' specific client. It manages PKCE (Proof Key for Code Exchange) if enabled, -#' and sets up necessary cookies before redirecting the user. +#' This function is invoked when the user is redirected to a client login +#' endpoint set by `login_path`. It handles redirection to the OAuth +#' authorization URL for a specific client. It manages state and PKCE (if +#' enabled) by setting cookies when redirecting the user to the OAuth endpoint. #' #' @param req A `shiny` request object. #' @param client A single client configuration object. +#' @param require_auth Logical, whether authentication is required. +#' @param cookie The name of the cookie where the app's token is stored. +#' @param key A secret key used to encrypt and decrypt tokens. #' #' @return An HTTP response object or `NULL` if the request path does not match #' the client's login path. #' @keywords internal -handle_oauth_client_login_redirect <- function(req, client) { +handle_oauth_client_login_redirect <- function(req, client, require_auth, cookie, key) { if (sub("^/", "", req$PATH_INFO) != client$login_path) { return(NULL) } + # Don't accept request for non-auth login endpoints if user is not authenticated + has_auth <- !is.null(oauth_shiny_get_app_token_from_request(req, cookie, key)) + if(require_auth && !has_auth && !client$auth_provider) { + return(NULL) + } + state <- paste0(client$id, base64_url_rand(32)) auth_params <- client$auth_params @@ -313,12 +326,15 @@ handle_oauth_client_callback <- function(req, client_config, require_auth, cooki #' #' @param req A `shiny` request object. #' @param client_config A list of client configurations used for OAuth. +#' @param require_auth Logical, whether authentication is required. +#' @param cookie The name of the cookie where the app's token is stored. +#' @param key A secret key used to encrypt and decrypt tokens. #' #' @return An HTTP response object or `NULL` if no client is matched. #' @keywords internal -handle_oauth_client_logout <- function(req, client_config) { +handle_oauth_client_logout <- function(req, client_config, require_auth, cookie, key) { for (client in client_config) { - resp <- handle_oauth_client_logout_delete_cookies(req, client) + resp <- handle_oauth_client_logout_delete_cookies(req, client, require_auth, cookie, key) if (!is.null(resp)) { return(resp) } @@ -332,15 +348,23 @@ handle_oauth_client_logout <- function(req, client_config) { #' #' @param req A `shiny` request object. #' @param client A single client configuration object. +#' @param require_auth Logical, whether authentication is required. +#' @param cookie The name of the cookie where the app's token is stored. +#' @param key A secret key used to encrypt and decrypt tokens. #' #' @return An HTTP response object or `NULL` if the request path does not match #' the client's logout path. #' @keywords internal -handle_oauth_client_logout_delete_cookies <- function(req, client) { +handle_oauth_client_logout_delete_cookies <- function(req, client, require_auth, cookie, key) { if (sub("^/", "", req$PATH_INFO) != client$logout_path) { return(NULL) } + has_auth <- !is.null(oauth_shiny_get_app_token_from_request(req, cookie, key)) + if(require_auth && !has_auth) { + return(NULL) + } + cookies_app <- names(parse_cookies(req)) cookies_match <- cookies_app[grepl(client$client_cookie_name, cookies_app)] cookies_del <- unlist(lapply(cookies_match, delete_cookie_header, cookie_options())) diff --git a/man/handle_oauth_client_login.Rd b/man/handle_oauth_client_login.Rd index 9e49e7ff..79cde3de 100644 --- a/man/handle_oauth_client_login.Rd +++ b/man/handle_oauth_client_login.Rd @@ -4,12 +4,18 @@ \alias{handle_oauth_client_login} \title{Handle OAuth Client Login} \usage{ -handle_oauth_client_login(req, client_config) +handle_oauth_client_login(req, client_config, require_auth, cookie, key) } \arguments{ \item{req}{A \code{shiny} request object.} \item{client_config}{A list of client configurations used for OAuth.} + +\item{require_auth}{Logical, whether authentication is required.} + +\item{cookie}{The name of the cookie where the app's token is stored.} + +\item{key}{A secret key used to encrypt and decrypt tokens.} } \value{ An HTTP response object or \code{NULL} if no client is matched. diff --git a/man/handle_oauth_client_login_redirect.Rd b/man/handle_oauth_client_login_redirect.Rd index c4b17b15..04812358 100644 --- a/man/handle_oauth_client_login_redirect.Rd +++ b/man/handle_oauth_client_login_redirect.Rd @@ -4,20 +4,27 @@ \alias{handle_oauth_client_login_redirect} \title{Handle OAuth Client Login Redirect} \usage{ -handle_oauth_client_login_redirect(req, client) +handle_oauth_client_login_redirect(req, client, require_auth, cookie, key) } \arguments{ \item{req}{A \code{shiny} request object.} \item{client}{A single client configuration object.} + +\item{require_auth}{Logical, whether authentication is required.} + +\item{cookie}{The name of the cookie where the app's token is stored.} + +\item{key}{A secret key used to encrypt and decrypt tokens.} } \value{ An HTTP response object or \code{NULL} if the request path does not match the client's login path. } \description{ -This function handles redirection to the OAuth authorization URL for a -specific client. It manages PKCE (Proof Key for Code Exchange) if enabled, -and sets up necessary cookies before redirecting the user. +This function is invoked when the user is redirected to a client login +endpoint set by \code{login_path}. It handles redirection to the OAuth +authorization URL for a specific client. It manages state and PKCE (if +enabled) by setting cookies when redirecting the user to the OAuth endpoint. } \keyword{internal} diff --git a/man/handle_oauth_client_logout.Rd b/man/handle_oauth_client_logout.Rd index aa617e78..22fd33e5 100644 --- a/man/handle_oauth_client_logout.Rd +++ b/man/handle_oauth_client_logout.Rd @@ -4,12 +4,18 @@ \alias{handle_oauth_client_logout} \title{Handle OAuth Client Logout} \usage{ -handle_oauth_client_logout(req, client_config) +handle_oauth_client_logout(req, client_config, require_auth, cookie, key) } \arguments{ \item{req}{A \code{shiny} request object.} \item{client_config}{A list of client configurations used for OAuth.} + +\item{require_auth}{Logical, whether authentication is required.} + +\item{cookie}{The name of the cookie where the app's token is stored.} + +\item{key}{A secret key used to encrypt and decrypt tokens.} } \value{ An HTTP response object or \code{NULL} if no client is matched. diff --git a/man/handle_oauth_client_logout_delete_cookies.Rd b/man/handle_oauth_client_logout_delete_cookies.Rd index 98a725be..05efecfc 100644 --- a/man/handle_oauth_client_logout_delete_cookies.Rd +++ b/man/handle_oauth_client_logout_delete_cookies.Rd @@ -4,12 +4,24 @@ \alias{handle_oauth_client_logout_delete_cookies} \title{Handle OAuth Client Logout - Delete Cookies} \usage{ -handle_oauth_client_logout_delete_cookies(req, client) +handle_oauth_client_logout_delete_cookies( + req, + client, + require_auth, + cookie, + key +) } \arguments{ \item{req}{A \code{shiny} request object.} \item{client}{A single client configuration object.} + +\item{require_auth}{Logical, whether authentication is required.} + +\item{cookie}{The name of the cookie where the app's token is stored.} + +\item{key}{A secret key used to encrypt and decrypt tokens.} } \value{ An HTTP response object or \code{NULL} if the request path does not match diff --git a/man/oauth_shiny_infer_app_url.Rd b/man/oauth_shiny_infer_app_url.Rd index 8b8a8c05..7c215a5c 100644 --- a/man/oauth_shiny_infer_app_url.Rd +++ b/man/oauth_shiny_infer_app_url.Rd @@ -15,7 +15,8 @@ The app url. \description{ Inferring the correct app url on the server requires some work. This function attempts to guess the correct server url, but may fail outside -of tested hosts (´127.0.0.1\code{and}shinyapps.io\verb{). To be sure, set the environment variable }HTTR2_OAUTH_APP_URL` explicitly. Logic inspired by +of tested hosts (\verb{127.0.0.1} and \code{shinyapps.io}). To be sure, set the +environment variable \code{HTTR2_OAUTH_APP_URL} explicitly. Logic inspired by \href{r4ds/shinyslack}{https://github.com/r4ds/shinyslack}. } \keyword{internal}