Spaces:
Running
Running
File size: 5,620 Bytes
bf0bce6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | """
check_auth.py — offline checks for login (services/auth.py). No network, no database.
uv run python scripts/check_auth.py
Covers the session token (sign / verify / tamper / expiry / wrong secret), the
company-domain rule, and verify_google_credential() against FAKE Google replies
(wrong audience, unverified email, wrong domain, wrong hd, good). A real Google
sign-in still needs a Google Client ID and is tested in the browser.
"""
from __future__ import annotations
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from app.config import settings # noqa: E402
from app.services import auth # noqa: E402
FAILED = 0
def check(label: str, ok: bool) -> None:
global FAILED
FAILED += 0 if ok else 1
print(f" [{'ok ' if ok else 'FAIL'}] {label}")
class FakeResp:
def __init__(self, status: int, data: dict):
self.status_code = status
self._data = data
def json(self):
return self._data
def google_reply(**overrides) -> dict:
base = {
"aud": "client-123", "iss": "https://accounts.google.com", "email_verified": "true",
"email": "ali@elipsestudio.com", "name": "Ali Khan", "picture": "http://x/p.png", "hd": "elipsestudio.com",
}
base.update(overrides)
return base
def expect_error(credential_reply: dict | None, status: int = 200) -> bool:
auth.requests.get = lambda *a, **k: FakeResp(status, credential_reply or {}) # type: ignore[assignment]
try:
auth.verify_google_credential("tok")
return False
except auth.AuthError:
return True
def main() -> int:
settings.auth_secret = "s" * 40
settings.google_client_id = "client-123"
settings.allowed_email_domains = "elipsestudio.com, other.org"
settings.allowed_emails = "" # ignore whatever the real .env lists, so this test is the same on every machine
settings.auth_token_days = 7
print("\nsession token")
tok = auth.create_token(7, "ali@elipsestudio.com", "Ali Khan")
claims = auth.decode_token(tok)
check("a fresh token verifies and carries the user id", claims is not None and claims["sub"] == "7")
check("three dot-separated JWT parts", tok.count(".") == 2)
h, p, sig = tok.split(".")
check("tampered payload is rejected", auth.decode_token(f"{h}.{p[:-2]}xx.{sig}") is None)
check("tampered signature is rejected", auth.decode_token(f"{h}.{p}.{sig[:-2]}xx") is None)
check("an expired token is rejected", auth.decode_token(auth.create_token(7, "a@elipsestudio.com", "A", days=-1)) is None)
check("garbage is rejected, not raised", auth.decode_token("not-a-token") is None and auth.decode_token("") is None)
settings.auth_secret = "t" * 40
check("a token signed with another secret is rejected", auth.decode_token(tok) is None)
settings.auth_secret = "s" * 40
settings.auth_secret = ""
check("no AUTH_SECRET -> cannot issue a token", _raises(lambda: auth.create_token(1, "a@b.c", "A")))
check("no AUTH_SECRET -> nothing verifies", auth.decode_token(tok) is None)
settings.auth_secret = "s" * 40
print("\ncompany domain")
check("elipsestudio.com allowed", auth.email_domain_allowed("Ali@ElipseStudio.com"))
check("second configured domain allowed", auth.email_domain_allowed("x@other.org"))
check("gmail.com refused", not auth.email_domain_allowed("ali@gmail.com"))
check("look-alike domain refused", not auth.email_domain_allowed("ali@elipsestudio.com.evil.io"))
check("sub-domain refused", not auth.email_domain_allowed("ali@mail.elipsestudio.com"))
check("no @ refused", not auth.email_domain_allowed("elipsestudio.com"))
print("\napproved emails (ALLOWED_EMAILS)")
settings.allowed_emails = "Boss@Gmail.com, sara@partner.io"
check("a listed email is allowed, case-insensitive", auth.email_allowed("boss@gmail.com"))
check("an unlisted gmail is still refused", not auth.email_allowed("other@gmail.com"))
check("a company-domain email is still allowed by domain", auth.email_allowed("x@elipsestudio.com"))
check("a look-alike of a listed email is refused", not auth.email_allowed("boss@gmail.com.evil.io"))
settings.allowed_emails = ""
print("\nverify_google_credential (fake Google replies)")
real_get = auth.requests.get
try:
auth.requests.get = lambda *a, **k: FakeResp(200, google_reply()) # type: ignore[assignment]
who = auth.verify_google_credential("tok")
check("a good reply returns email/name/picture", who["email"] == "ali@elipsestudio.com" and who["name"] == "Ali Khan")
check("Google says invalid (HTTP 400)", expect_error({}, 400))
check("wrong audience refused", expect_error(google_reply(aud="someone-else")))
check("wrong issuer refused", expect_error(google_reply(iss="https://evil.example")))
check("unverified email refused", expect_error(google_reply(email_verified="false")))
check("gmail address refused", expect_error(google_reply(email="ali@gmail.com", hd="")))
check("wrong hd refused even with a good-looking email", expect_error(google_reply(hd="evil.io")))
settings.google_client_id = ""
check("no GOOGLE_CLIENT_ID -> clear error", expect_error(google_reply()))
finally:
auth.requests.get = real_get
print()
print("All checks passed." if not FAILED else f"{FAILED} check(s) FAILED")
return 1 if FAILED else 0
def _raises(fn) -> bool:
try:
fn()
except auth.AuthError:
return True
return False
if __name__ == "__main__":
sys.exit(main())
|