|
1 | | -""" |
2 | | -Token service for SSO authentication. |
3 | | -
|
4 | | -This module provides secure token generation, hashing, and verification |
5 | | -using Argon2id with 2025-recommended security parameters. |
6 | | -""" |
7 | | - |
8 | | -import base64 |
9 | | -import secrets |
10 | | - |
11 | | -import pydantic |
12 | | -from argon2 import PasswordHasher |
13 | | -from argon2.exceptions import InvalidHashError, VerificationError, VerifyMismatchError |
14 | | - |
15 | | -from src.core.auth.sso.exceptions import TokenError |
16 | | - |
17 | | - |
18 | | -class GeneratedToken(pydantic.BaseModel): |
19 | | - """Result of token generation. |
20 | | -
|
21 | | - Contains both the plaintext token (for immediate use) and its hash |
22 | | - (for secure storage and verification). |
23 | | -
|
24 | | - Attributes: |
25 | | - plaintext: Base64url-encoded token (43+ characters) with 256-bit entropy |
26 | | - hash: Argon2id hash of the token for secure storage |
27 | | - """ |
28 | | - |
29 | | - plaintext: str |
30 | | - hash: str |
31 | | - |
32 | | - model_config = {"frozen": True} |
| 1 | +""" |
| 2 | +Token service for SSO authentication. |
| 3 | +
|
| 4 | +This module provides secure token generation, hashing, and verification |
| 5 | +using Argon2id with 2025-recommended security parameters. |
| 6 | +""" |
| 7 | + |
| 8 | +import base64 |
| 9 | +import secrets |
| 10 | + |
| 11 | +import pydantic |
| 12 | +from argon2 import PasswordHasher |
| 13 | +from argon2.exceptions import InvalidHashError, VerificationError, VerifyMismatchError |
| 14 | + |
| 15 | +from src.core.auth.sso.exceptions import TokenError |
| 16 | + |
| 17 | + |
| 18 | +class GeneratedToken(pydantic.BaseModel): |
| 19 | + """Result of token generation. |
| 20 | +
|
| 21 | + Contains both the plaintext token (for immediate use) and its hash |
| 22 | + (for secure storage and verification). |
| 23 | +
|
| 24 | + Attributes: |
| 25 | + plaintext: Base64url-encoded token (43+ characters) with 256-bit entropy |
| 26 | + hash: Argon2id hash of the token for secure storage |
| 27 | + """ |
| 28 | + |
| 29 | + plaintext: str |
| 30 | + hash: str |
| 31 | + |
| 32 | + model_config = {"frozen": True} |
| 33 | + |
| 34 | + def __iter__(self): # type: ignore[override] |
| 35 | + """Allow tuple unpacking for backward compatibility.""" |
| 36 | + yield self.plaintext |
| 37 | + yield self.hash |
33 | 38 |
|
34 | 39 |
|
35 | 40 | class TokenService: |
@@ -99,41 +104,41 @@ def __init__( |
99 | 104 | salt_len=16, # Always 16 bytes salt |
100 | 105 | ) |
101 | 106 |
|
102 | | - def generate_token(self) -> GeneratedToken: |
103 | | - """ |
104 | | - Generate a new agent token with 256-bit entropy. |
105 | | -
|
106 | | - The token is generated using cryptographically secure random bytes |
107 | | - and encoded as base64url for Bearer token compatibility. |
108 | | -
|
109 | | - Returns: |
110 | | - GeneratedToken: Object containing both plaintext token and its hash |
111 | | - - plaintext: Base64url-encoded token (43+ characters) |
112 | | - - hash: Argon2id hash of token |
113 | | -
|
114 | | - Raises: |
115 | | - TokenError: If token generation or hashing fails |
116 | | - """ |
117 | | - try: |
118 | | - # Generate 256 bits (32 bytes) of cryptographically secure random data |
119 | | - token_bytes = secrets.token_bytes(32) |
120 | | - |
121 | | - # Encode as base64url (URL-safe, no padding) |
122 | | - plaintext_token = ( |
123 | | - base64.urlsafe_b64encode(token_bytes).decode("ascii").rstrip("=") |
124 | | - ) |
125 | | - |
126 | | - # Hash the token using Argon2id |
127 | | - token_hash = self.hash_token(plaintext_token) |
128 | | - |
129 | | - return GeneratedToken(plaintext=plaintext_token, hash=token_hash) |
130 | | - |
131 | | - except Exception as e: |
132 | | - raise TokenError( |
133 | | - "Failed to generate token", |
134 | | - details={"error": str(e)}, |
135 | | - original_error=e, |
136 | | - ) from e |
| 107 | + def generate_token(self) -> GeneratedToken: |
| 108 | + """ |
| 109 | + Generate a new agent token with 256-bit entropy. |
| 110 | +
|
| 111 | + The token is generated using cryptographically secure random bytes |
| 112 | + and encoded as base64url for Bearer token compatibility. |
| 113 | +
|
| 114 | + Returns: |
| 115 | + GeneratedToken: Object containing both plaintext token and its hash |
| 116 | + - plaintext: Base64url-encoded token (43+ characters) |
| 117 | + - hash: Argon2id hash of token |
| 118 | +
|
| 119 | + Raises: |
| 120 | + TokenError: If token generation or hashing fails |
| 121 | + """ |
| 122 | + try: |
| 123 | + # Generate 256 bits (32 bytes) of cryptographically secure random data |
| 124 | + token_bytes = secrets.token_bytes(32) |
| 125 | + |
| 126 | + # Encode as base64url (URL-safe, no padding) |
| 127 | + plaintext_token = ( |
| 128 | + base64.urlsafe_b64encode(token_bytes).decode("ascii").rstrip("=") |
| 129 | + ) |
| 130 | + |
| 131 | + # Hash the token using Argon2id |
| 132 | + token_hash = self.hash_token(plaintext_token) |
| 133 | + |
| 134 | + return GeneratedToken(plaintext=plaintext_token, hash=token_hash) |
| 135 | + |
| 136 | + except Exception as e: |
| 137 | + raise TokenError( |
| 138 | + "Failed to generate token", |
| 139 | + details={"error": str(e)}, |
| 140 | + original_error=e, |
| 141 | + ) from e |
137 | 142 |
|
138 | 143 | def hash_token(self, token: str) -> str: |
139 | 144 | """ |
|
0 commit comments