rresthttrrjson

How to connect to the Copernicus Land Monitoring Service API with R


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).

--------------- EDIT ---------------

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


Solution

  • 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)