Skip to content

Commit df5cd96

Browse files
committed
Add experimental tooling for generating signed URLs
1 parent b7f48f1 commit df5cd96

File tree

1 file changed

+75
-0
lines changed

1 file changed

+75
-0
lines changed

src/stac_auth_proxy/signed_urls.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
from dataclasses import dataclass
2+
from datetime import datetime, timezone
3+
import hmac
4+
import hashlib
5+
import base64
6+
import urllib.parse
7+
8+
9+
@dataclass
10+
class SignedPayload:
11+
href: str
12+
exp: int
13+
meta: str
14+
sig: str
15+
16+
@classmethod
17+
def build(cls, *, secret_key: str, href: str, exp: int, **meta_dict) -> str:
18+
"""Generate a signed payload the given parameters using HMAC-SHA256."""
19+
b64_meta = base64.urlsafe_b64encode(stringify_dict(meta_dict).encode()).decode()
20+
21+
params = {"href": href, "exp": exp, "meta": b64_meta}
22+
params_str = stringify_dict(params)
23+
24+
signature = hmac.new(
25+
secret_key.encode(),
26+
params_str.encode(),
27+
hashlib.sha256,
28+
).digest()
29+
b64_signature = base64.urlsafe_b64encode(signature).decode()
30+
31+
return cls(
32+
href=href,
33+
exp=exp,
34+
meta=b64_meta,
35+
sig=b64_signature,
36+
)
37+
38+
@property
39+
def valid_expiration(self) -> bool:
40+
"""Verify that the signature is not expired."""
41+
return datetime.now(timezone.utc).timestamp() > self.exp
42+
43+
@property
44+
def valid_signature(self) -> bool:
45+
"""Verify that the payload is signed and not expired."""
46+
expected_signature = self.build(href=self.href, exp=self.exp, **self.meta)
47+
return hmac.compare_digest(expected_signature.sig, self.sig)
48+
49+
@property
50+
def meta_dict(self):
51+
"""Decoded meta data"""
52+
return {
53+
k: v[0]
54+
for k, v in urllib.parse.parse_qs(
55+
base64.urlsafe_b64decode(self.meta).decode()
56+
).items()
57+
}
58+
59+
def as_qs(self):
60+
"""Return the query string"""
61+
return urllib.parse.urlencode(
62+
{
63+
"href": self.href,
64+
"exp": self.exp,
65+
"meta": self.meta,
66+
"sig": self.sig,
67+
}
68+
)
69+
70+
71+
def stringify_dict(params: dict) -> str:
72+
"""
73+
Sort the parameters and return them as a string.
74+
"""
75+
return "&".join(f"{k}={params[k]}" for k in sorted(params))

0 commit comments

Comments
 (0)