Skip to content

Commit 499b1ba

Browse files
author
jarvis
committed
feat(auth): add OIDC multi-user mode
1 parent 6cbf63f commit 499b1ba

62 files changed

Lines changed: 1779 additions & 147 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,28 @@
1-
# Required authentication
1+
# Single-user authentication (used only when OIDC is not configured)
22
# Workers Runtime: set as an encrypted secret under Worker Variables and Secrets.
33
# Pages: set as an encrypted secret under the unified Variables and Secrets.
44
NUXT_SITE_TOKEN=replace-with-a-strong-private-token
55

6+
# OIDC multi-user authentication
7+
# Setting any required OIDC value selects OIDC mode. Set all four required
8+
# values (issuer, client ID, client secret, and session secret) together.
9+
# Register a confidential web client with this exact callback URL:
10+
# https://your-sink.example/api/auth/callback
11+
# Keep the client and session secrets in encrypted runtime secrets.
12+
NUXT_OIDC_ISSUER=
13+
NUXT_OIDC_CLIENT_ID=
14+
NUXT_OIDC_CLIENT_SECRET=
15+
NUXT_OIDC_REDIRECT_URI=
16+
NUXT_OIDC_SESSION_SECRET=
17+
NUXT_OIDC_SESSION_TTL_SECONDS=28800
18+
19+
# Optional OIDC multi-user administration and identity continuity
20+
# Comma-separated OIDC email claims allowed to run site-wide administration.
21+
NUXT_SITE_ADMIN_EMAILS=
22+
# JSON map from a current OIDC subject to prior owner IDs it may continue to access.
23+
# Example: {"current-subject":["previous-subject"]}
24+
NUXT_LINK_OWNER_ALIASES=
25+
626
# Required deployment configuration
727
# Workers Build: set in Workers Builds variables.
828
# Pages: set in the unified Variables and Secrets.

app/components/dashboard/Logout.vue

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,15 @@ const slots = defineSlots<{
1818
const open = defineModel<boolean>('open', { default: false })
1919
const { authMethod, accessEnabled, clearAuthSession } = useAuthSession()
2020
21-
function logOut() {
21+
async function logOut() {
2222
const method = authMethod.value || (getAuthToken() ? 'site-token' : 'access-user')
23+
24+
if (method === 'oidc-session') {
25+
clearAuthSession()
26+
await signOutFromOidc()
27+
return
28+
}
29+
2330
const shouldLogoutAccess = accessEnabled.value || method === 'access-user' || method === 'access-service'
2431
removeAuthToken()
2532
clearAuthSession()

app/components/login/index.vue

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
<script setup lang="ts">
2+
const { data: oidcConfig } = useFetch<{ enabled: boolean }>('/api/auth/config')
3+
</script>
4+
15
<template>
26
<Card class="w-full max-w-sm">
37
<CardHeader>
@@ -11,7 +15,12 @@
1115
</CardDescription>
1216
</CardHeader>
1317
<CardContent class="grid gap-4">
14-
<LoginForm />
18+
<template v-if="oidcConfig?.enabled">
19+
<Button type="button" class="w-full" @click="signInWithOidc()">
20+
{{ $t('login.oidc_submit') }}
21+
</Button>
22+
</template>
23+
<LoginForm v-else />
1524
</CardContent>
1625
</Card>
1726
</template>

app/utils/api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export async function useAPI(api: string, options?: APIOptions): Promise<unknown
1010
const { headers, ...fetchOptions } = options ?? {}
1111
const requestOptions: NitroFetchOptions<NitroFetchRequest> = {
1212
...fetchOptions,
13+
credentials: 'same-origin',
1314
headers: {
1415
'Authorization': `Bearer ${getAuthToken() ?? ''}`,
1516
'X-Requested-With': 'XMLHttpRequest',

app/utils/oidc.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { useAPI } from './api'
2+
3+
export function signInWithOidc(returnTo = '/dashboard'): void {
4+
if (!import.meta.client)
5+
return
6+
7+
const safeReturnTo = returnTo.startsWith('/dashboard') && !returnTo.startsWith('//')
8+
? returnTo
9+
: '/dashboard'
10+
window.location.assign(`/api/auth/login?returnTo=${encodeURIComponent(safeReturnTo)}`)
11+
}
12+
13+
export async function signOutFromOidc(): Promise<void> {
14+
await useAPI('/api/auth/logout', {
15+
method: 'POST',
16+
})
17+
window.location.assign('/dashboard/login')
18+
}

docs/api/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Send your site password in the `Authorization` header:
2323
Authorization: Bearer YOUR_SITE_TOKEN
2424
```
2525

26-
(`Bearer` means “here is the token”.) It must match `NUXT_SITE_TOKEN` exactly (at least 8 characters). With [Cloudflare Access](/configuration/cloudflare-access) enabled, browsers can also authenticate with a verified Access login.
26+
(`Bearer` means “here is the token”.) In single-user mode it must match `NUXT_SITE_TOKEN` exactly (at least 8 characters). In OIDC multi-user mode, protected APIs require the browser's OIDC session and reject the site token. OIDC access and ID tokens remain on the server and are never stored by the browser.
2727

2828
## CORS
2929

docs/configuration/cloudflare-access.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ description: Optional Zero Trust login for the Sink dashboard, while keeping sho
77

88
Cloudflare Access is **optional**. Use it when you want people to sign in to the dashboard with your company identity (Google, email OTP, SSO, and so on) instead of only sharing `NUXT_SITE_TOKEN`.
99

10+
This integration applies to Sink's single-user mode. Configuring OIDC selects the separate multi-user mode, where protected APIs accept only OIDC sessions and ignore both Cloudflare Access identity and the site token.
11+
1012
Short links stay public either way. Access only affects who can open the dashboard and call the API.
1113

1214
## What changes after you enable it

docs/configuration/index.md

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,31 @@ Set this yourself. It is the **dashboard login password** and the **API password
5252
If you leave it empty, Sink may invent a random password at build time that can change on the next deploy.
5353
:::
5454

55-
| Variable | When | Where | Purpose |
56-
| ------------------------ | ---------------- | ------------------------------------ | ----------------------------------------- |
57-
| `NUXT_SITE_TOKEN` | Runtime (secret) | Encrypted secret on Workers or Pages | Login + API password |
58-
| `DEPLOY_D1_DATABASE_ID` | Build | Workers Builds or Pages variables | D1 database ID (from the D1 detail page) |
59-
| `DEPLOY_KV_NAMESPACE_ID` | Build | Workers Builds or Pages variables | KV namespace ID (from the KV detail page) |
55+
| Variable | When | Where | Purpose |
56+
| ------------------------------- | ---------------- | ------------------------------------ | ---------------------------------------------------------- |
57+
| `NUXT_SITE_TOKEN` | Runtime (secret) | Encrypted secret on Workers or Pages | Single-user login + API password |
58+
| `NUXT_OIDC_ISSUER` | Runtime | Worker/Pages variable | Optional OpenID Connect issuer URL |
59+
| `NUXT_OIDC_CLIENT_ID` | Runtime | Worker/Pages variable | Confidential web client ID |
60+
| `NUXT_OIDC_CLIENT_SECRET` | Runtime (secret) | Encrypted secret on Workers or Pages | Confidential web client secret |
61+
| `NUXT_OIDC_REDIRECT_URI` | Runtime | Worker/Pages variable | Exact callback URL ending in `/api/auth/callback` |
62+
| `NUXT_OIDC_SESSION_SECRET` | Runtime (secret) | Encrypted secret on Workers or Pages | Random value of at least 32 characters for signed sessions |
63+
| `NUXT_OIDC_SESSION_TTL_SECONDS` | Runtime | Worker/Pages variable | Local session limit; defaults to `28800` |
64+
| `DEPLOY_D1_DATABASE_ID` | Build | Workers Builds or Pages variables | D1 database ID (from the D1 detail page) |
65+
| `DEPLOY_KV_NAMESPACE_ID` | Build | Workers Builds or Pages variables | KV namespace ID (from the KV detail page) |
66+
67+
### Authentication modes
68+
69+
Sink uses one authentication mode at a time:
70+
71+
- Without OIDC configuration, Sink remains a single-user application. The site token and Cloudflare Access use the shared `root` owner.
72+
- Configuring OIDC enables multi-user mode. Browser and API access then require an OIDC session; the site token and Cloudflare Access no longer authenticate protected APIs.
73+
- Setting only part of the required OIDC configuration fails closed. Configure the issuer, client ID, client secret, and session secret together.
74+
75+
### User ownership
76+
77+
In OIDC multi-user mode, each verified identity gets an isolated set of links, tags, and analytics. Sink uses the provider's stable subject identifier as the owner ID. In single-user mode, links created through the site token or Cloudflare Access and existing links upgraded from an earlier release belong to the shared `root` owner.
78+
79+
Short-link slugs remain globally unique because every public redirect shares the same hostname. A user therefore receives a conflict when another user already owns the requested slug, without gaining access to that link.
6080

6181
## Recommended (analytics)
6282

docs/zh-CN/api/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ description: Sink 的 OpenAPI 文档、身份认证、CORS 与端点索引。
2323
Authorization: Bearer YOUR_SITE_TOKEN
2424
```
2525

26-
`Bearer` 的意思是「后面是令牌」。)必须与 `NUXT_SITE_TOKEN` 完全一致(至少 8 个字符)。启用 [Cloudflare Access](/zh-CN/configuration/cloudflare-access) 后,浏览器也可以用已验证的 Access 登录访问 API
26+
`Bearer` 的意思是「后面是令牌」。)在单用户模式下,它必须与 `NUXT_SITE_TOKEN` 完全一致(至少 8 个字符)。在 OIDC 多用户模式下,受保护的 API 必须使用浏览器的 OIDC Session,并拒绝站点令牌。OIDC access token 和 ID token 始终保留在服务端,不会存入浏览器
2727

2828
## CORS
2929

docs/zh-CN/configuration/cloudflare-access.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ description: 为 Sink 仪表盘启用可选的 Zero Trust 登录,同时保持
77

88
Cloudflare Access 是**可选**功能。适合希望用公司身份(Google、邮箱 OTP、SSO 等)登录仪表盘,而不是只靠分享 `NUXT_SITE_TOKEN` 的场景。
99

10+
此集成只适用于 Sink 的单用户模式。配置 OIDC 后会进入独立的多用户模式,此时受保护的 API 只接受 OIDC Session,不再使用 Cloudflare Access 身份或站点令牌。
11+
1012
无论是否启用 Access,短链接都保持公开。Access 只影响谁能打开仪表盘、谁能调用 API。
1113

1214
## 启用后会发生什么

0 commit comments

Comments
 (0)