@openape/nuxt-auth-sp 0.1.11 → 0.4.0
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/dist/module.d.mts +47 -1
- package/dist/module.json +2 -2
- package/dist/module.mjs +13 -8
- package/dist/runtime/components/OpenApeAuth.d.vue.ts +38 -0
- package/dist/runtime/components/OpenApeAuth.vue +99 -0
- package/dist/runtime/components/OpenApeAuth.vue.d.ts +38 -0
- package/dist/runtime/server/api/callback.get.js +2 -1
- package/dist/runtime/server/handlers.d.ts +21 -0
- package/dist/runtime/server/handlers.js +100 -0
- package/dist/runtime/server/routes/well-known/auth.md.get.d.ts +2 -0
- package/dist/runtime/server/routes/well-known/auth.md.get.js +55 -0
- package/dist/runtime/server/routes/well-known/openape.json.get.d.ts +17 -0
- package/dist/runtime/server/routes/well-known/openape.json.get.js +29 -0
- package/dist/runtime/server/utils/grants.d.ts +30 -0
- package/dist/runtime/server/utils/grants.js +55 -0
- package/dist/types.d.mts +2 -6
- package/package.json +10 -3
- package/dist/module.cjs +0 -5
- package/dist/module.d.ts +0 -13
- package/dist/types.d.ts +0 -7
package/dist/module.d.mts
CHANGED
|
@@ -1,13 +1,59 @@
|
|
|
1
1
|
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
2
|
|
|
3
|
+
interface ManifestConfig {
|
|
4
|
+
service?: {
|
|
5
|
+
name?: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
url?: string;
|
|
8
|
+
icon?: string;
|
|
9
|
+
privacy_policy?: string;
|
|
10
|
+
terms?: string;
|
|
11
|
+
contact?: string;
|
|
12
|
+
};
|
|
13
|
+
auth?: {
|
|
14
|
+
ddisa_domain?: string;
|
|
15
|
+
oidc_client_id?: string;
|
|
16
|
+
supported_methods?: ('ddisa' | 'oidc')[];
|
|
17
|
+
login_url?: string;
|
|
18
|
+
};
|
|
19
|
+
scopes?: Record<string, {
|
|
20
|
+
name: string;
|
|
21
|
+
description: string;
|
|
22
|
+
risk: 'low' | 'medium' | 'high' | 'critical';
|
|
23
|
+
category?: string;
|
|
24
|
+
parameters?: Record<string, {
|
|
25
|
+
type: string;
|
|
26
|
+
description: string;
|
|
27
|
+
}>;
|
|
28
|
+
}>;
|
|
29
|
+
categories?: Record<string, {
|
|
30
|
+
name: string;
|
|
31
|
+
icon?: string;
|
|
32
|
+
}>;
|
|
33
|
+
policies?: {
|
|
34
|
+
agent_access?: string;
|
|
35
|
+
delegation?: 'allowed' | 'denied';
|
|
36
|
+
max_delegation_duration?: string | null;
|
|
37
|
+
require_grant_for_risk?: Record<string, string | null>;
|
|
38
|
+
require_mfa_for_risk?: Record<string, boolean>;
|
|
39
|
+
};
|
|
40
|
+
rate_limits?: Record<string, Record<string, number>>;
|
|
41
|
+
endpoints?: {
|
|
42
|
+
api_base?: string;
|
|
43
|
+
openapi?: string;
|
|
44
|
+
grant_verify?: string;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
3
47
|
interface ModuleOptions {
|
|
4
48
|
spId: string;
|
|
5
49
|
spName: string;
|
|
6
50
|
sessionSecret: string;
|
|
7
51
|
openapeUrl: string;
|
|
8
52
|
fallbackIdpUrl: string;
|
|
53
|
+
routes: boolean;
|
|
54
|
+
manifest?: ManifestConfig;
|
|
9
55
|
}
|
|
10
56
|
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
11
57
|
|
|
12
58
|
export { _default as default };
|
|
13
|
-
export type { ModuleOptions };
|
|
59
|
+
export type { ManifestConfig, ModuleOptions };
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { useLogger, defineNuxtModule, createResolver, addServerImportsDir, addIm
|
|
|
3
3
|
import { defu } from 'defu';
|
|
4
4
|
|
|
5
5
|
const logger = useLogger("@openape/nuxt-auth-sp");
|
|
6
|
-
const module = defineNuxtModule({
|
|
6
|
+
const module$1 = defineNuxtModule({
|
|
7
7
|
meta: {
|
|
8
8
|
name: "@openape/nuxt-auth-sp",
|
|
9
9
|
configKey: "openapeSp"
|
|
@@ -13,7 +13,8 @@ const module = defineNuxtModule({
|
|
|
13
13
|
spName: "OpenApe Service Provider",
|
|
14
14
|
sessionSecret: "change-me-sp-secret-at-least-32-chars-long",
|
|
15
15
|
openapeUrl: "",
|
|
16
|
-
fallbackIdpUrl: "https://id.openape.at"
|
|
16
|
+
fallbackIdpUrl: "https://id.openape.at",
|
|
17
|
+
routes: true
|
|
17
18
|
},
|
|
18
19
|
setup(options, nuxt) {
|
|
19
20
|
const { resolve } = createResolver(import.meta.url);
|
|
@@ -46,12 +47,16 @@ const module = defineNuxtModule({
|
|
|
46
47
|
addServerImportsDir(resolve("./runtime/server/utils"));
|
|
47
48
|
addImportsDir(resolve("./runtime/composables"));
|
|
48
49
|
addComponentsDir({ path: resolve(runtimeDir, "components") });
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
if (options.routes !== false) {
|
|
51
|
+
addServerHandler({ route: "/api/login", method: "post", handler: resolve("./runtime/server/api/login.post") });
|
|
52
|
+
addServerHandler({ route: "/api/callback", handler: resolve("./runtime/server/api/callback.get") });
|
|
53
|
+
addServerHandler({ route: "/api/logout", method: "post", handler: resolve("./runtime/server/api/logout.post") });
|
|
54
|
+
addServerHandler({ route: "/api/me", handler: resolve("./runtime/server/api/me.get") });
|
|
55
|
+
addServerHandler({ route: "/.well-known/sp-manifest.json", handler: resolve("./runtime/server/routes/well-known/sp-manifest.json.get") });
|
|
56
|
+
addServerHandler({ route: "/.well-known/auth.md", handler: resolve("./runtime/server/routes/well-known/auth.md.get") });
|
|
57
|
+
addServerHandler({ route: "/.well-known/openape.json", handler: resolve("./runtime/server/routes/well-known/openape.json.get") });
|
|
58
|
+
}
|
|
54
59
|
}
|
|
55
60
|
});
|
|
56
61
|
|
|
57
|
-
export { module as default };
|
|
62
|
+
export { module$1 as default };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
title?: string;
|
|
3
|
+
subtitle?: string;
|
|
4
|
+
buttonText?: string;
|
|
5
|
+
placeholder?: string;
|
|
6
|
+
};
|
|
7
|
+
declare var __VLS_1: {}, __VLS_3: {
|
|
8
|
+
error: any;
|
|
9
|
+
}, __VLS_5: {
|
|
10
|
+
submitting: any;
|
|
11
|
+
}, __VLS_7: {};
|
|
12
|
+
type __VLS_Slots = {} & {
|
|
13
|
+
header?: (props: typeof __VLS_1) => any;
|
|
14
|
+
} & {
|
|
15
|
+
error?: (props: typeof __VLS_3) => any;
|
|
16
|
+
} & {
|
|
17
|
+
button?: (props: typeof __VLS_5) => any;
|
|
18
|
+
} & {
|
|
19
|
+
footer?: (props: typeof __VLS_7) => any;
|
|
20
|
+
};
|
|
21
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
22
|
+
error: (error: Error) => any;
|
|
23
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
24
|
+
onError?: ((error: Error) => any) | undefined;
|
|
25
|
+
}>, {
|
|
26
|
+
title: string;
|
|
27
|
+
subtitle: string;
|
|
28
|
+
buttonText: string;
|
|
29
|
+
placeholder: string;
|
|
30
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
31
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
32
|
+
declare const _default: typeof __VLS_export;
|
|
33
|
+
export default _default;
|
|
34
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
35
|
+
new (): {
|
|
36
|
+
$slots: S;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
defineProps({
|
|
3
|
+
title: { type: String, required: false, default: "Sign in" },
|
|
4
|
+
subtitle: { type: String, required: false, default: "Enter your email to continue" },
|
|
5
|
+
buttonText: { type: String, required: false, default: "Continue" },
|
|
6
|
+
placeholder: { type: String, required: false, default: "you@example.com" }
|
|
7
|
+
});
|
|
8
|
+
const emit = defineEmits(["error"]);
|
|
9
|
+
const { user, loading, fetchUser, login } = useOpenApeAuth();
|
|
10
|
+
const email = ref("");
|
|
11
|
+
const error = ref("");
|
|
12
|
+
const submitting = ref(false);
|
|
13
|
+
const route = useRoute();
|
|
14
|
+
onMounted(async () => {
|
|
15
|
+
await fetchUser();
|
|
16
|
+
if (user.value) {
|
|
17
|
+
navigateTo("/dashboard");
|
|
18
|
+
}
|
|
19
|
+
if (route.query.error) {
|
|
20
|
+
error.value = String(route.query.error);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
async function handleSubmit() {
|
|
24
|
+
error.value = "";
|
|
25
|
+
if (!email.value || !email.value.includes("@")) {
|
|
26
|
+
error.value = "Please enter a valid email address";
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
submitting.value = true;
|
|
30
|
+
try {
|
|
31
|
+
await login(email.value);
|
|
32
|
+
} catch (e) {
|
|
33
|
+
const err = e instanceof Error ? e : new Error("Login failed");
|
|
34
|
+
error.value = e?.data?.message || err.message;
|
|
35
|
+
emit("error", err);
|
|
36
|
+
submitting.value = false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<template>
|
|
42
|
+
<div v-if="loading" class="openape-auth openape-auth--loading">
|
|
43
|
+
<div class="openape-auth-spinner" />
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div v-else class="openape-auth">
|
|
47
|
+
<slot name="header">
|
|
48
|
+
<div class="openape-auth-header">
|
|
49
|
+
<h2 class="openape-auth-title">
|
|
50
|
+
{{ title }}
|
|
51
|
+
</h2>
|
|
52
|
+
<p class="openape-auth-subtitle">
|
|
53
|
+
{{ subtitle }}
|
|
54
|
+
</p>
|
|
55
|
+
</div>
|
|
56
|
+
</slot>
|
|
57
|
+
|
|
58
|
+
<form class="openape-auth-form" @submit.prevent="handleSubmit">
|
|
59
|
+
<slot name="error" :error="error">
|
|
60
|
+
<p v-if="error" class="openape-auth-error">
|
|
61
|
+
{{ error }}
|
|
62
|
+
</p>
|
|
63
|
+
</slot>
|
|
64
|
+
|
|
65
|
+
<input
|
|
66
|
+
v-model="email"
|
|
67
|
+
type="email"
|
|
68
|
+
class="openape-auth-input"
|
|
69
|
+
:placeholder="placeholder"
|
|
70
|
+
required
|
|
71
|
+
:disabled="submitting"
|
|
72
|
+
autocomplete="email"
|
|
73
|
+
>
|
|
74
|
+
|
|
75
|
+
<slot name="button" :submitting="submitting">
|
|
76
|
+
<button
|
|
77
|
+
type="submit"
|
|
78
|
+
class="openape-auth-button"
|
|
79
|
+
:disabled="submitting || !email"
|
|
80
|
+
>
|
|
81
|
+
<span v-if="submitting" class="openape-auth-button-loading">
|
|
82
|
+
<svg class="openape-auth-spinner-icon" viewBox="0 0 24 24" fill="none">
|
|
83
|
+
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2.5" opacity="0.25" />
|
|
84
|
+
<path d="M12 2a10 10 0 0 1 10 10" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" />
|
|
85
|
+
</svg>
|
|
86
|
+
Redirecting…
|
|
87
|
+
</span>
|
|
88
|
+
<span v-else>{{ buttonText }}</span>
|
|
89
|
+
</button>
|
|
90
|
+
</slot>
|
|
91
|
+
</form>
|
|
92
|
+
|
|
93
|
+
<slot name="footer" />
|
|
94
|
+
</div>
|
|
95
|
+
</template>
|
|
96
|
+
|
|
97
|
+
<style>
|
|
98
|
+
.openape-auth{--oa-bg:#fff;--oa-border:#e2e2e2;--oa-text:#1a1a1a;--oa-text-muted:#6b7280;--oa-primary:#18181b;--oa-primary-hover:#27272a;--oa-primary-text:#fff;--oa-error:#dc2626;--oa-error-bg:#fef2f2;--oa-input-bg:#fff;--oa-input-border:#d1d5db;--oa-input-focus:#18181b;--oa-radius:8px;--oa-font:system-ui,-apple-system,sans-serif;background:var(--oa-bg);border:1px solid var(--oa-border);border-radius:var(--oa-radius);box-sizing:border-box;color:var(--oa-text);font-family:var(--oa-font);max-width:400px;padding:2rem;width:100%}.openape-auth--loading{align-items:center;display:flex;justify-content:center;min-height:200px}.openape-auth-spinner{animation:oa-spin .6s linear infinite;border:2.5px solid var(--oa-border);border-radius:50%;border-top-color:var(--oa-primary);height:24px;width:24px}.openape-auth-header{margin-bottom:1.5rem;text-align:center}.openape-auth-title{color:var(--oa-text);font-size:1.375rem;font-weight:600;letter-spacing:-.01em;margin:0 0 .375rem}.openape-auth-subtitle{color:var(--oa-text-muted);font-size:.875rem;margin:0}.openape-auth-form{display:flex;flex-direction:column;gap:.75rem}.openape-auth-error{background:var(--oa-error-bg);border-radius:calc(var(--oa-radius) - 2px);color:var(--oa-error);font-size:.8125rem;margin:0;padding:.625rem .75rem}.openape-auth-input{background:var(--oa-input-bg);border:1px solid var(--oa-input-border);border-radius:calc(var(--oa-radius) - 2px);box-sizing:border-box;color:var(--oa-text);font-family:var(--oa-font);font-size:.9375rem;outline:none;padding:.625rem .75rem;transition:border-color .15s;width:100%}.openape-auth-input:focus{border-color:var(--oa-input-focus);box-shadow:0 0 0 1px var(--oa-input-focus)}.openape-auth-input:disabled{cursor:not-allowed;opacity:.6}.openape-auth-button{background:var(--oa-primary);border:none;border-radius:calc(var(--oa-radius) - 2px);color:var(--oa-primary-text);cursor:pointer;font-family:var(--oa-font);font-size:.9375rem;font-weight:500;padding:.625rem 1rem;transition:background .15s}.openape-auth-button:hover:not(:disabled){background:var(--oa-primary-hover)}.openape-auth-button:disabled{cursor:not-allowed;opacity:.5}.openape-auth-button-loading{align-items:center;display:inline-flex;gap:.5rem;justify-content:center}.openape-auth-spinner-icon{animation:oa-spin .6s linear infinite;height:16px;width:16px}@keyframes oa-spin{to{transform:rotate(1turn)}}@media (prefers-color-scheme:dark){.openape-auth{--oa-bg:#18181b;--oa-border:#2e2e32;--oa-text:#f4f4f5;--oa-text-muted:#a1a1aa;--oa-primary:#f4f4f5;--oa-primary-hover:#e4e4e7;--oa-primary-text:#18181b;--oa-error-bg:#2d1215;--oa-input-bg:#1f1f23;--oa-input-border:#3f3f46;--oa-input-focus:#a1a1aa}}
|
|
99
|
+
</style>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
title?: string;
|
|
3
|
+
subtitle?: string;
|
|
4
|
+
buttonText?: string;
|
|
5
|
+
placeholder?: string;
|
|
6
|
+
};
|
|
7
|
+
declare var __VLS_1: {}, __VLS_3: {
|
|
8
|
+
error: any;
|
|
9
|
+
}, __VLS_5: {
|
|
10
|
+
submitting: any;
|
|
11
|
+
}, __VLS_7: {};
|
|
12
|
+
type __VLS_Slots = {} & {
|
|
13
|
+
header?: (props: typeof __VLS_1) => any;
|
|
14
|
+
} & {
|
|
15
|
+
error?: (props: typeof __VLS_3) => any;
|
|
16
|
+
} & {
|
|
17
|
+
button?: (props: typeof __VLS_5) => any;
|
|
18
|
+
} & {
|
|
19
|
+
footer?: (props: typeof __VLS_7) => any;
|
|
20
|
+
};
|
|
21
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
22
|
+
error: (error: Error) => any;
|
|
23
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
24
|
+
onError?: ((error: Error) => any) | undefined;
|
|
25
|
+
}>, {
|
|
26
|
+
title: string;
|
|
27
|
+
subtitle: string;
|
|
28
|
+
buttonText: string;
|
|
29
|
+
placeholder: string;
|
|
30
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
31
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
32
|
+
declare const _default: typeof __VLS_export;
|
|
33
|
+
export default _default;
|
|
34
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
35
|
+
new (): {
|
|
36
|
+
$slots: S;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
@@ -30,7 +30,8 @@ export default defineEventHandler(async (event) => {
|
|
|
30
30
|
clearFlowState(event);
|
|
31
31
|
const session = await getSpSession(event);
|
|
32
32
|
await session.update({
|
|
33
|
-
claims: result.claims
|
|
33
|
+
claims: result.claims,
|
|
34
|
+
authorizationDetails: result.authorizationDetails
|
|
34
35
|
});
|
|
35
36
|
return sendRedirect(event, "/dashboard");
|
|
36
37
|
} catch (err) {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { H3Event } from 'h3';
|
|
2
|
+
import type { DDISAAssertionClaims } from '@openape/core';
|
|
3
|
+
export interface LoginHandlerOptions {
|
|
4
|
+
callbackPath: string;
|
|
5
|
+
}
|
|
6
|
+
export interface CallbackHandlerOptions {
|
|
7
|
+
onSuccess: (event: H3Event, result: {
|
|
8
|
+
claims: DDISAAssertionClaims;
|
|
9
|
+
rawAssertion: string;
|
|
10
|
+
}) => Promise<void>;
|
|
11
|
+
onError?: (event: H3Event, error: Error) => Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
export interface SPManifestHandlerOptions {
|
|
14
|
+
callbackPath: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function defineOpenApeLoginHandler(options: LoginHandlerOptions): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
|
|
18
|
+
redirectUrl: string;
|
|
19
|
+
}>>;
|
|
20
|
+
export declare function defineOpenApeCallbackHandler(options: CallbackHandlerOptions): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
|
|
21
|
+
export declare function defineOpenApeSPManifestHandler(options: SPManifestHandlerOptions): import("h3").EventHandler<import("h3").EventHandlerRequest, import("@openape/core").SPManifest>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { createError, defineEventHandler, getQuery, getRequestURL, readBody, sendRedirect } from "h3";
|
|
2
|
+
import { createAuthorizationURL, createSPManifest, discoverIdP, handleCallback } from "@openape/auth";
|
|
3
|
+
import { getSpConfig, saveFlowState, getFlowState, clearFlowState } from "./utils/sp-config.js";
|
|
4
|
+
export function defineOpenApeLoginHandler(options) {
|
|
5
|
+
return defineEventHandler(async (event) => {
|
|
6
|
+
const body = await readBody(event);
|
|
7
|
+
const { spId, openapeUrl, fallbackIdpUrl } = getSpConfig();
|
|
8
|
+
const origin = getRequestURL(event).origin;
|
|
9
|
+
const redirectUri = `${origin}${options.callbackPath}`;
|
|
10
|
+
if (!body?.email || !body.email.includes("@")) {
|
|
11
|
+
throw createError({ statusCode: 400, statusMessage: "Valid email required" });
|
|
12
|
+
}
|
|
13
|
+
const email = body.email.trim();
|
|
14
|
+
const domain = email.split("@")[1];
|
|
15
|
+
let idpConfig;
|
|
16
|
+
if (openapeUrl) {
|
|
17
|
+
idpConfig = { idpUrl: openapeUrl, record: { version: "ddisa1", idp: openapeUrl, raw: `v=ddisa1; idp=${openapeUrl}` } };
|
|
18
|
+
} else {
|
|
19
|
+
idpConfig = await discoverIdP(email, { fallbackIdpUrl: fallbackIdpUrl || void 0 });
|
|
20
|
+
}
|
|
21
|
+
if (!idpConfig) {
|
|
22
|
+
throw createError({
|
|
23
|
+
statusCode: 404,
|
|
24
|
+
statusMessage: `No DDISA IdP found for domain "${domain}"`
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
const { url, flowState } = await createAuthorizationURL(idpConfig, {
|
|
28
|
+
spId,
|
|
29
|
+
redirectUri,
|
|
30
|
+
email
|
|
31
|
+
});
|
|
32
|
+
await saveFlowState(event, flowState.state, flowState);
|
|
33
|
+
return { redirectUrl: url };
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export function defineOpenApeCallbackHandler(options) {
|
|
37
|
+
return defineEventHandler(async (event) => {
|
|
38
|
+
const query = getQuery(event);
|
|
39
|
+
const { code, state, error, error_description } = query;
|
|
40
|
+
const { spId } = getSpConfig();
|
|
41
|
+
const origin = getRequestURL(event).origin;
|
|
42
|
+
if (error) {
|
|
43
|
+
const msg = error_description || error;
|
|
44
|
+
if (options.onError) {
|
|
45
|
+
await options.onError(event, new Error(msg));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
return sendRedirect(event, `/login?error=${encodeURIComponent(msg)}`);
|
|
49
|
+
}
|
|
50
|
+
if (!code || !state) {
|
|
51
|
+
const err = new Error("Missing code or state parameter");
|
|
52
|
+
if (options.onError) {
|
|
53
|
+
await options.onError(event, err);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
return sendRedirect(event, `/login?error=${encodeURIComponent(err.message)}`);
|
|
57
|
+
}
|
|
58
|
+
const flowState = await getFlowState(event, state);
|
|
59
|
+
if (!flowState) {
|
|
60
|
+
const err = new Error("Invalid or expired state \u2014 please try again");
|
|
61
|
+
if (options.onError) {
|
|
62
|
+
await options.onError(event, err);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
return sendRedirect(event, `/login?error=${encodeURIComponent(err.message)}`);
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const redirectUri = `${origin}${getRequestURL(event).pathname}`;
|
|
69
|
+
const result = await handleCallback({
|
|
70
|
+
code,
|
|
71
|
+
state,
|
|
72
|
+
flowState,
|
|
73
|
+
spId,
|
|
74
|
+
redirectUri
|
|
75
|
+
});
|
|
76
|
+
await clearFlowState(event);
|
|
77
|
+
await options.onSuccess(event, { claims: result.claims, rawAssertion: result.rawAssertion });
|
|
78
|
+
} catch (err) {
|
|
79
|
+
await clearFlowState(event);
|
|
80
|
+
const error2 = err instanceof Error ? err : new Error("Callback processing failed");
|
|
81
|
+
if (options.onError) {
|
|
82
|
+
await options.onError(event, error2);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
return sendRedirect(event, `/login?error=${encodeURIComponent(error2.message)}`);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
export function defineOpenApeSPManifestHandler(options) {
|
|
90
|
+
return defineEventHandler((event) => {
|
|
91
|
+
const { spId, spName } = getSpConfig();
|
|
92
|
+
const origin = getRequestURL(event).origin;
|
|
93
|
+
return createSPManifest({
|
|
94
|
+
sp_id: spId,
|
|
95
|
+
name: spName,
|
|
96
|
+
redirect_uris: [`${origin}${options.callbackPath}`],
|
|
97
|
+
description: options.description || `${spName} \u2014 OpenApe Service Provider`
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { defineEventHandler, getRequestURL, setResponseHeader } from "h3";
|
|
2
|
+
import { getSpConfig } from "../../utils/sp-config.js";
|
|
3
|
+
export default defineEventHandler((event) => {
|
|
4
|
+
const { spId, spName, fallbackIdpUrl } = getSpConfig();
|
|
5
|
+
const origin = getRequestURL(event).origin;
|
|
6
|
+
setResponseHeader(event, "Content-Type", "text/markdown; charset=utf-8");
|
|
7
|
+
return `# Authentication \u2014 ${spName}
|
|
8
|
+
|
|
9
|
+
## Protocol
|
|
10
|
+
DDISA v1 (DNS-Discoverable Identity & Service Authorization)
|
|
11
|
+
|
|
12
|
+
## Service Provider
|
|
13
|
+
- **SP ID:** \`${spId}\`
|
|
14
|
+
- **Origin:** \`${origin}\`
|
|
15
|
+
|
|
16
|
+
## Endpoints
|
|
17
|
+
|
|
18
|
+
| Method | Path | Description |
|
|
19
|
+
|--------|------|-------------|
|
|
20
|
+
| POST | /api/login | Start login \u2014 send \`{"email": "you@example.com"}\` |
|
|
21
|
+
| GET | /api/callback | OAuth callback (automatic) |
|
|
22
|
+
| GET | /api/me | Get current session (returns 401 if not authenticated) |
|
|
23
|
+
| POST | /api/logout | End session |
|
|
24
|
+
| GET | /.well-known/sp-manifest.json | SP metadata |
|
|
25
|
+
|
|
26
|
+
## How to Authenticate
|
|
27
|
+
|
|
28
|
+
### Step 1 \u2014 Start Login
|
|
29
|
+
\`\`\`
|
|
30
|
+
POST ${origin}/api/login
|
|
31
|
+
Content-Type: application/json
|
|
32
|
+
|
|
33
|
+
{"email": "your-identity@example.com"}
|
|
34
|
+
\`\`\`
|
|
35
|
+
Response: \`{"redirectUrl": "https://idp.example.com/authorize?..."}\`
|
|
36
|
+
|
|
37
|
+
### Step 2 \u2014 Authenticate at IdP
|
|
38
|
+
Follow the \`redirectUrl\`. The IdP at your email domain (discovered via DNS \`_ddisa.{domain}\`) handles authentication.
|
|
39
|
+
|
|
40
|
+
- **Humans:** WebAuthn passkey prompt
|
|
41
|
+
- **Agents:** Present your Bearer token (obtained via IdP challenge-response)
|
|
42
|
+
|
|
43
|
+
If no DNS record exists for your domain, the fallback IdP \`${fallbackIdpUrl}\` is used.
|
|
44
|
+
|
|
45
|
+
### Step 3 \u2014 Session Established
|
|
46
|
+
After successful authentication, you are redirected to \`/api/callback\`. A session cookie is set. Use \`GET /api/me\` to verify your session.
|
|
47
|
+
|
|
48
|
+
## Identity Discovery
|
|
49
|
+
This SP discovers your IdP via DNS TXT record:
|
|
50
|
+
\`\`\`
|
|
51
|
+
_ddisa.{your-domain} TXT "v=ddisa1 idp=https://your-idp.example.com"
|
|
52
|
+
\`\`\`
|
|
53
|
+
No DNS record? The fallback IdP (${fallbackIdpUrl}) is used automatically.
|
|
54
|
+
`;
|
|
55
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, {
|
|
2
|
+
endpoints?: {} | undefined;
|
|
3
|
+
rate_limits?: {} | undefined;
|
|
4
|
+
policies?: {} | undefined;
|
|
5
|
+
categories?: {} | undefined;
|
|
6
|
+
scopes?: {} | undefined;
|
|
7
|
+
version: string;
|
|
8
|
+
service: {
|
|
9
|
+
name: any;
|
|
10
|
+
url: string;
|
|
11
|
+
};
|
|
12
|
+
auth: {
|
|
13
|
+
ddisa_domain: any;
|
|
14
|
+
supported_methods: string[];
|
|
15
|
+
};
|
|
16
|
+
}>;
|
|
17
|
+
export default _default;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { defineEventHandler, getRequestURL, setResponseHeader } from "h3";
|
|
2
|
+
import { useRuntimeConfig } from "nitropack/runtime";
|
|
3
|
+
import { getSpConfig } from "../../utils/sp-config.js";
|
|
4
|
+
export default defineEventHandler((event) => {
|
|
5
|
+
const config = useRuntimeConfig();
|
|
6
|
+
const { spId, spName } = getSpConfig();
|
|
7
|
+
const origin = getRequestURL(event).origin;
|
|
8
|
+
const manifest = config.openapeSp.manifest;
|
|
9
|
+
setResponseHeader(event, "Access-Control-Allow-Origin", "*");
|
|
10
|
+
setResponseHeader(event, "Cache-Control", "public, max-age=3600");
|
|
11
|
+
return {
|
|
12
|
+
version: "1.0",
|
|
13
|
+
service: {
|
|
14
|
+
name: spName,
|
|
15
|
+
url: origin,
|
|
16
|
+
...manifest?.service || {}
|
|
17
|
+
},
|
|
18
|
+
auth: {
|
|
19
|
+
ddisa_domain: spId,
|
|
20
|
+
supported_methods: ["ddisa"],
|
|
21
|
+
...manifest?.auth || {}
|
|
22
|
+
},
|
|
23
|
+
...manifest?.scopes ? { scopes: manifest.scopes } : {},
|
|
24
|
+
...manifest?.categories ? { categories: manifest.categories } : {},
|
|
25
|
+
...manifest?.policies ? { policies: manifest.policies } : {},
|
|
26
|
+
...manifest?.rate_limits ? { rate_limits: manifest.rate_limits } : {},
|
|
27
|
+
...manifest?.endpoints ? { endpoints: manifest.endpoints } : {}
|
|
28
|
+
};
|
|
29
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { OpenApeAuthorizationDetail } from '@openape/core';
|
|
2
|
+
import type { H3Event } from 'h3';
|
|
3
|
+
/**
|
|
4
|
+
* Check if the current session has a grant for the given action.
|
|
5
|
+
*/
|
|
6
|
+
export declare function hasGrant(event: H3Event, action: string): Promise<boolean>;
|
|
7
|
+
/**
|
|
8
|
+
* Find a specific grant by action from the session's authorization_details.
|
|
9
|
+
*/
|
|
10
|
+
export declare function findGrant(event: H3Event, action: string): Promise<OpenApeAuthorizationDetail | null>;
|
|
11
|
+
/**
|
|
12
|
+
* Consume a 'once' grant by calling the IdP verify endpoint.
|
|
13
|
+
* Returns the verification result or throws on failure.
|
|
14
|
+
*/
|
|
15
|
+
export declare function consumeGrant(event: H3Event, grantId: string): Promise<{
|
|
16
|
+
valid: boolean;
|
|
17
|
+
}>;
|
|
18
|
+
/**
|
|
19
|
+
* Check if the current session is a delegated session (has act claim as object).
|
|
20
|
+
*/
|
|
21
|
+
export declare function isDelegated(event: H3Event): Promise<boolean>;
|
|
22
|
+
/**
|
|
23
|
+
* Get the actual actor (delegate) from a delegated session.
|
|
24
|
+
* Returns the delegate's identifier (e.g. agent email) or null if not delegated.
|
|
25
|
+
*/
|
|
26
|
+
export declare function getActor(event: H3Event): Promise<string | null>;
|
|
27
|
+
/**
|
|
28
|
+
* Get the subject (delegator — person being acted on behalf of) from the session.
|
|
29
|
+
*/
|
|
30
|
+
export declare function getSubject(event: H3Event): Promise<string | null>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { getSpSession } from "./sp-session.js";
|
|
2
|
+
import { getSpConfig } from "./sp-config.js";
|
|
3
|
+
export async function hasGrant(event, action) {
|
|
4
|
+
const session = await getSpSession(event);
|
|
5
|
+
const details = session.data.authorizationDetails;
|
|
6
|
+
if (!details) return false;
|
|
7
|
+
return details.some((d) => d.action === action);
|
|
8
|
+
}
|
|
9
|
+
export async function findGrant(event, action) {
|
|
10
|
+
const session = await getSpSession(event);
|
|
11
|
+
const details = session.data.authorizationDetails;
|
|
12
|
+
if (!details) return null;
|
|
13
|
+
return details.find((d) => d.action === action) ?? null;
|
|
14
|
+
}
|
|
15
|
+
export async function consumeGrant(event, grantId) {
|
|
16
|
+
const session = await getSpSession(event);
|
|
17
|
+
const details = session.data.authorizationDetails;
|
|
18
|
+
const detail = details?.find((d) => d.grant_id === grantId);
|
|
19
|
+
if (!detail) {
|
|
20
|
+
throw new Error(`Grant ${grantId} not found in session`);
|
|
21
|
+
}
|
|
22
|
+
const claims = session.data.claims;
|
|
23
|
+
const idpUrl = claims?.iss || getSpConfig().fallbackIdpUrl;
|
|
24
|
+
const response = await fetch(`${idpUrl}/api/grants/verify`, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: { "Content-Type": "application/json" },
|
|
27
|
+
body: JSON.stringify({ grant_id: grantId })
|
|
28
|
+
});
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
throw new Error(`Grant verification failed: ${response.status}`);
|
|
31
|
+
}
|
|
32
|
+
return response.json();
|
|
33
|
+
}
|
|
34
|
+
export async function isDelegated(event) {
|
|
35
|
+
const session = await getSpSession(event);
|
|
36
|
+
const claims = session.data.claims;
|
|
37
|
+
if (!claims) return false;
|
|
38
|
+
return typeof claims.act === "object" && claims.act !== null && "sub" in claims.act;
|
|
39
|
+
}
|
|
40
|
+
export async function getActor(event) {
|
|
41
|
+
const session = await getSpSession(event);
|
|
42
|
+
const claims = session.data.claims;
|
|
43
|
+
if (!claims) return null;
|
|
44
|
+
const act = claims.act;
|
|
45
|
+
if (typeof act === "object" && act !== null && "sub" in act) {
|
|
46
|
+
return act.sub;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
export async function getSubject(event) {
|
|
51
|
+
const session = await getSpSession(event);
|
|
52
|
+
const claims = session.data.claims;
|
|
53
|
+
if (!claims) return null;
|
|
54
|
+
return claims.sub ?? null;
|
|
55
|
+
}
|
package/dist/types.d.mts
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
|
|
1
|
+
export { default } from './module.mjs'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export type ModuleOptions = typeof Module extends NuxtModule<infer O> ? Partial<O> : Record<string, any>
|
|
6
|
-
|
|
7
|
-
export { default } from './module.js'
|
|
3
|
+
export { type ManifestConfig, type ModuleOptions } from './module.mjs'
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openape/nuxt-auth-sp",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.0",
|
|
5
5
|
"description": "OpenAPE Service Provider Nuxt module — adds OIDC login via DNS-based IdP discovery",
|
|
6
6
|
"author": "Delta Mind GmbH",
|
|
7
7
|
"license": "AGPL-3.0-or-later",
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
".": {
|
|
10
10
|
"types": "./dist/types.d.mts",
|
|
11
11
|
"import": "./dist/module.mjs"
|
|
12
|
+
},
|
|
13
|
+
"./handlers": {
|
|
14
|
+
"types": "./dist/runtime/server/handlers.d.ts",
|
|
15
|
+
"import": "./dist/runtime/server/handlers.js"
|
|
12
16
|
}
|
|
13
17
|
},
|
|
14
18
|
"main": "./dist/module.mjs",
|
|
@@ -31,7 +35,7 @@
|
|
|
31
35
|
"@nuxt/kit": "^3.21.1",
|
|
32
36
|
"@openape/auth": "^0.1.3",
|
|
33
37
|
"@openape/core": "^0.1.2",
|
|
34
|
-
"@openape/nuxt-auth-sp": "0.
|
|
38
|
+
"@openape/nuxt-auth-sp": "0.4.0",
|
|
35
39
|
"defu": "^6.1.4"
|
|
36
40
|
},
|
|
37
41
|
"peerDependencies": {
|
|
@@ -40,12 +44,15 @@
|
|
|
40
44
|
"devDependencies": {
|
|
41
45
|
"@antfu/eslint-config": "^7.6.1",
|
|
42
46
|
"@changesets/cli": "^2.30.0",
|
|
43
|
-
"@nuxt/module-builder": "^0.
|
|
47
|
+
"@nuxt/module-builder": "^1.0.2",
|
|
48
|
+
"@nuxt/ui": "^4.5.1",
|
|
44
49
|
"@types/node": "^22.19.13",
|
|
45
50
|
"eslint": "^9.39.3",
|
|
46
51
|
"nuxt": "^4.3.1",
|
|
52
|
+
"tailwindcss": "^4.2.1",
|
|
47
53
|
"typescript": "^5.9.3",
|
|
48
54
|
"vitest": "^3.2.4",
|
|
55
|
+
"vue": "^3.5.30",
|
|
49
56
|
"vue-tsc": "^3.2.5"
|
|
50
57
|
},
|
|
51
58
|
"engines": {
|
package/dist/module.cjs
DELETED
package/dist/module.d.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
|
-
|
|
3
|
-
interface ModuleOptions {
|
|
4
|
-
spId: string;
|
|
5
|
-
spName: string;
|
|
6
|
-
sessionSecret: string;
|
|
7
|
-
openapeUrl: string;
|
|
8
|
-
fallbackIdpUrl: string;
|
|
9
|
-
}
|
|
10
|
-
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
11
|
-
|
|
12
|
-
export { _default as default };
|
|
13
|
-
export type { ModuleOptions };
|
package/dist/types.d.ts
DELETED