Skip to content

Commit 99e9e09

Browse files
committed
Added tests to ensure that the JWT token based authentication is working as expected
1 parent 79ba19b commit 99e9e09

11 files changed

+464
-388
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,4 @@ jobs:
3535
env:
3636
BASE_URL: ${{ secrets.BASE_URL }}
3737
AUTH_TOKEN: ${{ secrets.AUTH_TOKEN }}
38+
JWT_KEY: ${{ secrets.JWT_KEY }}

spec/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import pytest
22
from dotenv import load_dotenv
33

4+
45
@pytest.fixture(scope="session", autouse=True)
56
def load_env():
67
"""Load the .env file for the entire test session."""

spec/sdk_test_using_personal_access_token_authentication_spec.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,31 @@
1-
import pytest
2-
import uuid
31
import os
2+
import uuid
3+
4+
import pytest
5+
46
import zitadel_client as zitadel
57
from zitadel_client.auth.personal_access_token_authenticator import PersonalAccessTokenAuthenticator
68
from zitadel_client.exceptions import UnauthorizedException
79

10+
811
@pytest.fixture
912
def valid_token():
1013
"""Fixture to return a valid personal access token."""
1114
return os.getenv("AUTH_TOKEN")
1215

16+
1317
@pytest.fixture
1418
def invalid_token():
1519
"""Fixture to return an invalid token."""
1620
return "whoops"
1721

22+
1823
@pytest.fixture
1924
def base_url():
2025
"""Fixture to return the base URL."""
2126
return os.getenv("BASE_URL")
2227

28+
2329
@pytest.fixture
2430
def user_id(valid_token, base_url):
2531
"""Fixture to create a user and return their ID."""
@@ -37,6 +43,7 @@ def user_id(valid_token, base_url):
3743
except Exception as e:
3844
pytest.fail(f"Exception while creating user: {e}")
3945

46+
4047
def test_should_deactivate_and_reactivate_user_with_valid_token(user_id, valid_token, base_url):
4148
"""Test to (de)activate the user with a valid token."""
4249
with zitadel.Zitadel(PersonalAccessTokenAuthenticator(base_url, valid_token)) as client:
@@ -51,6 +58,7 @@ def test_should_deactivate_and_reactivate_user_with_valid_token(user_id, valid_t
5158
except Exception as e:
5259
pytest.fail(f"Exception when calling deactivate_user or reactivate_user with valid token: {e}")
5360

61+
5462
def test_should_not_deactivate_or_reactivate_user_with_invalid_token(user_id, invalid_token, base_url):
5563
"""Test to attempt (de)activating the user with an invalid token."""
5664
with zitadel.Zitadel(PersonalAccessTokenAuthenticator(base_url, invalid_token)) as client:
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import os
2+
import tempfile
3+
import uuid
4+
5+
import pytest
6+
7+
import zitadel_client as zitadel
8+
from zitadel_client.auth.web_token_authenticator import WebTokenAuthenticator
9+
10+
11+
@pytest.fixture
12+
def key_file():
13+
jwt_key = os.getenv("JWT_KEY")
14+
if jwt_key is None:
15+
pytest.fail("JWT_KEY is not set in the environment")
16+
with tempfile.NamedTemporaryFile(delete=False, mode="w") as tf:
17+
tf.write(jwt_key)
18+
return tf.name
19+
20+
21+
@pytest.fixture
22+
def base_url():
23+
"""Fixture to return the base URL."""
24+
return os.getenv("BASE_URL")
25+
26+
27+
@pytest.fixture
28+
def user_id(key_file, base_url):
29+
"""Fixture to create a user and return their ID."""
30+
with zitadel.Zitadel(WebTokenAuthenticator.from_json(base_url, key_file)) as client:
31+
try:
32+
response = client.users.add_human_user(
33+
body=zitadel.models.V2AddHumanUserRequest(
34+
username=uuid.uuid4().hex,
35+
profile=zitadel.models.V2SetHumanProfile(given_name="John", family_name="Doe"),
36+
email=zitadel.models.V2SetHumanEmail(email=f"johndoe{uuid.uuid4().hex}@caos.ag")
37+
)
38+
)
39+
print("User created:", response)
40+
return response.user_id
41+
except Exception as e:
42+
pytest.fail(f"Exception while creating user: {e}")
43+
44+
45+
def test_should_deactivate_and_reactivate_user_with_valid_token(user_id, key_file, base_url):
46+
"""Test to (de)activate the user with a valid token."""
47+
with zitadel.Zitadel(WebTokenAuthenticator.from_json(base_url, key_file)) as client:
48+
try:
49+
deactivate_response = client.users.deactivate_user(user_id=user_id)
50+
print("User deactivated:", deactivate_response)
51+
52+
reactivate_response = client.users.reactivate_user(user_id=user_id)
53+
print("User reactivated:", reactivate_response)
54+
# Adjust based on actual response format
55+
# assert reactivate_response["status"] == "success"
56+
except Exception as e:
57+
pytest.fail(f"Exception when calling deactivate_user or reactivate_user with valid token: {e}")

zitadel_client/auth/authenticator.py

Lines changed: 69 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -6,95 +6,95 @@
66

77

88
class Authenticator(ABC):
9-
"""
10-
Abstract base class for authenticators.
11-
12-
This class defines the basic structure for any authenticator by requiring the implementation
13-
of a method to retrieve authentication headers, and provides a way to store and retrieve the host.
14-
"""
15-
16-
def __init__(self, host: str):
179
"""
18-
Initializes the Authenticator with the specified host.
10+
Abstract base class for authenticators.
1911
20-
:param host: The base URL or endpoint for the service.
12+
This class defines the basic structure for any authenticator by requiring the implementation
13+
of a method to retrieve authentication headers, and provides a way to store and retrieve the host.
2114
"""
22-
self.host = host
23-
24-
@abstractmethod
25-
def get_auth_headers(self) -> Dict[str, str]:
26-
"""
27-
Retrieves the authentication headers to be sent with requests.
28-
29-
Subclasses must override this method to return the appropriate headers.
3015

31-
:return: A dictionary mapping header names to their values.
32-
"""
33-
pass
16+
def __init__(self, host: str):
17+
"""
18+
Initializes the Authenticator with the specified host.
3419
35-
def get_host(self) -> str:
36-
"""
37-
Returns the stored host.
20+
:param host: The base URL or endpoint for the service.
21+
"""
22+
self.host = host
3823

39-
:return: The host as a string.
40-
"""
41-
return self.host
24+
@abstractmethod
25+
def get_auth_headers(self) -> Dict[str, str]:
26+
"""
27+
Retrieves the authentication headers to be sent with requests.
4228
29+
Subclasses must override this method to return the appropriate headers.
4330
44-
class Token:
45-
def __init__(self, access_token: str, expires_at: datetime):
46-
"""
47-
Initializes a new Token instance.
31+
:return: A dictionary mapping header names to their values.
32+
"""
33+
pass
4834

49-
Parameters:
50-
- access_token (str): The JWT or OAuth token.
51-
- expires_at (datetime): The expiration time of the token. It should be timezone-aware.
52-
If a naive datetime is provided, it will be converted to an aware datetime in UTC.
53-
"""
54-
self.access_token = access_token
35+
def get_host(self) -> str:
36+
"""
37+
Returns the stored host.
5538
56-
# Ensure expires_at is timezone-aware. If naive, assume UTC.
57-
if expires_at.tzinfo is None:
58-
self.expires_at = expires_at.replace(tzinfo=timezone.utc)
59-
else:
60-
self.expires_at = expires_at
39+
:return: The host as a string.
40+
"""
41+
return self.host
6142

62-
def is_expired(self) -> bool:
63-
"""
64-
Checks if the token is expired by comparing the current UTC time
65-
with the token's expiration time.
6643

67-
Returns:
68-
- bool: True if expired, False otherwise.
69-
"""
70-
return datetime.now(timezone.utc) >= self.expires_at
44+
class Token:
45+
def __init__(self, access_token: str, expires_at: datetime):
46+
"""
47+
Initializes a new Token instance.
48+
49+
Parameters:
50+
- access_token (str): The JWT or OAuth token.
51+
- expires_at (datetime): The expiration time of the token. It should be timezone-aware.
52+
If a naive datetime is provided, it will be converted to an aware datetime in UTC.
53+
"""
54+
self.access_token = access_token
55+
56+
# Ensure expires_at is timezone-aware. If naive, assume UTC.
57+
if expires_at.tzinfo is None:
58+
self.expires_at = expires_at.replace(tzinfo=timezone.utc)
59+
else:
60+
self.expires_at = expires_at
61+
62+
def is_expired(self) -> bool:
63+
"""
64+
Checks if the token is expired by comparing the current UTC time
65+
with the token's expiration time.
66+
67+
Returns:
68+
- bool: True if expired, False otherwise.
69+
"""
70+
return datetime.now(timezone.utc) >= self.expires_at
7171

7272

7373
T = TypeVar("T", bound="OAuthAuthenticatorBuilder")
7474

7575

7676
class OAuthAuthenticatorBuilder(ABC, Generic[T]):
77-
"""
78-
Abstract builder class for constructing OAuth authenticator instances.
79-
80-
This builder provides common configuration options such as the OpenId instance and authentication scopes.
81-
"""
82-
83-
def __init__(self, host: str):
8477
"""
85-
Initializes the OAuthAuthenticatorBuilder with a given host.
78+
Abstract builder class for constructing OAuth authenticator instances.
8679
87-
:param host: The base URL for the OAuth provider.
80+
This builder provides common configuration options such as the OpenId instance and authentication scopes.
8881
"""
89-
self.open_id = OpenId(host)
90-
self.auth_scopes = {"openid", "urn:zitadel:iam:org:project:id:zitadel:aud"}
9182

92-
def scopes(self: T, *auth_scopes: str) -> T:
93-
"""
94-
Sets the authentication scopes for the OAuth authenticator.
83+
def __init__(self, host: str):
84+
"""
85+
Initializes the OAuthAuthenticatorBuilder with a given host.
9586
96-
:param auth_scopes: A variable number of scope strings.
97-
:return: The builder instance to allow for method chaining.
98-
"""
99-
self.auth_scopes = set(auth_scopes)
100-
return self
87+
:param host: The base URL for the OAuth provider.
88+
"""
89+
self.open_id = OpenId(host)
90+
self.auth_scopes = {"openid", "urn:zitadel:iam:org:project:id:zitadel:aud"}
91+
92+
def scopes(self: T, *auth_scopes: str) -> T:
93+
"""
94+
Sets the authentication scopes for the OAuth authenticator.
95+
96+
:param auth_scopes: A variable number of scope strings.
97+
:return: The builder instance to allow for method chaining.
98+
"""
99+
self.auth_scopes = set(auth_scopes)
100+
return self

0 commit comments

Comments
 (0)