From 0908e8138f45112829f484c6afef47c4b7e1c6c0 Mon Sep 17 00:00:00 2001 From: Matthias Mohr Date: Tue, 13 Feb 2024 23:14:14 +0100 Subject: [PATCH 1/6] Client credentials flow --- NEWS.md | 1 + R/authentication.R | 40 +++++++++++++++++++ R/client.R | 27 ++++++++----- man/OIDCAuth.Rd | 6 +++ man/login.Rd | 1 + ...peneo-03-package-software-architecture.Rmd | 2 +- 6 files changed, 67 insertions(+), 10 deletions(-) diff --git a/NEWS.md b/NEWS.md index a51e1612..a199ab46 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,6 +2,7 @@ ## Added * internal functions to serialize and load the session information like connection, process collection, etc. +* Support for the OpenID Connect Client Credentials flow ## Changed * changed OIDC provider and default client selection diff --git a/R/authentication.R b/R/authentication.R index 5aa5fe4e..fd963aab 100644 --- a/R/authentication.R +++ b/R/authentication.R @@ -168,6 +168,7 @@ BasicAuth <- R6Class( #' \itemize{ #' \item{authorization_code} #' \item{authorization_code+pkce} +#' \item{urn:ietf:params:oauth:grant-type:device_code} #' \item{urn:ietf:params:oauth:grant-type:device_code+pkce} #' } #' @@ -616,6 +617,45 @@ OIDCAuthCodeFlow <- R6Class( ) ) +# [OIDCClientCredentialsFlow] ---- +OIDCClientCredentialsFlow <- R6Class( + "OIDCClientCredentialsFlow", + inherit = AbstractOIDCAuthentication, + # public ==== + public = list( + # functions #### + login = function() { + + client <- oauth_client( + id = private$client_id, + secret = private$secret, + token_url = private$endpoints$token_endpoint, + name = "openeo-r-oidc-auth" + ) + + private$auth = oauth_flow_client_credentials( + client = 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..1648354d 100644 --- a/R/client.R +++ b/R/client.R @@ -375,9 +375,11 @@ OpenEOClient <- R6Class( # 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 +390,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"]] @@ -439,12 +445,15 @@ OpenEOClient <- R6Class( stop("Please provide a client id or a valid combination of client_id and grant_type.") } } - - if (device_pkce == config$grant_type) { + + 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 (device_code == config$grant_type) { + } else if (has_grant && 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) { + } 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) } diff --git a/man/OIDCAuth.Rd b/man/OIDCAuth.Rd index 0d36d4fb..d089ed7a 100644 --- a/man/OIDCAuth.Rd +++ b/man/OIDCAuth.Rd @@ -20,6 +20,7 @@ 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} \item{urn:ietf:params:oauth:grant-type:device_code+pkce} } @@ -47,6 +48,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 From 66eba9ccf125a523a4b3549def528b5862c332fa Mon Sep 17 00:00:00 2001 From: Matthias Mohr Date: Fri, 26 Jul 2024 22:33:13 +0200 Subject: [PATCH 2/6] Fix changelog --- NEWS.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index a199ab46..31b57107 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,8 +1,13 @@ +# Version 1.4.0 + +## Added + +* Support for the OpenID Connect Client Credentials flow + # Version 1.3.1 ## Added * internal functions to serialize and load the session information like connection, process collection, etc. -* Support for the OpenID Connect Client Credentials flow ## Changed * changed OIDC provider and default client selection From ebc9c600da295ee0f92a7c4039e1317af6be1575 Mon Sep 17 00:00:00 2001 From: Matthias Mohr Date: Fri, 26 Jul 2024 22:36:55 +0200 Subject: [PATCH 3/6] Update R/authentication.R --- R/authentication.R | 1 + 1 file changed, 1 insertion(+) diff --git a/R/authentication.R b/R/authentication.R index fd963aab..49aa5816 100644 --- a/R/authentication.R +++ b/R/authentication.R @@ -168,6 +168,7 @@ 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} #' } From 7b498282b59cb74e5aecfd05a47ba9876c144ef7 Mon Sep 17 00:00:00 2001 From: Matthias Mohr Date: Fri, 26 Jul 2024 22:38:04 +0200 Subject: [PATCH 4/6] Update man/OIDCAuth.Rd --- man/OIDCAuth.Rd | 1 + 1 file changed, 1 insertion(+) diff --git a/man/OIDCAuth.Rd b/man/OIDCAuth.Rd index d089ed7a..5e9b756b 100644 --- a/man/OIDCAuth.Rd +++ b/man/OIDCAuth.Rd @@ -20,6 +20,7 @@ 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} } From 4526fea4ec336e2b4ee215dcb16e3cb066240971 Mon Sep 17 00:00:00 2001 From: Matthias Mohr Date: Sat, 27 Jul 2024 14:27:21 +0200 Subject: [PATCH 5/6] Code refactoring --- R/authentication.R | 55 ++++++---------------------------------------- R/client.R | 28 +++++++++++------------ 2 files changed, 20 insertions(+), 63 deletions(-) diff --git a/R/authentication.R b/R/authentication.R index 49aa5816..b62c3ef1 100644 --- a/R/authentication.R +++ b/R/authentication.R @@ -254,11 +254,6 @@ 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) { - private$scopes = c(private$scopes, "offline_access") - } } private$getEndpoints() @@ -269,8 +264,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.") } @@ -282,7 +277,6 @@ AbstractOIDCAuthentication <- R6Class( secret = config$secret ) } else { - private$oauth_client = oauth_client( id = private$client_id, token_url = private$endpoints$token_endpoint, @@ -448,16 +442,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 = " ") ), @@ -490,16 +477,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 @@ -535,16 +515,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, @@ -582,15 +555,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, @@ -626,16 +593,8 @@ OIDCClientCredentialsFlow <- R6Class( public = list( # functions #### login = function() { - - client <- oauth_client( - id = private$client_id, - secret = private$secret, - token_url = private$endpoints$token_endpoint, - name = "openeo-r-oidc-auth" - ) - private$auth = oauth_flow_client_credentials( - client = client, + client = private$oauth_client, scope = paste0(private$scopes, collapse = " ") ) diff --git a/R/client.R b/R/client.R index 1648354d..c99b9652 100644 --- a/R/client.R +++ b/R/client.R @@ -369,8 +369,6 @@ 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) @@ -418,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 @@ -439,24 +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.") } } - 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)) { + 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") } From dd3b888212b2e2b1ecefee664f6263eab882ac3f Mon Sep 17 00:00:00 2001 From: Matthias Mohr Date: Tue, 30 Jul 2024 16:19:57 +0200 Subject: [PATCH 6/6] Follow logic in Python client --- R/authentication.R | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/R/authentication.R b/R/authentication.R index b62c3ef1..4f270e33 100644 --- a/R/authentication.R +++ b/R/authentication.R @@ -254,6 +254,11 @@ AbstractOIDCAuthentication <- R6Class( private$scopes = list("openid") } else { private$scopes = provider$scopes + + #TODO remove later, this is used for automatic reconnect + if (private$grant_type != "client_credentials" && !"offline_access" %in% private$scopes) { + private$scopes = c(private$scopes, "offline_access") + } } private$getEndpoints()