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{