From ec0ae60b1041b828e31538805e88ba1c2a14792b Mon Sep 17 00:00:00 2001 From: Chris Kruining Date: Wed, 21 May 2025 15:53:59 +0200 Subject: [PATCH] blocked for now, but it's a start --- public/.well-known/web-identity | 5 ++ public/fedcm.json | 26 ++++++++ public/privacy-policy.txt | 1 + public/terms-of-service.txt | 1 + src/features/auth/index.ts | 95 +++++++++++++++++++++++++++ src/routes/auth/client/index.tsx | 29 ++++++++ src/routes/auth/idp/api/[...404].ts | 8 +++ src/routes/auth/idp/api/accounts.ts | 23 +++++++ src/routes/auth/idp/api/disconnect.ts | 11 ++++ src/routes/auth/idp/api/idtokens.ts | 11 ++++ src/routes/auth/idp/api/login.ts | 35 ++++++++++ src/routes/auth/idp/api/metadata.ts | 10 +++ src/routes/auth/idp/index.tsx | 17 +++++ 13 files changed, 272 insertions(+) create mode 100644 public/.well-known/web-identity create mode 100644 public/fedcm.json create mode 100644 public/privacy-policy.txt create mode 100644 public/terms-of-service.txt create mode 100644 src/features/auth/index.ts create mode 100644 src/routes/auth/client/index.tsx create mode 100644 src/routes/auth/idp/api/[...404].ts create mode 100644 src/routes/auth/idp/api/accounts.ts create mode 100644 src/routes/auth/idp/api/disconnect.ts create mode 100644 src/routes/auth/idp/api/idtokens.ts create mode 100644 src/routes/auth/idp/api/login.ts create mode 100644 src/routes/auth/idp/api/metadata.ts create mode 100644 src/routes/auth/idp/index.tsx diff --git a/public/.well-known/web-identity b/public/.well-known/web-identity new file mode 100644 index 0000000..0126daa --- /dev/null +++ b/public/.well-known/web-identity @@ -0,0 +1,5 @@ +{ + "provider_urls": [ + "http://localhost:3000/fedcm.json" + ] +} diff --git a/public/fedcm.json b/public/fedcm.json new file mode 100644 index 0000000..afd270f --- /dev/null +++ b/public/fedcm.json @@ -0,0 +1,26 @@ +{ + "accounts_endpoint": "/auth/idp/api/accounts", + "client_metadata_endpoint": "/auth/idp/api/metadata", + "id_assertion_endpoint": "/auth/idp/api/idtokens", + "disconnect_endpoint": "/auth/idp/api/disconnect", + "login_url": "/auth/idp", + "modes": { + "active": { + "supports_use_other_account": true + } + }, + "branding": { + "background_color": "#6200ee", + "color": "#ffffff", + "icons": [ + { + "url": "/images/favicon.dark.svg", + "size": 512 + }, + { + "url": "/images/favicon.light.svg", + "size": 512 + } + ] + } +} diff --git a/public/privacy-policy.txt b/public/privacy-policy.txt new file mode 100644 index 0000000..96d2fd0 --- /dev/null +++ b/public/privacy-policy.txt @@ -0,0 +1 @@ +Privacy Policy comes here. diff --git a/public/terms-of-service.txt b/public/terms-of-service.txt new file mode 100644 index 0000000..7301b27 --- /dev/null +++ b/public/terms-of-service.txt @@ -0,0 +1 @@ +Terms of Service comes here. diff --git a/src/features/auth/index.ts b/src/features/auth/index.ts new file mode 100644 index 0000000..6c6d4e3 --- /dev/null +++ b/src/features/auth/index.ts @@ -0,0 +1,95 @@ +import { json, redirect } from "@solidjs/router"; +import { APIEvent } from "@solidjs/start/server"; +import { useSession } from "vinxi/http"; + +export type Middleware = (event: APIEvent) => Response | Promise | void | Promise | Promise; +export interface User { + id: string; + username: string; + credential: string; + givenName: string; + familyName: string; + picture: string; + approvedClients: any[]; +} + +const USERS: User[] = [ + { id: '20d701f3-0f9f-4c21-a379-81b49f755f9e', username: 'chris', credential: 'test', givenName: 'Chris', familyName: 'Kruining', picture: '', approvedClients: [] }, + { id: '10199201-1564-47db-b67b-07088ff05de8', username: 'john', credential: 'test', givenName: 'John', familyName: 'Doe', picture: '', approvedClients: [] }, + { id: '633c44b3-8d3d-4dd1-8e1c-7de355d6dced', username: 'chris_alt', credential: 'test', givenName: 'Chris', familyName: 'Kruining', picture: '', approvedClients: [] }, + { id: 'b9759798-8a41-4961-94a6-feb2372de9cf', username: 'john_alt', credential: 'test', givenName: 'John', familyName: 'Doe', picture: '', approvedClients: [] }, +]; + +export const getUser = (idOrUsername: string) => { + return USERS.find(u => u.id === idOrUsername || u.username === idOrUsername); +}; + +export const signIn = async (user: User) => { + const { update } = await useSession<{ signedIn?: boolean, id?: string }>({ + password: process.env.SESSION_SECRET!, + }); + + await update({ signedIn: true, id: user.id }); +}; + +export const signOut = async () => { + const { update } = await useSession<{ signedIn?: boolean, id?: string }>({ + password: process.env.SESSION_SECRET!, + }); + + await update({}); +}; + +export const use = (...middlewares: Middleware[]) => { + return (event: APIEvent) => { + for (const handler of middlewares) { + const response = handler(event); + + if (response !== undefined) { + return response; + } + } + }; +}; + +export const assertCsrf: Middleware = async ({ request }: APIEvent) => { + if (request.headers.get('Sec-Fetch-Dest') !== 'webidentity') { + console.log('request failed the csrf test'); + + return json({ error: 'Invalid access' }, { status: 400 }); + } +}; + +export const assertSession: Middleware = async ({ request, locals }: APIEvent) => { + const user = await getSession(); + + if (user === undefined) { + console.log('user session not available'); + + return redirect('/auth/idp', { status: 401 }); + } + + locals.user = user; +}; + +export const assertApiSession = async ({ request, locals }: APIEvent) => { + const user = await getSession(); + + if (user === undefined) { + return json({ error: 'not signed in' }, { status: 401 }); + } + + locals.user = user; +}; + +const getSession = async () => { + const { data } = await useSession<{ signedIn?: boolean, id?: string }>({ + password: process.env.SESSION_SECRET!, + }); + + if (data.signedIn !== true) { + return; + } + + return USERS.find(u => u.id === data.id); +}; \ No newline at end of file diff --git a/src/routes/auth/client/index.tsx b/src/routes/auth/client/index.tsx new file mode 100644 index 0000000..aafdf75 --- /dev/null +++ b/src/routes/auth/client/index.tsx @@ -0,0 +1,29 @@ +import { onMount } from "solid-js"; + +export default function Index() { + onMount(async () => { + try { + // navigator.login.setStatus('logged-in'); + + const credential = await navigator.credentials.get({ + identity: { + providers: [{ + configURL: new URL('http://localhost:3000/fedcm.json'), + clientId: '/auth/client', + nonce: 'kaas', + loginHint: 'chris', + }], + mode: 'passive', + context: undefined, + }, + mediation: undefined, + }); + + console.log(credential); + } catch(e) { + console.error(e); + } + }); + + return 'WOOT'; +} \ No newline at end of file diff --git a/src/routes/auth/idp/api/[...404].ts b/src/routes/auth/idp/api/[...404].ts new file mode 100644 index 0000000..72eefc3 --- /dev/null +++ b/src/routes/auth/idp/api/[...404].ts @@ -0,0 +1,8 @@ +import { json } from "@solidjs/router"; +import { APIEvent } from "@solidjs/start/server"; + +export const GET = ({ request }: APIEvent) => { + console.error(`url not found ${request.url}`); + + // return json({ error: `url ${request.url} is not implemented` }, { status: 404 }) +}; \ No newline at end of file diff --git a/src/routes/auth/idp/api/accounts.ts b/src/routes/auth/idp/api/accounts.ts new file mode 100644 index 0000000..e660048 --- /dev/null +++ b/src/routes/auth/idp/api/accounts.ts @@ -0,0 +1,23 @@ +import { json } from "@solidjs/router"; +import { APIEvent } from "@solidjs/start/server"; +import { assertApiSession, assertCsrf, use, User } from "~/features/auth"; + +export const GET = use(assertCsrf, assertApiSession, async (event: APIEvent) => { + const user = event.locals.user as User; + + console.log('accounts endpoint', user); + + return json({ + accounts: [ + { + id: user.id, + given_name: user.givenName, + name: `${user.givenName} ${user.familyName}`, + email: user.username, + picture: user.picture, + login_hints: [user.username], + approved_clients: user.approvedClients, + } + ], + }); +}); \ No newline at end of file diff --git a/src/routes/auth/idp/api/disconnect.ts b/src/routes/auth/idp/api/disconnect.ts new file mode 100644 index 0000000..7a9df6b --- /dev/null +++ b/src/routes/auth/idp/api/disconnect.ts @@ -0,0 +1,11 @@ +import { json } from "@solidjs/router"; +import { APIEvent } from "@solidjs/start/server"; +import { use, assertCsrf, assertApiSession } from "~/features/auth"; + +export const POST = use(assertCsrf, assertApiSession, async ({ request, locals }: APIEvent) => { + console.log(locals, request); + + return json({ + account_id: locals.user.id, + }); +}); \ No newline at end of file diff --git a/src/routes/auth/idp/api/idtokens.ts b/src/routes/auth/idp/api/idtokens.ts new file mode 100644 index 0000000..a6897c8 --- /dev/null +++ b/src/routes/auth/idp/api/idtokens.ts @@ -0,0 +1,11 @@ +import { json } from "@solidjs/router"; +import { APIEvent } from "@solidjs/start/server"; +import { use, assertCsrf, assertApiSession } from "~/features/auth"; + +export const POST = use(assertCsrf, assertApiSession, async ({ request }: APIEvent) => { + console.log(request); + + return json({ + token: 'THIS IS A BEAUTIFUL TOKEN', + }); +}); \ No newline at end of file diff --git a/src/routes/auth/idp/api/login.ts b/src/routes/auth/idp/api/login.ts new file mode 100644 index 0000000..ab160cc --- /dev/null +++ b/src/routes/auth/idp/api/login.ts @@ -0,0 +1,35 @@ +import { json, redirect } from "@solidjs/router"; +import { APIEvent } from "@solidjs/start/server"; +import { getUser, signIn } from "~/features/auth"; + +export const POST = async ({ request }: APIEvent) => { + const formData = await request.formData(); + const username = formData.get('username'); + const password = formData.get('password'); + + if (typeof username !== 'string' || /^[a-z0-9-_]+$/.test(username) !== true) { + return json({ error: 'Bad request' }, { status: 400 }) + } + + if (typeof password !== 'string' || password.length === 0) { + return json({ error: 'Bad request' }, { status: 400 }) + } + + const user = getUser(username); + + if (user === undefined) { + return json({ error: 'Invalid credentials' }, { status: 400 }); + } + + if (user.credential !== password) { + return json({ error: 'Invalid credentials' }, { status: 400 }); + } + + await signIn(user); + + return redirect('/auth/client', { + headers: { + 'Set-Login': 'logged-in', + } + }); +}; \ No newline at end of file diff --git a/src/routes/auth/idp/api/metadata.ts b/src/routes/auth/idp/api/metadata.ts new file mode 100644 index 0000000..b9c2af3 --- /dev/null +++ b/src/routes/auth/idp/api/metadata.ts @@ -0,0 +1,10 @@ +import { json } from "@solidjs/router"; +import { APIEvent } from "@solidjs/start/server"; + +export const GET = ({ request }: APIEvent) => { + return json({ + privacy_policy_url: '/privacy-policy.txt', + terms_of_service_url: '/terms-of-service.txt', + icons: [] + }); +}; \ No newline at end of file diff --git a/src/routes/auth/idp/index.tsx b/src/routes/auth/idp/index.tsx new file mode 100644 index 0000000..00e5eb6 --- /dev/null +++ b/src/routes/auth/idp/index.tsx @@ -0,0 +1,17 @@ +import { useParams, useSearchParams } from "@solidjs/router" + + +export default function Login() { + const [params] = useSearchParams(); + + return
+

Login

+ +
+ + + + +
+
+} \ No newline at end of file