Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .changeset/auth-cookie-prefix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
"seamless-templates": minor
---

feat(templates): name auth cookies per application

Both API starters (`express` and `fastify`) now read `AUTH_COOKIE_PREFIX` and
derive `accessCookieName`, `refreshCookieName`, `registrationCookieName` and
`preAuthCookieName` from it, passing all four to the auth server and the matching
`cookieName` to `requireAuth`.

Cookies are scoped by host and ignore the port, so two Seamless applications
served from the same host share one cookie jar and overwrite each other's
session. Signing into one signs you out of the other. That is invisible in
production, where each application has its own domain, and unavoidable in
development, where they are all on localhost.

The guard has to be told the name as well as the server. Left on its default it
looks for `seamless-access` while the server has issued something else, and every
request 401s while holding a valid session.

The default prefix is `seamless-`, which reproduces the names
`@seamless-auth/express` already uses, so an existing project upgrades without
logging anyone out. `.env.example` and the README table in both starters document
the variable.

Note for anyone scaffolding several applications on one host: the variable is
read but not yet set by `template.json`, so a project created by the CLI still
gets the default names and still shares a cookie jar with its neighbours. Set
`AUTH_COOKIE_PREFIX` in the generated `.env` to separate them.
6 changes: 6 additions & 0 deletions templates/api/express/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ UI_ORIGINS=http://localhost:5173,http://localhost:5174
# Optional cookie domain for production, for example .example.com
COOKIE_DOMAIN=

# What this application calls its auth cookies. Cookies ignore the port, so two
# Seamless applications on the same host share one cookie jar and sign each other
# out. Give each one its own prefix to run them side by side on localhost.
# Leave unset to keep the default names.
AUTH_COOKIE_PREFIX=

# Secret to sign API-generated cookies with. The adapter refuses to start on
# anything shorter than 32 characters, so replace this with a real one:
# openssl rand -base64 48
Expand Down
29 changes: 15 additions & 14 deletions templates/api/express/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,21 @@ The committed contract lives in [.env.example](.env.example). Copy it before run
cp .env.example .env
```

| Variable | Purpose |
| --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `NODE_ENV` | `development` enables the dev messaging handlers that log OTP and magic-link tokens locally; set to `production` before deploying |
| `AUTH_SERVER_URL` | URL of your Seamless Auth server |
| `SERVE_ADMIN_CONSOLE` | `true` to serve the admin dashboard from this API at `/console`; `false` when it is hosted elsewhere |
| `UI_ORIGINS` | Comma-separated web origins allowed by CORS |
| `COOKIE_DOMAIN` | Optional cookie domain for production, for example `.example.com` |
| `COOKIE_SIGNING_KEY` | Secret used to sign API-generated cookies |
| `API_SERVICE_TOKEN` | Service token shared with Seamless Auth (from the portal) |
| `JWKS_KID` | JWKS key id the auth server signs with |
| `DATABASE_URL` | Full Postgres connection string. Wins over the `DB_*` values when set |
| `DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASSWORD`, `DB_NAME` | Postgres connection, used when `DATABASE_URL` is empty |
| `DB_SSL_REJECT_UNAUTHORIZED` | Set to `false` only for a certificate that does not chain to a public CA |
| `DB_LOGGING` | Set to `true` to log SQL in development |
| Variable | Purpose |
| --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `NODE_ENV` | `development` enables the dev messaging handlers that log OTP and magic-link tokens locally; set to `production` before deploying |
| `AUTH_SERVER_URL` | URL of your Seamless Auth server |
| `SERVE_ADMIN_CONSOLE` | `true` to serve the admin dashboard from this API at `/console`; `false` when it is hosted elsewhere |
| `UI_ORIGINS` | Comma-separated web origins allowed by CORS |
| `COOKIE_DOMAIN` | Optional cookie domain for production, for example `.example.com` |
| `AUTH_COOKIE_PREFIX` | Prefix for this application's auth cookie names, so several Seamless apps can run on one host without signing each other out. Defaults to `seamless-` |
| `COOKIE_SIGNING_KEY` | Secret used to sign API-generated cookies |
| `API_SERVICE_TOKEN` | Service token shared with Seamless Auth (from the portal) |
| `JWKS_KID` | JWKS key id the auth server signs with |
| `DATABASE_URL` | Full Postgres connection string. Wins over the `DB_*` values when set |
| `DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASSWORD`, `DB_NAME` | Postgres connection, used when `DATABASE_URL` is empty |
| `DB_SSL_REJECT_UNAUTHORIZED` | Set to `false` only for a certificate that does not chain to a public CA |
| `DB_LOGGING` | Set to `true` to log SQL in development |

`assertEnvironment` in [src/lib/env.ts](src/lib/env.ts) runs before the server is built. The auth
options are read once at startup, so a missing value used to surface as a 500 on the first
Expand Down
11 changes: 11 additions & 0 deletions templates/api/express/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ const rawOrigins = process.env.UI_ORIGINS;
const allowedOrigins = rawOrigins?.split(",").map((o) => o.trim()) ?? [];
const cookieDomain = process.env.COOKIE_DOMAIN?.trim() || undefined;

const cookiePrefix = process.env.AUTH_COOKIE_PREFIX?.trim() || "seamless-";

const cookieNames = {
accessCookieName: `${cookiePrefix}access`,
refreshCookieName: `${cookiePrefix}refresh`,
registrationCookieName: `${cookiePrefix}ephemeral`,
preAuthCookieName: `${cookiePrefix}ephemeral`,
};

// A request whose Origin is this server's own host is same-origin and was never
// a CORS concern. It has to be allowed explicitly because the admin console is
// served from this API at /console: browsers omit Origin on a same-origin GET but
Expand Down Expand Up @@ -97,6 +106,7 @@ const seamlessAuthOptions: SeamlessAuthServerOptions = {
audience: process.env.AUTH_SERVER_URL!,
jwksKid: process.env.JWKS_KID!,
cookieDomain,
...cookieNames,
messaging: devMessaging,
};

Expand Down Expand Up @@ -131,6 +141,7 @@ app.use("/auth", createSeamlessAuthServer(seamlessAuthOptions));
app.use(
requireAuth({
cookieSecret: seamlessAuthOptions.cookieSecret ?? "",
cookieName: seamlessAuthOptions.accessCookieName,
}),
);

Expand Down
6 changes: 6 additions & 0 deletions templates/api/fastify/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ UI_ORIGINS=http://localhost:5173,http://localhost:5174
# Optional cookie domain for production, for example .example.com
COOKIE_DOMAIN=

# What this application calls its auth cookies. Cookies ignore the port, so two
# Seamless applications on the same host share one cookie jar and sign each other
# out. Give each one its own prefix to run them side by side on localhost.
# Leave unset to keep the default names.
AUTH_COOKIE_PREFIX=

# Secret to sign API-generated cookies with. The adapter refuses to start on
# anything shorter than 32 characters, so replace this with a real one:
# openssl rand -base64 48
Expand Down
29 changes: 15 additions & 14 deletions templates/api/fastify/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,20 +57,21 @@ The committed contract lives in [.env.example](.env.example). Copy it before run
cp .env.example .env
```

| Variable | Purpose |
| --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `NODE_ENV` | `development` enables the dev messaging handlers that log OTP and magic-link tokens locally; set to `production` before deploying |
| `AUTH_SERVER_URL` | URL of your Seamless Auth server |
| `SERVE_ADMIN_CONSOLE` | `true` to serve the admin dashboard from this API at `/console`; `false` when it is hosted elsewhere |
| `UI_ORIGINS` | Comma-separated web origins allowed by CORS |
| `COOKIE_DOMAIN` | Optional cookie domain for production, for example `.example.com` |
| `COOKIE_SIGNING_KEY` | Secret used to sign API-generated cookies, 32 characters minimum |
| `API_SERVICE_TOKEN` | Service token shared with Seamless Auth (from the portal), 32 characters minimum |
| `JWKS_KID` | JWKS key id the auth server signs with |
| `DATABASE_URL` | Full Postgres connection string. Wins over the `DB_*` values when set |
| `DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASSWORD`, `DB_NAME` | Postgres connection, used when `DATABASE_URL` is empty |
| `DB_SSL_REJECT_UNAUTHORIZED` | Set to `false` only for a certificate that does not chain to a public CA |
| `DB_LOGGING` | Set to `true` to log SQL in development |
| Variable | Purpose |
| --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `NODE_ENV` | `development` enables the dev messaging handlers that log OTP and magic-link tokens locally; set to `production` before deploying |
| `AUTH_SERVER_URL` | URL of your Seamless Auth server |
| `SERVE_ADMIN_CONSOLE` | `true` to serve the admin dashboard from this API at `/console`; `false` when it is hosted elsewhere |
| `UI_ORIGINS` | Comma-separated web origins allowed by CORS |
| `COOKIE_DOMAIN` | Optional cookie domain for production, for example `.example.com` |
| `AUTH_COOKIE_PREFIX` | Prefix for this application's auth cookie names, so several Seamless apps can run on one host without signing each other out. Defaults to `seamless-` |
| `COOKIE_SIGNING_KEY` | Secret used to sign API-generated cookies, 32 characters minimum |
| `API_SERVICE_TOKEN` | Service token shared with Seamless Auth (from the portal), 32 characters minimum |
| `JWKS_KID` | JWKS key id the auth server signs with |
| `DATABASE_URL` | Full Postgres connection string. Wins over the `DB_*` values when set |
| `DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASSWORD`, `DB_NAME` | Postgres connection, used when `DATABASE_URL` is empty |
| `DB_SSL_REJECT_UNAUTHORIZED` | Set to `false` only for a certificate that does not chain to a public CA |
| `DB_LOGGING` | Set to `true` to log SQL in development |

`assertEnvironment` in [src/lib/env.ts](src/lib/env.ts) runs before the server is built. The auth
options are read once at startup, so a missing value would otherwise surface as a 500 on the first
Expand Down
15 changes: 14 additions & 1 deletion templates/api/fastify/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ const rawOrigins = process.env.UI_ORIGINS;
const allowedOrigins = rawOrigins?.split(",").map((o) => o.trim()) ?? [];
const cookieDomain = process.env.COOKIE_DOMAIN?.trim() || undefined;

const cookiePrefix = process.env.AUTH_COOKIE_PREFIX?.trim() || "seamless-";

const cookieNames = {
accessCookieName: `${cookiePrefix}access`,
refreshCookieName: `${cookiePrefix}refresh`,
registrationCookieName: `${cookiePrefix}ephemeral`,
preAuthCookieName: `${cookiePrefix}ephemeral`,
};

// A request whose Origin is this server's own host is same-origin and was never
// a CORS concern. It has to be allowed explicitly because the admin console is
// served from this API at /console: browsers omit Origin on a same-origin GET but
Expand Down Expand Up @@ -97,6 +106,7 @@ const seamlessAuthOptions: SeamlessAuthServerOptions = {
audience: process.env.AUTH_SERVER_URL!,
jwksKid: process.env.JWKS_KID!,
cookieDomain,
...cookieNames,
messaging: devMessaging,
};

Expand Down Expand Up @@ -140,7 +150,10 @@ await app.register(async (api) => {

secured.addHook(
"preHandler",
requireAuth({ cookieSecret: seamlessAuthOptions.cookieSecret }),
requireAuth({
cookieSecret: seamlessAuthOptions.cookieSecret,
cookieName: seamlessAuthOptions.accessCookieName,
}),
);
secured.addHook("preHandler", requireUser(seamlessAuthOptions));

Expand Down
Loading