Let httr2 take care of token refreshing

Change-Id: I68f35fa8debb9603e937c4a12328560ac31f61a1
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