chore: validate Android OAuth configuration

This commit is contained in:
Tomás Romero committed 2026-10-13 21:03:18 +00:00
1 parent 2ee87d1e07
commit ddf2ce63b3
9 files changed
+451 -44

No files matched your search

+1 -8
View File
@@ -29,14 +29,7 @@ if [[ ! -f "$FLECTAR_ANDROID_KEYSTORE" ]]; then
printf 'Production keystore not found: %s\n' "$FLECTAR_ANDROID_KEYSTORE" >&2
exit 1
fi
if [[ "$FLECTAR_GOOGLE_ANDROID_CLIENT_ID" != *.apps.googleusercontent.com || "$FLECTAR_GOOGLE_ANDROID_CLIENT_ID" == test.* ]]; then
printf 'FLECTAR_GOOGLE_ANDROID_CLIENT_ID is not a production Android OAuth client ID.\n' >&2
exit 1
fi
if [[ ! "$FLECTAR_MICROSOFT_ANDROID_CLIENT_ID" =~ ^[0-9A-Fa-f-]{36}$ || "$FLECTAR_MICROSOFT_ANDROID_CLIENT_ID" == 00000000-0000-0000-0000-000000000000 ]]; then
printf 'FLECTAR_MICROSOFT_ANDROID_CLIENT_ID is not a production Entra application ID.\n' >&2
exit 1
fi
python3 "$project_dir/scripts/verify-android-oauth.py" --require-all
if [[ ! "$FLECTAR_MICROSOFT_ANDROID_REDIRECT_URI" =~ ^msauth://com\.flectar\.mail/.+ ]]; then
printf 'Microsoft redirect must be msauth://com.flectar.mail/<signature-hash>.\n' >&2
exit 1
+4 -8
View File
@@ -6,6 +6,10 @@ target_dir="$project_dir/target"
android_sdk="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}"
android_ndk="${ANDROID_NDK_ROOT:-${ANDROID_NDK_HOME:-}}"
# Missing registrations are supported: the app shows Sign-in settings instead
# of sending a placeholder client ID to a provider. Reject invalid supplied IDs.
python3 "$project_dir/scripts/verify-android-oauth.py"
if [[ -z "$android_sdk" || ! -d "$android_sdk" ]]; then
printf 'Android SDK not found. Set ANDROID_HOME to your Android SDK directory.\n' >&2
exit 1
@@ -68,14 +72,6 @@ fi
export ANDROID_HOME="$android_sdk"
export CARGO_TARGET_DIR="$target_dir"
# Test builds have deterministic provider placeholders so static packaging and
# device startup can be exercised without borrowing production registrations.
# Provider sign-in intentionally remains unavailable until the release-owner
# values documented in ANDROID_FUNCTIONAL_APP_TODO.md are supplied.
export FLECTAR_GOOGLE_ANDROID_CLIENT_ID="${FLECTAR_GOOGLE_ANDROID_CLIENT_ID:-test.apps.googleusercontent.com}"
export FLECTAR_MICROSOFT_ANDROID_CLIENT_ID="${FLECTAR_MICROSOFT_ANDROID_CLIENT_ID:-00000000-0000-0000-0000-000000000000}"
export FLECTAR_MICROSOFT_ANDROID_REDIRECT_URI="${FLECTAR_MICROSOFT_ANDROID_REDIRECT_URI:-msauth://com.flectar.mail/test-signature-hash}"
cargo build \
--locked \
--manifest-path "$project_dir/platform/android/Cargo.toml" \
+129
View File
@@ -0,0 +1,129 @@
#!/usr/bin/env python3
"""Validate Android OAuth configuration and the actual build-script environment."""
import importlib.util
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
import unittest
sys.dont_write_bytecode = True
SCRIPTS = Path(__file__).resolve().parent
spec = importlib.util.spec_from_file_location("oauth", SCRIPTS / "verify-android-oauth.py")
oauth = importlib.util.module_from_spec(spec)
spec.loader.exec_module(oauth)
GOOGLE = "FLECTAR_GOOGLE_ANDROID_CLIENT_ID"
MICROSOFT = "FLECTAR_MICROSOFT_ANDROID_CLIENT_ID"
REDIRECT = "FLECTAR_MICROSOFT_ANDROID_REDIRECT_URI"
VALID = {
GOOGLE: "123456789-fixture.apps.googleusercontent.com",
MICROSOFT: "abcdef01-2345-4678-9abc-def012345678",
}
class AndroidOAuthTests(unittest.TestCase):
def test_optional_providers_and_production_requirements(self):
for values in ({}, {GOOGLE: "", MICROSOFT: " "}, {GOOGLE: VALID[GOOGLE]}, {MICROSOFT: VALID[MICROSOFT]}):
oauth.verify(values)
with self.assertRaises(ValueError):
oauth.verify(values, require_all=True)
oauth.verify(VALID, require_all=True)
oauth.verify({GOOGLE: VALID[GOOGLE], MICROSOFT: " " + VALID[MICROSOFT].upper() + " "})
def test_rejects_placeholders_and_malformed_ids_without_echoing_values(self):
for key, value in (
(MICROSOFT, "00000000-0000-0000-0000-000000000000"),
(MICROSOFT, "------------------------------------"),
(MICROSOFT, "a" * 36),
(MICROSOFT, VALID[MICROSOFT] + "?unexpected=value"),
(MICROSOFT, "invalid-app-id"),
(GOOGLE, "test.apps.googleusercontent.com"),
(GOOGLE, "TEST.apps.googleusercontent.com"),
(GOOGLE, "wrong-suffix"),
):
with self.subTest(key=key, value=value):
with self.assertRaises(ValueError) as error:
oauth.verify({key: value})
self.assertIn(key, str(error.exception))
self.assertNotIn(value, str(error.exception))
def run_build(self, script, configuration):
# Run the real shell entry points. Replace Cargo with a recorder that
# stops before compilation; no SDK downloads, signing or network calls.
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
(root / "scripts").mkdir()
for name in (script, "verify-android-oauth.py"):
shutil.copyfile(SCRIPTS / name, root / "scripts" / name)
sdk = root / "sdk"
sdk.mkdir()
toolchain = root / "ndk/toolchains/llvm/prebuilt/fixture/bin"
toolchain.mkdir(parents=True)
binaries = root / "bin"
binaries.mkdir()
for path in (toolchain / "aarch64-linux-android26-clang", binaries / "gradle", binaries / "keytool"):
path.write_text("#!/bin/sh\nexit 99\n")
path.chmod(0o755)
capture = root / "cargo-environment.json"
cargo = binaries / "cargo"
cargo.write_text(
"#!/usr/bin/env python3\nimport os,json,pathlib\n"
"pathlib.Path(os.environ['OAUTH_CAPTURE']).write_text(json.dumps({"
"key:os.environ.get(key) for key in " + repr((GOOGLE, MICROSOFT, REDIRECT)) + "}))\n"
"raise SystemExit(73)\n"
)
cargo.chmod(0o755)
keystore = root / "test.keystore"
keystore.touch()
env = {key: value for key, value in os.environ.items() if not key.startswith("FLECTAR_")}
env.update({
"PATH": str(binaries) + os.pathsep + env.get("PATH", ""),
"ANDROID_HOME": str(sdk),
"ANDROID_NDK_ROOT": str(root / "ndk"),
"GRADLE_CMD": str(binaries / "gradle"),
"XDG_DATA_HOME": str(root / "data"),
"FLECTAR_ANDROID_TEST_KEYSTORE": str(keystore),
"FLECTAR_ANDROID_KEYSTORE": str(keystore),
"FLECTAR_ANDROID_KEYSTORE_PASSWORD": "fixture",
"FLECTAR_ANDROID_KEY_ALIAS": "fixture",
"FLECTAR_ANDROID_KEY_PASSWORD": "fixture",
"FLECTAR_ANDROID_VERSION_CODE": "1",
"FLECTAR_ANDROID_VERSION_NAME": "0.1.0",
"OAUTH_CAPTURE": str(capture),
**configuration,
})
result = subprocess.run(["bash", str(root / "scripts" / script)],
env=env, capture_output=True, text=True, timeout=15)
recorded = json.loads(capture.read_text()) if capture.exists() else None
return result, recorded
def test_preview_never_injects_placeholder_defaults(self):
for values in ({}, {GOOGLE: "", MICROSOFT: "", REDIRECT: ""}, VALID):
with self.subTest(configuration=values):
result, recorded = self.run_build("build-android-test-apk.sh", values)
self.assertEqual(result.returncode, 73, result.stdout + result.stderr)
self.assertEqual(recorded, {key: values.get(key) for key in (GOOGLE, MICROSOFT, REDIRECT)})
def test_preview_rejects_invalid_ids_before_compilation(self):
for values in ({MICROSOFT: "00000000-0000-0000-0000-000000000000"}, {GOOGLE: "test.apps.googleusercontent.com"}):
result, recorded = self.run_build("build-android-test-apk.sh", values)
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
self.assertIsNone(recorded)
self.assertIn("not a valid", result.stderr)
def test_production_requires_real_ids_before_compilation(self):
values = {**VALID, REDIRECT: "msauth://com.flectar.mail/fixture-signature"}
result, recorded = self.run_build("build-android-release.sh", values)
self.assertEqual(result.returncode, 73, result.stdout + result.stderr)
self.assertEqual(recorded, values)
for invalid in ("", "00000000-0000-0000-0000-000000000000", "a" * 36):
result, recorded = self.run_build("build-android-release.sh", {**values, MICROSOFT: invalid})
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
self.assertIsNone(recorded)
if __name__ == "__main__":
unittest.main()
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env python3
"""Reject invalid bundled Android OAuth IDs without printing configuration values.
Preview APKs may omit either provider. Production builds require both. Local
validation cannot establish that an app is registered or that consent is allowed.
"""
import argparse
import os
import re
import sys
def verify(environment, require_all=False):
for provider, variable in (
("Google", "FLECTAR_GOOGLE_ANDROID_CLIENT_ID"),
("Microsoft", "FLECTAR_MICROSOFT_ANDROID_CLIENT_ID"),
):
value = environment.get(variable, "").strip()
if not value:
if require_all:
raise ValueError(f"Production Android build requires {variable}.")
continue
if provider == "Microsoft":
valid = (
re.fullmatch(r"[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}", value)
and value != "00000000-0000-0000-0000-000000000000"
)
else:
valid = (
re.fullmatch(r"[^\s]+\.apps\.googleusercontent\.com", value)
and not value.lower().startswith("test.")
)
if not valid:
raise ValueError(
f"{variable} is not a valid {provider} application ID. "
"Use a real app registration, or omit it for a preview without bundled sign-in."
)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--require-all", action="store_true")
args = parser.parse_args()
try:
verify(os.environ, args.require_all)
except ValueError as error:
print(error, file=sys.stderr)
sys.exit(1)