-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
119 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
# SPDX-License-Identifier: BSD-3-Clause | ||
# Copyright (c) 2024 SciCat Project (https://github.com/SciCatProject/scitacean) | ||
"""Tools for JSON web tokens.""" | ||
|
||
import base64 | ||
import json | ||
from datetime import datetime, timezone | ||
from typing import cast | ||
|
||
|
||
def decode(token: str) -> tuple[dict[str, str | int], dict[str, str | int], str]: | ||
"""Decode the components of a JSOn web token.""" | ||
h, p, signature = token.split(".") | ||
header = _decode_part(h) | ||
payload = _decode_part(p) | ||
return header, payload, signature | ||
|
||
|
||
def expiry(token: str) -> datetime: | ||
"""Return the expiration time of a JWT in UTC.""" | ||
_, payload, _ = decode(token) | ||
# 'exp' should always be given in UTC. Since we have no way of checking that, | ||
# assume that it is the case. | ||
return datetime.fromtimestamp(float(payload["exp"]), tz=timezone.utc) | ||
|
||
|
||
def _decode_part(s: str) -> dict[str, str | int]: | ||
# urlsafe_b64decode requires a properly padded input but SciCat | ||
# doesn't pad its tokens. | ||
padded = s + "=" * (len(s) % 4) | ||
decoded_str = base64.urlsafe_b64decode(padded).decode("utf-8") | ||
return cast(dict[str, str | int], json.loads(decoded_str)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,17 @@ | ||
# SPDX-License-Identifier: BSD-3-Clause | ||
# Copyright (c) 2024 SciCat Project (https://github.com/SciCatProject/scitacean) | ||
|
||
import base64 | ||
import json | ||
import pickle | ||
import time | ||
from datetime import datetime, timedelta, timezone | ||
from typing import Any | ||
|
||
import pytest | ||
|
||
from scitacean import PID, Client | ||
from scitacean.testing.backend.seed import INITIAL_DATASETS | ||
from scitacean.testing.client import FakeClient | ||
from scitacean.util.credentials import SecretStr | ||
|
||
|
@@ -29,9 +35,7 @@ def test_from_credentials_fake(): | |
) | ||
|
||
|
||
def test_from_credentials_real(scicat_access, scicat_backend): | ||
if not scicat_backend: | ||
pytest.skip("No backend") | ||
def test_from_credentials_real(scicat_access, require_scicat_backend): | ||
Client.from_credentials(url=scicat_access.url, **scicat_access.user.credentials) | ||
|
||
|
||
|
@@ -80,3 +84,46 @@ def test_fake_can_disable_functions(): | |
client.scicat.get_dataset_model(PID(pid="some-pid")) | ||
with pytest.raises(IndexError, match="custom index error"): | ||
client.scicat.get_orig_datablocks(PID(pid="some-pid")) | ||
|
||
|
||
def encode_jwt_part(part: dict[str, Any]) -> str: | ||
return base64.urlsafe_b64encode(json.dumps(part).encode("utf-8")).decode("ascii") | ||
|
||
|
||
def make_token(exp_in: timedelta) -> str: | ||
now = datetime.now(tz=timezone.utc) | ||
exp = now + exp_in | ||
|
||
# This is what a SciCat token looks like as of 2024-04-19 | ||
header = {"alg": "HS256", "typ": "JWT"} | ||
payload = { | ||
"_id": "7fc0856e50a8", | ||
"username": "Weatherwax", | ||
"email": "[email protected]", | ||
"authStrategy": "ldap", | ||
"id": "7fc0856e50a8", | ||
"userId": "7fc0856e50a8", | ||
"iat": now.timestamp(), | ||
"exp": exp.timestamp(), | ||
} | ||
# Scitacean never validates the signature because it doesn't have the secret key, | ||
# so it doesn't matter what we use here. | ||
signature = "123abc" | ||
|
||
return ".".join((encode_jwt_part(header), encode_jwt_part(payload), signature)) | ||
|
||
|
||
def test_detects_expired_token_init(): | ||
token = make_token(timedelta(milliseconds=0)) | ||
with pytest.raises(RuntimeError, match="SciCat login has expired"): | ||
Client.from_token(url="scicat.com", token=token) | ||
|
||
|
||
def test_detects_expired_token_get_dataset(scicat_access, require_scicat_backend): | ||
# The token is invalid, but the expiration should be detected before | ||
# even sending it to SciCat. | ||
token = make_token(timedelta(milliseconds=2100)) # > than denial period = 2s | ||
client = Client.from_token(url=scicat_access.url, token=token) | ||
time.sleep(0.5) | ||
with pytest.raises(RuntimeError, match="SciCat login has expired"): | ||
client.get_dataset(INITIAL_DATASETS["public"].pid) # type: ignore[arg-type] |