Repository: CS-SI/eodag
Pull request #1590 (2025-04-03 12:11:55)

fix(plugins): check expiration time in token auth
So far the expiration time of an access token was not stored and checked in the `TokenAuth` plugin. This is now done, so that no token request is sent if there is already an existing, valid token. 

The corresponding configuration has to be set using the new parameter `token_expiration_key`, see [TokenAuth configuration](https://eodag.readthedocs.io/en/latest/plugins_reference/generated/eodag.plugins.authentication.token.TokenAuth.html). It has been added for the providers `wekeo_main`, `wekeo_ecmwf` and `eumetsat_ds`.


github-actions[bot]: ## Test Results
    4 files  ±0      4 suites  ±0   6m 42s ⏱️ +2s
  615 tests +1    612 ✅ +1   3 💤 ±0  0 ❌ ±0 
2 460 runs  +4  2 362 ✅ +4  98 💤 ±0  0 ❌ ±0 

Results for commit 3d1d6871. ± Comparison against base commit 2d3f6dac.

[test-results]:data:application/gzip;base64,H4sIANV77mcC/03MyQ7CIBSF4VdpWLtgKBfwZQwyJMS2GIaV8d2lVbDL/zvJeSEfFpfRdZovE8o1lBG2Jl1C3PbEtEGbyj4C4b1uuRpzEP3TIzwbsQFeh6UBHuBSiuknqW77J52hV/+kDOig76eSHU6fR58vTVzXUFogZokFKQhTnHqvBfHAGQgtveNO3kFJhbmzEr0/Q1xmGAgBAAA=

github-actions[bot]: ![badge](https://raw.githubusercontent.com/CS-SI/eodag/_xml_coverage_reports/data/wekeo-access-token/./badge.svg)


## Code Coverage (Ubuntu)

<details>


```
Filename                                     Stmts    Miss  Cover    Missing
-----------------------------------------  -------  ------  -------  ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
__init__.py                                      8       0  100.00%
cli.py                                         323      64  80.19%   62-63, 87, 674-737, 839-890, 894
config.py                                      432      27  93.75%   80-82, 91, 99, 103-105, 176, 187, 698-700, 814-817, 860-861, 870-871, 976, 1035-1040, 1042
crunch.py                                        5       5  0.00%    20-24
api/__init__.py                                  0       0  100.00%
api/core.py                                    783      74  90.55%   316-317, 367, 657, 701-704, 742, 786, 820, 865-870, 896, 987, 1055, 1193, 1278-1290, 1326, 1328, 1356, 1360-1371, 1384-1390, 1473-1476, 1509-1529, 1581, 1598-1602, 1614-1617, 1953, 1977-1983, 2234, 2238-2242, 2251-2253, 2297-2298, 2327-2328
api/search_result.py                            58       4  93.10%   82, 91, 98, 112
api/product/__init__.py                          6       0  100.00%
api/product/_assets.py                          52       5  90.38%   97, 183, 191, 194-198
api/product/_product.py                        196      19  90.31%   70-72, 236-237, 312, 341, 398, 412-415, 428, 452-455, 515-518
api/product/metadata_mapping.py                702      65  90.74%   120-122, 220, 252-253, 299-300, 310-322, 324, 335, 360, 400-401, 438, 459-462, 485, 493-494, 587-588, 612-613, 619-622, 637-638, 787, 838, 991, 1000-1004, 1021-1026, 1153, 1167-1187, 1207, 1212, 1341, 1355, 1380, 1426, 1478, 1501-1502, 1522-1526, 1542, 1550
api/product/drivers/__init__.py                 20       2  90.00%   33-36
api/product/drivers/base.py                     29       1  96.55%   98
api/product/drivers/generic.py                   7       0  100.00%
api/product/drivers/sentinel1.py                15       0  100.00%
api/product/drivers/sentinel2.py                15       0  100.00%
plugins/__init__.py                              0       0  100.00%
plugins/base.py                                 21       2  90.48%   48, 55
plugins/manager.py                             172      14  91.86%   106-111, 161, 199, 221, 225, 249, 389-392, 404-405
plugins/apis/__init__.py                         0       0  100.00%
plugins/apis/base.py                             4       0  100.00%
plugins/apis/ecmwf.py                           97      10  89.69%   167-169, 216-217, 243-245, 298-299
plugins/apis/usgs.py                           180      26  85.56%   156, 258, 292, 327-329, 334, 360-361, 366, 396-403, 414-419, 441-447, 478
plugins/authentication/__init__.py               6       1  83.33%   31
plugins/authentication/aws_auth.py              20       0  100.00%
plugins/authentication/base.py                  17       2  88.24%   45, 58
plugins/authentication/generic.py               14       2  85.71%   51, 61
plugins/authentication/header.py                19       0  100.00%
plugins/authentication/keycloak.py              46       7  84.78%   151-154, 175-180
plugins/authentication/oauth.py                 13       7  46.15%   43-45, 49-52
plugins/authentication/openid_connect.py       208      28  86.54%   80-81, 93-111, 149, 155-183, 191, 323-326, 352, 386
plugins/authentication/qsauth.py                34       1  97.06%   91
plugins/authentication/sas_auth.py              47       1  97.87%   76
plugins/authentication/token.py                108      16  85.19%   149, 178, 186, 222-235, 291-295
plugins/authentication/token_exchange.py        36      14  61.11%   75, 92-120
plugins/crunch/__init__.py                       0       0  100.00%
plugins/crunch/base.py                          10       1  90.00%   43
plugins/crunch/filter_date.py                   59      14  76.27%   52-57, 69, 78, 87, 90, 102-104, 113-115, 122
plugins/crunch/filter_latest_intersect.py       47       8  82.98%   52-53, 69, 78-81, 83, 90-93
plugins/crunch/filter_latest_tpl_name.py        31       1  96.77%   83
plugins/crunch/filter_overlap.py                66      18  72.73%   28-30, 66-69, 76-79, 85, 93, 104-120
plugins/crunch/filter_property.py               30       5  83.33%   55-60, 63-64
plugins/download/__init__.py                     0       0  100.00%
plugins/download/aws.py                        528     158  70.08%   272, 285, 365-368, 382-386, 476-478, 482, 515-516, 522-526, 556, 589, 593, 600, 630-638, 642, 677-685, 696-698, 729-810, 828-886, 897-902, 955, 970-972, 975, 985-993, 1001-1015, 1025-1048, 1055-1068, 1106, 1132, 1177-1179, 1399
plugins/download/base.py                       253      42  83.40%   127, 155, 235-238, 291-292, 340-344, 350-351, 393, 396-410, 422, 426, 490-494, 524-525, 550-558, 560-564, 607, 629, 651, 659
plugins/download/creodias_s3.py                 25       9  64.00%   55-69
plugins/download/http.py                       529      95  82.04%   227, 323-326, 329, 336-341, 359-374, 391, 403, 451, 458-464, 482, 496, 510, 518-520, 536-541, 552, 570, 606-609, 638, 642, 662, 739, 758-769, 777-782, 792-809, 827, 857-866, 902, 927-928, 947-952, 958, 961, 978, 981, 996-997, 1032, 1094, 1109, 1168-1169, 1175, 1185, 1221, 1257, 1277, 1312-1314
plugins/download/s3rest.py                     116      24  79.31%   119, 153, 160, 195, 222-229, 232-234, 238, 249-255, 263-264, 267-271, 294, 315-318
plugins/search/__init__.py                      22       0  100.00%
plugins/search/base.py                         145      11  92.41%   101, 105, 129-135, 276, 296, 429
plugins/search/build_search_result.py          352      36  89.77%   241-242, 278, 281, 412-423, 559, 561, 622, 629, 653, 688, 739, 788-803, 848, 873, 876, 880, 1000-1001, 1007, 1018, 1097
plugins/search/cop_marine.py                   244      47  80.74%   56, 64-66, 76-77, 82, 87-88, 104, 106, 109, 175-176, 228, 234, 238, 242, 255, 266-267, 275, 303, 307, 322, 326, 330, 334, 338-342, 348-351, 354-368, 385, 434-438, 443, 455
plugins/search/creodias_s3.py                   25       1  96.00%   51
plugins/search/csw.py                          105      81  22.86%   98-99, 103-104, 112-160, 166-179, 187-219, 237-278
plugins/search/data_request_search.py          202      69  65.84%   189-192, 208, 219, 223-224, 235, 240, 245, 252, 265-268, 322-323, 327, 337-343, 348, 374-377, 385-396, 413, 415, 422-425, 427-428, 446-450, 483, 493, 504, 517, 523-538, 543
plugins/search/qssearch.py                     702      88  87.46%   450, 508, 522, 526-532, 560-564, 677-689, 734-737, 808-809, 857, 876, 883, 895, 952, 973, 976-977, 986-987, 996-997, 1006-1007, 1034, 1118-1123, 1127-1136, 1170, 1192, 1265-1266, 1272, 1362, 1459-1463, 1523, 1526, 1532-1533, 1554, 1582-1594, 1601, 1633-1635, 1645-1651, 1681, 1704, 1709-1710, 1725, 1741, 1826-1829, 1834-1837, 1846, 1878-1882, 1888
plugins/search/stac_list_assets.py              25      10  60.00%   44-51, 75-85
plugins/search/static_stac_search.py            75      13  82.67%   98-124, 140, 153
rest/__init__.py                                 4       2  50.00%   21-22
rest/cache.py                                   33       7  78.79%   35-37, 53-55, 59, 68
rest/config.py                                  25       0  100.00%
rest/constants.py                                6       0  100.00%
rest/core.py                                   260      67  74.23%   258, 266, 284-301, 316-352, 446, 483-522, 599, 705, 712-765
rest/errors.py                                  69       5  92.75%   106, 116, 127, 143-144
rest/server.py                                 192      24  87.50%   86, 109-111, 285-290, 318, 514-516, 533-538, 567, 569, 573-574, 578-579
rest/stac.py                                   319      63  80.25%   309, 331, 383-386, 413-440, 471-473, 496, 528-529, 611-651, 673-689, 781-785, 792, 846-847, 908, 998-1000
rest/types/__init__.py                           0       0  100.00%
rest/types/collections_search.py                13      13  0.00%    18-44
rest/types/eodag_search.py                     180       6  96.67%   225-229, 282, 285, 353, 385
rest/types/queryables.py                        64      12  81.25%   61, 71-79, 105-110, 174
rest/types/stac_search.py                      125       7  94.40%   128, 174, 189-191, 199, 203
rest/utils/__init__.py                          94      12  87.23%   101-102, 121-123, 175, 185-199
rest/utils/cql_evaluate.py                      48       5  89.58%   69, 76, 90, 97, 105
rest/utils/rfc3339.py                           23       3  86.96%   48, 60, 62
types/__init__.py                              148      40  72.97%   67, 80-84, 95-107, 135-137, 144-149, 239, 249-265, 270, 272, 294, 299, 307, 317
types/bbox.py                                   43      19  55.81%   46-61, 72-74, 85-87, 99-101, 113-115, 123
types/download_args.py                           9       0  100.00%
types/queryables.py                            108       1  99.07%   63
types/search_args.py                            70      18  74.29%   60-64, 71-88, 103
types/whoosh.py                                 81      16  80.25%   129-132, 136-143, 155-161, 174-176
utils/__init__.py                              538      40  92.57%   81, 198-199, 208-235, 238, 253, 333-337, 410-414, 433-435, 449, 541, 556, 596-597, 626, 1001-1004, 1012-1013, 1051-1052, 1099-1100, 1234
utils/exceptions.py                             46       0  100.00%
utils/import_system.py                          28      19  32.14%   64-78, 89-99
utils/logging.py                                28       1  96.43%   41
utils/notebook.py                               44      23  47.73%   25-29, 36-41, 58-62, 72-78, 83-87
utils/repr.py                                   38       8  78.95%   51, 53, 57, 98, 122-129
utils/requests.py                               55      29  47.27%   51-52, 64, 85-96, 107-124, 128
utils/rest.py                                   36       1  97.22%   55
utils/s3.py                                     69       3  95.65%   162-163, 226
utils/stac_reader.py                           111      45  59.46%   56-57, 63-85, 95-97, 101, 137, 153-156, 203-212, 222-252
TOTAL                                        10191    1617  84.13%
```

</details>


### Diff against develop

```
Filename                           Stmts    Miss  Cover
-------------------------------  -------  ------  -------
config.py                             +1       0  +0.01%
plugins/authentication/token.py       +8       0  +1.19%
TOTAL                                 +9       0  +0.01%
```


Results for commit: 3d1d68713952ffa71f65367a8fe5e8b698905ed8


_Minimum allowed coverage is `70%`_


:recycle: This comment has been updated with latest results


<!-- Sticky Pull Request Commentartifacts/unit-test-results-python3.13-ubuntu-latest/coverage.xml -->
github-actions[bot]: ![badge](https://raw.githubusercontent.com/CS-SI/eodag/_xml_coverage_reports_win/data/wekeo-access-token/./badge.svg)


## Code Coverage (Windows)

<details>


```
Filename                                     Stmts    Miss  Cover    Missing
-----------------------------------------  -------  ------  -------  ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
__init__.py                                      8       0  100.00%
cli.py                                         323      64  80.19%   62-63, 87, 674-737, 839-890, 894
config.py                                      432      28  93.52%   80-82, 91, 99, 103-105, 176, 187, 698-700, 814-817, 860-861, 870-871, 976, 1005, 1035-1040, 1042
crunch.py                                        5       5  0.00%    20-24
api/__init__.py                                  0       0  100.00%
api/core.py                                    783      74  90.55%   316-317, 367, 657, 701-704, 742, 786, 820, 865-870, 896, 987, 1055, 1193, 1278-1290, 1326, 1328, 1356, 1360-1371, 1384-1390, 1473-1476, 1509-1529, 1581, 1598-1602, 1614-1617, 1953, 1977-1983, 2234, 2238-2242, 2251-2253, 2297-2298, 2327-2328
api/search_result.py                            58       4  93.10%   82, 91, 98, 112
api/product/__init__.py                          6       0  100.00%
api/product/_assets.py                          52       5  90.38%   97, 183, 191, 194-198
api/product/_product.py                        196      19  90.31%   70-72, 236-237, 312, 341, 398, 412-415, 428, 452-455, 515-518
api/product/metadata_mapping.py                702      65  90.74%   120-122, 220, 252-253, 299-300, 310-322, 324, 335, 360, 400-401, 438, 459-462, 485, 493-494, 587-588, 612-613, 619-622, 637-638, 787, 838, 991, 1000-1004, 1021-1026, 1153, 1167-1187, 1207, 1212, 1341, 1355, 1380, 1426, 1478, 1501-1502, 1522-1526, 1542, 1550
api/product/drivers/__init__.py                 20       2  90.00%   33-36
api/product/drivers/base.py                     29       1  96.55%   98
api/product/drivers/generic.py                   7       0  100.00%
api/product/drivers/sentinel1.py                15       0  100.00%
api/product/drivers/sentinel2.py                15       0  100.00%
plugins/__init__.py                              0       0  100.00%
plugins/base.py                                 21       3  85.71%   48, 55, 68
plugins/manager.py                             172      14  91.86%   106-111, 161, 199, 221, 225, 249, 389-392, 404-405
plugins/apis/__init__.py                         0       0  100.00%
plugins/apis/base.py                             4       0  100.00%
plugins/apis/ecmwf.py                           97      10  89.69%   167-169, 216-217, 243-245, 298-299
plugins/apis/usgs.py                           180      26  85.56%   156, 258, 292, 327-329, 334, 360-361, 366, 396-403, 414-419, 441-447, 478
plugins/authentication/__init__.py               6       1  83.33%   31
plugins/authentication/aws_auth.py              20       0  100.00%
plugins/authentication/base.py                  17       2  88.24%   45, 58
plugins/authentication/generic.py               14       2  85.71%   51, 61
plugins/authentication/header.py                19       0  100.00%
plugins/authentication/keycloak.py              46       7  84.78%   151-154, 175-180
plugins/authentication/oauth.py                 13       7  46.15%   43-45, 49-52
plugins/authentication/openid_connect.py       208      28  86.54%   80-81, 93-111, 149, 155-183, 191, 323-326, 352, 386
plugins/authentication/qsauth.py                34       1  97.06%   91
plugins/authentication/sas_auth.py              47       1  97.87%   76
plugins/authentication/token.py                108      16  85.19%   149, 178, 186, 222-235, 291-295
plugins/authentication/token_exchange.py        36      14  61.11%   75, 92-120
plugins/crunch/__init__.py                       0       0  100.00%
plugins/crunch/base.py                          10       1  90.00%   43
plugins/crunch/filter_date.py                   59      14  76.27%   52-57, 69, 78, 87, 90, 102-104, 113-115, 122
plugins/crunch/filter_latest_intersect.py       47      33  29.79%   49-54, 67-112
plugins/crunch/filter_latest_tpl_name.py        31       1  96.77%   83
plugins/crunch/filter_overlap.py                66      18  72.73%   28-30, 66-69, 76-79, 85, 93, 104-120
plugins/crunch/filter_property.py               30       5  83.33%   55-60, 63-64
plugins/download/__init__.py                     0       0  100.00%
plugins/download/aws.py                        528     158  70.08%   272, 285, 365-368, 382-386, 476-478, 482, 515-516, 522-526, 556, 589, 593, 600, 630-638, 642, 677-685, 696-698, 729-810, 828-886, 897-902, 955, 970-972, 975, 985-993, 1001-1015, 1025-1048, 1055-1068, 1106, 1132, 1177-1179, 1399
plugins/download/base.py                       253      44  82.61%   127, 155, 222-224, 235-238, 291-292, 340-344, 350-351, 393, 396-410, 422, 426, 490-494, 524-525, 550-558, 560-564, 607, 629, 651, 659
plugins/download/creodias_s3.py                 25       9  64.00%   55-69
plugins/download/http.py                       529      95  82.04%   227, 323-326, 329, 336-341, 359-374, 391, 403, 451, 458-464, 482, 496, 510, 518-520, 536-541, 552, 570, 606-609, 638, 642, 662, 739, 758-769, 777-782, 792-809, 827, 857-866, 902, 927-928, 947-952, 958, 961, 978, 981, 996-997, 1032, 1094, 1109, 1168-1169, 1175, 1185, 1221, 1257, 1277, 1312-1314
plugins/download/s3rest.py                     116      24  79.31%   119, 153, 160, 195, 222-229, 232-234, 238, 249-255, 263-264, 267-271, 294, 315-318
plugins/search/__init__.py                      22       0  100.00%
plugins/search/base.py                         145      11  92.41%   101, 105, 129-135, 276, 296, 429
plugins/search/build_search_result.py          352      37  89.49%   241-242, 278, 281, 412-423, 559, 561, 622, 629, 653, 688, 739, 754, 788-803, 848, 873, 876, 880, 1000-1001, 1007, 1018, 1097
plugins/search/cop_marine.py                   244      47  80.74%   56, 64-66, 76-77, 82, 87-88, 104, 106, 109, 175-176, 228, 234, 238, 242, 255, 266-267, 275, 303, 307, 322, 326, 330, 334, 338-342, 348-351, 354-368, 385, 434-438, 443, 455
plugins/search/creodias_s3.py                   25       1  96.00%   51
plugins/search/csw.py                          105      81  22.86%   98-99, 103-104, 112-160, 166-179, 187-219, 237-278
plugins/search/data_request_search.py          202      69  65.84%   189-192, 208, 219, 223-224, 235, 240, 245, 252, 265-268, 322-323, 327, 337-343, 348, 374-377, 385-396, 413, 415, 422-425, 427-428, 446-450, 483, 493, 504, 517, 523-538, 543
plugins/search/qssearch.py                     702      98  86.04%   450, 508, 522, 526-532, 560-564, 677-689, 734-737, 808-809, 857, 876, 883, 895, 952, 973, 976-977, 986-987, 996-997, 1006-1007, 1034, 1118-1123, 1127-1136, 1170, 1192, 1265-1266, 1272, 1362, 1459-1463, 1523, 1526, 1532-1533, 1554, 1582-1594, 1601, 1633-1635, 1645-1651, 1681, 1704, 1709-1710, 1725, 1741, 1813-1814, 1826-1829, 1834-1837, 1846, 1867-1869, 1878-1882, 1887-1897, 1919
plugins/search/stac_list_assets.py              25      10  60.00%   44-51, 75-85
plugins/search/static_stac_search.py            75      13  82.67%   98-124, 140, 153
rest/__init__.py                                 4       2  50.00%   21-22
rest/cache.py                                   33      22  33.33%   35-37, 44-70
rest/config.py                                  25       1  96.00%   35
rest/constants.py                                6       0  100.00%
rest/core.py                                   260     153  41.15%   157, 159, 161, 164-165, 179-189, 198-199, 205, 208, 249-303, 316-352, 383-420, 435-451, 467-476, 483-522, 539, 581-666, 705, 712-765
rest/errors.py                                  69      49  28.99%   60, 65-100, 105-108, 115-118, 126-147, 155-160, 175-181
rest/server.py                                 192     192  0.00%    18-592
rest/stac.py                                   319      68  78.68%   243, 309, 331, 383-386, 413-440, 471-473, 496, 528-529, 611-651, 673-689, 716, 781-785, 792, 846-847, 853, 908, 946, 979, 998-1000
rest/types/__init__.py                           0       0  100.00%
rest/types/collections_search.py                13      13  0.00%    18-44
rest/types/eodag_search.py                     180      20  88.89%   225-229, 262-264, 282, 285, 291, 295, 353, 371-386
rest/types/queryables.py                        64      20  68.75%   54-55, 60-63, 68-79, 105-110, 119-120, 174
rest/types/stac_search.py                      125      11  91.20%   126-128, 174, 189-191, 199, 203, 251, 254
rest/utils/__init__.py                          94      30  68.09%   72-78, 98, 101-102, 121-123, 136, 143, 168-176, 183-204
rest/utils/cql_evaluate.py                      48       5  89.58%   69, 76, 90, 97, 105
rest/utils/rfc3339.py                           23       5  78.26%   43-44, 48, 60, 62
types/__init__.py                              148      42  71.62%   67, 71, 80-84, 95-107, 135-137, 144-149, 204, 239, 249-265, 270, 272, 294, 299, 307, 317
types/bbox.py                                   43      19  55.81%   46-61, 72-74, 85-87, 99-101, 113-115, 123
types/download_args.py                           9       0  100.00%
types/queryables.py                            108       1  99.07%   63
types/search_args.py                            70      18  74.29%   60-64, 71-88, 103
types/whoosh.py                                 81      16  80.25%   129-132, 136-143, 155-161, 174-176
utils/__init__.py                              538      40  92.57%   81, 198-199, 208-235, 238, 253, 333-337, 410-414, 433-435, 449, 541, 556, 596-597, 626, 1001-1004, 1012-1013, 1051-1052, 1099-1100, 1234
utils/exceptions.py                             46       0  100.00%
utils/import_system.py                          28      19  32.14%   64-78, 89-99
utils/logging.py                                28       1  96.43%   41
utils/notebook.py                               44      23  47.73%   25-29, 36-41, 58-62, 72-78, 83-87
utils/repr.py                                   38       8  78.95%   51, 53, 57, 98, 122-129
utils/requests.py                               55      29  47.27%   51-52, 64, 85-96, 107-124, 128
utils/rest.py                                   36       1  97.22%   55
utils/s3.py                                     69       3  95.65%   162-163, 226
utils/stac_reader.py                           111      45  59.46%   56-57, 63-85, 95-97, 101, 137, 153-156, 203-212, 222-252
TOTAL                                        10191    2024  80.14%
```

</details>


### Diff against develop

```
Filename                           Stmts    Miss  Cover
-------------------------------  -------  ------  -------
config.py                             +1       0  +0.02%
plugins/authentication/token.py       +8       0  +1.19%
TOTAL                                 +9       0  +0.02%
```


Results for commit: 3d1d68713952ffa71f65367a8fe5e8b698905ed8


_Minimum allowed coverage is `70%`_


:recycle: This comment has been updated with latest results


<!-- Sticky Pull Request Commentartifacts/unit-test-results-python3.13-windows-latest/coverage.xml -->

Patch:
diff --git a/eodag/config.py b/eodag/config.py
index 581f217b..7d6e8199 100644
--- a/eodag/config.py
+++ b/eodag/config.py
@@ -615,6 +615,9 @@ class PluginConfig(yaml.YAMLObject):
     #: :class:`~eodag.plugins.authentication.token.TokenAuth`
     #: type of the token
     token_type: str
+    #: :class:`~eodag.plugins.authentication.token.TokenAuth`
+    #: key to get the expiration time of the token
+    token_expiration_key: str
     #: :class:`~eodag.plugins.authentication.token_exchange.OIDCTokenExchangeAuth`
     #: The full :class:`~eodag.plugins.authentication.openid_connect.OIDCAuthorizationCodeFlowAuth` plugin configuration
     #: used to retrieve subject token
diff --git a/eodag/plugins/authentication/token.py b/eodag/plugins/authentication/token.py
index 51d7068e..5295d8f7 100644
--- a/eodag/plugins/authentication/token.py
+++ b/eodag/plugins/authentication/token.py
@@ -18,6 +18,7 @@
 from __future__ import annotations
 
 import logging
+from datetime import datetime, timedelta
 from typing import TYPE_CHECKING, Any, Optional
 from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
 
@@ -72,6 +73,8 @@ class TokenAuth(Authentication):
           key to get the access token in the response to the token request
         * :attr:`~eodag.config.PluginConfig.refresh_token_key` (``str``): key to get the refresh
           token in the response to the token request
+        * :attr:`~eodag.config.PluginConfig.token_expiration_key` (``str``): key to get expiration time of
+          the token (given in s)
         * :attr:`~eodag.config.PluginConfig.ssl_verify` (``bool``): if the ssl certificates
           should be verified in the requests; default: ``True``
         * :attr:`~eodag.config.PluginConfig.auth_error_code` (``int``): which error code is
@@ -91,6 +94,7 @@ class TokenAuth(Authentication):
         super(TokenAuth, self).__init__(provider, config)
         self.token = ""
         self.refresh_token = ""
+        self.token_expiration = datetime.now()
 
     def validate_config_credentials(self) -> None:
         """Validate configured credentials"""
@@ -131,7 +135,11 @@ class TokenAuth(Authentication):
     def authenticate(self) -> AuthBase:
         """Authenticate"""
         self.validate_config_credentials()
-
+        if self.token and self.token_expiration > datetime.now():
+            logger.debug("using existing access token")
+            return RequestsTokenAuth(
+                self.token, "header", headers=getattr(self.config, "headers", {})
+            )
         s = requests.Session()
         try:
             # First get the token
@@ -168,6 +176,12 @@ class TokenAuth(Authentication):
             self.token = token
             if getattr(self.config, "refresh_token_key", None):
                 self.refresh_token = response.json()[self.config.refresh_token_key]
+            if getattr(self.config, "token_expiration_key", None):
+                expiration_time = response.json()[self.config.token_expiration_key]
+                self.token_expiration = datetime.now() + timedelta(
+                    seconds=expiration_time
+                )
+
             if not hasattr(self.config, "headers"):
                 raise MisconfiguredError(f"Missing headers configuration for {self}")
             # Return auth class set with obtained token
@@ -179,6 +193,7 @@ class TokenAuth(Authentication):
         self,
         session: requests.Session,
     ) -> requests.Response:
+
         retry_total = getattr(self.config, "retry_total", REQ_RETRY_TOTAL)
         retry_backoff_factor = getattr(
             self.config, "retry_backoff_factor", REQ_RETRY_BACKOFF_FACTOR
diff --git a/eodag/plugins/search/build_search_result.py b/eodag/plugins/search/build_search_result.py
index adf60d55..b7f5c993 100644
--- a/eodag/plugins/search/build_search_result.py
+++ b/eodag/plugins/search/build_search_result.py
@@ -22,7 +22,7 @@ import hashlib
 import logging
 import re
 from collections import OrderedDict
-from datetime import datetime, timedelta
+from datetime import date, datetime, timedelta, timezone
 from typing import TYPE_CHECKING, Annotated, Any, Optional, Union
 from urllib.parse import quote_plus, unquote_plus
 
@@ -286,6 +286,125 @@ def ecmwf_format(v: str) -> str:
     return ECMWF_PREFIX + v if v in ALLOWED_KEYWORDS else v
 
 
+def get_min_max(
+    value: Optional[Union[str, list[str]]] = None,
+) -> tuple[Optional[str], Optional[str]]:
+    """Returns the min and max from a list of strings or the same string if a single string is given."""
+    if isinstance(value, list):
+        sorted_values = sorted(value)
+        return sorted_values[0], sorted_values[-1]
+    return value, value
+
+
+def append_time(input_date: date, time: Optional[str]) -> datetime:
+    """
+    Parses a time string in format HHMM and appends it to a date.
+
+    if the time string is in format HH:MM we convert it to HHMM
+    """
+    if not time:
+        time = "0000"
+    time = time.replace(":", "")
+    if time == "2400":
+        time = "0000"
+    dt = datetime.combine(input_date, datetime.strptime(time, "%H%M").time())
+    dt.replace(tzinfo=timezone.utc)
+    return dt
+
+
+def parse_date(
+    date_str: str, time: Optional[Union[str, list[str]]]
+) -> tuple[datetime, datetime]:
+    """Parses a date string in format YYYY-MM-DD or YYYY-MM-DD/YYYY-MM-DD or YYYY-MM-DD/to/YYYY-MM-DD."""
+    if "to" in date_str:
+        start_date_str, end_date_str = date_str.split("/to/")
+    elif "/" in date_str:
+        dates = date_str.split("/")
+        start_date_str = dates[0]
+        end_date_str = dates[-1]
+    else:
+        start_date_str = end_date_str = date_str
+
+    start_date = datetime.fromisoformat(start_date_str.rstrip("Z"))
+    end_date = datetime.fromisoformat(end_date_str.rstrip("Z"))
+
+    if time:
+        start_t, end_t = get_min_max(time)
+        start_date = append_time(start_date.date(), start_t)
+        end_date = append_time(end_date.date(), end_t)
+
+    return start_date, end_date
+
+
+def parse_year_month_day(
+    year: Union[str, list[str]],
+    month: Optional[Union[str, list[str]]] = None,
+    day: Optional[Union[str, list[str]]] = None,
+    time: Optional[Union[str, list[str]]] = None,
+) -> tuple[datetime, datetime]:
+    """Extracts and returns the year, month, day, and time from the parameters."""
+
+    def build_date(year, month=None, day=None, time=None) -> datetime:
+        """Datetime from default_date with updated year, month, day and time."""
+        updated_date = datetime(int(year), 1, 1).replace(
+            month=int(month) if month is not None else 1,
+            day=int(day) if day is not None else 1,
+        )
+        if time is not None:
+            updated_date = append_time(updated_date.date(), time)
+        return updated_date
+
+    start_y, end_y = get_min_max(year)
+    start_m, end_m = get_min_max(month)
+    start_d, end_d = get_min_max(day)
+    start_t, end_t = get_min_max(time)
+
+    start_date = build_date(start_y, start_m, start_d, start_t)
+    end_date = build_date(end_y, end_m, end_d, end_t)
+
+    return start_date, end_date
+
+
+def ecmwf_temporal_to_eodag(
+    params: dict[str, Any]
+) -> tuple[Optional[str], Optional[str]]:
+    """
+    Converts ECMWF temporal parameters to EODAG temporal parameters.
+
+    ECMWF temporal parameters:
+        - **year** or **hyear**: Union[str, list[str]] — Year(s) as a string or list of strings.
+        - **month** or **hmonth**: Union[str, list[str]] — Month(s) as a string or list of strings.
+        - **day** or **hday**: Union[str, list[str]] — Day(s) as a string or list of strings.
+        - **time**: str — A string representing the time in the format `HHMM` (e.g., `0200`, `0800`, `1400`).
+        - **date**: str — A string in one of the formats:
+            - `YYYY-MM-DD`
+            - `YYYY-MM-DD/YYYY-MM-DD`
+            - `YYYY-MM-DD/to/YYYY-MM-DD`
+
+    :param params: Dictionary containing ECMWF temporal parameters.
+    :return: A tuple with:
+        - **start**: A string in the format `YYYY-MM-DDTHH:MM:SSZ`.
+        - **end**: A string in the format `YYYY-MM-DDTHH:MM:SSZ`.
+    """
+    start = end = None
+
+    if date := params.get("date"):
+        start, end = parse_date(date, params.get("time"))
+
+    elif year := params.get("year") or params.get("hyear"):
+        year = params.get("year") or params.get("hyear")
+        month = params.get("month") or params.get("hmonth")
+        day = params.get("day") or params.get("hday")
+        time = params.get("time")
+
+        start, end = parse_year_month_day(year, month, day, time)
+
+    if start and end:
+        return start.strftime("%Y-%m-%dT%H:%M:%SZ"), end.strftime("%Y-%m-%dT%H:%M:%SZ")
+    else:
+        return None, None
+
+
 class ECMWFSearch(PostJsonSearch):
     """ECMWF search plugin.
 
@@ -507,12 +626,8 @@ class ECMWFSearch(PostJsonSearch):
                 ]
                 # if startTime is not given but other time params (e.g. year/month/(day)) are given,
                 # no default date is required
-                in_keywords = True
-                for tp in time_params:
-                    if tp not in keywords:
-                        in_keywords = False
-                        break
-                if not in_keywords:
+                start, end = ecmwf_temporal_to_eodag(keywords)
+                if start is None:
                     keywords[START] = self.get_product_type_cfg_value(
                         "missionStartDate", DEFAULT_MISSION_START_DATE
                     )
@@ -523,6 +638,9 @@ class ECMWFSearch(PostJsonSearch):
                             "missionEndDate", today().isoformat()
                         )
                     )
+                else:
+                    keywords[START] = start
+                    keywords[END] = end
 
     def _get_product_type_queryables(
         self, product_type: Optional[str], alias: Optional[str], filters: dict[str, Any]
diff --git a/eodag/resources/providers.yml b/eodag/resources/providers.yml
index f6e252fb..4df7e10e 100644
--- a/eodag/resources/providers.yml
+++ b/eodag/resources/providers.yml
@@ -3442,6 +3442,7 @@
     token_type: json
     token_key: access_token
     refresh_token_key: refresh_token
+    token_expiration_key: expires_in
   download: !plugin
     type: HTTPDownload
     base_uri: https://gateway.prod.wekeo2.eu/hda-broker/api/v1/dataaccess
@@ -3913,6 +3914,7 @@
     token_type: json
     token_key: access_token
     refresh_token_key: refresh_token
+    token_expiration_key: expires_in
   download: !plugin
     type: HTTPDownload
     auth_error_code: 401
@@ -5503,6 +5505,7 @@
       grant_type: client_credentials
     token_type: json
     token_key: access_token
+    token_expiration_key: expires_in
     ssl_verify: true
 
 ---
@@ -5735,6 +5738,13 @@
       cloudCover:
       - '{{"query":{{"eo:cloud_cover":{{"lte":{cloudCover}}}}}}}'
       - '$.properties."eo:cloud_cover"'
+      # OpenSearch Parameters for Acquistion Parameters Search (Table 6)
+      startTimeFromAscendingNode:
+        - '{{"query":{{"end_datetime":{{"gte":"{startTimeFromAscendingNode#to_iso_utc_datetime}"}}}}}}'
+        - '$.properties.start_datetime'
+      completionTimeFromAscendingNode:
+        - '{{"query":{{"start_datetime":{{"lte":"{completionTimeFromAscendingNode#to_iso_utc_datetime}"}}}}}}'
+        - '$.properties.end_datetime'
       swathIdentifier:
         - '{{"query":{{"swath":{{"eq":"{swathIdentifier}"}}}}}}'
         - '$.properties.swath'
@@ -5851,6 +5861,13 @@
       cloudCover:
       - '{{"query":{{"eo:cloud_cover":{{"lte":{cloudCover}}}}}}}'
       - '$.properties."eo:cloud_cover"'
+      # OpenSearch Parameters for Acquistion Parameters Search (Table 6)
+      startTimeFromAscendingNode:
+        - '{{"query":{{"end_datetime":{{"gte":"{startTimeFromAscendingNode#to_iso_utc_datetime}"}}}}}}'
+        - '$.properties.start_datetime'
+      completionTimeFromAscendingNode:
+        - '{{"query":{{"start_datetime":{{"lte":"{completionTimeFromAscendingNode#to_iso_utc_datetime}"}}}}}}'
+        - '$.properties.end_datetime'
       swathIdentifier:
         - '{{"query":{{"swath":{{"eq":"{swathIdentifier}"}}}}}}'
         - '$.properties.swath'


Test patch:
diff --git a/tests/units/test_auth_plugins.py b/tests/units/test_auth_plugins.py
index a0c87d75..c12e23ab 100644
--- a/tests/units/test_auth_plugins.py
+++ b/tests/units/test_auth_plugins.py
@@ -17,7 +17,7 @@
 # limitations under the License.
 
 import unittest
-from datetime import timedelta
+from datetime import datetime, timedelta
 from unittest import mock
 
 import responses
@@ -113,6 +113,16 @@ class TestAuthPluginTokenAuth(BaseAuthPluginTest):
                         "token_key": "token_is_here",
                     },
                 },
+                "provider_json_token_with_expiration": {
+                    "products": {"foo_product": {}},
+                    "auth": {
+                        "type": "TokenAuth",
+                        "auth_uri": "http://foo.bar",
+                        "token_type": "json",
+                        "token_key": "token_is_here",
+                        "token_expiration_key": "token_expiration",
+                    },
+                },
                 "provider_text_token_req_data": {
                     "products": {"foo_product": {}},
                     "auth": {
@@ -250,7 +260,7 @@ class TestAuthPluginTokenAuth(BaseAuthPluginTest):
     )
     def test_plugins_auth_tokenauth_json_token_authenticate(self, mock_requests_post):
         """TokenAuth.authenticate must return a RequestsTokenAuth object using json token"""
-        auth_plugin = self.get_auth_plugin("provider_json_token_simple_url")
+        auth_plugin = self.get_auth_plugin("provider_json_token_with_expiration")
 
         auth_plugin.config.credentials = {"foo": "bar", "baz": "qux"}
 
@@ -259,6 +269,7 @@ class TestAuthPluginTokenAuth(BaseAuthPluginTest):
         mock_requests_post.return_value.json.return_value = {
             "not_token": "this_is_not_test_token",
             "token_is_here": "this_is_test_token",
+            "token_expiration": 1000,
         }
 
         # check if returned auth object is an instance of requests.AuthBase
@@ -270,6 +281,71 @@ class TestAuthPluginTokenAuth(BaseAuthPluginTest):
         auth(req)
         assert req.headers["Authorization"] == "Bearer this_is_test_token"
 
+    @mock.patch(
+        "eodag.plugins.authentication.token.requests.Session.request", autospec=True
+    )
+    def test_plugins_auth_tokenauth_json_token_with_expiration_time(
+        self, mock_requests_post
+    ):
+        """TokenAuth.authenticate must return a RequestsTokenAuth object using json token
+        If there is an existing, valid token this token msut be used
+        """
+        auth_plugin = self.get_auth_plugin("provider_json_token_with_expiration")
+        auth_plugin.__init__(
+            auth_plugin.provider, auth_plugin.config
+        )  # ensure that variables are reset
+
+        auth_plugin.config.credentials = {"foo": "bar", "baz": "qux"}
+
+        # mock token post request response
+        mock_requests_post.return_value = mock.Mock()
+        mock_requests_post.return_value.json.return_value = {
+            "not_token": "this_is_not_test_token",
+            "token_is_here": "this_is_test_token",
+            "token_expiration": 1000,
+        }
+
+        # check if returned auth object is an instance of requests.AuthBase
+        auth = auth_plugin.authenticate()
+        self.assertTrue(isinstance(auth, AuthBase))
+        self.assertEqual("this_is_test_token", auth_plugin.token)
+        # token request should be sent
+        mock_requests_post.assert_called_once_with(
+            mock.ANY,
+            method="POST",
+            url=auth_plugin.config.auth_uri,
+            timeout=HTTP_REQ_TIMEOUT,
+            headers=USER_AGENT,
+            data=auth_plugin.config.credentials,
+            verify=True,
+            auth=None,
+        )
+        mock_requests_post.reset_mock()
+        # second call should use existing token
+        auth = auth_plugin.authenticate()
+        self.assertTrue(isinstance(auth, AuthBase))
+        mock_requests_post.assert_not_called()
+        # reset expiration -> token should be fetched again
+        auth_plugin.token_expiration = datetime.now()
+        mock_requests_post.return_value.json.return_value = {
+            "not_token": "this_is_not_test_token",
+            "token_is_here": "this_is_a_new_token",
+            "token_expiration": 1000,
+        }
+        auth = auth_plugin.authenticate()
+        self.assertTrue(isinstance(auth, AuthBase))
+        self.assertEqual("this_is_a_new_token", auth_plugin.token)
+        mock_requests_post.assert_called_once_with(
+            mock.ANY,
+            method="POST",
+            url=auth_plugin.config.auth_uri,
+            timeout=HTTP_REQ_TIMEOUT,
+            headers=USER_AGENT,
+            data=auth_plugin.config.credentials,
+            verify=True,
+            auth=None,
+        )
+
     @mock.patch(
         "eodag.plugins.authentication.token.requests.Session.request", autospec=True
     )