@frigear-nu/oauth-server 0.1.2 → 0.1.3
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 +8 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +24 -20
- package/dist/runtime/app/composables/useOAuthApplicationActions.d.ts +3 -0
- package/dist/runtime/app/composables/useOAuthApplicationActions.js +29 -13
- package/dist/runtime/app/composables/useOAuthApplications.d.ts +15 -14
- package/dist/runtime/app/composables/useOAuthApplications.js +25 -14
- package/dist/runtime/app/pages/admin.vue +113 -62
- package/dist/runtime/app/pages/applications.vue +51 -34
- package/dist/runtime/app/pages/authorize.vue +3 -2
- package/dist/runtime/i18n/lang/da.json +6 -1
- package/dist/runtime/i18n/lang/en.json +6 -1
- package/dist/runtime/server/middleware/oauth-cors.js +6 -5
- package/dist/runtime/server/routes/api/admin/application-revoke.post.js +5 -3
- package/dist/runtime/server/routes/api/admin/application-secret.post.js +3 -1
- package/dist/runtime/server/routes/api/admin/application.delete.js +4 -3
- package/dist/runtime/server/routes/api/admin/application.get.js +6 -3
- package/dist/runtime/server/routes/api/admin/application.patch.js +4 -3
- package/dist/runtime/server/routes/api/admin/applications.get.js +20 -8
- package/dist/runtime/server/routes/api/admin/applications.post.js +3 -3
- package/dist/runtime/server/routes/api/consent.get.js +4 -3
- package/dist/runtime/server/routes/oidc/authorize.get.js +5 -6
- package/dist/runtime/server/routes/oidc/authorize.post.js +1 -2
- package/dist/runtime/server/routes/oidc/openid-configuration.get.js +6 -5
- package/dist/runtime/server/routes/oidc/revoke.post.js +15 -3
- package/dist/runtime/server/routes/oidc/token.post.js +20 -5
- package/dist/runtime/server/utils/oauth-access.d.ts +2 -0
- package/dist/runtime/server/utils/oauth-access.js +7 -1
- package/dist/runtime/server/utils/oauth-applications.d.ts +7 -2
- package/dist/runtime/server/utils/oauth-applications.js +39 -12
- package/dist/runtime/server/utils/oauth-authorize.d.ts +8 -4
- package/dist/runtime/server/utils/oauth-authorize.js +10 -8
- package/dist/runtime/server/utils/oauth-config.d.ts +5 -0
- package/dist/runtime/server/utils/oauth-config.js +5 -0
- package/dist/runtime/server/utils/oauth-keys.js +3 -2
- package/dist/runtime/utils/api.d.ts +28 -0
- package/dist/runtime/utils/api.js +27 -0
- package/dist/runtime/utils/index.d.ts +1 -0
- package/dist/runtime/utils/index.js +1 -0
- package/dist/types.d.mts +1 -1
- package/package.json +14 -9
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
"new": "Tilføj applikation",
|
|
17
17
|
"forbiddenTitle": "Du har ikke adgang til at administrere OAuth-applikationer",
|
|
18
18
|
"forbiddenDescription": "Bed en administrator om adgang.",
|
|
19
|
+
"loadFailed": "OAuth-applikationer kunne ikke indlæses",
|
|
20
|
+
"tryAgain": "Prøv igen",
|
|
19
21
|
"application": "Applikation",
|
|
20
22
|
"access": "Adgang",
|
|
21
23
|
"signedIn": "Logget ind",
|
|
@@ -44,7 +46,10 @@
|
|
|
44
46
|
"enabled": "Applikationen er aktiveret",
|
|
45
47
|
"disabledToast": "Applikationen er deaktiveret",
|
|
46
48
|
"secretReplaced": "Klienthemmeligheden er skiftet",
|
|
47
|
-
"signedOut": "Alle er logget ud"
|
|
49
|
+
"signedOut": "Alle er logget ud",
|
|
50
|
+
"search": "Søg på navn eller klient-ID...",
|
|
51
|
+
"noMatches": "Ingen applikationer matcher",
|
|
52
|
+
"clearSearch": "Ryd søgning"
|
|
48
53
|
},
|
|
49
54
|
"form": {
|
|
50
55
|
"createTitle": "Tilføj applikation",
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
"new": "Add application",
|
|
17
17
|
"forbiddenTitle": "You are not allowed to manage OAuth applications",
|
|
18
18
|
"forbiddenDescription": "Ask an administrator for access.",
|
|
19
|
+
"loadFailed": "OAuth applications could not be loaded",
|
|
20
|
+
"tryAgain": "Try again",
|
|
19
21
|
"application": "Application",
|
|
20
22
|
"access": "Access",
|
|
21
23
|
"signedIn": "Signed in",
|
|
@@ -44,7 +46,10 @@
|
|
|
44
46
|
"enabled": "Application enabled",
|
|
45
47
|
"disabledToast": "Application disabled",
|
|
46
48
|
"secretReplaced": "Client secret replaced",
|
|
47
|
-
"signedOut": "Everyone signed out"
|
|
49
|
+
"signedOut": "Everyone signed out",
|
|
50
|
+
"search": "Search name or client ID...",
|
|
51
|
+
"noMatches": "No applications match",
|
|
52
|
+
"clearSearch": "Clear search"
|
|
48
53
|
},
|
|
49
54
|
"form": {
|
|
50
55
|
"createTitle": "Add application",
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { oidc } from "../../utils/api.js";
|
|
1
2
|
const METHODS = {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
[oidc.token]: "POST, OPTIONS",
|
|
4
|
+
[oidc.revoke]: "POST, OPTIONS",
|
|
5
|
+
[oidc.userinfo]: "GET, OPTIONS",
|
|
6
|
+
[oidc.configuration]: "GET, OPTIONS",
|
|
7
|
+
[oidc.jwks]: "GET, OPTIONS"
|
|
7
8
|
};
|
|
8
9
|
export default defineEventHandler((event) => {
|
|
9
10
|
const methods = METHODS[getRequestURL(event).pathname];
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { idParams } from "@frigear-nu/module-kit/server";
|
|
2
|
+
import { toAdminApplicationDto } from "../../../utils/oauth-applications.js";
|
|
1
3
|
export default defineEventHandler(async (event) => {
|
|
2
4
|
await requireOAuthAdmin(event);
|
|
3
|
-
const id =
|
|
4
|
-
|
|
5
|
+
const { id } = await getValidatedRouterParams(event, idParams().parse);
|
|
6
|
+
const application = await requireApplication(id);
|
|
5
7
|
await revokeApplicationGrants(id);
|
|
6
|
-
return
|
|
8
|
+
return toAdminApplicationDto(application, 0);
|
|
7
9
|
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { idParams } from "@frigear-nu/module-kit/server";
|
|
1
2
|
export default defineEventHandler(async (event) => {
|
|
2
3
|
await requireOAuthAdmin(event);
|
|
3
|
-
const {
|
|
4
|
+
const { id } = await getValidatedRouterParams(event, idParams().parse);
|
|
5
|
+
const { secret } = await rotateApplicationSecret(id);
|
|
4
6
|
return { clientSecret: secret };
|
|
5
7
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { idParams, noContent } from "@frigear-nu/module-kit/server";
|
|
1
2
|
export default defineEventHandler(async (event) => {
|
|
2
3
|
await requireOAuthAdmin(event);
|
|
3
|
-
await
|
|
4
|
-
|
|
5
|
-
return
|
|
4
|
+
const { id } = await getValidatedRouterParams(event, idParams().parse);
|
|
5
|
+
await deleteApplication(id);
|
|
6
|
+
return noContent(event);
|
|
6
7
|
});
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { idParams } from "@frigear-nu/module-kit/server";
|
|
2
|
+
import { toAdminApplicationDto } from "../../../utils/oauth-applications.js";
|
|
1
3
|
export default defineEventHandler(async (event) => {
|
|
2
4
|
await requireOAuthAdmin(event);
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
const { id } = await getValidatedRouterParams(event, idParams().parse);
|
|
6
|
+
const application = await requireApplication(id);
|
|
7
|
+
const grants = await countActiveGrants([id]);
|
|
8
|
+
return toAdminApplicationDto(application, grants[id] ?? 0);
|
|
6
9
|
});
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { idParams } from "@frigear-nu/module-kit/server";
|
|
2
|
+
import { applicationPatchSchema, toAdminApplicationDto } from "../../../utils/oauth-applications.js";
|
|
2
3
|
export default defineEventHandler(async (event) => {
|
|
3
4
|
await requireOAuthAdmin(event);
|
|
4
|
-
const id =
|
|
5
|
+
const { id } = await getValidatedRouterParams(event, idParams().parse);
|
|
5
6
|
const patch = await readValidatedBody(event, applicationPatchSchema.parse);
|
|
6
7
|
const application = await updateApplication(id, patch);
|
|
7
8
|
if (patch.isActive === false) await revokeApplicationGrants(id);
|
|
8
|
-
return application;
|
|
9
|
+
return toAdminApplicationDto(application);
|
|
9
10
|
});
|
|
@@ -1,11 +1,23 @@
|
|
|
1
|
+
import { db, schema } from "@nuxthub/db";
|
|
2
|
+
import { count, or } from "drizzle-orm";
|
|
3
|
+
import { likeContains, orderByQuery, pageQuery, pageRange, paginate } from "@frigear-nu/module-kit/server";
|
|
4
|
+
import { toApplication, toAdminApplicationDto } from "../../../utils/oauth-applications.js";
|
|
5
|
+
const querySchema = pageQuery({ sort: ["createdAt", "name", "priority"] });
|
|
1
6
|
export default defineEventHandler(async (event) => {
|
|
2
7
|
await requireOAuthAdmin(event);
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
const query = await getValidatedQuery(event, querySchema.parse);
|
|
9
|
+
const table = schema.oauthApplications;
|
|
10
|
+
const where = query.q ? or(likeContains(table.name, query.q), likeContains(table.id, query.q)) : void 0;
|
|
11
|
+
const orderBy = orderByQuery({ createdAt: table.createdAt, name: table.name, priority: table.priority }, query, { tiebreaker: table.id });
|
|
12
|
+
const { limit, offset } = pageRange(query);
|
|
13
|
+
const [rows, [totals]] = await Promise.all([
|
|
14
|
+
db.select().from(table).where(where).orderBy(...orderBy).limit(limit).offset(offset),
|
|
15
|
+
db.select({ total: count() }).from(table).where(where)
|
|
16
|
+
]);
|
|
17
|
+
const grants = await countActiveGrants(rows.map((row) => row.id));
|
|
18
|
+
return paginate({
|
|
19
|
+
total: totals?.total ?? 0,
|
|
20
|
+
query,
|
|
21
|
+
items: rows.map((row) => toAdminApplicationDto(toApplication(row), grants[row.id] ?? 0))
|
|
22
|
+
});
|
|
11
23
|
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { created } from "@frigear-nu/module-kit/server";
|
|
2
|
+
import { applicationInputSchema, toAdminApplicationDto } from "../../../utils/oauth-applications.js";
|
|
2
3
|
export default defineEventHandler(async (event) => {
|
|
3
4
|
const { userId } = await requireOAuthAdmin(event);
|
|
4
5
|
const input = await readValidatedBody(event, applicationInputSchema.parse);
|
|
5
6
|
const { application, secret } = await createApplication(input, userId);
|
|
6
|
-
|
|
7
|
-
return { ...application, clientId: application.id, clientSecret: secret };
|
|
7
|
+
return created(event, { ...toAdminApplicationDto(application, 0), clientId: application.id, clientSecret: secret });
|
|
8
8
|
});
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { getQuery } from "h3";
|
|
2
|
+
import { parseAuthorizeRequest } from "../../utils/oauth-authorize.js";
|
|
2
3
|
export default defineEventHandler(async (event) => {
|
|
3
4
|
const userId = await requireSessionUserId(event);
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
|
|
5
|
+
const query = parseAuthorizeRequest(getQuery(event));
|
|
6
|
+
const { application, scope } = await validateAuthorizeRequest(query);
|
|
7
|
+
const user = await requireOAuthUser(userId);
|
|
7
8
|
await assertApplicationAccess(event, user, application);
|
|
8
9
|
return {
|
|
9
10
|
application: {
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { getQuery, getRequestURL, sendRedirect } from "h3";
|
|
2
|
+
import { parseAuthorizeRequest } from "../../utils/oauth-authorize.js";
|
|
2
3
|
export default defineEventHandler(async (event) => {
|
|
3
|
-
const query = getQuery(event);
|
|
4
|
+
const query = parseAuthorizeRequest(getQuery(event));
|
|
4
5
|
const { application } = await validateAuthorizeRequest(query);
|
|
6
|
+
const url = getRequestURL(event);
|
|
5
7
|
const session = await getUserSession(event);
|
|
6
8
|
if (!session.user) {
|
|
7
|
-
|
|
8
|
-
return sendRedirect(event, `${getSignInPath()}?redirect=${encodeURIComponent(`${url2.pathname}${url2.search}`)}`, 302);
|
|
9
|
+
return sendRedirect(event, `${getSignInPath()}?redirect=${encodeURIComponent(`${url.pathname}${url.search}`)}`, 302);
|
|
9
10
|
}
|
|
10
|
-
const user = await
|
|
11
|
-
if (!user) throw createError({ statusCode: 401, statusMessage: "Unknown user." });
|
|
11
|
+
const user = await requireOAuthUser(await requireSessionUserId(event));
|
|
12
12
|
await assertApplicationAccess(event, user, application);
|
|
13
|
-
const url = getRequestURL(event);
|
|
14
13
|
return sendRedirect(event, `${useOAuthConfig().authorizePath}${url.search}`, 302);
|
|
15
14
|
});
|
|
@@ -11,8 +11,7 @@ export default defineEventHandler(async (event) => {
|
|
|
11
11
|
redirect.searchParams.set("error", "access_denied");
|
|
12
12
|
return { redirectUrl: redirect.toString() };
|
|
13
13
|
}
|
|
14
|
-
const user = await
|
|
15
|
-
if (!user) throw createError({ statusCode: 401, statusMessage: "Unknown user." });
|
|
14
|
+
const user = await requireOAuthUser(userId);
|
|
16
15
|
await assertApplicationAccess(event, user, application);
|
|
17
16
|
const code = await createAuthorizationCode({
|
|
18
17
|
application,
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
+
import { oidc } from "../../../utils/api.js";
|
|
1
2
|
export default defineEventHandler((event) => {
|
|
2
3
|
const issuer = getOAuthIssuer(event);
|
|
3
4
|
setResponseHeader(event, "Cache-Control", "public, max-age=3600");
|
|
4
5
|
return {
|
|
5
6
|
issuer,
|
|
6
|
-
authorization_endpoint: `${issuer}
|
|
7
|
-
token_endpoint: `${issuer}
|
|
8
|
-
userinfo_endpoint: `${issuer}
|
|
9
|
-
revocation_endpoint: `${issuer}
|
|
10
|
-
jwks_uri: `${issuer}
|
|
7
|
+
authorization_endpoint: `${issuer}${oidc.authorize}`,
|
|
8
|
+
token_endpoint: `${issuer}${oidc.token}`,
|
|
9
|
+
userinfo_endpoint: `${issuer}${oidc.userinfo}`,
|
|
10
|
+
revocation_endpoint: `${issuer}${oidc.revoke}`,
|
|
11
|
+
jwks_uri: `${issuer}${oidc.jwks}`,
|
|
11
12
|
response_types_supported: ["code"],
|
|
12
13
|
response_modes_supported: ["query"],
|
|
13
14
|
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
@@ -1,13 +1,25 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const param = z.string().max(4096).optional();
|
|
3
|
+
const bodySchema = z.object({
|
|
4
|
+
token: param,
|
|
5
|
+
token_type_hint: param,
|
|
6
|
+
client_id: param,
|
|
7
|
+
client_secret: param
|
|
8
|
+
});
|
|
2
9
|
export default defineEventHandler(async (event) => {
|
|
3
|
-
|
|
10
|
+
let body;
|
|
11
|
+
try {
|
|
12
|
+
body = await readValidatedBody(event, bodySchema.parse);
|
|
13
|
+
} catch (error) {
|
|
14
|
+
return oauthError(event, 400, "invalid_request", invalidRequestDescription(error));
|
|
15
|
+
}
|
|
4
16
|
const client = await authenticateClient(event, body);
|
|
5
17
|
if (!("row" in client)) {
|
|
6
18
|
if (client.basic) setResponseHeader(event, "WWW-Authenticate", "Basic");
|
|
7
19
|
return oauthError(event, 401, "invalid_client", "Client authentication failed.");
|
|
8
20
|
}
|
|
9
21
|
const row = client.row;
|
|
10
|
-
if (
|
|
22
|
+
if (body.token) await revokeRefreshToken(body.token, row.id);
|
|
11
23
|
setResponseStatus(event, 200);
|
|
12
24
|
return {};
|
|
13
25
|
});
|
|
@@ -1,8 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const param = z.string().max(4096).optional();
|
|
3
|
+
const bodySchema = z.object({
|
|
4
|
+
grant_type: param,
|
|
5
|
+
code: param,
|
|
6
|
+
redirect_uri: param,
|
|
7
|
+
code_verifier: param,
|
|
8
|
+
refresh_token: param,
|
|
9
|
+
client_id: param,
|
|
10
|
+
client_secret: param
|
|
11
|
+
});
|
|
2
12
|
export default defineEventHandler(async (event) => {
|
|
3
13
|
setResponseHeader(event, "Cache-Control", "no-store");
|
|
4
14
|
setResponseHeader(event, "Pragma", "no-cache");
|
|
5
|
-
|
|
15
|
+
let body;
|
|
16
|
+
try {
|
|
17
|
+
body = await readValidatedBody(event, bodySchema.parse);
|
|
18
|
+
} catch (error) {
|
|
19
|
+
return oauthError(event, 400, "invalid_request", invalidRequestDescription(error));
|
|
20
|
+
}
|
|
6
21
|
const client = await authenticateClient(event, body);
|
|
7
22
|
if (!("row" in client)) {
|
|
8
23
|
if (client.basic) setResponseHeader(event, "WWW-Authenticate", "Basic");
|
|
@@ -10,12 +25,12 @@ export default defineEventHandler(async (event) => {
|
|
|
10
25
|
}
|
|
11
26
|
const row = client.row;
|
|
12
27
|
const application = toApplication(row);
|
|
13
|
-
const grantType = body
|
|
28
|
+
const grantType = body.grant_type;
|
|
14
29
|
let grant;
|
|
15
30
|
let refreshToken;
|
|
16
31
|
if (grantType === "authorization_code") {
|
|
17
32
|
const { code, redirect_uri: redirectUri, code_verifier: codeVerifier } = body;
|
|
18
|
-
if (
|
|
33
|
+
if (!code || !redirectUri || !codeVerifier) {
|
|
19
34
|
return oauthError(event, 400, "invalid_request", "code, redirect_uri and code_verifier are required.");
|
|
20
35
|
}
|
|
21
36
|
const result = await exchangeAuthorizationCode({ code, application, redirectUri, codeVerifier });
|
|
@@ -23,7 +38,7 @@ export default defineEventHandler(async (event) => {
|
|
|
23
38
|
grant = result.grant;
|
|
24
39
|
refreshToken = (await createRefreshToken({ applicationId: application.id, ...grant })).token;
|
|
25
40
|
} else if (grantType === "refresh_token") {
|
|
26
|
-
if (
|
|
41
|
+
if (!body.refresh_token) return oauthError(event, 400, "invalid_request", "refresh_token is required.");
|
|
27
42
|
const result = await rotateRefreshToken({ token: body.refresh_token, application });
|
|
28
43
|
if ("error" in result) return oauthError(event, 400, "invalid_grant", result.error);
|
|
29
44
|
grant = result.grant;
|
|
@@ -3,6 +3,8 @@ import type { OAuthApplication, OAuthUser, TokenType } from '../../../types.js';
|
|
|
3
3
|
export declare function requireOAuthAdmin(event: H3Event): Promise<{
|
|
4
4
|
userId: number;
|
|
5
5
|
}>;
|
|
6
|
+
/** The signed-in user as stored, or 401 `unauthorized` when the account no longer exists. */
|
|
7
|
+
export declare function requireOAuthUser(userId: number): Promise<OAuthUser>;
|
|
6
8
|
/** The user's roles as reported by the `oauth:user:roles` hook listeners (none without a listener). */
|
|
7
9
|
export declare function resolveUserRoles(userId: number, event?: H3Event): Promise<string[]>;
|
|
8
10
|
/** The stored user without the password hash; includes columns the host added to `users`. */
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
import { apiError, forbidden } from "@frigear-nu/module-kit/server";
|
|
1
2
|
import { matchesAllowedRoles } from "../../utils/index.js";
|
|
2
3
|
const adminGate = definePolicyGate("oauth:authorize");
|
|
3
4
|
export async function requireOAuthAdmin(event) {
|
|
4
5
|
const userId = await requireSessionUserId(event);
|
|
5
|
-
await adminGate.
|
|
6
|
+
if (!await adminGate.isAllowed(event, userId)) throw forbidden("You are not allowed to manage OAuth applications.");
|
|
6
7
|
return { userId };
|
|
7
8
|
}
|
|
9
|
+
export async function requireOAuthUser(userId) {
|
|
10
|
+
const user = await getOAuthUser(userId);
|
|
11
|
+
if (!user) throw apiError(401, "unauthorized", "Unknown user.");
|
|
12
|
+
return user;
|
|
13
|
+
}
|
|
8
14
|
export async function resolveUserRoles(userId, event) {
|
|
9
15
|
const ctx = { event, userId, roles: [] };
|
|
10
16
|
await useNitroApp().hooks.callHook("oauth:user:roles", ctx);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { schema } from '@nuxthub/db';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import type { H3Event } from 'h3';
|
|
4
|
-
import type { ApplicationInput, OAuthApplication, UserApplicationDto } from '../../../types.js';
|
|
4
|
+
import type { AdminApplicationDto, ApplicationInput, OAuthApplication, UserApplicationDto } from '../../../types.js';
|
|
5
5
|
type ApplicationRow = typeof schema.oauthApplications.$inferSelect;
|
|
6
6
|
/** Validation shared by the admin API and `createApplication` / `updateApplication`. */
|
|
7
7
|
export declare const applicationInputSchema: z.ZodObject<{
|
|
@@ -39,10 +39,15 @@ export declare const applicationPatchSchema: z.ZodObject<{
|
|
|
39
39
|
isActive: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
|
|
40
40
|
}, z.core.$strip>;
|
|
41
41
|
export declare function toApplication(row: ApplicationRow): OAuthApplication;
|
|
42
|
+
/** The redirect URI an application has to accept on its side: its website plus the configured callback path. */
|
|
43
|
+
export declare function applicationCallbackUrl(application: Pick<OAuthApplication, 'websiteUrl'>): string;
|
|
44
|
+
/** The admin view of an application (never the secret hash), with the active sign-ins when counted. */
|
|
45
|
+
export declare function toAdminApplicationDto(application: OAuthApplication, activeGrants?: number): AdminApplicationDto;
|
|
46
|
+
/** One application for a staff route, or 404 `not_found`. */
|
|
47
|
+
export declare function requireApplication(id: string): Promise<OAuthApplication>;
|
|
42
48
|
/** The row including `secretHash`; only for credential checks. Use `getApplication` everywhere else. */
|
|
43
49
|
export declare function getApplicationRow(id: string): Promise<ApplicationRow | null>;
|
|
44
50
|
export declare function getApplication(id: string): Promise<OAuthApplication | null>;
|
|
45
|
-
export declare function listApplications(): Promise<OAuthApplication[]>;
|
|
46
51
|
/**
|
|
47
52
|
* Creates an application. The returned `secret` is the only time the plain secret is available; it is `null` for
|
|
48
53
|
* `public` clients, which have none.
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { and, desc, eq, inArray } from "drizzle-orm";
|
|
2
2
|
import { db, schema } from "@nuxthub/db";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
+
import { badRequest, isoDate, notFound } from "@frigear-nu/module-kit/server";
|
|
4
5
|
import { CLIENT_TYPES, isNativeRedirectUri, isSecureOrigin, matchesAllowedRoles, newId, normalizeWebsiteUrl } from "../../utils/index.js";
|
|
5
|
-
const
|
|
6
|
+
const applicationNotFound = () => notFound("Application not found.");
|
|
6
7
|
const httpsUrl = z.string().trim().max(2048).refine((value) => {
|
|
7
8
|
try {
|
|
8
9
|
return isSecureOrigin(new URL(value));
|
|
@@ -38,13 +39,43 @@ export function toApplication(row) {
|
|
|
38
39
|
const { secretHash: _secretHash, ...application } = row;
|
|
39
40
|
return application;
|
|
40
41
|
}
|
|
42
|
+
export function applicationCallbackUrl(application) {
|
|
43
|
+
return `${application.websiteUrl}${useOAuthConfig().callbackPath}`;
|
|
44
|
+
}
|
|
45
|
+
export function toAdminApplicationDto(application, activeGrants) {
|
|
46
|
+
return {
|
|
47
|
+
id: application.id,
|
|
48
|
+
clientType: application.clientType,
|
|
49
|
+
name: application.name,
|
|
50
|
+
description: application.description,
|
|
51
|
+
icon: application.icon,
|
|
52
|
+
priority: application.priority,
|
|
53
|
+
websiteUrl: application.websiteUrl,
|
|
54
|
+
loginUrl: application.loginUrl,
|
|
55
|
+
previewUrlPattern: application.previewUrlPattern,
|
|
56
|
+
redirectUris: application.redirectUris,
|
|
57
|
+
allowedRoles: application.allowedRoles,
|
|
58
|
+
tags: application.tags,
|
|
59
|
+
ownerId: application.ownerId,
|
|
60
|
+
isActive: application.isActive,
|
|
61
|
+
createdAt: isoDate(application.createdAt),
|
|
62
|
+
updatedAt: isoDate(application.updatedAt),
|
|
63
|
+
...activeGrants === void 0 ? {} : { activeGrants },
|
|
64
|
+
callbackUrl: applicationCallbackUrl(application)
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export async function requireApplication(id) {
|
|
68
|
+
const application = await getApplication(id);
|
|
69
|
+
if (!application) throw applicationNotFound();
|
|
70
|
+
return application;
|
|
71
|
+
}
|
|
41
72
|
function normalizeInput(input) {
|
|
42
73
|
const next = { ...input };
|
|
43
74
|
if (next.websiteUrl !== void 0) {
|
|
44
75
|
try {
|
|
45
76
|
next.websiteUrl = normalizeWebsiteUrl(next.websiteUrl);
|
|
46
77
|
} catch (error) {
|
|
47
|
-
throw
|
|
78
|
+
throw badRequest(error.message);
|
|
48
79
|
}
|
|
49
80
|
}
|
|
50
81
|
if (next.redirectUris) next.redirectUris = [...new Set(next.redirectUris)];
|
|
@@ -54,7 +85,7 @@ function normalizeInput(input) {
|
|
|
54
85
|
}
|
|
55
86
|
function assertRedirectUris(uris, clientType) {
|
|
56
87
|
if (clientType === "public" || !uris) return;
|
|
57
|
-
if (uris.some((uri) => !isSecureOrigin(new URL(uri)))) throw
|
|
88
|
+
if (uris.some((uri) => !isSecureOrigin(new URL(uri)))) throw badRequest("Only public clients may use custom-scheme redirect URIs.");
|
|
58
89
|
}
|
|
59
90
|
export async function getApplicationRow(id) {
|
|
60
91
|
const [row] = await db.select().from(schema.oauthApplications).where(eq(schema.oauthApplications.id, id)).limit(1);
|
|
@@ -64,10 +95,6 @@ export async function getApplication(id) {
|
|
|
64
95
|
const row = await getApplicationRow(id);
|
|
65
96
|
return row ? toApplication(row) : null;
|
|
66
97
|
}
|
|
67
|
-
export async function listApplications() {
|
|
68
|
-
const rows = await db.select().from(schema.oauthApplications).orderBy(desc(schema.oauthApplications.priority), schema.oauthApplications.name);
|
|
69
|
-
return rows.map(toApplication);
|
|
70
|
-
}
|
|
71
98
|
export async function createApplication(input, ownerId) {
|
|
72
99
|
const data = normalizeInput(applicationInputSchema.parse(input));
|
|
73
100
|
const clientType = data.clientType ?? "confidential";
|
|
@@ -86,8 +113,8 @@ export async function createApplication(input, ownerId) {
|
|
|
86
113
|
export async function updateApplication(id, patch) {
|
|
87
114
|
const { clientType, ...data } = normalizeInput(applicationPatchSchema.parse(patch));
|
|
88
115
|
const existing = await getApplicationRow(id);
|
|
89
|
-
if (!existing) throw
|
|
90
|
-
if (clientType && clientType !== existing.clientType) throw
|
|
116
|
+
if (!existing) throw applicationNotFound();
|
|
117
|
+
if (clientType && clientType !== existing.clientType) throw badRequest("The client type cannot be changed.");
|
|
91
118
|
assertRedirectUris(data.redirectUris, existing.clientType);
|
|
92
119
|
if (!Object.keys(data).length) return toApplication(existing);
|
|
93
120
|
const [row] = await db.update(schema.oauthApplications).set(data).where(eq(schema.oauthApplications.id, id)).returning();
|
|
@@ -97,14 +124,14 @@ export async function deleteApplication(id) {
|
|
|
97
124
|
await db.delete(schema.oauthRefreshTokens).where(eq(schema.oauthRefreshTokens.applicationId, id));
|
|
98
125
|
await db.delete(schema.oauthAuthorizationCodes).where(eq(schema.oauthAuthorizationCodes.applicationId, id));
|
|
99
126
|
const deleted = await db.delete(schema.oauthApplications).where(eq(schema.oauthApplications.id, id)).returning({ id: schema.oauthApplications.id });
|
|
100
|
-
if (!deleted.length) throw
|
|
127
|
+
if (!deleted.length) throw applicationNotFound();
|
|
101
128
|
}
|
|
102
129
|
export async function rotateApplicationSecret(id) {
|
|
103
130
|
const existing = await getApplicationRow(id);
|
|
104
|
-
if (existing?.clientType === "public") throw
|
|
131
|
+
if (existing?.clientType === "public") throw badRequest("Public clients have no client secret.");
|
|
105
132
|
const secret = generateSecret();
|
|
106
133
|
const updated = await db.update(schema.oauthApplications).set({ secretHash: await sha256Hex(secret) }).where(eq(schema.oauthApplications.id, id)).returning({ id: schema.oauthApplications.id });
|
|
107
|
-
if (!updated.length) throw
|
|
134
|
+
if (!updated.length) throw applicationNotFound();
|
|
108
135
|
await revokeApplicationGrants(id);
|
|
109
136
|
return { secret };
|
|
110
137
|
}
|
|
@@ -14,11 +14,15 @@ export declare const authorizeRequestSchema: z.ZodObject<{
|
|
|
14
14
|
}, z.core.$strip>;
|
|
15
15
|
export type AuthorizeRequest = z.infer<typeof authorizeRequestSchema>;
|
|
16
16
|
/**
|
|
17
|
-
* Parses
|
|
18
|
-
*
|
|
19
|
-
* be trusted to carry them yet.
|
|
17
|
+
* Parses the authorization request parameters (a query or a body). Failures are a
|
|
18
|
+
* plain 400 `bad_request` naming the bad parameters, because the redirect URI cannot be trusted to carry them yet.
|
|
20
19
|
*/
|
|
21
|
-
export declare function
|
|
20
|
+
export declare function parseAuthorizeRequest(input: unknown): AuthorizeRequest;
|
|
21
|
+
/**
|
|
22
|
+
* Validates a parsed authorization request: known, active client, an allowed redirect URI and the scopes (PKCE with
|
|
23
|
+
* S256 is enforced by the schema). Failures are plain HTTP errors because the redirect URI cannot be trusted yet.
|
|
24
|
+
*/
|
|
25
|
+
export declare function validateAuthorizeRequest(request: AuthorizeRequest): Promise<{
|
|
22
26
|
request: AuthorizeRequest;
|
|
23
27
|
application: OAuthApplication;
|
|
24
28
|
scope: string[];
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { badRequest, forbidden } from "@frigear-nu/module-kit/server";
|
|
2
3
|
import { resolveScope, validateRedirectUri } from "../../utils/index.js";
|
|
3
4
|
export const authorizeRequestSchema = z.object({
|
|
4
5
|
client_id: z.string().min(1),
|
|
@@ -10,22 +11,23 @@ export const authorizeRequestSchema = z.object({
|
|
|
10
11
|
scope: z.string().max(1024).optional(),
|
|
11
12
|
nonce: z.string().max(512).optional()
|
|
12
13
|
});
|
|
13
|
-
|
|
14
|
-
export async function validateAuthorizeRequest(input) {
|
|
14
|
+
export function parseAuthorizeRequest(input) {
|
|
15
15
|
const parsed = authorizeRequestSchema.safeParse(input);
|
|
16
|
-
if (!parsed.success) throw
|
|
17
|
-
|
|
16
|
+
if (!parsed.success) throw badRequest(`Invalid authorization request: ${parsed.error.issues.map((issue) => `${issue.path.join(".")} ${issue.message}`).join(", ")}`);
|
|
17
|
+
return parsed.data;
|
|
18
|
+
}
|
|
19
|
+
export async function validateAuthorizeRequest(request) {
|
|
18
20
|
const application = await getApplication(request.client_id);
|
|
19
|
-
if (!application || !application.isActive) throw
|
|
21
|
+
if (!application || !application.isActive) throw badRequest("Unknown or disabled application.");
|
|
20
22
|
const valid = validateRedirectUri(request.redirect_uri, { ...application, callbackPath: useOAuthConfig().callbackPath, allowNativeRedirects: application.clientType === "public" });
|
|
21
|
-
if (!valid) throw
|
|
23
|
+
if (!valid) throw badRequest("The redirect_uri is not allowed for this application.");
|
|
22
24
|
const scope = resolveScope(request.scope, getSupportedScopes());
|
|
23
|
-
if (scope.length === 0) throw
|
|
25
|
+
if (scope.length === 0) throw badRequest("None of the requested scopes is supported.");
|
|
24
26
|
return { request, application, scope };
|
|
25
27
|
}
|
|
26
28
|
export async function assertApplicationAccess(event, user, application) {
|
|
27
29
|
const access = await checkApplicationAccess(user, application, event);
|
|
28
|
-
if (!access.allowed) throw
|
|
30
|
+
if (!access.allowed) throw forbidden(access.reason ?? "You do not have access to this application.");
|
|
29
31
|
}
|
|
30
32
|
export function getSignInPath() {
|
|
31
33
|
const config = useRuntimeConfig();
|
|
@@ -19,3 +19,8 @@ export declare function oauthError(event: H3Event, statusCode: number, error: st
|
|
|
19
19
|
error_description?: string | undefined;
|
|
20
20
|
error: string;
|
|
21
21
|
};
|
|
22
|
+
/**
|
|
23
|
+
* The `error_description` for a token or revocation request whose body failed `readValidatedBody` (h3's 400 with the
|
|
24
|
+
* zod issues in `data`, or unparsable JSON), so the route can still answer the RFC 6749 `invalid_request` body.
|
|
25
|
+
*/
|
|
26
|
+
export declare function invalidRequestDescription(error: unknown): string;
|
|
@@ -24,3 +24,8 @@ export function oauthError(event, statusCode, error, description) {
|
|
|
24
24
|
setResponseHeader(event, "Cache-Control", "no-store");
|
|
25
25
|
return { error, ...description ? { error_description: description } : {} };
|
|
26
26
|
}
|
|
27
|
+
export function invalidRequestDescription(error) {
|
|
28
|
+
const issues = error?.data?.issues;
|
|
29
|
+
if (!Array.isArray(issues) || !issues.length) return "The request body is malformed.";
|
|
30
|
+
return `Invalid request: ${issues.map((issue) => [issue.path?.map(String).join("."), issue.message].filter(Boolean).join(" ")).join(", ")}`;
|
|
31
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { exportJWK, importPKCS8, importSPKI } from "jose";
|
|
2
|
+
import { apiError } from "@frigear-nu/module-kit/server";
|
|
2
3
|
export function normalizePemKey(raw) {
|
|
3
4
|
return raw.trim().replace(/^["']|["']$/g, "").replace(/\\n/g, "\n").trim();
|
|
4
5
|
}
|
|
5
|
-
const notConfigured = () =>
|
|
6
|
+
const notConfigured = () => apiError(500, "not_configured", "The OAuth server signing keys are not configured.");
|
|
6
7
|
let cache;
|
|
7
8
|
async function loadKeys() {
|
|
8
9
|
const config = useOAuthConfig();
|
|
@@ -31,7 +32,7 @@ function parsePreviousKeys() {
|
|
|
31
32
|
const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
32
33
|
return Object.fromEntries(Object.entries(parsed).map(([kid, pem]) => [kid, normalizePemKey(String(pem))]));
|
|
33
34
|
} catch {
|
|
34
|
-
throw
|
|
35
|
+
throw apiError(500, "not_configured", "The OAuth server previous public keys are not valid JSON.");
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
let previousCache;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** The module API: the consent data and the caller's applications under `/api/oauth-server`, staff routes under `/api/admin/oauth-server`. */
|
|
2
|
+
export declare const oauthServerApi: {
|
|
3
|
+
/** `GET`: data for the consent screen (the authorization request's parameters as the query). */
|
|
4
|
+
readonly consent: "/api/oauth-server/consent";
|
|
5
|
+
readonly me: {
|
|
6
|
+
/** `GET`: the applications the signed-in user may sign in to. */
|
|
7
|
+
readonly applications: "/api/oauth-server/me/applications";
|
|
8
|
+
};
|
|
9
|
+
readonly admin: {
|
|
10
|
+
/** `GET` (paginated) and `POST` (create, 201). */
|
|
11
|
+
readonly applications: "/api/admin/oauth-server/applications";
|
|
12
|
+
/** `GET`, `PATCH`, `DELETE` (204). */
|
|
13
|
+
readonly application: (id: string) => string;
|
|
14
|
+
/** `POST`: replaces the client secret and answers `{ clientSecret }` once. */
|
|
15
|
+
readonly rotateSecret: (id: string) => string;
|
|
16
|
+
/** `POST`: signs everyone out of the application; answers the application. */
|
|
17
|
+
readonly revoke: (id: string) => string;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
/** The OAuth 2.0 / OpenID Connect endpoints, at their standard paths. */
|
|
21
|
+
export declare const oidc: {
|
|
22
|
+
readonly configuration: "/.well-known/openid-configuration";
|
|
23
|
+
readonly jwks: "/.well-known/jwks.json";
|
|
24
|
+
readonly authorize: "/oauth/authorize";
|
|
25
|
+
readonly token: "/oauth/token";
|
|
26
|
+
readonly revoke: "/oauth/revoke";
|
|
27
|
+
readonly userinfo: "/oauth/userinfo";
|
|
28
|
+
};
|