diff --git a/.env.example b/.env.example
index bd78ca8..e708a3b 100644
--- a/.env.example
+++ b/.env.example
@@ -20,9 +20,9 @@ AUTH_TRUST_HOST=true
#GOOGLE_CLIENT_ID=
#GOOGLE_CLIENT_SECRET=
-# Optional: enable passkey auth (default: enabled)
-# Set to "true" to allow passkey login and enrollment.
-AUTH_PASSKEYS_ENABLED=true
+# Optional: enable demo login (default: disabled)
+# Set to "true" to expose a demo admin login button on the sign-in screen.
+ENABLE_DEMO_MODE=false
# Optional: logging level (default: debug in dev, info in prod)
#LOG_LEVEL=info
diff --git a/AGENTS.md b/AGENTS.md
index cb4482e..5aaa941 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -72,7 +72,7 @@ Use `eslint-plugin-boundaries` and `no-restricted-imports` to discourage cross
- Do not duplicate auth checks in child layouts/pages under the group. Rely on the group layout for auth.
- Keep `src/app/(protected-routes)/settings/layout.tsx` for the admin-only rule; it should only enforce `session.user.role === ADMIN` (assumes auth already passed).
- Keep public auth at `src/app/(public-routes)/auth/signin/**`.
- - Passkey enrollment happens from the account page after first login via a one-time link.
+ - Demo mode login is optional; when enabled it should expose a single button for the initial admin on the sign-in page.
- The homepage `/` is under the protected group and does not need page-level `auth()`.
## API Architecture
diff --git a/README.md b/README.md
index 621dcb7..b16ea6a 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@ Red Team Assessment Platform (RTAP) is built for internal Red Teams to plan and
User Docs:
- [Installation](docs/installation.md)
-- [Getting Started Workflow](docs/getting-started.md)
+- [Getting Started Workflow](docs/getting-started.md) (look here for UI screenshots)
Development Docs:
- [Development](docs/development.md)
@@ -41,7 +41,7 @@ Initially based on the T3 Stack - Next.js, tRPC, Prisma, TypeScript. Type-safe A
Local development runs the Next.js dev server against a local PostgreSQL container. Production workloads also use Docker (web + Postgres) behind your own reverse proxy.
-Authentication is all passwordless using NextAuth - with an option for passkeys and/or OAuth providers (initial support includes Google SSO).
+Authentication is passwordless and SSO-only using NextAuth. For development and trials you can enable a demo admin sign-in button via `ENABLE_DEMO_MODE=true`.
## Licensing
diff --git a/deploy/docker/.env.example b/deploy/docker/.env.example
index 3226292..56546c1 100644
--- a/deploy/docker/.env.example
+++ b/deploy/docker/.env.example
@@ -1,7 +1,6 @@
# Full base URL where the app will be reachable (include http:// or https://).
# Cookies are sent over HTTPS only when this starts with https://.
# If you put a TLS-terminating proxy in front of rtap-web, use its public URL.
-# Important: Passkey auth works only over HTTPS or on localhost
RTAP_AUTH_URL=http://localhost:3000
# Secure values: generate with `openssl rand -base64 32`
@@ -9,8 +8,8 @@ RTAP_AUTH_SECRET=REPLACE_WITH_A_SECURE_RANDOM_VALUE
RTAP_INITIAL_ADMIN_EMAIL=admin@example.com
# Optional SSO
-# Toggle passkey provider (default enabled)
-RTAP_AUTH_PASSKEYS_ENABLED=true
+# Demo mode (default disabled): expose a demo admin login button
+RTAP_ENABLE_DEMO_MODE=false
# Register Google provider when present (optional)
#RTAP_GOOGLE_CLIENT_ID=
#RTAP_GOOGLE_CLIENT_SECRET=
diff --git a/deploy/docker/docker-compose.yml b/deploy/docker/docker-compose.yml
index 280477a..56171f0 100644
--- a/deploy/docker/docker-compose.yml
+++ b/deploy/docker/docker-compose.yml
@@ -22,7 +22,7 @@ services:
AUTH_URL: ${RTAP_AUTH_URL}
AUTH_SECRET: ${RTAP_AUTH_SECRET}
INITIAL_ADMIN_EMAIL: ${RTAP_INITIAL_ADMIN_EMAIL}
- AUTH_PASSKEYS_ENABLED: ${RTAP_AUTH_PASSKEYS_ENABLED}
+ ENABLE_DEMO_MODE: ${RTAP_ENABLE_DEMO_MODE}
GOOGLE_CLIENT_ID: ${RTAP_GOOGLE_CLIENT_ID}
GOOGLE_CLIENT_SECRET: ${RTAP_GOOGLE_CLIENT_SECRET}
ports:
diff --git a/docs/dev/DESIGN.md b/docs/dev/DESIGN.md
index b645370..f6a4419 100644
--- a/docs/dev/DESIGN.md
+++ b/docs/dev/DESIGN.md
@@ -18,7 +18,7 @@ Plan and execute red‑team operations and measure defensive effectiveness (dete
- Next.js 15 (App Router) + TypeScript
- tRPC v11 (Zod validation); Prisma targeting PostgreSQL (local dev uses a Docker container, production uses managed Postgres)
-- NextAuth (passkey-first, with optional OAuth)
+- NextAuth (SSO-first, with optional demo admin login in development)
- Access helpers enforce scoping and rights: `getAccessibleOperationFilter`, `checkOperationAccess`.
### Conventions (where things live)
diff --git a/docs/development.md b/docs/development.md
index d1b5a40..c97a84a 100644
--- a/docs/development.md
+++ b/docs/development.md
@@ -17,8 +17,7 @@ docker compose -f deploy/docker/docker-compose.dev.yml up -d
# Apply migrations and seed first-run admin + MITRE content
npm run init
-# If not using SSO, generate a one-time login URL to enroll your first passkey
-npm run generate-admin-login
+# Optional: enable demo admin login (set ENABLE_DEMO_MODE=true in .env)
# Optionally seed demo taxonomy/operation data (FOR DEMO PURPOSES ONLY)
npm run seed:demo
@@ -43,4 +42,4 @@ All PRs should pass the following:
npm run check
npm run test
npm run build
-```
\ No newline at end of file
+```
diff --git a/docs/installation.md b/docs/installation.md
index 9c55ced..d6e63c3 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -1,6 +1,8 @@
# Installation
-Follow these instructions to set up Red Team Assessment Platform (RTAP) in local development or production environments.
+Follow these instructions to set up Red Team Assessment Platform (RTAP) for production or local testing purposes. This uses pre-built Docker containers.
+
+For development environments, you'll probably instead want to run a local npm dev server - not a pre-built container. Additional information is available [here](./development.md).
## Docker Installation
@@ -17,36 +19,27 @@ docker compose up -d
# Optionally - seed demo taxonomy/operation data (FOR DEMO PURPOSES ONLY)
docker exec rtap-web npm run seed:demo
-# If not using SSO, generate 1-time login URL to set up your first passkey
-docker exec rtap-web npm run generate-admin-login
+# Optional: enable demo admin login for trials (see Authentication below)
```
## Authentication
### How it Works
-Let's be the change we want to see in the world. There is no support for passwords! Currently supported options are:
-
-- Passkeys (required TLS or localhost)
-- Google OAuth (SSO)
+Authentication is SSO-only, with an optional demo-mode button for trials.
-The platform uses NextAuth, so adding additional SSO providers would be pretty easy.
+Currently, only Google SSO is enabled. However, [NextAuth supports tons of providers](https://next-auth.js.org/v3/configuration/providers#oauth-providers). Open an issue and I will add providers for you.
**Admin bootstrap:**
- On first run, the application creates an admin account using `INITIAL_ADMIN_EMAIL` from your `.env`.
-- If using Google SSO, just sign in with the matching Google account.
-- If using passkeys, you must generate a one-time login URL (`npm run generate-admin-login`) and register a passkey for that account.
+- If using SSO, sign in with the matching account and it will just work.
+- If using demo mode, click "Sign in as Demo Admin" (requires `ENABLE_DEMO_MODE=true`).
**Ongoing user management:**
- Once logged in as admin, you can create additional users.
-- Google SSO users: just log in with the matching Google email.
-- Passkey users: must receive a one-time login URL from the admin, then register a passkey.
-
-**Recovery:**
-
-- If locked out, re-run `npm run generate-admin-login` to obtain another single-use login URL for the initial admin account.
+- SSO users: log in with the matching email.
Accounts must be created inside the platform; SSO logins for unknown emails will be rejected.
@@ -55,10 +48,10 @@ Accounts must be created inside the platform; SSO logins for unknown emails will
Authentication options are configured in your `.env` file. The names are slightly different depending on whether you are doing local development or docker compose - the correct values are provided in the appropriate `.env-example` files.
```
-# Enable or disable passkey authentication
-AUTH_PASSKEYS_ENABLED=true
+# Demo mode: expose a demo admin login button on the sign-in page
+ENABLE_DEMO_MODE=false
-# Configuring the follow values will enable Google SSO
+# Configuring the following values will enable Google SSO
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
```
diff --git a/package-lock.json b/package-lock.json
index cc227d4..3e6a11d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,8 +16,6 @@
"@hookform/resolvers": "^5.2.2",
"@prisma/client": "^6.19.0",
"@radix-ui/react-select": "^2.2.6",
- "@simplewebauthn/browser": "^9.0.1",
- "@simplewebauthn/server": "^9.0.3",
"@t3-oss/env-nextjs": "^0.13.0",
"@tanstack/react-query": "^5.90.11",
"@trpc/client": "^11.7.2",
@@ -1392,7 +1390,9 @@
"version": "1.1.28",
"resolved": "https://registry.npmjs.org/@hexagon/base64/-/base64-1.1.28.tgz",
"integrity": "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==",
- "license": "MIT"
+ "license": "MIT",
+ "optional": true,
+ "peer": true
},
"node_modules/@hookform/resolvers": {
"version": "5.2.2",
@@ -1992,7 +1992,9 @@
"version": "0.2.11",
"resolved": "https://registry.npmjs.org/@levischuck/tiny-cbor/-/tiny-cbor-0.2.11.tgz",
"integrity": "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==",
- "license": "MIT"
+ "license": "MIT",
+ "optional": true,
+ "peer": true
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.12",
@@ -2224,6 +2226,8 @@
"resolved": "https://registry.npmjs.org/@peculiar/asn1-android/-/asn1-android-2.5.0.tgz",
"integrity": "sha512-t8A83hgghWQkcneRsgGs2ebAlRe54ns88p7ouv8PW2tzF1nAW4yHcL4uZKrFpIU+uszIRzTkcCuie37gpkId0A==",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
"@peculiar/asn1-schema": "^2.5.0",
"asn1js": "^3.0.6",
@@ -2235,6 +2239,8 @@
"resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.5.0.tgz",
"integrity": "sha512-t4eYGNhXtLRxaP50h3sfO6aJebUCDGQACoeexcelL4roMFRRVgB20yBIu2LxsPh/tdW9I282gNgMOyg3ywg/mg==",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
"@peculiar/asn1-schema": "^2.5.0",
"@peculiar/asn1-x509": "^2.5.0",
@@ -2247,6 +2253,8 @@
"resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.5.0.tgz",
"integrity": "sha512-qMZ/vweiTHy9syrkkqWFvbT3eLoedvamcUdnnvwyyUNv5FgFXA3KP8td+ATibnlZ0EANW5PYRm8E6MJzEB/72Q==",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
"@peculiar/asn1-schema": "^2.5.0",
"@peculiar/asn1-x509": "^2.5.0",
@@ -2259,6 +2267,8 @@
"resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.5.0.tgz",
"integrity": "sha512-YM/nFfskFJSlHqv59ed6dZlLZqtZQwjRVJ4bBAiWV08Oc+1rSd5lDZcBEx0lGDHfSoH3UziI2pXt2UM33KerPQ==",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
"asn1js": "^3.0.6",
"pvtsutils": "^1.3.6",
@@ -2270,6 +2280,8 @@
"resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.5.0.tgz",
"integrity": "sha512-CpwtMCTJvfvYTFMuiME5IH+8qmDe3yEWzKHe7OOADbGfq7ohxeLaXwQo0q4du3qs0AII3UbLCvb9NF/6q0oTKQ==",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
"@peculiar/asn1-schema": "^2.5.0",
"asn1js": "^3.0.6",
@@ -3196,6 +3208,8 @@
"resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-9.0.1.tgz",
"integrity": "sha512-wD2WpbkaEP4170s13/HUxPcAV5y4ZXaKo1TfNklS5zDefPinIgXOpgz1kpEvobAsaLPa2KeH7AKKX/od1mrBJw==",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
"@simplewebauthn/types": "^9.0.1"
}
@@ -3205,6 +3219,8 @@
"resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-9.0.3.tgz",
"integrity": "sha512-FMZieoBosrVLFxCnxPFD9Enhd1U7D8nidVDT4MsHc6l4fdVcjoeHjDueeXCloO1k5O/fZg1fsSXXPKbY2XTzDA==",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
"@hexagon/base64": "^1.1.27",
"@levischuck/tiny-cbor": "^0.2.2",
@@ -3225,7 +3241,9 @@
"resolved": "https://registry.npmjs.org/@simplewebauthn/types/-/types-9.0.1.tgz",
"integrity": "sha512-tGSRP1QvsAvsJmnOlRQyw/mvK9gnPtjEc5fg2+m8n+QUa+D7rvrKkOYyfpy42GTs90X3RDOnqJgfHt+qO67/+w==",
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
- "license": "MIT"
+ "license": "MIT",
+ "optional": true,
+ "peer": true
},
"node_modules/@standard-schema/spec": {
"version": "1.0.0",
@@ -5006,6 +5024,8 @@
"resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.6.tgz",
"integrity": "sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==",
"license": "BSD-3-Clause",
+ "optional": true,
+ "peer": true,
"dependencies": {
"pvtsutils": "^1.3.6",
"pvutils": "^1.1.3",
@@ -5469,6 +5489,8 @@
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz",
"integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
"node-fetch": "^2.7.0"
}
@@ -8828,6 +8850,8 @@
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
"whatwg-url": "^5.0.0"
},
@@ -8854,19 +8878,25 @@
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
- "license": "MIT"
+ "license": "MIT",
+ "optional": true,
+ "peer": true
},
"node_modules/node-fetch/node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
- "license": "BSD-2-Clause"
+ "license": "BSD-2-Clause",
+ "optional": true,
+ "peer": true
},
"node_modules/node-fetch/node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
@@ -9591,6 +9621,8 @@
"resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz",
"integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
"tslib": "^2.8.1"
}
@@ -9600,6 +9632,8 @@
"resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz",
"integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"engines": {
"node": ">=6.0.0"
}
diff --git a/package.json b/package.json
index 5232b61..1d99de6 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,6 @@
"build": "next build",
"check": "eslint . && tsc --project tsconfig.check.json --noEmit",
"init": "tsx scripts/init.ts",
- "generate-admin-login": "tsx scripts/generate-admin-login.ts",
"db:migrate": "prisma migrate dev --schema prisma/schema.prisma",
"db:deploy": "prisma migrate deploy --schema prisma/schema.prisma",
"db:reset": "prisma migrate reset --force --skip-generate --skip-seed --schema prisma/schema.prisma && npm run init",
@@ -38,8 +37,6 @@
"@hookform/resolvers": "^5.2.2",
"@prisma/client": "^6.19.0",
"@radix-ui/react-select": "^2.2.6",
- "@simplewebauthn/browser": "^9.0.1",
- "@simplewebauthn/server": "^9.0.3",
"@t3-oss/env-nextjs": "^0.13.0",
"@tanstack/react-query": "^5.90.11",
"@trpc/client": "^11.7.2",
diff --git a/scripts/generate-admin-login.ts b/scripts/generate-admin-login.ts
deleted file mode 100644
index a9bd4e4..0000000
--- a/scripts/generate-admin-login.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Manual utility to generate a one-time login link for the initial admin user.
- *
- * - Requires DATABASE_URL and AUTH_SECRET to be configured (same as init script)
- * - Looks up the user defined by INITIAL_ADMIN_EMAIL (defaults to admin@example.com)
- * - Prints the generated login URL and expiry to stdout without emitting info-level logs
- */
-import 'dotenv/config';
-
-import { PrismaClient } from '@prisma/client';
-
-async function main() {
- const requiredEnv = ['AUTH_SECRET', 'DATABASE_URL'];
- const missing = requiredEnv.filter((key) => !process.env[key] || String(process.env[key]).trim() === '');
-
- if (missing.length > 0) {
- throw new Error(`Missing required env vars: ${missing.join(', ')}`);
- }
-
- const initialEmail = process.env.INITIAL_ADMIN_EMAIL?.trim().toLowerCase() ?? 'admin@example.com';
-
- const db = new PrismaClient({ log: ['error'] });
- try {
- const adminUser = await db.user.findUnique({ where: { email: initialEmail } });
- if (!adminUser) {
- throw new Error(
- `No user found for ${initialEmail}. Run \`npm run init\` first to provision the initial admin account.`,
- );
- }
-
- const { createLoginLink } = await import('@server/auth/login-link');
- const { url, expires } = await createLoginLink(db, {
- email: initialEmail,
- baseUrl: process.env.AUTH_URL,
- });
-
- console.log('Generated a one-time login link for the initial admin user.');
- console.log(url);
- console.log(`Expires at ${expires.toISOString()}`);
- } finally {
- await db.$disconnect();
- }
-}
-
-void main().catch((error) => {
- console.error('[generate-admin-login] Failed to generate admin login link:', error);
- process.exit(1);
-});
diff --git a/src/app/(protected-routes)/account/page.tsx b/src/app/(protected-routes)/account/page.tsx
index 39e77c9..e6d6d90 100644
--- a/src/app/(protected-routes)/account/page.tsx
+++ b/src/app/(protected-routes)/account/page.tsx
@@ -1,12 +1,10 @@
"use client";
-import { useState } from "react";
-import { signIn as registerPasskey } from "next-auth/webauthn";
import { api } from "@/trpc/react";
import { Button, Card, CardContent, CardHeader, CardTitle } from "@components/ui";
-import { parseUserWithPasskey, type UserWithPasskey } from "@features/shared/users/user-validators";
+import { parseUserProfile, type UserProfile } from "@features/shared/users/user-validators";
-const renderLastLogin = (lastLogin: UserWithPasskey["lastLogin"]) => {
+const renderLastLogin = (lastLogin: UserProfile["lastLogin"]) => {
if (!lastLogin) return "Never";
if (lastLogin instanceof Date) {
@@ -25,27 +23,7 @@ const renderLastLogin = (lastLogin: UserWithPasskey["lastLogin"]) => {
export default function AccountPage() {
const { data: meData, refetch, isLoading } = api.users.me.useQuery();
- const me = parseUserWithPasskey(meData);
- const [status, setStatus] = useState<"idle" | "registering" | "success" | "error">("idle");
-
- const handleRegisterPasskey = async () => {
- setStatus("registering");
- try {
- const res = await registerPasskey("passkey", { action: "register", redirect: false });
- if (!res) {
- setStatus("error");
- return;
- }
- if (res.error) {
- setStatus("error");
- return;
- }
- setStatus("success");
- await refetch();
- } catch {
- setStatus("error");
- }
- };
+ const me = parseUserProfile(meData);
return (