Nothing
# Authentication Interface ----
#' IAuth
#'
#' An interface that states the intended behavior for the authentication.
#'
#' @field access_token The access_token to query password restricted webservices of an openEO back-end
#' @field id_token The id_token retrieved when exchanging the access_token at the identity provider
#'
#' @name IAuth
#'
#' @section Methods:
#' \describe{
#' \item{`$login()`}{Initiates the authentication / login in order to obtain the access_token}
#' \item{`$logout()`}{Terminates the access_token session and logs out the user on the openEO back-end}
#' }
#'
#' @seealso [BasicAuth()], [OIDCAuth()]
NULL
# IAuth ----
IAuth <- R6Class(
"IAuth",
public = list(
login = function() {
},
logout = function() {
}
),
active = list(
access_token = function() {
},
id_token = function() {
}
)
)
# [Basic Authentication] ----
#' Basic Authentication class
#'
#' This class handles the authentication to an openEO back-end that supports "basic" as login type. The class handles the retrieval
#' of an access token by sending the encoded token consisting of user name and the password via HTTP header 'Authorization'.
#' The authentication will be done once via [login()] or multiple times when the lease time runs out. This class
#' is created and registered in the [OpenEOClient()]. After the login the user_id and the access_token are obtained and
#' used as "bearer token" for the password restricted web services.
#'
#' The class inherits all fields and function from [IAuth()]
#'
#' @name BasicAuth
#'
#' @section Methods:
#' \describe{
#' \item{`$new(endpoint,user,password)`}{the constructor with the login endpoint and the credentials}
#' }
#'
#' @section Arguments:
#' \describe{
#' \item{`endpoint`}{the basic authentication endpoint as absolute URL}
#' \item{`user`}{the user name}
#' \item{`password`}{the user password}
#' }
#'
#' @return an object of type [R6Class()] representing basic authentication
#' @importFrom R6 R6Class
NULL
BasicAuth <- R6Class(
"BasicAuth",
inherit = IAuth,
# public ====
public = list(
initialize = function(endpoint, user, password) {
private$endpoint <- endpoint
private$user <- user
private$password <- password
},
login = function() {
req = req_auth_basic(
req=req_method(
request(private$endpoint),
method = "GET"),
username = private$user,
password = private$password)
res = req_perform(req)
if (is.debugging()) {
print(res)
}
if (res$status_code == 200) {
cont = resp_body_json(res)
private$.access_token <- cont$access_token
return(cont$user_id)
} else {
stop("Login failed.")
}
},
logout = function() {
private$.access_token <- NA
}
),
# active ====
active = list(
access_token = function() {
if (length(private$.access_token) == 0 || is.na(private$.access_token)) {
stop("No bearer token available. Please login first.")
} else {
return(paste("basic","",private$.access_token,sep="/"))
}
},
id_token = function() {
stop("Not provided in basic authentication")
}
),
# private ====
private = list(
endpoint = NA,
user = NA,
password = NA,
.access_token = NA
)
)
#' OIDC Authentication
#'
#' defines classes for different OpenID connect interaction mechanisms. The classes are modeled in generalized
#' fashion by inheriting functions from `IAuth` and `AbstractOIDCAuthentication`.
#'
#' @field access_token The access_token to query password restricted webservices of an openEO back-end
#' @field id_token The id_token retrieved when exchanging the access_token at the identity provider
#'
#' @section Methods:
#' \describe{
#' \item{`$new(provider, config=NULL, ...)`}{the constructor for the authentication}
#' \item{`$login()`}{Initiates the authentication / login in order to obtain the access_token}
#' \item{`$logout()`}{Terminates the access_token session and logs out the user on the openEO back-end}
#' \item{`$getUserData()`}{queries the OIDC provider for the user data like the 'user_id'}
#' \item{`$getAuth()`}{returns the internal authentication client as created from package 'httr2'}
#' }
#'
#' @section Arguments:
#' \describe{
#' \item{`provider`}{the name of an OIDC provider registered on the back-end or a provider object as returned by `list_oidc_providers()`}
#' \item{`config`}{either a JSON file containing information about 'client_id' and
#' 'secret' or a named list. Experienced user and developer can also add 'scopes' to
#' overwrite the default settings of the OIDC provider}
#' \item{`...`}{additional parameter might contain `force=TRUE` specifying to force the use
#' of a specific authentication flow}
#' }
#'
#' @details
#' The openEO conformant back-ends shall offer either a basic authentication and / or an OpenID
#' Connect (OIDC) authentication. The first is covered at [BasicAuth]. And since OIDC is based
#' on the OAuth2.0 protocol there are several mechanisms defined to interact with an OIDC provider. The OIDC provider can be the
#' back-end provider themselves, but they can also delegate the user management to other platforms like EGI, Github, Google,
#' etc, by pointing to the respective endpoints during the service discovery of the back-end. Normally
#' users would not create those classes manually, but state the general login type (oidc or basic) and some
#' additional information (see [login]).
#'
#' This client supports the following interaction mechanisms (grant types):
#' \itemize{
#' \item{authorization_code}
#' \item{authorization_code+pkce}
#' \item{urn:ietf:params:oauth:grant-type:device_code+pkce}
#' }
#'
#' \subsection{authorization_code}{
#' During the login process an internet browser window will be opened and you will be asked to enter your credentials.
#' The website belongs to the OIDC provider of the chosen openEO back-end. Meanwhile, the client will start a server daemon in
#' the background that listens to the callback from the OIDC provider. For this to work the user needs to get in contact with
#' the openEO service provider and ask them for a configuration file that will contain information about the `client_id` and
#' `secret`. The redirect URL requested from the provider is `http://localhost:1410/`
#' }
#'
#' \subsection{authorization_code+pkce}{
#' This procedure also spawns a temporary web server to capture the redirect URL from the OIDC provider. The benefit of this
#' mechanism is that it does not require a client secret issued from the OIDC provider anymore. However, it will still open
#' the internet browser and asks the user for credentials and authorization.
#' }
#'
#' \subsection{device_code+pkce}{
#' This mechanism does not need to spawn a web server anymore. It will poll the endpoint of the OIDC provider until the user
#' enters a specific device code that will be printed onto the R console. To enter the code either the URL is printed also to
#' the console or if R runs in the interactive mode the internet browser will be opened automatically.
#' }
#'
#' \subsection{device_code}{
#' This mechanism uses a designated device code for human confirmation. It is closely related to the device_code+pkce code flow,
#' but without the additional PKCE negotiation.
#' }
#'
#' @seealso
#' \describe{
#' \item{openEO definition on Open ID connect}{<https://openeo.org/documentation/1.0/authentication.html#openid-connect>}
#' \item{Open ID Connect (OIDC)}{<https://openid.net/connect/>}
#' \item{OAuth 2.0 Device Authorization Grant}{<https://datatracker.ietf.org/doc/html/rfc8628>}
#' \item{Proof Key for Code Exchange by OAuth Public Clients}{<https://datatracker.ietf.org/doc/html/rfc7636>}
#' }
#'
#' @name OIDCAuth
#' @import httr2
#' @importFrom R6 R6Class
#' @importFrom base64enc base64decode
#' @importFrom jsonlite fromJSON
#' @importFrom rlang is_interactive
NULL
# [AbstractOIDCAuthentication] ----
AbstractOIDCAuthentication <- R6Class(
"AbstractOIDCAuthentication",
inherit = IAuth,
# public ====
public = list(
# attributes ####
# functions ####
initialize = function(provider, config = list(),...) {
args = list(...)
if ("force" %in% names(args)) private$force_use = args[["force"]]
# comfort function select provider by name if one is provided
provider = .get_oidc_provider(provider)
private$setIssuer(provider$issuer)
private$id = provider$id
private$title = provider$title
private$description = provider$description
if (is.character(config)) {
if (file.exists(config)) {
config = jsonlite::read_json(config)
} else {
stop("Please provide any configuration details about the client_id and the secret. If you add the credentials as file please provide a valid file path.")
}
}
if (length(config) > 0 && !is.list(config)) {
stop("Please provide any configuration details about the client_id and the secret. Either as list object or specify a valid file path.")
}
# user knows best, allow custom scopes...
if (length(config$scopes) > 0 && is.character(config$scopes)) {
private$scopes = config$scopes
} else if (length(provider$scopes) == 0) {
private$scopes = list("openid")
} else {
private$scopes = provider$scopes
#TODO remove later, this is used for automatic reconnect
if (!"offline_access" %in% private$scopes) {
private$scopes = c(private$scopes, "offline_access")
}
}
private$getEndpoints()
if (!(is.list(config) && all(c("client_id") %in% names(config)))) {
stop("'client_id' is not present in the configuration.")
}
private$client_id = config$client_id
if (private$grant_type == "authorization_code") {
# in this case we need a client_id and secrect, which is basically the old OIDC Auth Code implementation
if (!all(c("client_id","secret") %in% names(config))) {
stop("'client_id' and 'secret' are not present in the configuration.")
}
private$oauth_client = oauth_client(
id = private$client_id,
token_url = private$endpoints$token_endpoint,
name = "openeo-r-oidc-auth",
secret = config$secret
)
} else {
private$oauth_client = oauth_client(
id = private$client_id,
token_url = private$endpoints$token_endpoint,
name = "openeo-r-oidc-auth"
)
}
return(self)
},
login = function() {
# the initial login / getting access_token must be implemented by inheriting classes
},
logout = function() {
if (is.null(private$auth)) {
message("Not logged in.")
return(NULL)
}
if (length(private$endpoints$end_session_endpoint) == 1) {
url = url_parse(private$endpoints$end_session_endpoint)
response = req_perform(req_url_query(
.req=request(url),
id_token_hint = private$auth$id_token))
if (response$status_code < 400) {
message("Successfully logged out.")
private$auth <- NULL
invisible(TRUE)
} else {
return(resp_body_json(response))
}
} else {
private$auth <- NULL
invisible(TRUE)
}
},
# fetches the oidc user data
getUserData = function() {
url = url_parse(private$endpoints$userinfo_endpoint)
response <- req_perform(
req_auth_bearer_token(
req=req_headers(
request(url),
Accept="application/json"),
private$auth$access_token))
if (response$status_code < 400) {
return(resp_body_json(response))
} else {
message("User endpoint not supported at the authentication provider")
invisible(NULL)
}
},
getAuth = function() {
return(private$auth)
}
),
# active ====
active = list(
access_token = function() {
if (!is.null(private$auth)) {
if (private$isExpired(private$auth)) {
if (!is.null(private$auth$refresh_token)) {
private$auth = oauth_flow_refresh(client=private$oauth_client,refresh_token = private$auth$refresh_token)
} else {
stop("Cannot refresh access_token. Reason: no refresh token provided by the authentication service. You have to log in again.")
return(invisible(NULL))
}
}
return(paste("oidc",private$id,private$auth$access_token,sep="/"))
} else {
stop("Please login first, in order to obtain an access token")
return(invisible(NULL))
}
},
id_token = function() {
if (!is.null(private$auth)) {
private$auth$id_token
} else {
stop("Please login before accessing the identity token")
return(invisible(NULL))
}
}
),
# private ====
private = list(
# attributes ####
id = NA,
title = NA,
description = NA,
scopes = list(),
issuer = NA, # the url of the endpoint in (issuer)
client_id = NULL,
secret = NULL,
endpoints = list(),
grant_type = "", # not used internally by httr2, but maybe useful in openeo
oauth_client = NULL,
force_use=NULL,
auth = NULL, # httr2 oauth2.0 token object
isGrantTypeSupported = function(grant_types) {
# to be implemented in inheriting class
if (!any(c("authorization_code+pkce","authorization_code") %in% grant_types)) {
stop("Authorization code flow with pkce is not supported by the authentication provider")
}
invisible(TRUE)
},
# functions ####
isValid = function() {
# use the endpoint
},
isExpired = function(token) {
return(token$expires_at <= Sys.time())
},
isInteractive = function() {
return(if (is_jupyter()) FALSE else rlang::is_interactive())
},
decodeToken = function(access_token, token_part) {
tokens <- unlist(strsplit(access_token, "\\."))
fromJSON(rawToChar(base64decode(tokens[token_part])))
},
getEndpoints = function() {
endpoint <- ".well-known/openid-configuration"
url <- paste(private$issuer, endpoint, sep = "")
response <- req_perform(request(url))
if (response$status_code < 400) {
private$endpoints <- resp_body_json(response)
} else {
message("Cannot access openid configuration endpoint.")
}
invisible(self)
},
setIssuer = function(issuer) {
if (!endsWith(issuer, "/")) {
issuer <- paste(issuer, "/", sep = "")
}
private$issuer = issuer
invisible(self)
}
)
)
# [OIDCDeviceCodeFlow] ----
OIDCDeviceCodeFlow <- R6Class(
"OIDCDeviceCodeFlow",
inherit = AbstractOIDCAuthentication,
# public ====
public = list(
# functions ####
login = function() {
client <- oauth_client(
id = private$client_id,
token_url = private$endpoints$token_endpoint,
name = "openeo-r-oidc-auth"
)
private$auth = rlang::with_interactive(
oauth_flow_device(
client = client,
auth_url = private$endpoints$device_authorization_endpoint,
scope = paste0(private$scopes, collapse = " ")
),
value = private$isInteractive()
)
invisible(self)
}
),
# private ====
private = list(
# attributes ####
grant_type = "urn:ietf:params:oauth:grant-type:device_code", # not used internally by httr2, but maybe useful in openeo
# functions ####
isGrantTypeSupported = function(grant_types) {
if (!"urn:ietf:params:oauth:grant-type:device_code" %in% grant_types) {
stop("Device code flow is not supported by the authentication provider")
}
invisible(TRUE)
}
)
)
# [OIDCDeviceCodeFlowPkce] ----
OIDCDeviceCodeFlowPkce <- R6Class(
"OIDCDeviceCodeFlowPkce",
inherit = AbstractOIDCAuthentication,
# public ====
public = list(
# functions ####
login = function() {
client <- oauth_client(
id = private$client_id,
token_url = private$endpoints$token_endpoint,
name = "openeo-r-oidc-auth"
)
private$auth = rlang::with_interactive(
oauth_flow_device(
client = client,
auth_url = private$endpoints$device_authorization_endpoint,
scope = paste0(private$scopes, collapse = " "),
pkce = TRUE
),
value = private$isInteractive()
)
invisible(self)
}
),
# private ====
private = list(
# attributes ####
grant_type = "urn:ietf:params:oauth:grant-type:device_code+pkce", # not used internally by httr2, but maybe useful in openeo
# functions ####
isGrantTypeSupported = function(grant_types) {
# to be implemented in inheriting class
if (!"urn:ietf:params:oauth:grant-type:device_code+pkce" %in% grant_types) {
stop("Device code flow with pkce is not supported by the authentication provider")
}
invisible(TRUE)
}
)
)
# [OIDCAuthCodeFlowPKCE] ----
OIDCAuthCodeFlowPKCE <- R6Class(
"OIDCAuthCodeFlowPKCE",
inherit = AbstractOIDCAuthentication,
# public ====
public = list(
# attributes ####
# functions ####
login = function() {
client <- oauth_client(
id = private$client_id,
token_url = private$endpoints$token_endpoint,
name = "openeo-r-oidc-auth"
)
private$auth = rlang::with_interactive(
oauth_flow_auth_code(
client = client,
auth_url = private$endpoints$authorization_endpoint,
scope = paste0(private$scopes, collapse = " "),
pkce = TRUE,
port = 1410
),
value = private$isInteractive()
)
invisible(self)
}
),
# private ====
private = list(
# attributes ####
grant_type = "authorization_code+pkce", # not used internally by httr2, but maybe useful in openeo
isGrantTypeSupported = function(grant_types) {
# to be implemented in inheriting class
if (!any(c("authorization_code+pkce","authorization_code") %in% grant_types)) {
stop("Authorization code flow with pkce is not supported by the authentication provider")
}
invisible(TRUE)
}
)
)
# [OIDCAuthCodeFlow] ----
OIDCAuthCodeFlow <- R6Class(
"OIDCAuthCodeFlow",
inherit = AbstractOIDCAuthentication,
# public ====
public = list(
# attributes ####
# functions ####
login = function() {
client <- oauth_client(
id = private$client_id,
token_url = private$endpoints$token_endpoint,
name = "openeo-r-oidc-auth"
)
private$auth = rlang::with_interactive(
oauth_flow_auth_code(
client = client,
auth_url = private$endpoints$authorization_endpoint,
scope = paste0(private$scopes, collapse = " "),
pkce = FALSE,
port = 1410
),
value = private$isInteractive()
)
}
),
# private ====
private = list(
grant_type = "authorization_code",
# functions ####
isGrantTypeSupported = function(grant_types) {
# to be implemented in inheriting class
if (isTRUE(private$force_use)) return(invisible(TRUE))
if (!any(c("authorization_code") %in% grant_types)) {
stop("Authorization code flow is not supported by the authentication provider")
}
invisible(TRUE)
}
)
)
# utility functions ----
.get_oidc_provider = function(provider) {
if (length(provider) > 0 && is.character(provider)) {
oidc_providers = list_oidc_providers()
if (provider %in% names(oidc_providers)) {
return(oidc_providers[[provider]])
} else {
stop(paste0("The selected provider '",provider,"' is not supported. Check with list_oidc_providers() the available providers."))
}
}
return(provider)
}
.get_client = function(clients, grant, config) {
supported = which(sapply(clients, function(p) grant %in% p$grant_types))
if (length(supported) > 0) {
config$client_id = clients[[supported[[1]]]]$id
config$grant_type = grant
return(config)
}
else {
return (NULL)
}
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.