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())