Server-Rendered Login Sessions: Creation, Verification, Refresh, Logout, and Phone Recovery A developer outlines a server-rendered authentication design for an edtech app, emphasizing distinct state transitions for session creation, verification, refresh, logout, and phone recovery. The approach uses an opaque session cookie and an auditable session record to track learner, device, and recovery status, with a Python transport adapter example. The developer stresses keeping the flow simple and separating concerns to make failures visible. Short answer: for a server-rendered learning app, create a short-lived session only after the phone code is verified, keep refresh as a separate state transition, and make recovery a deliberate path rather than an accidental logout loop. The useful design artifact is an auditable session record tied to a learner, device context, and recovery status. I build RAG and agent features in Python, so I tend to move from a notebook test to a production boundary quickly. Authentication deserves a slower handoff. In an edtech app, a learner may lose a phone while a parent, teacher, or school administrator still needs a safe way to recover the account. The browser should receive only an opaque session cookie; the server owns the lifecycle and records why each transition happened. Treat the four actions as different state changes. Code verification proves possession of a phone channel. Session creation establishes a browser session. Verification checks whether that session is still active. Refresh extends a valid session under a stricter policy. Logout revokes one session, while an account-recovery event may need to revoke every session. That separation makes failure visible. A refresh request must not silently create a new account. A logout request must not be interpreted as proof that the phone number is still controlled. For a school district, the audit trail should answer: which learner was affected, which session changed, what policy allowed it, and when the change took place. The request flow is intentionally plain: Keep it boring. The browser never needs to know whether the session store is SQL, Redis, or another service. It needs stable cookie behavior and a clear response when the session is no longer valid. The following example is a transport adapter, not a complete identity provider. It keeps endpoint knowledge in one place, sends credentials from the server process, and makes the application decide when a transition is allowed. The paths are the four lifecycle operations used by this design. python import json import os import urllib.error import urllib.request AUTH BASE = os.environ "AUTH BASE URL" .rstrip "/" AUTH TOKEN = os.environ "AUTH SERVICE TOKEN" def auth request path: str, method: str, payload: dict | None = None - dict: body = None if payload is None else json.dumps payload .encode "utf-8" request = urllib.request.Request f"{AUTH BASE}{path}", data=body, method=method, headers={ "Authorization": f"Bearer {AUTH TOKEN}", "Content-Type": "application/json", }, try: with urllib.request.urlopen request, timeout=8 as response: return json.load response except urllib.error.HTTPError as error: Normalize the response; do not copy provider details into the UI. raise RuntimeError f"authentication transition rejected: {error.code}" from error def create session learner id: str, verified phone: str - str: result = auth request "/v1/auth/session/create", "POST", {"user id": learner id, "verified phone": verified phone}, return result "session id" def verify session session id: str - bool: result = auth request f"/v1/auth/session/verify/{session id}", "GET", return bool result.get "valid" def refresh session refresh transition, refresh token: str - dict: Inject the verified refresh transition so its service path stays behind this adapter. return refresh transition refresh token=refresh token, idempotency key=os.urandom 12 .hex , In a real server-rendered framework, create session runs after the one-time-code verifier has consumed the code. Set the cookie with Secure , HttpOnly , and an appropriate SameSite policy; rotate it after login so an attacker can't reuse a pre-login identifier. A CSRF token still matters for state-changing form posts. OWASP's authentication guidance also calls for generic account-recovery responses, rate limits, and careful session invalidation. I keep the adapter this small because my eval harness can exercise it without a browser. The test fixture includes a normal learner, an expired session, a replayed refresh token, and a lost-phone recovery. Your mileage may vary on cookie lifetime: younger learners using shared school devices may need shorter idle windows than staff using managed laptops. For transient service limits, retry a 429 with bounded exponential backoff and jitter, and stop after the request deadline. Every create, refresh, or revoke write needs an application idempotency key so a retry cannot create a second active session or apply the same recovery action twice. Expiry is not recovery. A 30-minute idle timeout limits exposure, but it does not help a learner whose phone was destroyed in a flood. Recovery should require a different verified factor or an approved school workflow, then revoke sessions according to the risk decision. For example, a district can require a guardian email link for a minor account, while a staff account may require an administrator approval. The application should store the recovery case ID beside the session events, not put sensitive evidence in a browser cookie. If no second factor exists, say so plainly and route the case to human review; do not weaken the session check to make the support ticket disappear. One subtle trap is refresh-token reuse. If a refresh operation succeeds, mark the old refresh credential as spent and bind the replacement to the same account and device policy. If a spent credential appears again, treat it as a recovery signal and revoke the affected session family. That policy is more useful than endlessly extending a cookie that may already be copied. The failure matrix below is the minimum I would put in an eval notebook before shipping. It tests decisions, not just HTTP status codes. | Event | Required decision | Audit fields | |---|---|---| | Correct code, new browser | Create one session and rotate the cookie | learner ID, verification time, device hint | | Expired session | Deny protected data and offer login | session ID, expiry reason | | Refresh after idle limit | Require fresh phone verification | old session, policy version | | Current-device logout | Revoke only that session | session ID, actor, timestamp | | Lost phone recovery | Revoke the session family or all sessions | recovery case, approving role | | Replay of spent refresh credential | Revoke affected sessions and alert | credential family, correlation ID | The table is intentionally explicit. “Logout worked” is not a sufficient test assertion if another browser remains active after a lost-phone report. Start with a deterministic fixture set. I use Python tests to assert that an unauthenticated request cannot load a lesson, that a verified session can load it, and that revocation takes effect on the next protected request. Then I add property-style cases: refreshing twice cannot produce two active replacements, and logging out one device does not revoke another unless the recovery policy says so. Measure the transitions that affect learners: code-to-session latency, verification rejection rate, refresh rejection rate, and time from recovery approval to global revocation. Avoid logging phone numbers, codes, raw cookies, or full lesson URLs with identifiers. A correlation ID and a hashed internal account key are enough for most investigations. The catch is that this architecture is not suitable when the product cannot operate a server-side session store or needs a completely offline login. In those cases, a managed identity system or a local device credential may be a better fit, with its own recovery rules. Stick with a simpler signed-cookie design only when its revocation and recovery semantics are demonstrably acceptable; portability is less important than being able to end access when a phone is lost. I am not sure a single expiry number can ever be universal. Classroom schedules, minor-protection requirements, and staff workflows pull in different directions. What resolves that uncertainty is an observed risk review backed by the transition tests, not a default copied from a sample app. Before release, I check the server-side key boundary, cookie flags, CSRF coverage, code-verification handoff, refresh rotation, single-session logout, global recovery revocation, generic error text, and the audit retention policy. I run the fixture suite in CI and keep the prompt and test data versioned beside the application, because an authentication change that passes a happy-path browser demo can still break account recovery.