Skip to content

Imeplement three-tiered auth system#100

Open
rasek-sls wants to merge 60 commits intomasterfrom
read-auth-extension
Open

Imeplement three-tiered auth system#100
rasek-sls wants to merge 60 commits intomasterfrom
read-auth-extension

Conversation

@rasek-sls
Copy link
Contributor

This PR extends the authentication system in the API to have 3 levels of access:

  • Reader access: access to read basic data, endpoints are protected by a @reader_auth_required() wrapper, which does nothing unless the API config has reader_auth_required: True, at which point JWT login with a user account with a verified email address is required
  • CMS user read access: access to read basic data protected by @reader_auth_required but also access to some tools endpoints that don't change data. Endpoints are protected by a @cms_required() wrapper, which checks that the logged in user has the cms_user flag set in the auth database
  • CMS user edit access: access to all above data, but also to tools endpoints which add, edit, or delete data. Endpoints are protected by a @cms_required(edit=True) wrapper, which checks that the logged in user has the cms_user flag set, and has access to the project they are trying to edit.

To support this feature, this PR also implements the following:

  • cms_user boolean in the users auth table, to show that a user is a CMS user and should have at least CMS read access
  • email_verified boolean in the users auth table, to show that a user's email has been verified
  • email address verification, through the auth/verify_email endpoint, which is protected by a fresh JWT access token requirement
    • a verification email is sent on user account registration, which contains a link with a fresh JWT access token in the query parameters
    • TODO: determine whether the email verification link should point at the frontend or the backend. Either would work, but the frontend is easier to make "pretty"
  • password reset flow:
    • Users can request a password reset by sending a POST request to the auth/forgot_password endpoint with their email in the JSON body of the request.
    • This initiates a password reset, which sends a link to the user over email, containing a fresh JWT access token in the query parameters
    • TODO: this link needs to point to the frontend, which needs to provide a form for the user to provide a new password
    • TODO: the frontend form needs to send a POST request to the auth/reset_password endpoint, with the user's new password in the JSON body of the request, and the fresh JWT token from the link in the query parameters
  • created_timestamp and last_login_timestamp fields for the user auth table
    • this is so that account "liveness" can be checked, so that old accounts can be deleted if desired

@rasek-sls rasek-sls marked this pull request as ready for review December 29, 2025 11:18
@SebastianKohler
Copy link
Contributor

Could we add a session probe GET endpoint that the frontend can use to validate the current access-token session? There are some auth protected routes in the frontend that currently don't make any backend requests, so they can have stale auth information. Ideally the endpoint would live outside /auth/* so the frontend HTTP interceptors can attach access tokens and run refresh logic uniformly.

For example a sls_api/endpoints/session.py something like this:

from flask import Blueprint, jsonify
from flask_jwt_extended import jwt_required, get_jwt, get_jwt_identity
from sls_api import rate_limiter
from sls_api.models import User

"""
Session probe endpoints for frontend authentication checks.

These routes are intentionally outside `/auth/*` so frontend request
interceptors can attach access tokens and run refresh logic uniformly.
"""

session = Blueprint('session', __name__, url_prefix="/session")


def _invalid_credentials_response():
    response = jsonify({"msg": "Invalid credentials", "err": "INCORRECT_CREDENTIALS"})
    response.headers["Cache-Control"] = "no-store"
    response.headers["Pragma"] = "no-cache"
    return response, 401


@session.route("/validate", methods=["GET"])
@rate_limiter.limit("60/minute")
@jwt_required()  # access token required
def validate_session():
    """
    Validate current access-token session.

    Returns 200 only when the token is valid, not invalidated, and the
    user's email is verified. Returns 401 for all invalid states.
    """

    claims = get_jwt()
    identity = get_jwt_identity()
    jwt_issued_at = claims.get("iat")

    user = User.find_by_email(identity)
    if not user:
        return _invalid_credentials_response()

    if not jwt_issued_at:
        return _invalid_credentials_response()

    # Reject invalidated tokens
    if not User.check_token_validity(identity, jwt_issued_at):
        return _invalid_credentials_response()

    # Unverified users are treated as unauthorized for app session
    if not user.email_is_verified():
        return _invalid_credentials_response()

    response = jsonify({"authenticated": True})
    response.headers["Cache-Control"] = "no-store"
    response.headers["Pragma"] = "no-cache"
    return response, 200

@rasek-sls
Copy link
Contributor Author

In theory, yes. But in practice, /auth/test already does most of this. If we want to have an endpoint like this outside of /auth/, we should also remove /auth/test, so that we don't have two endpoints that do almost-the-same thing.

Would you prefer we extend /auth/test to work as your proposed /session/validate, or should we just remove /auth/test and instead implement /session/validate?

@SebastianKohler
Copy link
Contributor

In theory, yes. But in practice, /auth/test already does most of this. If we want to have an endpoint like this outside of /auth/, we should also remove /auth/test, so that we don't have two endpoints that do almost-the-same thing.

Would you prefer we extend /auth/test to work as your proposed /session/validate, or should we just remove /auth/test and instead implement /session/validate?

I'd prefer to remove /auth/test and implement /session/validate as a GET instead. With the current frontend flow it's easier to use it then when it's not in /auth.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants