{
  "title": "Security Design to Consider When Building a Login-less App",
  "description": "Removing login means replacing identity verification and permission management with other mechanisms. Using Nobo Page as an example, this note organizes design topics like authorization, sessions, CSRF, share URLs, tokens, and XSS.",
  "locale": "en",
  "slug": "login-less-app-security-design",
  "url": "https://engineer-blog.tomoki-ttttt.workers.dev/en/articles/login-less-app-security-design/",
  "publishedAt": "2026-06-05T12:00:00.000Z",
  "updatedAt": "2026-06-07T18:00:00.000Z",
  "tags": [
    "Security",
    "Web",
    "Design",
    "Nobo Page"
  ],
  "projectIds": [
    "nobo-page"
  ],
  "markdown": "## Introduction\n\nAn app you can use without logging in is convenient.\n\nThere's no need to enter an email address or set a password — you open the page and start using it right away. It fits especially well with services not meant for long-term use: temporary notes, event announcements, simple shared boards, and so on.\n\nAt the same time, removing the login screen doesn't necessarily simplify the design.\n\nA login feature isn't just for displaying a username; it also plays roles like these.\n\n- Confirming who created the data (authentication)\n- Deciding who may view, edit, or delete it (authorization)\n- Recovering lost permissions\n- Identifying unauthorized actions\n\nHere, \"authentication\" and \"authorization\" are distinct concepts. Authentication is verifying the identity of a user, process, or device; authorization is granting or checking access rights to a resource. When you remove login, you have to cover these roles with another mechanism or restrict the features themselves.\n\nThis post organizes what's worth considering when designing a login-less app, using **Nobo Page** — which I'm currently considering building — as an example.\n\n## Even without login, you still need \"authorization\"\n\nIn an app with login, you can decide permissions based on the user's account.\n\nFor example: \"this user is the creator of this data, so they may edit it.\" Here you use the result of account authentication to perform authorization.\n\nWithout login, you can't verify identity (authenticate) via an account. What you can confirm is usually not \"is this person really the creator,\" but rather \"did they present the correct edit token\" or \"do they hold the same session identifier as at creation time.\"\n\nIn other words, what a login-less app actually does is closer to **confirming possession of a secret that proves a permission**, rather than verifying a human's identity. Common means include:\n\n- the browser session\n- cookies\n- a randomly issued token\n- dedicated view / edit URLs\n- identifying information stored on the device\n\nA URL that functions as a permission in this way is generally called a **capability URL**. Rather than \"removing login,\" it's closer to replacing account authentication with authorization based on possession of a secret.\n\nAs an aside, while the term and idea of a capability URL are sound, the W3C document people often cite is a First Public Working Draft from 2014 — not a W3C Recommendation. That's worth keeping in mind.\n\n## If you don't share, the design can be quite simple\n\nEven a login-less app needs far less consideration if the data isn't shared with others.\n\nFor example, a design where data can only be viewed from the session of the browser that created it.\n\n```text\nCreate data\n  ↓\nSave to server\n  ↓\nRetrievable only from the creating session\n```\n\nIn this case there's no need to put a permission in the URL.\n\nIf you manage the session with a secure cookie, you can more easily avoid the problem of a leaked URL exposing the data itself. That said, \"only the creating browser can retrieve it\" is a convenient simplification — what can actually access it is **any client that can present that session cookie**. If the session identifier is copied or stolen, it can be used from another browser too.\n\nSo if you use cookies, at minimum keep these in mind.\n\n- Set `Secure`, `HttpOnly`, and an appropriate `SameSite` on the cookie\n- Generate the session identifier with a cryptographically secure RNG, and make it long enough (128 bits or more as a guideline)\n- Don't leave the lifetime to the cookie's Max-Age alone; manage the session's lifetime on the server too\n\n### If you use cookies, don't forget CSRF\n\nA common blind spot with cookie-based sessions is **CSRF (cross-site request forgery)**.\n\nThe browser automatically attaches cookies to requests aimed at the target site. So merely opening an attacker's page can cause the user to send unintended edit or delete requests. For state-changing requests, combine measures like these.\n\n- Don't perform side effects such as edit/delete via GET\n- Use your framework's CSRF protection or CSRF tokens\n- Also validate the `Origin` header and use Fetch Metadata\n- Set the `SameSite` attribute appropriately\n\n`SameSite` suppresses many CSRF cases, but it isn't a single, complete defense in every configuration. As long as you use cookies, CSRF needs separate thought.\n\nNone of this makes XSS go away, either. If malicious JavaScript runs inside the page, it can send requests to the API using the current session even without reading the cookie value directly. A session-only design reduces risk, but it doesn't remove the need for XSS defenses.\n\n## The hard part is \"sharing without login\"\n\nA login-less app's design gets especially complex when you let created data be shared with others.\n\nIf the recipient also has no account, the server can't use an account to decide \"is this person authorized to view it.\"\n\nSo one approach is to issue a URL containing a random string and treat possession of that URL as the permission.\n\n```text\nHolds the view URL    → can view\nHolds the edit URL    → can edit\nHolds the admin URL   → can delete or change settings\n```\n\nIt's a handy mechanism, but this URL isn't just a page's address. Holding the URL is close to a state of knowing a password.\n\n## \"Only people who know the link\" isn't necessarily private\n\nServices that use share URLs often explain it as \"only people who know the link can view it.\"\n\nBut this phrasing needs a little care.\n\nWhoever receives the link can forward it to someone else. It remains in chat history and email, and it can appear in screen shares or screenshots. Fully recalling a once-leaked link isn't easy either.\n\nSo it's risky to flatly claim \"it's safely private\" for a service using share URLs. More accurately, the situation is:\n\n> Anyone who knows this link can access it.\n\nYou also need to clearly explain to users that it isn't suited for storing confidential information or sensitive personal data.\n\n## Separate view, edit, and admin permissions\n\nIssuing a single share link that lets its holder view, edit, and delete is simple. But the damage when the link leaks is also large.\n\nSo one approach is to separate permissions by purpose.\n\n```text\nView link\nEdit link\nAdmin link\n```\n\nThe view link permits only viewing; the edit link permits changing content. High-impact operations like deletion or changing the retention period are limited to the admin link.\n\nFor Nobo Page too, if it offers sharing, separating permissions like this is a candidate design. With permissions separated, even if a view link is shared widely, you avoid also granting edit or delete. This is exactly the principle of least privilege.\n\n## Don't store permission tokens as-is in the DB\n\nThe random token in a share URL is a secret that grants an operation simply by being presented. So you want to avoid storing the token as-is in the database.\n\n```text\nShare URL:\nhttps://example.com/page/123#edit=abc123...\n\nDB:\nedit_token = abc123...\n```\n\nWith this, anyone who can read the database — or anyone who obtains the data through a leak — could use the edit permission directly.\n\nInstead, store only a digest (hash) of the token on the server.\n\n```text\nWhat the user holds:\nedit_token = a random value of 128+ bits from a CSPRNG\n\nWhat the DB stores:\nedit_token_digest = HMAC-SHA-256(server_secret, edit_token)\n```\n\nWhat's worth noting here is that **this differs in purpose from password hashing.**\n\nBecause a password a user memorizes is easy to guess, it needs a deliberately slow hash like Argon2id or bcrypt. A sufficiently long token generated by a cryptographically secure RNG, on the other hand, is inherently hard to target with dictionary or brute-force attacks. So a costly password hash isn't mandatory for tokens; securing entropy matters more.\n\nEven a plain `SHA-256(token)` provides brute-force resistance for a high-entropy token. Using `HMAC-SHA-256` with a server-side secret makes it harder for an attacker who only obtained the DB — and not the secret key — to compute the same digest.\n\nFor implementation, also keep these in mind.\n\n- Use a cryptographically secure random generator (CSPRNG)\n- Bind each token to its permission and expiry on the server side\n- Make tokens revocable and rotatable\n- For comparison, use a safe (timing-resistant) comparison function from an existing library\n- Don't emit tokens to access logs, analytics platforms, or error bodies\n\nSo it isn't that \"hashing makes it safe\"; this is accurately understood as **a measure to mitigate the damage when the DB alone leaks.** Because the actual token exists in the user's browser, browser-side safety remains important too.\n\n## Why use a URL fragment, and what comes after\n\nWhen putting a share token in a URL, one option is to use a URL fragment rather than a query parameter.\n\n```text\nhttps://example.com/page/123#edit=secret-token\n```\n\nThe part after `#` is normally not sent to the server in an ordinary HTTP request, and it isn't included in the `Referer` header either. So it's easier to avoid the token ending up as-is in CDN or web server access logs.\n\nHowever, for the server to verify the edit permission, JavaScript must ultimately send the token to the server. Putting it in the fragment doesn't complete the story. Typically the flow is:\n\n```text\nOpen /page/123#edit=secret-token\n  ↓\nJavaScript reads location.hash\n  ↓\nSends it via the POST body or the Authorization header\n  ↓\nThe server verifies it\n  ↓\nhistory.replaceState() removes the token from the URL\n```\n\nYou need to make sure the token you sent isn't recorded into APM, a WAF, application logs, or error reports. And after verification, clear the token from the address bar and history with something like `history.replaceState()`, so it isn't re-exposed by sharing or the back button.\n\nTo lean toward the safer side, instead of sending the raw edit token from JavaScript every time, this composition is also a candidate.\n\n1. Hand the fragment token to the server once to exchange it\n2. Issue a short-lived session scoped to that operation\n3. Use that session via an `HttpOnly` cookie thereafter\n4. Remove the original token from the URL\n\nBut since this composition uses cookies, the CSRF measures above are needed as a set. A URL fragment is, at most, one means of keeping the token out of server logs — it doesn't make the token itself safe.\n\n## Sharing amplifies the impact of XSS\n\nXSS is a vulnerability where strings entered by a user aren't handled safely and get executed as HTML or JavaScript. In a login-less sharing app, it needs especially careful thought.\n\nFor example, suppose an attacker manages to save content like this to a board.\n\n```html\n<script>\n  // malicious code\n</script>\n```\n\nIf this content is displayed as raw HTML, the script may run in the browser of another user who opens the share link.\n\nIn a non-sharing, session-only app, the path to making an ordinary other user trip over saved content is narrower. But that doesn't make Stored XSS disappear. Saved content can reach other people's eyes on screens like these.\n\n- The operator's admin / report-review screen\n- Support handling screens\n- Preview or export features\n- Screens used for incident investigation\n- Sharing features added in the future\n\nIf any of these display the saved content, it can still be Stored XSS against another person. Moreover, Reflected XSS and DOM-based XSS occur regardless of whether sharing exists.\n\nMain defenses include:\n\n- Don't render user input directly as HTML\n- Apply context-appropriate output encoding (HTML escaping) thoroughly\n- Use safe DOM APIs and avoid raw assignment to `innerHTML`\n- Reject dangerous URL schemes (such as `javascript:`)\n- Don't allow arbitrary scripts or embedded code\n\nContent Security Policy (CSP) is also useful, but it's accurately positioned as one layer of **defense in depth**, not the primary defense. The basics are safe DOM APIs, context-appropriate output encoding, and HTML sanitization only where needed.\n\nWhen handling Markdown, rather than \"sanitize the Markdown input and then turn it into HTML,\" it's safer to **convert the Markdown to HTML and then pass that HTML through a trusted sanitizer**, because dangerous HTML can be produced during conversion.\n\n## HttpOnly cookies alone don't prevent XSS\n\nUsing HttpOnly cookies for session management is important. Setting HttpOnly prevents JavaScript from reading the cookie directly via `document.cookie`.\n\nHowever, when XSS occurs, the attacker's script runs on the same page as the user. So even without obtaining the cookie itself, it can make the browser auto-send the cookie and call the API.\n\n```text\nSteal the cookie\n  → easier to prevent with HttpOnly\n\nOperate the API with the current session\n  → still possible if XSS exists\n```\n\nHttpOnly cookies are a measure against exfiltrating session information. They don't prevent reading the screen or performing unauthorized operations via XSS.\n\n## Don't leave too much permission info in the browser\n\nIn a login-less app, you may be tempted to store permission info in the browser so the user can edit the same data again — for example, saving the edit token in LocalStorage.\n\nBut LocalStorage can be read by JavaScript on the page. If XSS occurs, the saved edit or admin token could be sent to an external party. The same goes for sessionStorage and IndexedDB; they are equally weak against XSS. Also, when using a shared PC or someone else's device, the permission info keeps lingering on that device.\n\nSo you need to decide carefully what to store in the browser. Information like language or theme settings should be considered separately from edit or admin permissions.\n\nFor Nobo Page, rather than storing permissions long-term for convenience, a design where users keep the admin link themselves as needed leans toward the safer side.\n\n## Don't let confidential responses be cached\n\nView / edit screens and API responses that carry tokens should specify cache control explicitly depending on their content.\n\nWith nothing specified, confidential responses may remain in the browser, a CDN, a proxy, a Service Worker, and so on. This can even lead to incidents where a previous user's content is shown on a shared device.\n\n- Consider `Cache-Control: no-store` for confidential responses\n- Note that `no-cache` doesn't mean \"don't store\"; it means \"revalidate before use\"\n- Keep URLs and responses containing tokens out of intermediate caches\n\n## Don't perform deletion or permission changes via GET\n\nAvoid a design where merely opening a capability URL performs deletion, publishing, or permission changes.\n\nLink-preview generation, search-engine crawlers, and security scanners may access the URL automatically. If they trip over a URL with side effects, unintended deletion or changes can happen.\n\nAlways perform state-changing operations through explicit requests like POST, combined with the CSRF measures above. The principle is to give \"opening a URL\" no destructive side effects.\n\n## Prepare for abuse unique to anonymous access\n\nWithout login, it's hard to grasp who is using how much on a per-user basis. So prepare for abuse unique to anonymous services.\n\n- Spam posts and mass automatic board creation\n- Abuse aimed at consuming storage\n- Creating and distributing phishing pages\n\nFor each create / update / view API, providing rate limiting that doesn't rely on IP alone, size and count limits, anomaly detection, and means to report and revoke leans toward the safer side.\n\nIf you offer file attachments in the future, you'll separately need type and size restrictions, storage outside the web root, virus scanning, download controls, and so on.\n\n## Don't assume things can be recovered\n\nIn a service with login, you can verify identity with an email address and recover an account or data.\n\nWithout login, there's no way to confirm whether a user who lost the admin link is really the creator. Even if someone inquires \"I'm the one who made this board,\" reissuing admin permission without proof is dangerous.\n\nSo in exchange for the lightness of being login-less, constraints like these are needed.\n\n- Losing the admin link means it can't be recovered\n- Deleted data, in principle, can't be brought back\n- Data past its retention period can't be recovered\n- Permissions aren't reissued on an inquiry alone\n\nAlso, if you explain \"it auto-deletes\" or \"it can't be recovered,\" the scope should match reality. Even if you delete the primary data, copies remaining in places like these would contradict the explanation.\n\n- Backups\n- CDN caches\n- Search indexes\n- Thumbnails and converted files\n- Audit logs and incident-analysis data\n\nIt looks inconvenient, but it's also a restriction needed to avoid handing data to third parties.\n\n## Separate external scripts by \"origin,\" not just by \"screen\"\n\nWeb services accumulate various external scripts: analytics, ads, heatmaps, chat support, and more. An external script runs as JavaScript with the same privileges as the page it's embedded in, so it brings risks like arbitrary code execution, leaking confidential information, and compromise of the provider.\n\nSo rather than putting the same script on every page, there's a way of thinking that separates by the screen's role.\n\nBut merely \"not placing it on the board screen\" can be insufficient isolation as long as both share the same origin. To separate more strongly, separate the origin itself.\n\n```text\nwww.example.com\n  → intro / ads / analytics\n\napp.example.com\n  → board viewing / editing\n  → external scripts disallowed in principle\n```\n\nWeb Storage and IndexedDB are isolated per origin, so a separate origin also isolates client-side data. Along with that, it's important not to carelessly set `Domain=.example.com` on cookies, and to limit them to the necessary origin.\n\n## For Nobo Page, keep the value of sharing\n\nRemove sharing and make it a session-only app, and Nobo Page's design can be quite simple. There's no need to put permissions in the URL, and no need to separate view, edit, and admin links.\n\nBut for Nobo Page, being able to hand created content to others right away is one of its values. Day-of event guidance, short-lived shared notes, info pages opened from a QR code — these don't fulfill their purpose if only the creator can view them.\n\nSo rather than removing sharing itself, you need to design on the premise of the risks that sharing adds. Concretely, a policy like this.\n\n- Separate view, edit, and admin permissions (least privilege)\n- Issue tokens of 128+ bits from a CSPRNG\n- Don't store raw permission tokens in the DB; keep digests instead\n- Give permission links an expiry and a revocation method\n- If using cookies, set `Secure` / `HttpOnly` / `SameSite` and CSRF measures\n- Display user input safely (output encoding and HTML sanitization)\n- Don't let confidential responses be cached\n- Don't perform state changes via GET\n- Provide rate limiting and abuse measures for anonymous use\n- Restrict external scripts on the board screen, ideally on a separate origin\n- Don't make the retention period longer than necessary\n- Clearly communicate that it isn't for confidential information\n\nBeing login-less doesn't make something dangerous on its own. But since you remove the authentication and authorization that login provided, you must not leave the replacement mechanism vague.\n\n## Conclusion\n\nA login-less app greatly reduces the burden before someone can start using it. It fits especially well with temporary uses and situations that don't warrant creating an account.\n\nOn the other hand, when you remove login, you have to think about the authentication and authorization the account provided in another way. A non-sharing, session-only app can have a relatively simple structure — but as long as it uses cookies, you still need to think about CSRF and session management. When you share data with others without login, the URL and token themselves become the permission.\n\nIn that case, these points matter.\n\n- Treat the URL as a key, not just an address\n- Don't make permissions broader than necessary\n- Generate and store tokens safely\n- Prevent permission leakage and unauthorized operations via XSS and CSRF\n- Limit caching, retention period, and recovery scope\n- Honestly communicate the service's limits to users\n\nThe ease of being login-less doesn't mean you don't have to think about security. If anything, to let users use it safely without being aware of it, the backend needs a design different from a normal login app.\n\n## References\n\n- [Authentication — NIST Computer Security Resource Center Glossary](https://csrc.nist.gov/glossary/term/authentication)\n- [Good Practices for Capability URLs (W3C Working Draft)](https://www.w3.org/TR/capability-urls/)\n- [Cross-Site Request Forgery Prevention — OWASP Cheat Sheet Series](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html)\n- [Session Management — OWASP Cheat Sheet Series](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html)\n- [Cross Site Scripting Prevention — OWASP Cheat Sheet Series](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html)\n- [Same-origin policy — MDN](https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Same-origin_policy)"
}
