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