Let httr2 take care of token refreshing

Change-Id: I68f35fa8debb9603e937c4a12328560ac31f61a1
diff --git a/NAMESPACE b/NAMESPACE
index 872d6ad..6526101 100644
--- a/NAMESPACE
+++ b/NAMESPACE
@@ -101,14 +101,6 @@
 importFrom(ggplot2,theme)
 importFrom(httr2,oauth_client)
 importFrom(httr2,oauth_flow_auth_code)
-importFrom(httr2,req_headers)
-importFrom(httr2,req_perform)
-importFrom(httr2,req_timeout)
-importFrom(httr2,req_user_agent)
-importFrom(httr2,resp_body_json)
-importFrom(httr2,resp_body_string)
-importFrom(httr2,resp_content_type)
-importFrom(httr2,resp_status)
 importFrom(httr2,url_build)
 importFrom(httr2,url_parse)
 importFrom(jsonlite,fromJSON)
@@ -134,4 +126,6 @@
 importFrom(tidyr,pivot_wider)
 importFrom(tidyr,unchop)
 importFrom(tidyr,unnest)
+importFrom(urltools,param_get)
+importFrom(urltools,url_decode)
 importFrom(urltools,url_encode)
diff --git a/R/KorAPConnection.R b/R/KorAPConnection.R
index e8a7e49..c810492 100644
--- a/R/KorAPConnection.R
+++ b/R/KorAPConnection.R
@@ -3,6 +3,7 @@
 ################################################################################
 setClassUnion("characterOrNULL", c("character", "NULL"))
 setClassUnion("listOrNULL", c("list", "NULL"))
+# setOldClass("httr2_oauth_client")
 
 #' Class KorAPConnection
 #'
@@ -17,6 +18,8 @@
 #' @slot indexRevision   indexRevision code as reported from API via `X-Index-Revision` HTTP header.
 #' @slot apiUrl          full URL of API including version.
 #' @slot accessToken     OAuth2 access token.
+#' @slot oauthClient     OAuth2 client object.
+#' @slot oauthScope      OAuth2 scope.
 #' @slot userAgent       user agent string used for connection the API.
 #' @slot timeout         tineout in seconds for API requests (this does not influence server internal timeouts)
 #' @slot verbose         logical that decides whether operations will default to be verbose.
@@ -24,7 +27,7 @@
 #' @slot welcome         list containing HTTP response received from KorAP server welcome function.
 
 #' @export
-KorAPConnection <- setClass("KorAPConnection", slots=c(KorAPUrl="character", apiVersion="character", indexRevision="characterOrNULL", apiUrl="character", accessToken="characterOrNULL", userAgent="character", timeout="numeric", verbose="logical", cache="logical", welcome="listOrNULL"))
+KorAPConnection <- setClass("KorAPConnection", slots=c(KorAPUrl="character", apiVersion="character", indexRevision="characterOrNULL", apiUrl="character", accessToken="characterOrNULL", oauthClient="ANY", oauthScope="characterOrNULL", userAgent="character", timeout="numeric", verbose="logical", cache="logical", welcome="listOrNULL"))
 
 #' @param .Object KorAPConnection object
 #' @param KorAPUrl URL of the web user interface of the KorAP server instance you want to access.
@@ -65,6 +68,12 @@
 #'   new("KorAPConnection", cache=FALSE)` or use
 #'   [clearCache()] to clear the cache completely.
 #'
+#'   An alternative to using an access token is to use a browser-based oauth2 workflow
+#'   to obtain an access token. This can be done with the [auth()] method.
+#'
+#' @param oauthClient     OAuth2 client object.
+#' @param oauthScope      OAuth2 scope.
+#' @param authorizationPossible logical that indicates if authorization is possible/necessary for the current KorAP instance. Automatically set during initialization.
 #' @param userAgent user agent string.
 #' @param timeout tineout in seconds for API requests (this does not influence server internal timeouts).
 #' @param verbose logical that decides whether following operations will default to
@@ -74,6 +83,7 @@
 #' @return [KorAPConnection()] object that can be used e.g. with
 #'   [corpusQuery()]
 #'
+#' @import httr2
 #' @examples
 #' \dontrun{
 #'
@@ -93,7 +103,7 @@
 #' @rdname KorAPConnection-class
 #' @export
 setMethod("initialize", "KorAPConnection",
-          function(.Object, KorAPUrl = "https://korap.ids-mannheim.de/", apiVersion = 'v1.0', apiUrl, accessToken = getAccessToken(KorAPUrl), userAgent = "R-KorAP-Client", timeout=240, verbose = FALSE, cache = TRUE) {
+          function(.Object, KorAPUrl = "https://korap.ids-mannheim.de/", apiVersion = 'v1.0', apiUrl, accessToken = getAccessToken(KorAPUrl), oauthClient = NULL, oauthScope = "search match_info", userAgent = "R-KorAP-Client", timeout=240, verbose = FALSE, cache = TRUE) {
             .Object <- callNextMethod()
             m <- regexpr("https?://[^?]+", KorAPUrl, perl = TRUE)
             .Object@KorAPUrl <- regmatches(KorAPUrl, m)
@@ -106,8 +116,10 @@
               .Object@apiUrl = apiUrl
             }
             .Object@accessToken = accessToken
+            .Object@oauthClient = oauthClient
             .Object@apiVersion = apiVersion
             .Object@userAgent = userAgent
+            .Object@oauthScope = oauthScope
             .Object@timeout = timeout
             .Object@verbose = verbose
             .Object@cache = cache
@@ -146,6 +158,10 @@
 #' @seealso [clearAccessToken()], [auth()]
 #'
 setMethod("persistAccessToken", "KorAPConnection",  function(kco, accessToken = kco@accessToken) {
+  if (! is.null(kco@oauthClient)) {
+    warning("Short lived access tokens from a confidential application cannot be persisted.")
+    return(kco)
+  }
   if (is.null(accessToken))
     stop("It seems that you have not supplied any access token that could be persisted.", call. = FALSE)
 
@@ -180,8 +196,17 @@
 
 generic_kor_app_id = "99FbPHH7RrN36hbndF7b6f"
 
+kustvakt_redirekt_uri = "http://localhost:1410/"
+kustvakt_auth_path = "settings/oauth/authorize"
 
-setGeneric("auth", function(kco,  app_id = generic_kor_app_id, scope = "search match_info") standardGeneric("auth") )
+oauthRefresh <- function(req, client, scope, kco) {
+  httr2::req_oauth_auth_code(req, client,  scope = scope,
+                             auth_url = paste0(kco@KorAPUrl, kustvakt_auth_path),
+                             redirect_uri = kustvakt_redirekt_uri,
+                             cache_key = kco@KorAPUrl)
+}
+
+setGeneric("auth", function(kco,  app_id = generic_kor_app_id, app_secret = NULL, scope = kco@oauthScope) standardGeneric("auth") )
 
 #' Authorize RKorAPClient
 #'
@@ -194,6 +219,7 @@
 #'
 #' @param kco KorAPConnection object
 #' @param app_id OAuth2 application id. Defaults to the generic KorAP client application id.
+#' @param app_secret OAuth2 application secret. Used with confidential client applications. Defaults to `NULL`.
 #' @param scope OAuth2 scope. Defaults to "search match_info".
 #' @return KorAPConnection object with access token set in `@accessToken`.
 #'
@@ -208,25 +234,36 @@
 #' @seealso [persistAccessToken()], [clearAccessToken()]
 #'
 #' @export
-setMethod("auth", "KorAPConnection", function(kco, app_id = generic_kor_app_id, scope = "search match_info") {
+setMethod("auth", "KorAPConnection", function(kco, app_id = generic_kor_app_id, app_secret = NULL, scope = kco@oauthScope) {
   if ( kco@KorAPUrl != "https://korap.ids-mannheim.de/" & app_id == generic_kor_app_id) {
     warning(paste("You can use the default app_id only for the IDS Mannheim KorAP main instance for querying DeReKo. Please provide your own app_id for accesing", kco@KorAPUrl))
     return(kco)
   }
   if (is.null(kco@accessToken) || is.null(kco@welcome)) { # if access token is not set or invalid
-    kco@accessToken <- (
+    client <- if (! is.null(kco@oauthClient)) kco@oauthClient else
       httr2::oauth_client(
         id =  app_id,
+        secret = app_secret,
         token_url = paste0(kco@apiUrl, "oauth2/token")
-      ) %>%
+      )
+    if (is.null(app_secret)) {
+      kco@accessToken <- ( client |>
         httr2::oauth_flow_auth_code(
           scope = scope,
-          auth_url = paste0(kco@KorAPUrl, "settings/oauth/authorize"),
-          redirect_uri = "http://localhost:1410"
-        )
-    )$access_token
+          auth_url = paste0(kco@KorAPUrl, kustvakt_auth_path),
+          redirect_uri = kustvakt_redirekt_uri
+        ))$access_token
+      log_info(kco@verbose, "Client authorized. New access token set.")
+    } else {
+      kco@oauthClient <- client
+      kco@oauthScope <- scope
+      req <- request(kco@apiUrl) |>
+        oauthRefresh(client, scope, kco) |>
+        req_perform()
+      log_info(kco@verbose, "Client authorized. Short lived access token will be refreshed automatically.")
+    }
   } else {
-    log_info(kco@verbose, "Client authorized. Access token already set.")
+    log_info(kco@verbose, "Access token already set.")
   }
   return(kco)
 })
@@ -247,11 +284,12 @@
 
 
 warnIfNoAccessToken <- function(kco) {
-  if (is.null(kco@accessToken)) {
+  if (is.null(kco@accessToken) & is.null(kco@oauthClient)) {
     warning(
       paste0(
-        "In order to receive KWICSs also from corpora with restricted licenses, you need an access token.\n",
-        "To generate an access token, login to KorAP and navigite to KorAP's OAuth settings <",
+        "In order to receive KWICSs also from corpora with restricted licenses, you may need to\n",
+        "authorize your application with an access token or the auth() method.\n",
+        "To generate an access token, login to KorAP and navigate to KorAP's OAuth settings <",
         kco@KorAPUrl,
         "settings/oauth#page-top>"
       )
@@ -282,7 +320,7 @@
 #' @param getHeaders logical that determines if headers and content should be returned (as a list)
 #' @importFrom jsonlite fromJSON
 #' @importFrom curl has_internet
-#' @importFrom httr2 req_user_agent req_timeout req_headers req_perform resp_status resp_body_string resp_body_json resp_content_type
+#' @import httr2
 #' @export
 setMethod("apiCall", "KorAPConnection", function(kco, url, json = TRUE, getHeaders = FALSE, cache = kco@cache, timeout = kco@timeout) {
   result <- ""
@@ -307,9 +345,10 @@
     httr2::req_user_agent(kco@userAgent) |>
     httr2::req_timeout(timeout)
 
-  # Add authorization header if access token is available
-  if (!is.null(kco@accessToken)) {
-    req <- req |> httr2::req_headers(Authorization = paste("Bearer", kco@accessToken))
+  if (! is.null(kco@oauthClient)) {
+    req <-  req |> oauthRefresh(kco@oauthClient, scope = kco@oauthScope, kco)
+  } else if (!is.null(kco@accessToken)) {
+    req <- req |> httr2::req_auth_bearer_token(kco@accessToken)
   }
 
   # Perform the request and handle errors
@@ -320,7 +359,7 @@
       e$resp
     }
   )
-
+#
   if (is.null(resp)) return(invisible(NULL))
 
   # Check response status
diff --git a/R/collocationAnalysis.R b/R/collocationAnalysis.R
index a920cc8..aedadec 100644
--- a/R/collocationAnalysis.R
+++ b/R/collocationAnalysis.R
@@ -98,7 +98,7 @@
               stop(sprintf("Not empty withinSpan (='%s') requires exactFrequencies=TRUE", withinSpan), call. = FALSE)
             }
 
-            warnIfNoAccessToken(kco)
+            warnIfNotAuthorized(kco)
 
             if (lemmatizeNodeQuery) {
               node <- lemmatizeWordQuery(node)
diff --git a/man/KorAPConnection-class.Rd b/man/KorAPConnection-class.Rd
index 4c75799..55b88cd 100644
--- a/man/KorAPConnection-class.Rd
+++ b/man/KorAPConnection-class.Rd
@@ -18,6 +18,8 @@
   apiVersion = "v1.0",
   apiUrl,
   accessToken = getAccessToken(KorAPUrl),
+  oauthClient = NULL,
+  oauthScope = "search match_info",
   userAgent = "R-KorAP-Client",
   timeout = 240,
   verbose = FALSE,
@@ -76,7 +78,16 @@
 IDS, because of the special license situation. This concerns also cached
 results which do not take into account from where a request was issued. If
 you experience problems or unexpected results, please try \code{kco <- new("KorAPConnection", cache=FALSE)} or use
-\code{\link[=clearCache]{clearCache()}} to clear the cache completely.}
+\code{\link[=clearCache]{clearCache()}} to clear the cache completely.
+
+An alternative to using an access token is to use a browser-based oauth2 workflow
+to obtain an access token. This can be done with the \code{\link[=auth]{auth()}} method.}
+
+\item{oauthClient}{OAuth2 client object.}
+
+\item{oauthScope}{OAuth2 scope.}
+
+\item{authorizationPossible}{logical that indicates if authorization is possible/necessary for the current KorAP instance. Automatically set during initialization.}
 
 \item{userAgent}{user agent string.}
 
@@ -119,6 +130,10 @@
 
 \item{\code{accessToken}}{OAuth2 access token.}
 
+\item{\code{oauthClient}}{OAuth2 client object.}
+
+\item{\code{oauthScope}}{OAuth2 scope.}
+
 \item{\code{userAgent}}{user agent string used for connection the API.}
 
 \item{\code{timeout}}{tineout in seconds for API requests (this does not influence server internal timeouts)}
diff --git a/man/auth-KorAPConnection-method.Rd b/man/auth-KorAPConnection-method.Rd
index ff41b6b..cf06c01 100644
--- a/man/auth-KorAPConnection-method.Rd
+++ b/man/auth-KorAPConnection-method.Rd
@@ -5,13 +5,20 @@
 \alias{auth}
 \title{Authorize RKorAPClient}
 \usage{
-\S4method{auth}{KorAPConnection}(kco, app_id = generic_kor_app_id, scope = "search match_info")
+\S4method{auth}{KorAPConnection}(
+  kco,
+  app_id = generic_kor_app_id,
+  app_secret = NULL,
+  scope = kco@oauthScope
+)
 }
 \arguments{
 \item{kco}{KorAPConnection object}
 
 \item{app_id}{OAuth2 application id. Defaults to the generic KorAP client application id.}
 
+\item{app_secret}{OAuth2 application secret. Used with confidential client applications. Defaults to \code{NULL}.}
+
 \item{scope}{OAuth2 scope. Defaults to "search match_info".}
 }
 \value{