@frigear-nu/oauth-server 0.0.7 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/module.d.mts +1 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +26 -27
- package/dist/runtime/app/pages/admin.vue +112 -116
- package/dist/runtime/app/pages/authorize.vue +1 -1
- package/dist/runtime/menus.js +10 -0
- package/dist/runtime/server/mcp/tools/my-oauth-applications.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/my-oauth-applications.js +6 -0
- package/dist/runtime/server/resolvers/access-token.d.ts +12 -0
- package/dist/runtime/server/resolvers/access-token.js +14 -0
- package/dist/runtime/server/utils/oauth-access.d.ts +1 -7
- package/dist/runtime/server/utils/oauth-access.js +3 -6
- package/dist/runtime/server/utils/oauth-authorize.d.ts +1 -1
- package/dist/runtime/server/utils/oauth-authorize.js +1 -1
- package/dist/runtime/server/utils/oauth-config.d.ts +4 -1
- package/dist/runtime/server/utils/oauth-config.js +3 -2
- package/package.json +13 -5
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ export default defineNitroPlugin((nitro) => {
|
|
|
39
39
|
})
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
Full documentation with examples: `apps/docs/content/4.modules/
|
|
42
|
+
Full documentation with examples: `apps/docs/content/4.modules/12.oauth-server.md`.
|
|
43
43
|
|
|
44
44
|
## Development
|
|
45
45
|
|
package/dist/module.d.mts
CHANGED
|
@@ -20,6 +20,7 @@ type ModuleOptions = {
|
|
|
20
20
|
/** Extra scopes advertised and accepted next to `openid profile email`. Enrich their claims with `oauth:claims`. */
|
|
21
21
|
scopes?: string[];
|
|
22
22
|
settings?: {
|
|
23
|
+
mcp?: boolean;
|
|
23
24
|
pages?: {
|
|
24
25
|
/** Consent screen. On by default (`/authorize`); pass `false` to provide your own page at that path. */
|
|
25
26
|
authorize?: string | boolean;
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import { defineNuxtModule, createResolver, hasNuxtModule, addServerHandler, addServerImportsDir, addImports
|
|
2
|
-
import { registerModule, registerLocales, registerDatabaseTable,
|
|
1
|
+
import { defineNuxtModule, createResolver, hasNuxtModule, addServerHandler, addServerImportsDir, addImports } from '@nuxt/kit';
|
|
2
|
+
import { registerModule, registerTailwindSource, registerLocales, resolvePagePath, registerDatabaseTable, addModulePages, addOptionalMenuSource, moduleApiUrl } from '@frigear-nu/module-kit/utils';
|
|
3
|
+
import { adminRoute } from '@frigear-nu/admin/utils';
|
|
4
|
+
import { addAuthScope, addAuthResolver } from '@frigear-nu/auth/utils';
|
|
5
|
+
import { addMcpDefinitions } from '@frigear-nu/mcp/utils';
|
|
3
6
|
|
|
4
7
|
const MODULE_NAME = "@frigear-nu/oauth-server";
|
|
5
8
|
const API_NAME = "oauthServer";
|
|
6
|
-
function resolvePagePath(setting, defaultPath) {
|
|
7
|
-
if (typeof setting === "string") return setting;
|
|
8
|
-
if (setting === true) return defaultPath;
|
|
9
|
-
return void 0;
|
|
10
|
-
}
|
|
11
9
|
const module$1 = defineNuxtModule({
|
|
12
10
|
meta: { name: MODULE_NAME, configKey: "oauthServer" },
|
|
13
11
|
defaults: {
|
|
@@ -22,17 +20,29 @@ const module$1 = defineNuxtModule({
|
|
|
22
20
|
moduleDependencies: {
|
|
23
21
|
"@nuxthub/core": { version: ">=0.10" },
|
|
24
22
|
"@frigear-nu/module-kit": { version: ">=0.0.1" },
|
|
23
|
+
"@frigear-nu/auth": { version: ">=0.0.1" },
|
|
24
|
+
"@frigear-nu/mcp": { version: ">=0.0.1" },
|
|
25
25
|
"@frigear-nu/users": { version: ">=0.0.1" },
|
|
26
|
+
"@frigear-nu/ui": { version: ">=0.0.1" },
|
|
27
|
+
// Provides the shared page components of the admin page (`AdminPage`, `AdminTable`, ...).
|
|
28
|
+
"@frigear-nu/admin": { version: ">=0.0.0", optional: true },
|
|
26
29
|
"@frigear-nu/menu": { version: ">=0.0.1", optional: true }
|
|
27
30
|
},
|
|
28
31
|
async setup(options, nuxt) {
|
|
29
32
|
registerModule(MODULE_NAME, nuxt);
|
|
33
|
+
addAuthScope(nuxt, { name: "oauth:read", description: "See which applications you can sign in to (MCP)" });
|
|
30
34
|
const resolver = createResolver(import.meta.url);
|
|
35
|
+
addAuthResolver(nuxt, {
|
|
36
|
+
name: "oauth-access-token",
|
|
37
|
+
jwt: true,
|
|
38
|
+
order: 60,
|
|
39
|
+
resolver: resolver.resolve("./runtime/server/resolvers/access-token")
|
|
40
|
+
});
|
|
41
|
+
registerTailwindSource(nuxt, resolver.resolve("./runtime/app"));
|
|
31
42
|
const settings = options.settings ?? {};
|
|
32
|
-
const withMenus = hasNuxtModule("@frigear-nu/menu");
|
|
33
43
|
registerLocales(nuxt, resolver.resolve("./runtime/i18n/lang"));
|
|
34
44
|
const authorizePath = settings.pages?.authorize === false ? "/authorize" : resolvePagePath(settings.pages?.authorize ?? true, "/authorize");
|
|
35
|
-
const adminPath = resolvePagePath(settings.pages?.admin, "
|
|
45
|
+
const adminPath = hasNuxtModule("@frigear-nu/admin") ? resolvePagePath(settings.pages?.admin, adminRoute(nuxt, "oauth")) : void 0;
|
|
36
46
|
const applicationsPath = resolvePagePath(settings.pages?.applications, "/account/applications");
|
|
37
47
|
const existing = nuxt.options.runtimeConfig.oauthServer;
|
|
38
48
|
nuxt.options.runtimeConfig.oauthServer = {
|
|
@@ -80,28 +90,17 @@ const module$1 = defineNuxtModule({
|
|
|
80
90
|
api("post", "admin/applications/:id/secret", "api/admin/application-secret.post");
|
|
81
91
|
api("post", "admin/applications/:id/revoke", "api/admin/application-revoke.post");
|
|
82
92
|
addServerImportsDir(resolver.resolve("./runtime/server/utils"));
|
|
93
|
+
if (settings.mcp === true) addMcpDefinitions(nuxt, resolver.resolve("./runtime/server/mcp"));
|
|
83
94
|
addImports([
|
|
84
95
|
{ name: "useOAuthApplications", from: resolver.resolve("./runtime/app/composables/useOAuthApplications") },
|
|
85
96
|
{ name: "useMyApplications", from: resolver.resolve("./runtime/app/composables/useOAuthApplications") }
|
|
86
97
|
]);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (adminPath && !existingPaths.has(adminPath)) {
|
|
94
|
-
pages.push(defineModulePage(MODULE_NAME, { name: "oauth-admin", path: adminPath, file: resolver.resolve("./runtime/app/pages/admin.vue") }));
|
|
95
|
-
}
|
|
96
|
-
if (applicationsPath && !existingPaths.has(applicationsPath)) {
|
|
97
|
-
pages.push(defineModulePage(MODULE_NAME, { name: "oauth-applications", path: applicationsPath, file: resolver.resolve("./runtime/app/pages/applications.vue") }));
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
if (withMenus) {
|
|
102
|
-
const { registerMenuSource } = await import('@frigear-nu/menu/utils');
|
|
103
|
-
registerMenuSource(nuxt, { file: resolver.resolve("./runtime/menus"), menu: "dashboard.account" });
|
|
104
|
-
}
|
|
98
|
+
addModulePages(MODULE_NAME, resolver, [
|
|
99
|
+
{ name: "oauth-authorize", path: settings.pages?.authorize !== false ? authorizePath : void 0, file: "./runtime/app/pages/authorize.vue" },
|
|
100
|
+
{ name: "oauth-admin", path: adminPath, file: "./runtime/app/pages/admin.vue" },
|
|
101
|
+
{ name: "oauth-applications", path: applicationsPath, file: "./runtime/app/pages/applications.vue" }
|
|
102
|
+
]);
|
|
103
|
+
addOptionalMenuSource(nuxt, { file: resolver.resolve("./runtime/menus"), menu: "dashboard.account" });
|
|
105
104
|
}
|
|
106
105
|
});
|
|
107
106
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
defineOptions({ name: "OAuthAdminPage" });
|
|
3
|
+
const t = useModuleT("oauthServer");
|
|
4
|
+
const confirmAction = useConfirmDialog();
|
|
3
5
|
const { applications, status, error, create, update, remove, rotateSecret, revokeGrants } = useOAuthApplications();
|
|
4
6
|
const emptyForm = () => ({
|
|
5
7
|
name: "",
|
|
@@ -77,7 +79,12 @@ function submit() {
|
|
|
77
79
|
});
|
|
78
80
|
}
|
|
79
81
|
const rotate = (application) => run(async () => {
|
|
80
|
-
if (!
|
|
82
|
+
if (!await confirmAction({
|
|
83
|
+
title: t("admin.rotateTitle", "Replace the client secret of {name}?", { name: application.name }),
|
|
84
|
+
description: t("admin.rotateDescription", "The old secret stops working and users are signed out."),
|
|
85
|
+
confirmText: t("admin.newSecret", "New secret"),
|
|
86
|
+
destructive: true
|
|
87
|
+
})) return;
|
|
81
88
|
const clientSecret = await rotateSecret(application.id);
|
|
82
89
|
secret.value = { name: application.name, clientId: application.id, clientSecret };
|
|
83
90
|
});
|
|
@@ -85,146 +92,135 @@ const toggle = (application) => run(async () => {
|
|
|
85
92
|
await update(application.id, { isActive: !application.isActive });
|
|
86
93
|
});
|
|
87
94
|
const signOutEveryone = (application) => run(async () => {
|
|
88
|
-
if (
|
|
95
|
+
if (await confirmAction({
|
|
96
|
+
title: t("admin.signOutTitle", "Sign everyone out of {name}?", { name: application.name }),
|
|
97
|
+
confirmText: t("admin.signOutEveryone", "Sign everyone out"),
|
|
98
|
+
destructive: true
|
|
99
|
+
})) await revokeGrants(application.id);
|
|
89
100
|
});
|
|
90
101
|
const destroy = (application) => run(async () => {
|
|
91
|
-
if (
|
|
102
|
+
if (await confirmAction({
|
|
103
|
+
title: t("admin.deleteTitle", "Delete {name}?", { name: application.name }),
|
|
104
|
+
description: t("admin.deleteDescription", "This cannot be undone."),
|
|
105
|
+
confirmText: t("admin.delete", "Delete"),
|
|
106
|
+
destructive: true
|
|
107
|
+
})) await remove(application.id);
|
|
92
108
|
});
|
|
109
|
+
const columns = [
|
|
110
|
+
{ accessorKey: "name", header: t("admin.application", "Application") },
|
|
111
|
+
{ id: "access", header: t("admin.access", "Access") },
|
|
112
|
+
{ id: "grants", header: t("admin.signedIn", "Signed in") },
|
|
113
|
+
{ id: "actions", header: "" }
|
|
114
|
+
];
|
|
115
|
+
const rowActions = (application) => [[
|
|
116
|
+
{ label: t("admin.edit", "Edit"), icon: "i-lucide-pencil", onSelect: () => openEdit(application) },
|
|
117
|
+
{ label: application.isActive ? t("admin.disable", "Disable") : t("admin.enable", "Enable"), icon: application.isActive ? "i-lucide-pause" : "i-lucide-play", onSelect: () => toggle(application) },
|
|
118
|
+
{ label: t("admin.newSecret", "New secret"), icon: "i-lucide-key-round", onSelect: () => rotate(application) },
|
|
119
|
+
{ label: t("admin.signOutEveryone", "Sign everyone out"), icon: "i-lucide-log-out", onSelect: () => signOutEveryone(application) }
|
|
120
|
+
], [
|
|
121
|
+
{ label: t("admin.delete", "Delete"), icon: "i-lucide-trash", color: "error", onSelect: () => destroy(application) }
|
|
122
|
+
]];
|
|
93
123
|
const copy = (value) => navigator.clipboard?.writeText(value);
|
|
94
124
|
</script>
|
|
95
125
|
|
|
96
126
|
<template>
|
|
97
|
-
<
|
|
98
|
-
<
|
|
99
|
-
<
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
>
|
|
107
|
-
|
|
108
|
-
</UButton>
|
|
109
|
-
</div>
|
|
127
|
+
<AdminPage>
|
|
128
|
+
<AdminPageHeader :title="t('admin.title', 'OAuth applications')">
|
|
129
|
+
<template #actions>
|
|
130
|
+
<UButton
|
|
131
|
+
v-if="!forbidden"
|
|
132
|
+
icon="i-lucide-plus"
|
|
133
|
+
:label="t('admin.new', 'New application')"
|
|
134
|
+
@click="openCreate"
|
|
135
|
+
/>
|
|
136
|
+
</template>
|
|
137
|
+
</AdminPageHeader>
|
|
110
138
|
|
|
111
139
|
<UAlert
|
|
112
140
|
v-if="forbidden"
|
|
113
141
|
color="error"
|
|
114
142
|
variant="subtle"
|
|
115
|
-
title="You are not allowed to manage OAuth applications."
|
|
116
|
-
/>
|
|
117
|
-
<UAlert
|
|
118
|
-
v-else-if="failure && !formOpen"
|
|
119
|
-
class="mb-4"
|
|
120
|
-
color="error"
|
|
121
|
-
variant="subtle"
|
|
122
|
-
:title="failure"
|
|
143
|
+
:title="t('admin.forbidden', 'You are not allowed to manage OAuth applications.')"
|
|
123
144
|
/>
|
|
145
|
+
<template v-else>
|
|
146
|
+
<UAlert
|
|
147
|
+
v-if="failure && !formOpen"
|
|
148
|
+
color="error"
|
|
149
|
+
variant="subtle"
|
|
150
|
+
:title="failure"
|
|
151
|
+
/>
|
|
124
152
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
No applications yet.
|
|
130
|
-
</p>
|
|
131
|
-
|
|
132
|
-
<div class="grid gap-4">
|
|
133
|
-
<UCard
|
|
134
|
-
v-for="application in applications"
|
|
135
|
-
:key="application.id"
|
|
153
|
+
<AdminTable
|
|
154
|
+
:data="applications"
|
|
155
|
+
:columns="columns"
|
|
156
|
+
:loading="status === 'pending'"
|
|
136
157
|
>
|
|
137
|
-
<
|
|
158
|
+
<template #name-cell="{ row }">
|
|
138
159
|
<div class="flex min-w-0 items-start gap-3">
|
|
139
160
|
<UIcon
|
|
140
|
-
:name="
|
|
141
|
-
class="mt-
|
|
161
|
+
:name="row.original.icon || 'i-lucide-globe'"
|
|
162
|
+
class="mt-0.5 size-5 shrink-0"
|
|
142
163
|
/>
|
|
143
164
|
<div class="min-w-0">
|
|
144
|
-
<
|
|
145
|
-
{{
|
|
165
|
+
<p class="flex items-center gap-2 font-medium">
|
|
166
|
+
{{ row.original.name }}
|
|
146
167
|
<UBadge
|
|
147
|
-
v-if="!
|
|
168
|
+
v-if="!row.original.isActive"
|
|
148
169
|
color="neutral"
|
|
149
170
|
variant="subtle"
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
</h2>
|
|
171
|
+
:label="t('admin.disabled', 'Disabled')"
|
|
172
|
+
/>
|
|
173
|
+
</p>
|
|
154
174
|
<p class="truncate text-sm text-muted">
|
|
155
|
-
{{
|
|
175
|
+
{{ row.original.websiteUrl }}
|
|
156
176
|
</p>
|
|
157
177
|
<p class="truncate text-xs text-muted">
|
|
158
|
-
Callback: {{
|
|
178
|
+
{{ t("admin.callback", "Callback") }}: {{ row.original.callbackUrl }}
|
|
159
179
|
</p>
|
|
160
|
-
<div class="mt-2 flex flex-wrap gap-1">
|
|
161
|
-
<UBadge
|
|
162
|
-
v-for="role in application.allowedRoles"
|
|
163
|
-
:key="role"
|
|
164
|
-
variant="subtle"
|
|
165
|
-
>
|
|
166
|
-
{{ role }}
|
|
167
|
-
</UBadge>
|
|
168
|
-
<UBadge
|
|
169
|
-
v-if="!application.allowedRoles.length"
|
|
170
|
-
color="neutral"
|
|
171
|
-
variant="subtle"
|
|
172
|
-
>
|
|
173
|
-
Every signed-in user
|
|
174
|
-
</UBadge>
|
|
175
|
-
</div>
|
|
176
180
|
</div>
|
|
177
181
|
</div>
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
:
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
size="sm"
|
|
190
|
-
color="neutral"
|
|
191
|
-
variant="outline"
|
|
192
|
-
:disabled="busy"
|
|
193
|
-
@click="toggle(application)"
|
|
194
|
-
>
|
|
195
|
-
{{ application.isActive ? "Disable" : "Enable" }}
|
|
196
|
-
</UButton>
|
|
197
|
-
<UButton
|
|
198
|
-
size="sm"
|
|
199
|
-
color="neutral"
|
|
200
|
-
variant="outline"
|
|
201
|
-
:disabled="busy"
|
|
202
|
-
@click="rotate(application)"
|
|
203
|
-
>
|
|
204
|
-
New secret
|
|
205
|
-
</UButton>
|
|
206
|
-
<UButton
|
|
207
|
-
size="sm"
|
|
182
|
+
</template>
|
|
183
|
+
<template #access-cell="{ row }">
|
|
184
|
+
<div class="flex flex-wrap gap-1">
|
|
185
|
+
<UBadge
|
|
186
|
+
v-for="role in row.original.allowedRoles"
|
|
187
|
+
:key="role"
|
|
188
|
+
variant="subtle"
|
|
189
|
+
:label="role"
|
|
190
|
+
/>
|
|
191
|
+
<UBadge
|
|
192
|
+
v-if="!row.original.allowedRoles.length"
|
|
208
193
|
color="neutral"
|
|
209
|
-
variant="
|
|
210
|
-
:
|
|
211
|
-
|
|
212
|
-
>
|
|
213
|
-
Sign everyone out
|
|
214
|
-
</UButton>
|
|
215
|
-
<UButton
|
|
216
|
-
size="sm"
|
|
217
|
-
color="error"
|
|
218
|
-
variant="outline"
|
|
219
|
-
:disabled="busy"
|
|
220
|
-
@click="destroy(application)"
|
|
221
|
-
>
|
|
222
|
-
Delete
|
|
223
|
-
</UButton>
|
|
194
|
+
variant="subtle"
|
|
195
|
+
:label="t('admin.everyone', 'Every signed-in user')"
|
|
196
|
+
/>
|
|
224
197
|
</div>
|
|
225
|
-
</
|
|
226
|
-
|
|
227
|
-
|
|
198
|
+
</template>
|
|
199
|
+
<template #grants-cell="{ row }">
|
|
200
|
+
{{ row.original.activeGrants ?? 0 }}
|
|
201
|
+
</template>
|
|
202
|
+
<template #actions-cell="{ row }">
|
|
203
|
+
<div class="flex justify-end">
|
|
204
|
+
<UDropdownMenu :items="rowActions(row.original)">
|
|
205
|
+
<UButton
|
|
206
|
+
icon="i-lucide-ellipsis"
|
|
207
|
+
color="neutral"
|
|
208
|
+
variant="ghost"
|
|
209
|
+
size="sm"
|
|
210
|
+
:disabled="busy"
|
|
211
|
+
:aria-label="t('admin.actions', 'Actions')"
|
|
212
|
+
/>
|
|
213
|
+
</UDropdownMenu>
|
|
214
|
+
</div>
|
|
215
|
+
</template>
|
|
216
|
+
<template #empty>
|
|
217
|
+
<AdminEmpty
|
|
218
|
+
icon="i-lucide-app-window"
|
|
219
|
+
:title="t('admin.empty', 'No applications yet.')"
|
|
220
|
+
/>
|
|
221
|
+
</template>
|
|
222
|
+
</AdminTable>
|
|
223
|
+
</template>
|
|
228
224
|
|
|
229
225
|
<UModal
|
|
230
226
|
v-model:open="formOpen"
|
|
@@ -248,7 +244,7 @@ const copy = (value) => navigator.clipboard?.writeText(value);
|
|
|
248
244
|
</UFormField>
|
|
249
245
|
<UFormField
|
|
250
246
|
label="Website URL"
|
|
251
|
-
|
|
247
|
+
description="The origin of the application, without a path. The callback is the website URL plus the configured callback path."
|
|
252
248
|
required
|
|
253
249
|
>
|
|
254
250
|
<UInput
|
|
@@ -260,7 +256,7 @@ const copy = (value) => navigator.clipboard?.writeText(value);
|
|
|
260
256
|
</UFormField>
|
|
261
257
|
<UFormField
|
|
262
258
|
label="Login URL"
|
|
263
|
-
|
|
259
|
+
description="Where the Open button goes. Defaults to the website URL."
|
|
264
260
|
>
|
|
265
261
|
<UInput
|
|
266
262
|
v-model="form.loginUrl"
|
|
@@ -269,7 +265,7 @@ const copy = (value) => navigator.clipboard?.writeText(value);
|
|
|
269
265
|
</UFormField>
|
|
270
266
|
<UFormField
|
|
271
267
|
label="Preview URL pattern"
|
|
272
|
-
|
|
268
|
+
description="Optional, e.g. https://*.vercel.app, for preview deployments."
|
|
273
269
|
>
|
|
274
270
|
<UInput
|
|
275
271
|
v-model="form.previewUrlPattern"
|
|
@@ -278,7 +274,7 @@ const copy = (value) => navigator.clipboard?.writeText(value);
|
|
|
278
274
|
</UFormField>
|
|
279
275
|
<UFormField
|
|
280
276
|
label="Allowed roles"
|
|
281
|
-
|
|
277
|
+
description="Leave empty to allow every signed-in user."
|
|
282
278
|
>
|
|
283
279
|
<UInputTags
|
|
284
280
|
v-model="form.allowedRoles"
|
|
@@ -380,5 +376,5 @@ const copy = (value) => navigator.clipboard?.writeText(value);
|
|
|
380
376
|
</div>
|
|
381
377
|
</template>
|
|
382
378
|
</UModal>
|
|
383
|
-
</
|
|
379
|
+
</AdminPage>
|
|
384
380
|
</template>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
defineOptions({ name: "OAuthAuthorizePage" });
|
|
3
3
|
const route = useRoute();
|
|
4
|
-
const signInPath =
|
|
4
|
+
const signInPath = useAuthPages().login;
|
|
5
5
|
const { data: consent, error } = await useFetch("/api/oauth-server/consent", {
|
|
6
6
|
query: route.query,
|
|
7
7
|
key: `oauth-server:consent:${route.fullPath}`
|
package/dist/runtime/menus.js
CHANGED
|
@@ -27,5 +27,15 @@ export default defineMenuSource(() => {
|
|
|
27
27
|
order: 90,
|
|
28
28
|
tags: ["admin"]
|
|
29
29
|
}));
|
|
30
|
+
extendMenuItem("admin.nav", defineMenuItem({
|
|
31
|
+
id: "oauth-admin",
|
|
32
|
+
label: "OAuth applications",
|
|
33
|
+
labelKey: "oauthServer.menu.admin.label",
|
|
34
|
+
description: "Manage the applications users can sign in to.",
|
|
35
|
+
descriptionKey: "oauthServer.menu.admin.description",
|
|
36
|
+
icon: "i-lucide-key-round",
|
|
37
|
+
to: config.pages.admin,
|
|
38
|
+
order: 40
|
|
39
|
+
}));
|
|
30
40
|
}
|
|
31
41
|
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { H3Event } from 'h3';
|
|
2
|
+
import type { AuthPrincipal } from '@frigear-nu/auth/utils';
|
|
3
|
+
/**
|
|
4
|
+
* Lets an application call the API as the user who signed in to it: resolves an OAuth access
|
|
5
|
+
* token (`at+jwt`) into a principal carrying the application (`client_id`) and the granted scopes.
|
|
6
|
+
*
|
|
7
|
+
* The token's audience is the application, so it is deliberately not matched against this server:
|
|
8
|
+
* the application is the one holding the token. Everything else is re-checked on every request
|
|
9
|
+
* (the application is still active, the user still exists and may still use it), so disabling an
|
|
10
|
+
* application or removing a role takes effect immediately instead of when the token expires.
|
|
11
|
+
*/
|
|
12
|
+
export default function resolveAccessToken(event: H3Event, token: string): Promise<AuthPrincipal | null>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { parseScopeString } from "@frigear-nu/auth/utils";
|
|
2
|
+
export default async function resolveAccessToken(event, token) {
|
|
3
|
+
const payload = await verifyAccessToken(event, token);
|
|
4
|
+
if (!payload) return null;
|
|
5
|
+
const [application, oauthUser] = await Promise.all([
|
|
6
|
+
getApplication(payload.client_id),
|
|
7
|
+
getOAuthUser(Number(payload.sub))
|
|
8
|
+
]);
|
|
9
|
+
if (!application || !oauthUser) return null;
|
|
10
|
+
if (!(await checkApplicationAccess(oauthUser, application, event)).allowed) return null;
|
|
11
|
+
const user = await loadAuthUser(oauthUser.id);
|
|
12
|
+
if (!user) return null;
|
|
13
|
+
return { kind: "oauth", user, scopes: parseScopeString(payload.scope), client: { id: application.id } };
|
|
14
|
+
}
|
|
@@ -1,17 +1,11 @@
|
|
|
1
1
|
import type { H3Event } from 'h3';
|
|
2
2
|
import type { OAuthApplication, OAuthUser, TokenType } from '../../../types.js';
|
|
3
|
-
/**
|
|
4
|
-
* Gate for the admin routes. This repo has no shared permission system, so the host decides through
|
|
5
|
-
* the `oauth:authorize` Nitro hook. Denied by default:
|
|
6
|
-
*
|
|
7
|
-
* nitroApp.hooks.hook('oauth:authorize', (ctx) => { ctx.allowed = isStaff(ctx.userId) })
|
|
8
|
-
*/
|
|
9
3
|
export declare function requireOAuthAdmin(event: H3Event): Promise<{
|
|
10
4
|
userId: number;
|
|
11
5
|
}>;
|
|
12
6
|
/** The user's roles as reported by the `oauth:user:roles` hook listeners (none without a listener). */
|
|
13
7
|
export declare function resolveUserRoles(userId: number, event?: H3Event): Promise<string[]>;
|
|
14
|
-
/** The user
|
|
8
|
+
/** The stored user without the password hash; includes columns the host added to `users`. */
|
|
15
9
|
export declare function getOAuthUser(userId: number): Promise<OAuthUser | null>;
|
|
16
10
|
export type ApplicationAccess = {
|
|
17
11
|
allowed: boolean;
|
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
import { eq } from "drizzle-orm";
|
|
2
|
-
import { db, schema } from "@nuxthub/db";
|
|
3
1
|
import { matchesAllowedRoles } from "../../utils/index.js";
|
|
2
|
+
const adminGate = definePolicyGate("oauth:authorize");
|
|
4
3
|
export async function requireOAuthAdmin(event) {
|
|
5
4
|
const userId = await requireSessionUserId(event);
|
|
6
|
-
|
|
7
|
-
await useNitroApp().hooks.callHook("oauth:authorize", ctx);
|
|
8
|
-
if (!ctx.allowed) throw createError({ statusCode: 403, statusMessage: "You are not allowed to manage OAuth applications." });
|
|
5
|
+
await adminGate.assert(event, userId, "You are not allowed to manage OAuth applications.");
|
|
9
6
|
return { userId };
|
|
10
7
|
}
|
|
11
8
|
export async function resolveUserRoles(userId, event) {
|
|
@@ -14,7 +11,7 @@ export async function resolveUserRoles(userId, event) {
|
|
|
14
11
|
return [...new Set(ctx.roles)];
|
|
15
12
|
}
|
|
16
13
|
export async function getOAuthUser(userId) {
|
|
17
|
-
const
|
|
14
|
+
const row = await useAuthUserStore().findById(userId);
|
|
18
15
|
if (!row) return null;
|
|
19
16
|
const { password: _password, ...user } = row;
|
|
20
17
|
return user;
|
|
@@ -25,5 +25,5 @@ export declare function validateAuthorizeRequest(input: unknown): Promise<{
|
|
|
25
25
|
}>;
|
|
26
26
|
/** Throws 403 unless the user may use the application (role check, active, `oauth:application:access`). */
|
|
27
27
|
export declare function assertApplicationAccess(event: H3Event, user: OAuthUser, application: OAuthApplication): Promise<void>;
|
|
28
|
-
/** Where to send people who are not signed in: the configured path or the `@frigear-nu/
|
|
28
|
+
/** Where to send people who are not signed in: the configured path or the `@frigear-nu/auth` login page. */
|
|
29
29
|
export declare function getSignInPath(): string;
|
|
@@ -29,5 +29,5 @@ export async function assertApplicationAccess(event, user, application) {
|
|
|
29
29
|
}
|
|
30
30
|
export function getSignInPath() {
|
|
31
31
|
const config = useRuntimeConfig();
|
|
32
|
-
return config.oauthServer.signInPath ||
|
|
32
|
+
return config.oauthServer.signInPath || getLoginPath();
|
|
33
33
|
}
|
|
@@ -2,7 +2,10 @@ import type { H3Event } from 'h3';
|
|
|
2
2
|
export declare function useOAuthConfig(): any;
|
|
3
3
|
/** The configured issuer, or the origin the request came in on (honouring `x-forwarded-*`). */
|
|
4
4
|
export declare function getOAuthIssuer(event: H3Event): string;
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* Scopes this server accepts: the OIDC defaults, whatever the host configured, and the API scopes
|
|
7
|
+
* modules register with `@frigear-nu/auth` (what an access token may do against the API).
|
|
8
|
+
*/
|
|
6
9
|
export declare function getSupportedScopes(): string[];
|
|
7
10
|
/** The signed-in user's numeric id (the `users` table uses integer ids). Responds 401 when nobody is signed in. */
|
|
8
11
|
export declare function requireSessionUserId(event: H3Event): Promise<number>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { scopes as apiScopes } from "#auth/scopes";
|
|
1
2
|
export function useOAuthConfig() {
|
|
2
3
|
return useRuntimeConfig().oauthServer;
|
|
3
4
|
}
|
|
@@ -7,10 +8,10 @@ export function getOAuthIssuer(event) {
|
|
|
7
8
|
return issuer.replace(/\/+$/, "");
|
|
8
9
|
}
|
|
9
10
|
export function getSupportedScopes() {
|
|
10
|
-
return [.../* @__PURE__ */ new Set(["openid", "profile", "email", ...useOAuthConfig().scopes])];
|
|
11
|
+
return [.../* @__PURE__ */ new Set(["openid", "profile", "email", ...useOAuthConfig().scopes, ...apiScopes.map((scope) => scope.name)])];
|
|
11
12
|
}
|
|
12
13
|
export async function requireSessionUserId(event) {
|
|
13
|
-
const
|
|
14
|
+
const user = await requireUser(event, { sessionOnly: true });
|
|
14
15
|
return Number(user.id);
|
|
15
16
|
}
|
|
16
17
|
export function oauthError(event, statusCode, error, description) {
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frigear-nu/oauth-server",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "OAuth 2.0 / OpenID Connect server for the frigear ecosystem: register applications that users with certain roles may sign in to.",
|
|
5
|
-
"url": "https://docs.frigear.
|
|
5
|
+
"url": "https://docs.frigear.dev/modules/oauth-server",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Frigear",
|
|
8
8
|
"email": "kontakt@frigear.nu",
|
|
@@ -37,9 +37,12 @@
|
|
|
37
37
|
"playground"
|
|
38
38
|
],
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@frigear-nu/
|
|
41
|
-
"@frigear-nu/module-kit": "0.1.
|
|
42
|
-
"@frigear-nu/
|
|
40
|
+
"@frigear-nu/admin": "0.0.2",
|
|
41
|
+
"@frigear-nu/module-kit": "0.1.7",
|
|
42
|
+
"@frigear-nu/ui": "0.0.7",
|
|
43
|
+
"@frigear-nu/auth": "0.0.3",
|
|
44
|
+
"@frigear-nu/mcp": "0.0.3",
|
|
45
|
+
"@frigear-nu/users": "1.0.9",
|
|
43
46
|
"@libsql/client": "^0.18.0",
|
|
44
47
|
"@nuxt/kit": "^4.6.0",
|
|
45
48
|
"drizzle-kit": "^0.31.11",
|
|
@@ -49,15 +52,20 @@
|
|
|
49
52
|
"zod": "^4.6.5"
|
|
50
53
|
},
|
|
51
54
|
"peerDependencies": {
|
|
55
|
+
"@frigear-nu/menu": "0.0.9",
|
|
52
56
|
"@nuxt/ui": "^4.10.0",
|
|
53
57
|
"@nuxthub/core": "^0.10.8"
|
|
54
58
|
},
|
|
55
59
|
"peerDependenciesMeta": {
|
|
60
|
+
"@frigear-nu/menu": {
|
|
61
|
+
"optional": true
|
|
62
|
+
},
|
|
56
63
|
"@nuxt/ui": {
|
|
57
64
|
"optional": true
|
|
58
65
|
}
|
|
59
66
|
},
|
|
60
67
|
"devDependencies": {
|
|
68
|
+
"@frigear-nu/menu": "0.0.9",
|
|
61
69
|
"@nuxt/devtools": "^3.4.2",
|
|
62
70
|
"@nuxt/eslint-config": "^1.17.0",
|
|
63
71
|
"@nuxt/module-builder": "^1.0.3",
|