diff --git a/NEWS.md b/NEWS.md index a51e1612..31b57107 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,9 @@ +# Version 1.4.0 + +## Added + +* Support for the OpenID Connect Client Credentials flow + # Version 1.3.1 ## Added diff --git a/R/authentication.R b/R/authentication.R index 5aa5fe4e..4f270e33 100644 --- a/R/authentication.R +++ b/R/authentication.R @@ -168,6 +168,8 @@ BasicAuth <- R6Class( #' \itemize{ #' \item{authorization_code} #' \item{authorization_code+pkce} +#' \item{client_credentials} +#' \item{urn:ietf:params:oauth:grant-type:device_code} #' \item{urn:ietf:params:oauth:grant-type:device_code+pkce} #' } #' @@ -252,9 +254,9 @@ AbstractOIDCAuthentication <- R6Class( private$scopes = list("openid") } else { private$scopes = provider$scopes - + #TODO remove later, this is used for automatic reconnect - if (!"offline_access" %in% private$scopes) { + if (private$grant_type != "client_credentials" && !"offline_access" %in% private$scopes) { private$scopes = c(private$scopes, "offline_access") } } @@ -267,8 +269,8 @@ AbstractOIDCAuthentication <- R6Class( 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 (private$grant_type == "authorization_code" || private$grant_type == "client_credentials") { + # in this case we need a client_id and secrect if (!all(c("client_id","secret") %in% names(config))) { stop("'client_id' and 'secret' are not present in the configuration.") } @@ -280,7 +282,6 @@ AbstractOIDCAuthentication <- R6Class( secret = config$secret ) } else { - private$oauth_client = oauth_client( id = private$client_id, token_url = private$endpoints$token_endpoint, @@ -446,16 +447,9 @@ OIDCDeviceCodeFlow <- R6Class( 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, + client = private$oauth_client, auth_url = private$endpoints$device_authorization_endpoint, scope = paste0(private$scopes, collapse = " ") ), @@ -488,16 +482,9 @@ OIDCDeviceCodeFlowPkce <- R6Class( 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, + client = private$oauth_client, auth_url = private$endpoints$device_authorization_endpoint, scope = paste0(private$scopes, collapse = " "), pkce = TRUE @@ -533,16 +520,9 @@ OIDCAuthCodeFlowPKCE <- R6Class( # 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, + client = private$oauth_client, auth_url = private$endpoints$authorization_endpoint, scope = paste0(private$scopes, collapse = " "), pkce = TRUE, @@ -580,15 +560,9 @@ OIDCAuthCodeFlow <- R6Class( # 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, + client = private$oauth_client, auth_url = private$endpoints$authorization_endpoint, scope = paste0(private$scopes, collapse = " "), pkce = FALSE, @@ -616,6 +590,37 @@ OIDCAuthCodeFlow <- R6Class( ) ) +# [OIDCClientCredentialsFlow] ---- +OIDCClientCredentialsFlow <- R6Class( + "OIDCClientCredentialsFlow", + inherit = AbstractOIDCAuthentication, + # public ==== + public = list( + # functions #### + login = function() { + private$auth = oauth_flow_client_credentials( + client = private$oauth_client, + scope = paste0(private$scopes, collapse = " ") + ) + + invisible(self) + } + ), + # private ==== + private = list( + # attributes #### + grant_type = "client_credentials", # not used internally by httr2, but maybe useful in openeo + + # functions #### + isGrantTypeSupported = function(grant_types) { + if (!"client_credentials" %in% grant_types) { + stop("Client Credentials flow is not supported by the authentication provider") + } + invisible(TRUE) + } + ) +) + # utility functions ---- .get_oidc_provider = function(provider, oidc_providers=NULL) { if (is.null(oidc_providers)) { diff --git a/R/client.R b/R/client.R index 382935dc..c99b9652 100644 --- a/R/client.R +++ b/R/client.R @@ -369,15 +369,15 @@ OpenEOClient <- R6Class( loginOIDC = function(provider = NULL, config = NULL) { suppressWarnings({ tryCatch({ - - # old implementation # probably fetch resolve the potential string into a provider here provider = .get_oidc_provider(provider) + auth_code = "authorization_code" auth_pkce = "authorization_code+pkce" device_pkce = "urn:ietf:params:oauth:grant-type:device_code+pkce" device_code = "urn:ietf:params:oauth:grant-type:device_code" + client_credentials = "client_credentials" has_default_clients = "default_clients" %in% names(provider) && length(provider[["default_clients"]]) > 0 client_id_given = "client_id" %in% names(config) @@ -388,14 +388,18 @@ OpenEOClient <- R6Class( } full_credentials = all(c("client_id","secret") %in% names(config)) - is_auth_code = length(config$grant_type) > 0 && config$grant_type == 'authorization_code' + is_auth_code = length(config$grant_type) > 0 && config$grant_type == auth_code + is_client_credentials = length(config$grant_type) > 0 && config$grant_type == client_credentials - # either credentials are set and / or authorization_code as grant_type - if (full_credentials && (is_auth_code || is.null(config$grant_type))) { + # either credentials are set and / or authorization_code or client_credentials as grant_type + if (full_credentials && is_client_credentials) { + private$auth_client = OIDCClientCredentialsFlow$new(provider = provider, config = config, force=TRUE) + } + else if (full_credentials && (is_auth_code || is.null(config$grant_type))) { private$auth_client = OIDCAuthCodeFlow$new(provider = provider, config = config, force=TRUE) } - else if (is_auth_code) { - stop("For grant type 'authorization_code' a client_id and secret must be provided") + else if (is_auth_code || is_client_credentials) { + stop("For grant type 'authorization_code' and 'client_credentials' a client_id and secret must be provided") } else if (client_id_given && has_default_clients) { default_clients = provider[["default_clients"]] @@ -412,7 +416,7 @@ OpenEOClient <- R6Class( } } - if (has_default_clients && !client_id_given) { + if (is.null(private$auth_client) && has_default_clients && !client_id_given) { default_clients = provider[["default_clients"]] # check whether user has chosen a grant type @@ -433,21 +437,24 @@ OpenEOClient <- R6Class( } } - - if (is.null(config$client_id)) { stop("Please provide a client id or a valid combination of client_id and grant_type.") } } - - if (device_pkce == config$grant_type) { - private$auth_client = OIDCDeviceCodeFlowPkce$new(provider=provider, config = config) - } else if (device_code == config$grant_type) { - private$auth_client = OIDCDeviceCodeFlow$new(provider=provider, config = config) - } else if (is.null(config$grant_type) || auth_pkce == config$grant_type) { - private$auth_client = OIDCAuthCodeFlowPKCE$new(provider=provider, config = config) - } + if (is.null(private$auth_client)) { + has_grant = "grant_type" %in% names(config) + if (has_grant && device_pkce == config$grant_type) { + private$auth_client = OIDCDeviceCodeFlowPkce$new(provider=provider, config = config) + } else if (has_grant && device_code == config$grant_type) { + private$auth_client = OIDCDeviceCodeFlow$new(provider=provider, config = config) + } else if (has_grant && client_credentials == config$grant_type) { + private$auth_client = OIDCClientCredentialsFlow$new(provider=provider, config = config) + } else if (is.null(config$grant_type) || (has_grant && auth_pkce == config$grant_type)) { + private$auth_client = OIDCAuthCodeFlowPKCE$new(provider=provider, config = config) + } + } + if (is.null(private$auth_client)) { stop("The grant_type selected is not supported") } diff --git a/man/OIDCAuth.Rd b/man/OIDCAuth.Rd index 0d36d4fb..5e9b756b 100644 --- a/man/OIDCAuth.Rd +++ b/man/OIDCAuth.Rd @@ -20,6 +20,8 @@ This client supports the following interaction mechanisms (grant types): \itemize{ \item{authorization_code} \item{authorization_code+pkce} +\item{client_credentials} +\item{urn:ietf:params:oauth:grant-type:device_code} \item{urn:ietf:params:oauth:grant-type:device_code+pkce} } @@ -47,6 +49,11 @@ the console or if R runs in the interactive mode the internet browser will be op 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. } + +\subsection{client_credentials}{ +This mechanism is used to verify a certain client, not a specific user. It requires a client id and secret and is meant for machine-to-machine workflows. +} + } \section{Fields}{ diff --git a/man/login.Rd b/man/login.Rd index 698682cc..cdf77edb 100644 --- a/man/login.Rd +++ b/man/login.Rd @@ -52,6 +52,7 @@ list. You can then use the following values: \item authorization_code+pkce \item urn:ietf:params:oauth:grant-type:device_code \item urn:ietf:params:oauth:grant-type:device_code+pkce +\item client_credentials } } \section{Configuration options}{ diff --git a/vignettes/openeo-03-package-software-architecture.Rmd b/vignettes/openeo-03-package-software-architecture.Rmd index 136a72db..c960ec09 100644 --- a/vignettes/openeo-03-package-software-architecture.Rmd +++ b/vignettes/openeo-03-package-software-architecture.Rmd @@ -58,7 +58,7 @@ R6 is an object oriented programming style like S3 or S4 in R that is based on [ # Authentication -To get access to the computation capabilities and user stored data openEO you need to be a registered user at the openEO back-end, where you want to carry out your analysis. In order to proof that to the system you need to be authenticated. The openEO API offers Open ID Connect (OIDC) and Basic Authentication as authentication methods. In this package the we use primarily OIDC, but also offer Basic Authentication for legacy support. As OIDC is based on OAuth2 there are several different sub mechanisms, e.g. Authcode Flow or Device Code Flow with or without PKCE. The mechanisms are covered by the httr2 package `httr2::oauth_flow_device()`, which is used to negotiate the authentication and to obtain the required access token. In the code the different authentication methods are realized by the different R6 classes that share a common interface `IAuth`. Inheriting classes implement and overload those functions so that by using the function `..$login()` or the active field access_token all objects behave in the same manor and ultimately provide the access token. +To get access to the computation capabilities and user stored data openEO you need to be a registered user at the openEO back-end, where you want to carry out your analysis. In order to proof that to the system you need to be authenticated. The openEO API offers Open ID Connect (OIDC) and Basic Authentication as authentication methods. In this package the we use primarily OIDC, but also offer Basic Authentication for legacy support. As OIDC is based on OAuth2 there are several different sub mechanisms, e.g. Auth Code Flow or Device Code Flow with or without PKCE. The mechanisms are covered by the httr2 package `httr2::oauth_flow_device()`, which is used to negotiate the authentication and to obtain the required access token. In the code the different authentication methods are realized by the different R6 classes that share a common interface `IAuth`. Inheriting classes implement and overload those functions so that by using the function `..$login()` or the active field access_token all objects behave in the same manor and ultimately provide the access token. # Visual Components