|
6 | 6 |
|
7 | 7 | from flask import Flask, redirect, request, jsonify, url_for, Response |
8 | 8 | from models.device_instance import DeviceInstance # noqa: E402 |
| 9 | +from models.parameters_instance import ParametersInstance # noqa: E402 |
9 | 10 | from flask_cors import CORS |
10 | 11 | from werkzeug.exceptions import HTTPException |
11 | 12 |
|
|
94 | 95 | DbQueryRequest, DbQueryResponse, |
95 | 96 | DbQueryUpdateRequest, DbQueryDeleteRequest, |
96 | 97 | AddToQueueRequest, GetSettingResponse, |
97 | | - RecentEventsRequest, SetDeviceAliasRequest |
| 98 | + RecentEventsRequest, SetDeviceAliasRequest, |
| 99 | + ValidateRememberRequest, ValidateRememberResponse, |
| 100 | + SaveRememberRequest, SaveRememberResponse |
98 | 101 | ) |
99 | 102 |
|
100 | 103 | from .sse_endpoint import ( # noqa: E402 [flake8 lint suppression] |
@@ -1933,6 +1936,146 @@ def check_auth(payload=None): |
1933 | 1936 | return jsonify({"success": True, "message": "Authentication check successful"}), 200 |
1934 | 1937 |
|
1935 | 1938 |
|
| 1939 | +# -------------------------- |
| 1940 | +# Remember Me Validation endpoint |
| 1941 | +# -------------------------- |
| 1942 | +@app.route("/auth/validate-remember", methods=["POST"]) |
| 1943 | +@validate_request( |
| 1944 | + operation_id="validate_remember", |
| 1945 | + summary="Validate Remember Me Token", |
| 1946 | + description="Validate a persistent Remember Me token against stored hash. Called from login page (no auth required).", |
| 1947 | + request_model=ValidateRememberRequest, |
| 1948 | + response_model=ValidateRememberResponse, |
| 1949 | + tags=["auth"], |
| 1950 | + auth_callable=None # No auth required - used on login page |
| 1951 | +) |
| 1952 | +def validate_remember(payload=None): |
| 1953 | + """ |
| 1954 | + Validate a Remember Me token from persistent cookie. |
| 1955 | +
|
| 1956 | + Security: Uses timing-safe hash comparison to prevent timing attacks. |
| 1957 | + Token format: hex-encoded 32 random bytes (64 chars) from bin2hex(random_bytes(32)) |
| 1958 | + """ |
| 1959 | + try: |
| 1960 | + # Extract token from request |
| 1961 | + data = request.get_json() or {} |
| 1962 | + token = data.get("token") |
| 1963 | + |
| 1964 | + if not token: |
| 1965 | + mylog("verbose", ["[auth/validate-remember] Missing token in request"]) |
| 1966 | + return jsonify({ |
| 1967 | + "success": True, |
| 1968 | + "valid": False, |
| 1969 | + "message": "Token validation failed: missing token" |
| 1970 | + }), 200 |
| 1971 | + |
| 1972 | + # Validate token against stored hash |
| 1973 | + params_instance = ParametersInstance() |
| 1974 | + result = params_instance.validate_token(token) |
| 1975 | + |
| 1976 | + if result['valid']: |
| 1977 | + mylog("verbose", ["[auth/validate-remember] Token validation successful"]) |
| 1978 | + return jsonify({ |
| 1979 | + "success": True, |
| 1980 | + "valid": True, |
| 1981 | + "message": "Token validation successful" |
| 1982 | + }), 200 |
| 1983 | + else: |
| 1984 | + mylog("verbose", ["[auth/validate-remember] Token validation failed"]) |
| 1985 | + return jsonify({ |
| 1986 | + "success": True, |
| 1987 | + "valid": False, |
| 1988 | + "message": "Token validation failed" |
| 1989 | + }), 200 |
| 1990 | + |
| 1991 | + except Exception as e: |
| 1992 | + mylog("verbose", [f"[auth/validate-remember] Unexpected error: {e}"]) |
| 1993 | + return jsonify({ |
| 1994 | + "success": False, |
| 1995 | + "valid": False, |
| 1996 | + "error": "Internal server error", |
| 1997 | + "message": "An unexpected error occurred during token validation" |
| 1998 | + }), 500 |
| 1999 | + |
| 2000 | + |
| 2001 | +# -------------------------- |
| 2002 | +# Remember Me Save endpoint |
| 2003 | +# -------------------------- |
| 2004 | +@app.route("/auth/remember-me/save", methods=["POST"]) |
| 2005 | +@validate_request( |
| 2006 | + operation_id="save_remember", |
| 2007 | + summary="Save Remember Me Token", |
| 2008 | + description="Save a Remember Me token to the database. Called after successful login to enable persistent authentication.", |
| 2009 | + request_model=SaveRememberRequest, |
| 2010 | + response_model=SaveRememberResponse, |
| 2011 | + tags=["auth"], |
| 2012 | + auth_callable=None # No auth required - used on login page |
| 2013 | +) |
| 2014 | +def save_remember(payload=None): |
| 2015 | + """ |
| 2016 | + Save a Remember Me token. |
| 2017 | +
|
| 2018 | + Flow: |
| 2019 | + 1. User logs in with "Remember Me" checkbox |
| 2020 | + 2. Password validated successfully |
| 2021 | + 3. Token generated: bin2hex(random_bytes(32)) |
| 2022 | + 4. This endpoint called: saves hash(token) to Parameters table |
| 2023 | + 5. Token (unhashed) set in persistent cookie |
| 2024 | + 6. Session created and user redirected |
| 2025 | +
|
| 2026 | + Security: Only the HASH is stored in the database, not the token itself. |
| 2027 | + If database is compromised, attacker cannot use stolen hashes without the original token. |
| 2028 | + """ |
| 2029 | + try: |
| 2030 | + import uuid |
| 2031 | + import hashlib |
| 2032 | + |
| 2033 | + # Extract token from request |
| 2034 | + data = request.get_json() or {} |
| 2035 | + token = data.get("token") |
| 2036 | + |
| 2037 | + if not token or len(token) < 64: |
| 2038 | + mylog("verbose", ["[auth/remember-me/save] Invalid or missing token"]) |
| 2039 | + return jsonify({ |
| 2040 | + "success": False, |
| 2041 | + "error": "Invalid token", |
| 2042 | + "message": "Token must be 64+ hex characters" |
| 2043 | + }), 400 |
| 2044 | + |
| 2045 | + # Hash the token |
| 2046 | + token_hash = hashlib.sha256(token.encode('utf-8')).hexdigest() |
| 2047 | + |
| 2048 | + # Generate UUID-based parameter ID |
| 2049 | + token_id = f"remember_me_token_{uuid.uuid4()}" |
| 2050 | + |
| 2051 | + # Store hash in Parameters table |
| 2052 | + params_instance = ParametersInstance() |
| 2053 | + success = params_instance.set_parameter(token_id, token_hash) |
| 2054 | + |
| 2055 | + if success: |
| 2056 | + mylog("verbose", [f"[auth/remember-me/save] Token saved successfully: {token_id}"]) |
| 2057 | + return jsonify({ |
| 2058 | + "success": True, |
| 2059 | + "message": "Remember Me token saved successfully", |
| 2060 | + "token_id": token_id |
| 2061 | + }), 200 |
| 2062 | + else: |
| 2063 | + mylog("verbose", ["[auth/remember-me/save] Failed to save token to database"]) |
| 2064 | + return jsonify({ |
| 2065 | + "success": False, |
| 2066 | + "error": "Database error", |
| 2067 | + "message": "Failed to save Remember Me token" |
| 2068 | + }), 500 |
| 2069 | + |
| 2070 | + except Exception as e: |
| 2071 | + mylog("verbose", [f"[auth/remember-me/save] Unexpected error: {e}"]) |
| 2072 | + return jsonify({ |
| 2073 | + "success": False, |
| 2074 | + "error": "Internal server error", |
| 2075 | + "message": "An unexpected error occurred while saving Remember Me token" |
| 2076 | + }), 500 |
| 2077 | + |
| 2078 | + |
1936 | 2079 | # -------------------------- |
1937 | 2080 | # Health endpoint |
1938 | 2081 | # -------------------------- |
|
0 commit comments