@frigear-nu/oauth-server 0.1.2 → 0.2.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.
Files changed (44) hide show
  1. package/dist/module.d.mts +8 -1
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +24 -20
  4. package/dist/runtime/app/components/OAuthApplicationSlideover.vue +1 -0
  5. package/dist/runtime/app/components/OAuthCredentialsModal.vue +1 -0
  6. package/dist/runtime/app/composables/useOAuthApplicationActions.d.ts +3 -0
  7. package/dist/runtime/app/composables/useOAuthApplicationActions.js +30 -13
  8. package/dist/runtime/app/composables/useOAuthApplications.d.ts +34 -13
  9. package/dist/runtime/app/composables/useOAuthApplications.js +26 -14
  10. package/dist/runtime/app/pages/admin.vue +114 -62
  11. package/dist/runtime/app/pages/applications.vue +52 -34
  12. package/dist/runtime/app/pages/authorize.vue +4 -2
  13. package/dist/runtime/i18n/lang/da.json +6 -1
  14. package/dist/runtime/i18n/lang/en.json +6 -1
  15. package/dist/runtime/menus.js +1 -0
  16. package/dist/runtime/server/middleware/oauth-cors.js +6 -5
  17. package/dist/runtime/server/routes/api/admin/application-revoke.post.js +5 -3
  18. package/dist/runtime/server/routes/api/admin/application-secret.post.js +3 -1
  19. package/dist/runtime/server/routes/api/admin/application.delete.js +4 -3
  20. package/dist/runtime/server/routes/api/admin/application.get.js +6 -3
  21. package/dist/runtime/server/routes/api/admin/application.patch.js +4 -3
  22. package/dist/runtime/server/routes/api/admin/applications.get.js +20 -8
  23. package/dist/runtime/server/routes/api/admin/applications.post.js +3 -3
  24. package/dist/runtime/server/routes/api/consent.get.js +4 -3
  25. package/dist/runtime/server/routes/oidc/authorize.get.js +5 -6
  26. package/dist/runtime/server/routes/oidc/authorize.post.js +1 -2
  27. package/dist/runtime/server/routes/oidc/openid-configuration.get.js +6 -5
  28. package/dist/runtime/server/routes/oidc/revoke.post.js +15 -3
  29. package/dist/runtime/server/routes/oidc/token.post.js +20 -5
  30. package/dist/runtime/server/utils/oauth-access.d.ts +2 -0
  31. package/dist/runtime/server/utils/oauth-access.js +7 -1
  32. package/dist/runtime/server/utils/oauth-applications.d.ts +7 -2
  33. package/dist/runtime/server/utils/oauth-applications.js +39 -12
  34. package/dist/runtime/server/utils/oauth-authorize.d.ts +8 -4
  35. package/dist/runtime/server/utils/oauth-authorize.js +10 -8
  36. package/dist/runtime/server/utils/oauth-config.d.ts +5 -0
  37. package/dist/runtime/server/utils/oauth-config.js +5 -0
  38. package/dist/runtime/server/utils/oauth-keys.js +3 -2
  39. package/dist/runtime/utils/api.d.ts +28 -0
  40. package/dist/runtime/utils/api.js +27 -0
  41. package/dist/runtime/utils/index.d.ts +1 -0
  42. package/dist/runtime/utils/index.js +1 -0
  43. package/dist/types.d.mts +1 -1
  44. package/package.json +14 -9
@@ -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 and validates an authorization request: known, active client, an allowed redirect URI, PKCE
18
- * (S256 is mandatory) and the scopes. Failures are plain HTTP errors because the redirect URI cannot
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 validateAuthorizeRequest(input: unknown): Promise<{
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
- const fail = (statusCode, statusMessage) => createError({ statusCode, statusMessage });
14
- export async function validateAuthorizeRequest(input) {
14
+ export function parseAuthorizeRequest(input) {
15
15
  const parsed = authorizeRequestSchema.safeParse(input);
16
- if (!parsed.success) throw fail(400, `Invalid authorization request: ${parsed.error.issues.map((issue) => `${issue.path.join(".")} ${issue.message}`).join(", ")}`);
17
- const request = parsed.data;
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 fail(400, "Unknown or disabled application.");
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 fail(400, "The redirect_uri is not allowed for this application.");
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 fail(400, "None of the requested scopes is supported.");
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 fail(403, access.reason ?? "You do not have access to this application.");
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 = () => createError({ statusCode: 500, statusMessage: "The OAuth server signing keys are not configured." });
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 createError({ statusCode: 500, statusMessage: "The OAuth server previous public keys are not valid JSON." });
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
+ };
@@ -0,0 +1,27 @@
1
+ const enc = encodeURIComponent;
2
+ export const oauthServerApi = {
3
+ /** `GET`: data for the consent screen (the authorization request's parameters as the query). */
4
+ consent: "/api/oauth-server/consent",
5
+ me: {
6
+ /** `GET`: the applications the signed-in user may sign in to. */
7
+ applications: "/api/oauth-server/me/applications"
8
+ },
9
+ admin: {
10
+ /** `GET` (paginated) and `POST` (create, 201). */
11
+ applications: "/api/admin/oauth-server/applications",
12
+ /** `GET`, `PATCH`, `DELETE` (204). */
13
+ application: (id) => `/api/admin/oauth-server/applications/${enc(id)}`,
14
+ /** `POST`: replaces the client secret and answers `{ clientSecret }` once. */
15
+ rotateSecret: (id) => `/api/admin/oauth-server/applications/${enc(id)}/secret`,
16
+ /** `POST`: signs everyone out of the application; answers the application. */
17
+ revoke: (id) => `/api/admin/oauth-server/applications/${enc(id)}/revoke`
18
+ }
19
+ };
20
+ export const oidc = {
21
+ configuration: "/.well-known/openid-configuration",
22
+ jwks: "/.well-known/jwks.json",
23
+ authorize: "/oauth/authorize",
24
+ token: "/oauth/token",
25
+ revoke: "/oauth/revoke",
26
+ userinfo: "/oauth/userinfo"
27
+ };
@@ -1,4 +1,5 @@
1
1
  export type * from '../../types';
2
+ export { oauthServerApi, oidc } from './api.js';
2
3
  export declare const DEFAULT_SCOPES: readonly ["openid", "profile", "email"];
3
4
  export declare const DEFAULT_CALLBACK_PATH = "/auth/frigear";
4
5
  /** `confidential` clients hold a secret; `public` clients (native and single-page apps) cannot and rely on PKCE alone. */
@@ -1,3 +1,4 @@
1
+ export { oauthServerApi, oidc } from "./api.js";
1
2
  export const DEFAULT_SCOPES = ["openid", "profile", "email"];
2
3
  export const DEFAULT_CALLBACK_PATH = "/auth/frigear";
3
4
  export const CLIENT_TYPES = ["confidential", "public"];
package/dist/types.d.mts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { default } from './module.mjs'
2
2
 
3
- export { type AdminApplicationDto, type ApplicationAccessContext, type ApplicationInput, type AuthorizeContext, type ClaimsContext, type ClientType, type ConsentDto, type CreatedApplicationDto, type ModuleOptions, type OAuthApplication, type OAuthServerRuntimeConfig, type OAuthUser, type PublicModuleOptions, type TokenIssuedContext, type TokenRevokedContext, type TokenType, type UserApplicationDto, type UserRolesContext } from './module.mjs'
3
+ export { type AdminApplicationDto, type AdminApplicationsPage, type ApplicationAccessContext, type ApplicationInput, type AuthorizeContext, type ClaimsContext, type ClientType, type ConsentDto, type CreatedApplicationDto, type ModuleOptions, type OAuthApplication, type OAuthServerRuntimeConfig, type OAuthUser, type PublicModuleOptions, type TokenIssuedContext, type TokenRevokedContext, type TokenType, type UserApplicationDto, type UserRolesContext } from './module.mjs'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frigear-nu/oauth-server",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
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
5
  "url": "https://docs.frigear.dev/modules/oauth-server",
6
6
  "author": {
@@ -28,6 +28,11 @@
28
28
  "types": "./dist/runtime/utils/index.d.ts",
29
29
  "import": "./dist/runtime/utils/index.js",
30
30
  "default": "./dist/runtime/utils/index.js"
31
+ },
32
+ "./api": {
33
+ "types": "./dist/runtime/utils/api.d.ts",
34
+ "import": "./dist/runtime/utils/api.js",
35
+ "default": "./dist/runtime/utils/api.js"
31
36
  }
32
37
  },
33
38
  "files": [
@@ -37,12 +42,12 @@
37
42
  "playground"
38
43
  ],
39
44
  "dependencies": {
40
- "@frigear-nu/admin": "0.1.2",
41
- "@frigear-nu/module-kit": "1.1.2",
42
- "@frigear-nu/ui": "0.1.2",
43
- "@frigear-nu/auth": "0.1.3",
44
- "@frigear-nu/mcp": "0.1.2",
45
- "@frigear-nu/users": "1.1.3",
45
+ "@frigear-nu/admin": "0.2.0",
46
+ "@frigear-nu/module-kit": "1.2.0",
47
+ "@frigear-nu/ui": "0.2.0",
48
+ "@frigear-nu/auth": "0.2.0",
49
+ "@frigear-nu/mcp": "0.2.0",
50
+ "@frigear-nu/users": "1.2.0",
46
51
  "@libsql/client": "^0.18.0",
47
52
  "@nuxt/kit": "^4.6.1",
48
53
  "drizzle-kit": "^0.31.11",
@@ -52,7 +57,7 @@
52
57
  "zod": "^4.6.5"
53
58
  },
54
59
  "peerDependencies": {
55
- "@frigear-nu/menu": "0.1.2",
60
+ "@frigear-nu/menu": "0.2.0",
56
61
  "@nuxt/ui": "^4.12.0",
57
62
  "@nuxthub/core": "^0.10.8"
58
63
  },
@@ -65,7 +70,7 @@
65
70
  }
66
71
  },
67
72
  "devDependencies": {
68
- "@frigear-nu/menu": "0.1.2",
73
+ "@frigear-nu/menu": "0.2.0",
69
74
  "@nuxt/devtools": "^3.4.2",
70
75
  "@nuxt/eslint-config": "^1.17.0",
71
76
  "@nuxt/module-builder": "^1.0.3",