Refresh UI components and showcase; release updated framework packages
This commit is contained in:
1 parent
d800228585
commit
c77f5e1cec
293 files changed
+22544
-35778
No files matched your search
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/auth",
|
||||
"version": "0.8.32",
|
||||
"version": "0.8.33",
|
||||
"description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@@ -2081,7 +2081,9 @@ export function createAuthEngine(options: AuthEngineOptions): AuthEngine {
|
||||
const verification = verificationUnavailable(user);
|
||||
if (verification) return verification;
|
||||
account = await engine.linkOAuth(user.id, providerName, normalizedProfile, tokens);
|
||||
const deviceCheck = await newDeviceRequirement(user, {
|
||||
const providerVerifiesDevice = verifiedEmail &&
|
||||
options.verifiedOAuthDeviceProviders?.some((name) => normalizedOAuthProvider(name) === providerName);
|
||||
const deviceCheck = providerVerifiesDevice ? undefined : await newDeviceRequirement(user, {
|
||||
...sessionInput,
|
||||
oauthProvider: providerName,
|
||||
});
|
||||
|
||||
@@ -28,6 +28,7 @@ export {
|
||||
} from "./http/index.ts";
|
||||
export {
|
||||
authPlugin,
|
||||
configureAuthRuntime,
|
||||
authComponentsDir,
|
||||
validateProductionAuthConfig,
|
||||
type AuthConfig,
|
||||
|
||||
+21
-21
@@ -207,6 +207,26 @@ function resolveConfig(
|
||||
};
|
||||
}
|
||||
|
||||
/** Restore auth state in a production server using its runtime configuration. */
|
||||
export function configureAuthRuntime(config: AuthConfig): void {
|
||||
applyRuntimeConfig(resolveConfig({ auth: config }, {}));
|
||||
}
|
||||
|
||||
function applyRuntimeConfig(value: ResolvedAuthConfig): void {
|
||||
if (value.enabled && value.engine) setDefaultAuthEngine(value.engine);
|
||||
else clearDefaultAuthEngine();
|
||||
setDefaultAuthSchemas(value.schemas);
|
||||
setDefaultAuthRouteOptions({
|
||||
baseUrl: value.baseUrl,
|
||||
csrf: value.csrf,
|
||||
passkey: value.passkey,
|
||||
sessionCookieName: value.sessionCookieName,
|
||||
onSessionVerification: value.onSessionVerification,
|
||||
oauth: value.enabled ? value.oauth : {},
|
||||
oauthMfaPath: value.oauthMfaPath,
|
||||
});
|
||||
}
|
||||
|
||||
function fallbackConfig(options: AuthPluginOptions): ResolvedAuthConfig {
|
||||
return {
|
||||
enabled: true,
|
||||
@@ -432,27 +452,7 @@ export function authPlugin(options: AuthPluginOptions = {}) {
|
||||
schemas: value.schemas,
|
||||
};
|
||||
|
||||
if (value.engine) {
|
||||
setDefaultAuthEngine(value.engine);
|
||||
} else {
|
||||
clearDefaultAuthEngine();
|
||||
}
|
||||
|
||||
setDefaultAuthSchemas(value.schemas);
|
||||
|
||||
setDefaultAuthRouteOptions({
|
||||
baseUrl: value.baseUrl,
|
||||
|
||||
csrf: value.csrf,
|
||||
|
||||
passkey: value.passkey,
|
||||
|
||||
sessionCookieName: value.sessionCookieName,
|
||||
|
||||
onSessionVerification: value.onSessionVerification,
|
||||
oauth: value.oauth,
|
||||
oauthMfaPath: value.oauthMfaPath,
|
||||
});
|
||||
applyRuntimeConfig(value);
|
||||
|
||||
context.metadata.set("@wrnexus/auth:component-dir", value.componentDir);
|
||||
|
||||
|
||||
@@ -440,6 +440,8 @@ export interface AuthEngineOptions {
|
||||
trustedDeviceTtlMs?: number;
|
||||
/** Require verified email/SMS confirmation before a new browser receives a session. */
|
||||
requireTrustedDeviceVerification?: boolean;
|
||||
/** Verified email from these OAuth providers satisfies the additional new-device check. Account MFA still applies. */
|
||||
verifiedOAuthDeviceProviders?: string[];
|
||||
/** Remember the registration browser after required account verification succeeds. */
|
||||
trustSignupDevice?: boolean;
|
||||
tokenTtlMs?: Partial<Record<AuthTokenPurpose, number>>;
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { createAuthEngine } from "../src/engine.ts";
|
||||
import { MemoryAuthStore } from "../src/stores/memory.ts";
|
||||
|
||||
const profile = { id: "google-user", email: "google@example.test", raw: { email_verified: true } };
|
||||
function setup(providers?: string[]) {
|
||||
const store = new MemoryAuthStore();
|
||||
const engine = createAuthEngine({ store, secret: "oauth-device-policy-test-at-least-32-characters",
|
||||
requireVerifiedEmail: true, requireTrustedDeviceVerification: true,
|
||||
verifiedOAuthDeviceProviders: providers,
|
||||
isOAuthEmailVerified: (_provider, value) => value.raw.email_verified === true,
|
||||
});
|
||||
return { engine, store };
|
||||
}
|
||||
test("explicit verified Google policy satisfies the device check without trusting future password logins", async () => {
|
||||
const { engine } = setup(["google"]);
|
||||
const result = await engine.loginWithOAuth("google", profile, undefined, { fingerprint: "new-browser" });
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.session).toBeDefined();
|
||||
expect(await engine.listTrustedDevices(result.user!.id)).toHaveLength(0);
|
||||
});
|
||||
test("device verification remains the default and applies to other providers", async () => {
|
||||
expect((await setup().engine.loginWithOAuth("google", profile)).code).toBe("mfa-required");
|
||||
expect((await setup(["google"]).engine.loginWithOAuth("other", profile)).code).toBe("mfa-required");
|
||||
});
|
||||
test("unverified provider emails cannot satisfy verification", async () => {
|
||||
const { engine } = setup(["google"]);
|
||||
const result = await engine.loginWithOAuth("google", { ...profile, raw: { email_verified: false } });
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.session).toBeUndefined();
|
||||
});
|
||||
test("explicit account MFA remains required after verified Google sign-in", async () => {
|
||||
const { engine } = setup(["google"]);
|
||||
const first = await engine.loginWithOAuth("google", profile);
|
||||
await engine.generateRecoveryCodes(first.user!.id);
|
||||
const result = await engine.loginWithOAuth("google", profile);
|
||||
expect(result.code).toBe("mfa-required");
|
||||
expect(result.session).toBeUndefined();
|
||||
expect(result.requires?.mfa).toContain("recovery-code");
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import { afterEach, expect, test } from "bun:test";
|
||||
import { createContext } from "@wrnexus/core";
|
||||
import { createAuthEngine } from "../src/engine.ts";
|
||||
import { MemoryAuthStore } from "../src/stores/memory.ts";
|
||||
import { configureAuthRuntime } from "../src/plugin.ts";
|
||||
import { clearDefaultAuthEngine, getDefaultAuthRouteOptions, setDefaultAuthRouteOptions } from "../src/runtime.ts";
|
||||
import { invokeAuthHandler } from "../src/routes/api.ts";
|
||||
|
||||
afterEach(() => { clearDefaultAuthEngine(); setDefaultAuthRouteOptions({}); });
|
||||
|
||||
test("production startup restores runtime-only providers and passkey settings for generated routes", async () => {
|
||||
const engine = createAuthEngine({
|
||||
store: new MemoryAuthStore(), secret: "production-runtime-test-secret-at-least-32-characters",
|
||||
passkeys: {
|
||||
async authenticationOptions({ rpId }) { return { challenge: "test-challenge", rpId, timeout: 60000, userVerification: "required" }; },
|
||||
async registrationOptions() { throw new Error("not used"); },
|
||||
async verifyRegistration() { return { verified: false }; },
|
||||
async verifyAuthentication() { return { verified: false }; },
|
||||
},
|
||||
});
|
||||
// Build-time credentials absent, followed by server-startup runtime secrets.
|
||||
configureAuthRuntime({ engine, oauth: {} });
|
||||
const ctx = createContext(new Request("https://sso.example/api/auth/oauth/google"), new URL("https://sso.example/api/auth/oauth/google"));
|
||||
ctx.params = { provider: "google" };
|
||||
expect((await invokeAuthHandler("startOAuth", ctx)).status).toBe(404);
|
||||
configureAuthRuntime({ engine, baseUrl: "https://sso.example", sessionCookieName: "custom_session",
|
||||
oauth: { google: { clientId: "runtime-client", clientSecret: "runtime-secret" } },
|
||||
passkey: { rpId: "sso.example", origin: "https://sso.example" }, oauthMfaPath: "/mfa" });
|
||||
const response = await invokeAuthHandler("startOAuth", ctx);
|
||||
expect(response.status).toBe(302);
|
||||
const location = new URL(response.headers.get("location")!);
|
||||
expect(location.hostname).toBe("accounts.google.com");
|
||||
expect(location.searchParams.get("client_id")).toBe("runtime-client");
|
||||
expect(location.searchParams.get("redirect_uri")).toBe("https://sso.example/api/auth/oauth/google/callback");
|
||||
expect(getDefaultAuthRouteOptions().sessionCookieName).toBe("custom_session");
|
||||
const request = new Request("https://sso.example/api/auth/passkeys/login/options", { method: "POST",
|
||||
headers: { "content-type": "application/json", origin: "https://sso.example", cookie: "wrn-csrf=runtime-test-csrf", "x-csrf-token": "runtime-test-csrf" }, body: "{}" });
|
||||
const result = await invokeAuthHandler("passkeyAuthenticationOptions", createContext(request, new URL(request.url)));
|
||||
expect(result.status).toBe(200);
|
||||
expect((await result.json()).options.rpId).toBe("sso.example");
|
||||
configureAuthRuntime({ enabled: false, engine });
|
||||
expect((await invokeAuthHandler("startOAuth", ctx)).status).toBe(503);
|
||||
});
|
||||
Reference in new issue
Block a user