Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for OIDC Client credentials flow #149

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# Version 1.4.0

## Added

* Support for the OpenID Connect Client Credentials flow

# Version 1.3.1

## Added
Expand Down
77 changes: 41 additions & 36 deletions R/authentication.R
Original file line number Diff line number Diff line change
Expand Up @@ -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}
#' }
#'
Expand Down Expand Up @@ -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")
}
}
Expand All @@ -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.")
}
Expand All @@ -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,
Expand Down Expand Up @@ -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 = " ")
),
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)) {
Expand Down
43 changes: 25 additions & 18 deletions R/client.R
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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"]]
Expand All @@ -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
Expand All @@ -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")
}
Expand Down
7 changes: 7 additions & 0 deletions man/OIDCAuth.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions man/login.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion vignettes/openeo-03-package-software-architecture.Rmd
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down