I am trying to connect to the CLMS API in order to download data with R. I have already created a token, and saved it in the file land-copernicus.json
.
Here is my attempt to connect:
## Connection
# 1. The information about the connection using the CLMS API can be found here:
# https://eea.github.io/clms-api-docs/authentication.html
# 2. The use of API authentification flow involves four steps:
# 2.1 Get a service key online
# 2.2 R uses the private key to create and sign a JWT authorisation grant
# 2.3 R exchanges the JWT authorisation grant for a short-lived access token at the @@oauth2-token endpoint
# 2.4 The client then uses this access token to authenticate requests to protected resources
#
library(httr2)
## Read the keys created at step 2.1
service_key = rjson::fromJSON(file = "./land-copernicus.json")
private_key = service_key[["private_key"]]
## Create the authorisation grant from private key
claim = jwt_claim(
iss = service_key[["client_id"]],
sub = service_key[["user_id"]],
aud = service_key[["token_uri"]],
iat = Sys.time(),
exp = Sys.time() + 60*60 # Gives an expiration date of one hour (the maximum authorised)
)
grant = jwt_encode_hmac(claim = claim, secret = private_key)
# grant = jwt_encode_hmac(claim = claim, secret = private_key, size = 256, header = list(alg = "RS256"))
## Exchange the JWT authorization grant at the @@oauth2-token endpoint
req = request(base_url = service_key[["token_uri"]]) |>
req_headers("Accept" = "application/json", "Content-Type" = "application/x-www-form-urlencoded") |>
req_body_json(data = list(grant_type = "urn:ietf:params:oauth:grant-type:jwt-bearer", assertion = grant)) |>
req_perform(verbosity = 3)
I get the following error:
* Uses proxy env variable no_proxy == 'localhost,HP2011P079.ign.fr'
* Uses proxy env variable https_proxy == 'http://proxy.ign.fr:3128/'
* Trying 10.128.81.101:3128...
* Connected to (nil) (10.128.81.101) port 3128 (#2)
* allocate connect buffer!
* Establish HTTP proxy tunnel to land.copernicus.eu:443
-> CONNECT land.copernicus.eu: 443 HTTP/1.1
-> Host: land.copernicus.eu:443
-> User-Agent: httr2/1.0.5 r-curl/5.2.3 libcurl/7.81.0
-> Proxy-Connection: Keep-Alive
->
<- HTTP/1.1 200 Connection established
<-
* Proxy replied 200 to CONNECT request
* CONNECT phase completed!
* ALPN, offering h2
* ALPN, offering http/1.1
* CAfile: /etc/ssl/certs/ca-certificates.crt
* CApath: /etc/ssl/certs
* TLSv1.0 (OUT), TLS header, Certificate Status (22):
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS header, Certificate Status (22):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS header, Certificate Status (22):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS header, Certificate Status (22):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS header, Certificate Status (22):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS header, Certificate Status (22):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS header, Finished (20):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS header, Certificate Status (22):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS header, Finished (20):
* TLSv1.2 (IN), TLS header, Certificate Status (22):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
* ALPN, server accepted to use h2
* Server certificate:
* subject: CN=land.copernicus.eu
* start date: Oct 4 10:29:01 2024 GMT
* expire date: Jan 2 10:29:00 2025 GMT
* subjectAltName: host "land.copernicus.eu" matched cert's "land.copernicus.eu"
* issuer: C=US; O=Let's Encrypt; CN=R10
* SSL certificate verify ok.
* Using HTTP2, server supports multiplexing
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* Using Stream ID: 1 (easy handle 0x5d9ca6076e20)
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
-> POST /@@oauth2-token HTTP/2
-> Host: land.copernicus.eu
-> user-agent: httr2/1.0.5 r-curl/5.2.3 libcurl/7.81.0
-> accept-encoding: deflate, gzip, br, zstd
-> accept: application/json
-> content-type: application/x-www-form-urlencoded
-> content-length: 461
->
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
>> {"grant_type":"urn:ietf:params:oauth:grant-type:jwt-bearer","assertion":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImFsZy4xIjoiUlMyNTYifQ.eyJpc3MiOiIyYjhlMTdmYzBkOTg4YzVkNGY2OTg1ZTMyYjdmY2VkNCIsInN1YiI6Im4wMGg3ZGthIiwiYXVkIjoiaHR0cHM6Ly9sYW5kLmNvcGVybmljdXMuZXUvQEBvYXV0aDItdG9rZW4iLCJleHAiOjE3Mjg0Njk4NDcsIm5iZiI6MTcyODQ2NjI0NywiaWF0IjoxNzI4NDY2MjQ3LCJqdGkiOiJZcEhTSmpOSk0zeE85SFdTbVk1NFpqS0J6QlROdUtWUEtLNGRsZ05oSmtVIn0.1opDHqRqVMuKgojnUvgf1DTOtBAY3fLQB6AGr1C0jYY"}
* We are completely uploaded and fine
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
<- HTTP/2 400
<- cache-control: no-store
<- content-length: 73
<- content-type: application/json
<- date: Wed, 09 Oct 2024 09:59:41 GMT
<- pragma: no-cache
<- server: waitress
<- via: waitress
<- x-frame-options: SAMEORIGIN
<- x-powered-by: Zope (www.zope.dev), Python (www.python.org)
<- strict-transport-security: max-age=31536000; includeSubDomains; preload
<- x-content-type-options: nosniff
<- x-xss-protection: 1; mode=block
<- access-control-allow-headers: Authorization, Content-Type, content-type
<- vary: Origin
<-
<< {"error": "invalid_request", "error_description": "Missing 'grant_type'"}
* Connection #2 to host (nil) left intact
Error in `req_perform()`:
! HTTP 400 Bad Request.
It seems that the error lies in {"error": "invalid_request", "error_description": "Missing 'grant_type'"} which I do not understand since I provide a grant_type
.
In case of it helps, my code is inspired from a jupyter notebook that can be found here
I also tried the following request, where I specify the algorithm:
grant = jwt_encode_hmac(claim = claim, secret = private_key, size = 256, header = list(alg = "RS256"))
req2 = request(base_url = service_key[["token_uri"]]) |>
req_headers("Accept" = "application/json", "Content-Type" = "application/x-www-form-urlencoded") |>
req_body_raw(body = paste0("grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=", grant)) |>
req_perform(verbosity = 3)
and got the error {"error": "invalid_request", "error_description": "Only RS256 signature algorithm is supported"}. Again I do not understand why since I provide the algorithm RS256
(more information here).
When I copy-paste in R the grant generated by python with this code:
import json
import jwt
import time
import requests
base_url = 'https://land.copernicus.eu'
# Load saved key from filesystem
service_key = json.load(open('./land-copernicus.json', 'rb'))
private_key = service_key['private_key'].encode('utf-8')
claim_set = {
"iss": service_key['client_id'],
"sub": service_key['user_id'],
"aud": service_key['token_uri'],
"iat": int(time.time()),
"exp": int(time.time() + (60 * 60)),
}
grant = jwt.encode(claim_set, private_key, algorithm='RS256')
then it works. Therefore, I deduce that the problem comes from httr2::jwt_encode_hmac
given that the service and privates keys, and the claim are identical
You need to use jwt_encode_sig
instead of jwt_encode_hmac
and also follow their docs. This works:
library(httr2)
## JSON file from website
## See: https://eea.github.io/clms-api-docs/authentication.html#create-api-tokens
service_key <- jsonlite::read_json("~/Desktop/my_saved_key.json")
claim <- jwt_claim(
iss = service_key$client_id,
sub = service_key$user_id,
aud = service_key$token_uri,
nbf = NULL)
## Sign the authorisation grant using private key
grant <- jose::jwt_encode_sig(claim, service_key$private_key)
req <- request(base_url = service_key$token_uri) |>
req_body_form(grant_type = "urn:ietf:params:oauth:grant-type:jwt-bearer", assertion = grant) |>
req_perform(verbosity = 2)
resp_body_json(req)