Authentik OIDC login (oppgave 3.2)
Implementerer autentisering med Authentik via @auth/sveltekit:
- OIDC authorization code flow med PKCE og state-verifisering
- JWT-callback lagrer authentik_sub (SHA256-hash, ikke UUID) for
konsistens med maskinrommets auth_identities-tabell
- Server hooks: alle ruter unntatt /signin og /auth/* krever sesjon
- Uautentiserte brukere redirectes til /signin (303)
- Innloggingsside med client-side signIn('authentik')
- Hovedside viser innlogget bruker med logg ut-knapp
- TypeScript-typer utvidet med JWT.authentik_sub
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
073de4b148
commit
6bc742cc8d
12 changed files with 341 additions and 121 deletions
|
|
@ -36,7 +36,7 @@ callbacks: {
|
||||||
|
|
||||||
`profile` er kun tilgjengelig i JWT-callbacken ved innlogging (ikke ved token-refresh), derfor må `authentik_sub` lagres i tokenet.
|
`profile` er kun tilgjengelig i JWT-callbacken ved innlogging (ikke ved token-refresh), derfor må `authentik_sub` lagres i tokenet.
|
||||||
|
|
||||||
**Referanse:** `web/src/lib/server/auth.ts`
|
**Referanse:** `frontend/src/auth.ts`
|
||||||
|
|
||||||
## 3. Redirect-URI i Authentik
|
## 3. Redirect-URI i Authentik
|
||||||
|
|
||||||
|
|
|
||||||
307
frontend/package-lock.json
generated
307
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -21,5 +21,9 @@
|
||||||
"tailwindcss": "^4.2.1",
|
"tailwindcss": "^4.2.1",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^7.3.1"
|
"vite": "^7.3.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@auth/core": "^0.34.3",
|
||||||
|
"@auth/sveltekit": "^1.11.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
15
frontend/src/app.d.ts
vendored
15
frontend/src/app.d.ts
vendored
|
|
@ -1,13 +1,22 @@
|
||||||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||||
// for information about these interfaces
|
// Types are augmented by @auth/sveltekit (see node_modules/@auth/sveltekit/dist/types.d.ts)
|
||||||
|
|
||||||
|
import type { Session } from '@auth/core/types';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
namespace App {
|
namespace App {
|
||||||
// interface Error {}
|
// interface Error {}
|
||||||
// interface Locals {}
|
// interface Locals {} — extended by @auth/sveltekit
|
||||||
// interface PageData {}
|
|
||||||
// interface PageState {}
|
// interface PageState {}
|
||||||
// interface Platform {}
|
// interface Platform {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extend JWT token with authentik_sub
|
||||||
|
declare module '@auth/core/jwt' {
|
||||||
|
interface JWT {
|
||||||
|
authentik_sub?: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export {};
|
export {};
|
||||||
|
|
|
||||||
55
frontend/src/auth.ts
Normal file
55
frontend/src/auth.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { SvelteKitAuth } from '@auth/sveltekit';
|
||||||
|
import type { OIDCConfig } from '@auth/core/providers';
|
||||||
|
import { env } from '$env/dynamic/private';
|
||||||
|
|
||||||
|
interface AuthentikProfile {
|
||||||
|
sub: string;
|
||||||
|
email: string;
|
||||||
|
name: string;
|
||||||
|
preferred_username: string;
|
||||||
|
groups: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const { handle, signIn, signOut } = SvelteKitAuth({
|
||||||
|
trustHost: true,
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
id: 'authentik',
|
||||||
|
name: 'Authentik',
|
||||||
|
type: 'oidc',
|
||||||
|
issuer: env.AUTHENTIK_ISSUER,
|
||||||
|
clientId: env.AUTHENTIK_CLIENT_ID,
|
||||||
|
clientSecret: env.AUTHENTIK_CLIENT_SECRET,
|
||||||
|
authorization: {
|
||||||
|
params: {
|
||||||
|
scope: 'openid email profile offline_access'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
checks: ['pkce', 'state'],
|
||||||
|
profile(profile: AuthentikProfile) {
|
||||||
|
return {
|
||||||
|
id: profile.sub,
|
||||||
|
name: profile.name ?? profile.preferred_username,
|
||||||
|
email: profile.email
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} satisfies OIDCConfig<AuthentikProfile>
|
||||||
|
],
|
||||||
|
callbacks: {
|
||||||
|
jwt({ token, profile }) {
|
||||||
|
// profile is only available on initial sign-in, not on refresh.
|
||||||
|
// Store authentik_sub in the token so it persists across refreshes.
|
||||||
|
if (profile?.sub) {
|
||||||
|
token.authentik_sub = profile.sub;
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
},
|
||||||
|
session({ session, token }) {
|
||||||
|
if (session.user) {
|
||||||
|
// Use Authentik sub as user ID (not @auth/sveltekit's internal ID)
|
||||||
|
session.user.id = (token.authentik_sub ?? token.sub) as string;
|
||||||
|
}
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
23
frontend/src/hooks.server.ts
Normal file
23
frontend/src/hooks.server.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { redirect, type Handle } from '@sveltejs/kit';
|
||||||
|
import { handle as authHandle } from './auth';
|
||||||
|
import { sequence } from '@sveltejs/kit/hooks';
|
||||||
|
|
||||||
|
/** Protect all routes except /signin and /auth/* (OIDC callback paths). */
|
||||||
|
const authorizationHandle: Handle = async ({ event, resolve }) => {
|
||||||
|
const path = event.url.pathname;
|
||||||
|
|
||||||
|
// Allow auth-related routes through without session check
|
||||||
|
if (path.startsWith('/auth/') || path === '/signin') {
|
||||||
|
return resolve(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await event.locals.auth();
|
||||||
|
if (!session?.user) {
|
||||||
|
throw redirect(303, '/signin');
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolve(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Authentication first (sets up locals.auth), then authorization
|
||||||
|
export const handle: Handle = sequence(authHandle, authorizationHandle);
|
||||||
7
frontend/src/routes/+layout.server.ts
Normal file
7
frontend/src/routes/+layout.server.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import type { LayoutServerLoad } from './$types';
|
||||||
|
|
||||||
|
export const load: LayoutServerLoad = async (event) => {
|
||||||
|
return {
|
||||||
|
session: await event.locals.auth()
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -1,6 +1,22 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { signOut } from '@auth/sveltekit/client';
|
||||||
|
</script>
|
||||||
|
|
||||||
<div class="flex min-h-screen items-center justify-center bg-gray-50">
|
<div class="flex min-h-screen items-center justify-center bg-gray-50">
|
||||||
<div class="text-center">
|
<div class="text-center space-y-4">
|
||||||
<h1 class="text-4xl font-bold text-gray-900">Synops</h1>
|
<h1 class="text-4xl font-bold text-gray-900">Synops</h1>
|
||||||
<p class="mt-2 text-gray-600">Plattform for redaksjonelt arbeid</p>
|
<p class="text-gray-600">Plattform for redaksjonelt arbeid</p>
|
||||||
|
{#if $page.data.session?.user}
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
Innlogget som <span class="font-medium text-gray-700">{$page.data.session.user.name ?? $page.data.session.user.email}</span>
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onclick={() => signOut()}
|
||||||
|
class="rounded-lg bg-gray-200 px-4 py-2 text-sm text-gray-700 hover:bg-gray-300"
|
||||||
|
>
|
||||||
|
Logg ut
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
4
frontend/src/routes/signin/+page.server.ts
Normal file
4
frontend/src/routes/signin/+page.server.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
import { signIn } from '../../auth';
|
||||||
|
import type { Actions } from './$types';
|
||||||
|
|
||||||
|
export const actions: Actions = { default: signIn };
|
||||||
18
frontend/src/routes/signin/+page.svelte
Normal file
18
frontend/src/routes/signin/+page.svelte
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { signIn } from '@auth/sveltekit/client';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex min-h-screen items-center justify-center bg-gray-50">
|
||||||
|
<div class="w-full max-w-sm space-y-6 text-center">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-4xl font-bold text-gray-900">Synops</h1>
|
||||||
|
<p class="mt-2 text-gray-600">Logg inn for å fortsette</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onclick={() => signIn('authentik')}
|
||||||
|
class="w-full rounded-lg bg-gray-900 px-4 py-3 text-sm font-medium text-white hover:bg-gray-800 focus:ring-2 focus:ring-gray-900 focus:ring-offset-2 focus:outline-none"
|
||||||
|
>
|
||||||
|
Logg inn med Authentik
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
4
frontend/src/routes/signout/+page.server.ts
Normal file
4
frontend/src/routes/signout/+page.server.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
import { signOut } from '../../auth';
|
||||||
|
import type { Actions } from './$types';
|
||||||
|
|
||||||
|
export const actions: Actions = { default: signOut };
|
||||||
3
tasks.md
3
tasks.md
|
|
@ -61,8 +61,7 @@ Uavhengige faser kan fortsatt plukkes.
|
||||||
## Fase 3: Frontend — skjelett
|
## Fase 3: Frontend — skjelett
|
||||||
|
|
||||||
- [x] 3.1 SvelteKit-prosjekt: opprett `frontend/` med TypeScript, TailwindCSS. PWA-manifest. Lokal dev med HMR.
|
- [x] 3.1 SvelteKit-prosjekt: opprett `frontend/` med TypeScript, TailwindCSS. PWA-manifest. Lokal dev med HMR.
|
||||||
- [~] 3.2 Authentik login: OIDC-flow (authorization code + PKCE). Session-håndtering. Redirect til login ved 401.
|
- [x] 3.2 Authentik login: OIDC-flow (authorization code + PKCE). Session-håndtering. Redirect til login ved 401.
|
||||||
> Påbegynt: 2026-03-17T13:40
|
|
||||||
- [ ] 3.3 STDB WebSocket-klient: abonner på noder og edges. Reaktiv Svelte-store som oppdateres ved endringer.
|
- [ ] 3.3 STDB WebSocket-klient: abonner på noder og edges. Reaktiv Svelte-store som oppdateres ved endringer.
|
||||||
- [ ] 3.4 Mottaksflaten v0: vis noder med edge til innlogget bruker, sortert på `created_at`. Enkel liste med tittel og utdrag.
|
- [ ] 3.4 Mottaksflaten v0: vis noder med edge til innlogget bruker, sortert på `created_at`. Enkel liste med tittel og utdrag.
|
||||||
- [ ] 3.5 TipTap-editor: enkel preset (tekst, markdown, lenker). Send `create_node`-intensjon til maskinrommet ved submit.
|
- [ ] 3.5 TipTap-editor: enkel preset (tekst, markdown, lenker). Send `create_node`-intensjon til maskinrommet ved submit.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue