-
Notifications
You must be signed in to change notification settings - Fork 0
Feature/auth package #24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e2a0a95
53e0365
776414b
97f33c0
990a083
2db2b57
4cf1ed7
db45c58
aebdcd7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| --- | ||
| "@thexjs/auth": minor | ||
| --- | ||
|
|
||
| New `@thexjs/auth` package: plug-and-play authentication for x apps. | ||
|
|
||
| - `defineAuth()` with credentials (username/password) and OAuth2 providers, | ||
| including a preconfigured GitHub preset. | ||
| - Passwords hashed with Argon2 via `Bun.password` (`hashPassword` / | ||
| `verifyPassword`). | ||
| - Session stores on the framework data layer (`createSQLiteSessionStore`, | ||
| `createPostgresSessionStore`); tokens are opaque, HMAC-SHA256 digests stored | ||
| at rest, individually revocable, and expire after `sessionMaxAge`. | ||
| - Single catch-all handler (`auth.handleRequest`) serving | ||
| `/api/auth/signin/:id`, `/api/auth/callback/:id`, `/api/auth/signout`, and | ||
| `/api/auth/session`, with automatic CSRF protection (`checkCsrf`) on auth | ||
| POST endpoints and OAuth state-challenge verification on callbacks. | ||
| - Server-side `getSession()` plus `setSessionCookie` / `clearSessionCookie` | ||
| helpers. | ||
|
|
||
| Credentials and OAuth callback flows, session expiry/revocation, and CSRF | ||
| integration are covered by unit tests in `packages/auth/src/auth.test.ts`. |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| MIT License | ||
|
|
||
| Copyright (c) 2026 Abdelkabir Ouadoukou | ||
|
|
||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| of this software and associated documentation files (the "Software"), to deal | ||
| in the Software without restriction, including without limitation the rights | ||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| copies of the Software, and to permit persons to whom the Software is | ||
| furnished to do so, subject to the following conditions: | ||
|
|
||
| The above copyright notice and this permission notice shall be included in all | ||
| copies or substantial portions of the Software. | ||
|
|
||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| SOFTWARE. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| # @thexjs/auth | ||
|
|
||
| Plug-and-play authentication for [x](https://www.npmjs.com/package/@thexjs/core) framework apps. Add credentials (username/password) and OAuth2 (including GitHub) sign-in with one `defineAuth()` call, a sessions table in SQLite or Postgres via the framework's data layer, and a single catch-all API route. Passwords are hashed with Argon2 via `Bun.password`, session tokens are HMAC'd at rest, and every mutating endpoint is protected by the core CSRF module automatically. | ||
|
|
||
| ```sh | ||
| bun add @thexjs/auth | ||
| ``` | ||
|
|
||
| ## Quick start | ||
|
|
||
| ```ts | ||
| // lib/auth.ts | ||
| import { defineAuth, createSQLiteSessionStore, hashPassword, verifyPassword } from "@thexjs/auth"; | ||
|
|
||
| export const auth = defineAuth({ | ||
| secret: process.env.AUTH_SECRET!, | ||
| store: createSQLiteSessionStore(), // or createPostgresSessionStore(client) | ||
| providers: [ | ||
| { | ||
| id: "local", | ||
| name: "Local", | ||
| type: "credentials", | ||
| async authorize({ email, password }) { | ||
| const user = await db.query("SELECT * FROM users WHERE email = ?").get(email); | ||
| if (!user) return null; | ||
| if (!(await verifyPassword(password, user.password_hash))) return null; | ||
| return { id: String(user.id), name: user.name, email: user.email }; | ||
|
Comment on lines
+23
to
+27
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Initialize the application user database in the quick start. Line 24 uses Show a server-side database initialization through the data-layer helper before 🤖 Prompt for AI Agents |
||
| }, | ||
| }, | ||
| { | ||
| id: "github", | ||
| name: "GitHub", | ||
| type: "oauth", | ||
| clientId: process.env.GITHUB_CLIENT_ID!, | ||
| clientSecret: process.env.GITHUB_CLIENT_SECRET!, | ||
| }, | ||
| ], | ||
| }); | ||
| ``` | ||
|
|
||
| Wire it up with one catch-all API route: | ||
|
|
||
| ```ts | ||
| // api/auth/[...auth].ts | ||
| import { auth } from "../../lib/auth"; | ||
|
|
||
| export async function POST(req: Request) { | ||
| return auth.handleRequest(req); | ||
| } | ||
|
|
||
| export async function GET(req: Request) { | ||
| return auth.handleRequest(req); | ||
| } | ||
| ``` | ||
|
|
||
| Create users at sign-up with `hashPassword` (Argon2id) and store the hash — never plaintext: | ||
|
|
||
| ```ts | ||
| import { hashPassword } from "@thexjs/auth"; | ||
|
|
||
| await hashPassword("correct horse battery staple"); | ||
| ``` | ||
|
|
||
| ## Endpoints | ||
|
|
||
| `handleRequest` routes the path below `api/auth`: | ||
|
|
||
| | Route | Method | Purpose | | ||
| |---|---|---| | ||
| | `/api/auth/signin/<id>` | POST | credentials provider: `application/x-www-form-urlencoded` or `multipart` body with the provider's fields (e.g. `email`, `password`) | | ||
| | `/api/auth/signin/<id>` | GET | OAuth2 provider: redirects the browser to the provider's authorization URL | | ||
| | `/api/auth/callback/<id>` | GET | OAuth2 callback: exchanges the code, validates the state challenge, signs the user in | | ||
| | `/api/auth/signout` | POST | revokes the session and clears the cookie | | ||
| | `/api/auth/session` | GET | JSON `{ "user": { ... } }` or `401` | | ||
|
|
||
| A sign-in form POSTs to `/api/auth/signin/local`; after success the browser follows the `302` to `successRedirect` (default `/`). For OAuth, the button/link is just a GET to `/api/auth/signin/github`. | ||
|
|
||
| ## Reading the session | ||
|
|
||
| ```ts | ||
| // middleware or loader | ||
| const session = await auth.getSession(request); | ||
| if (!session) return new Response("Unauthorized", { status: 401 }); | ||
| session.user; // { id, name?, email? } snapshot from sign-in | ||
| ``` | ||
|
|
||
| `getSession` hashes the `x_session` cookie, looks up the token in the store, and returns `null` for expired/revoked sessions. `setSessionCookie(res, user, provider)` and `clearSessionCookie(res, req?)` are also exported for programmatic flows. | ||
|
|
||
| ## Security | ||
|
|
||
| - **Passwords** — Argon2id via `Bun.password` (`hashPassword` / `verifyPassword`). | ||
| - **Session tokens** — opaque random strings; only an HMAC-SHA256 digest (`secret`) is stored, so a database leak doesn't expose usable session cookies. Tokens are random 128-bit values, revocable, and expire after `sessionMaxAge` (default 7 days). | ||
| - **OAuth state** — a `x_oauth_state` cookie challenge must match the `state` param on the callback (HMAC'd, 5-minute expiry), preventing login-CSRF / session-fixation via crafted callbacks. | ||
| - **CSRF** — POST endpoints (`signin`, `signout`) run the core `checkCsrf` (Origin/Referer verification by default; pass `requireToken` for double-submit defense in depth) and reject non-conforming requests with `403`. | ||
| - **Cookies** — `HttpOnly`, `SameSite=Lax`, `Secure` in production. | ||
|
|
||
| Set a **stable `secret`** in production. If omitted, a random per-process secret is generated and a warning printed, which means sessions won't survive restarts. | ||
|
|
||
| ## Session stores | ||
|
|
||
| Both stores use a single `x_sessions` table and implement the `SessionStore` interface (`create`, `find`, `revoke`) if you want to bring your own. | ||
|
|
||
| ```ts | ||
| createSQLiteSessionStore({ path: "data/auth.db" }); // default: data/auth.db | ||
| createPostgresSessionStore(connectPostgres({ url: process.env.DATABASE_URL })); | ||
| ``` | ||
|
|
||
| The Postgres store ensures the table lazily on first use and takes a client returned by `connectPostgres` from `@thexjs/core/data`, so it inherits the connection pool, TLS policy, and retry behavior of the framework. | ||
|
|
||
| ## License | ||
|
|
||
| MIT | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Set explicit read-only token permissions.
This job has no
permissionsblock. It inherits the repository defaultGITHUB_TOKENpermissions. A compromised action or executed dependency can use write access when the repository default permits it.Add
permissions: { contents: read }totest-postgres.🧰 Tools
🪛 Checkov (3.3.9)
[medium] 79-80: Basic Auth Credentials
(CKV_SECRET_4)
🪛 zizmor (1.29.0)
[warning] 44-80: overly broad permissions (excessive-permissions): default permissions used due to no permissions: block
(excessive-permissions)
🤖 Prompt for AI Agents
Source: Linters/SAST tools