alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
700 lines (592 loc) • 20.9 kB
text/typescript
import { Alepha } from "alepha";
import { AlephaApiVerification } from "alepha/api/verifications";
import { DateTimeProvider } from "alepha/datetime";
import { AlephaEmail, MemoryEmailProvider } from "alepha/email";
import { AlephaOrmPostgres } from "alepha/orm/postgres";
import { AlephaSecurity, CryptoProvider } from "alepha/security";
import { BadRequestError, HttpError } from "alepha/server";
import { describe, it } from "vitest";
import {
AlephaApiUsers,
CredentialService,
RealmProvider,
SessionService,
UserNotifications,
UserService,
} from "../index.ts";
const setup = async () => {
const alepha = Alepha.create({
env: { LOG_LEVEL: "error" },
});
alepha.with(AlephaOrmPostgres);
alepha.with(AlephaSecurity);
alepha.with(AlephaEmail);
alepha.with(AlephaApiVerification);
alepha.with(AlephaApiUsers);
alepha.with(UserNotifications);
await alepha.start();
// Enable notifications for the default realm
const realmProvider = alepha.inject(RealmProvider);
realmProvider.register("default", {
features: {
notifications: true,
},
settings: {
resetPasswordAllowed: true,
},
});
const emailProvider = alepha.inject(MemoryEmailProvider);
emailProvider.records = [];
return {
alepha,
credentialService: alepha.inject(CredentialService),
userService: alepha.inject(UserService),
sessionService: alepha.inject(SessionService),
cryptoProvider: alepha.inject(CryptoProvider),
dateTimeProvider: alepha.inject(DateTimeProvider),
emailProvider,
};
};
// Helper to extract code from email
const extractCode = (emailBody: string): string => {
const match = emailBody.match(/(\d{6})/);
if (!match) throw new Error("Code not found in email");
return match[1];
};
// Helper to create a user with credentials
const createUserWithCredentials = async (
userService: UserService,
credentialService: CredentialService,
cryptoProvider: CryptoProvider,
email: string,
password: string,
) => {
const user = await userService.users().create({
email,
username: email.split("@")[0],
roles: ["user"],
});
const hashedPassword = await cryptoProvider.hashPassword(password);
await credentialService.identities().create({
userId: user.id,
provider: "credentials",
password: hashedPassword,
});
return user;
};
describe("alepha/api/users - CredentialService", () => {
describe("Phase 1: createPasswordResetIntent", () => {
it("should create a password reset intent for existing user", async ({
expect,
}) => {
const { credentialService, userService, cryptoProvider, emailProvider } =
await setup();
await createUserWithCredentials(
userService,
credentialService,
cryptoProvider,
"reset@example.com",
"OldPassword123!",
);
const result =
await credentialService.createPasswordResetIntent("reset@example.com");
expect(result.intentId).toBeDefined();
expect(result.expiresAt).toBeDefined();
// Verify email was sent
await expect.poll(() => emailProvider.records.length).toBe(1);
expect(emailProvider.records[0].to).toBe("reset@example.com");
expect(emailProvider.records[0].subject).toBe("Reset your password");
});
it("should return fake intent for non-existent email (security)", async ({
expect,
}) => {
const { credentialService, emailProvider } = await setup();
// Should still return a response (security - don't reveal if email exists)
const result = await credentialService.createPasswordResetIntent(
"nonexistent@example.com",
);
expect(result.intentId).toBeDefined();
expect(result.expiresAt).toBeDefined();
// No email should be sent
expect(emailProvider.records.length).toBe(0);
});
it("should return fake intent for user without credentials identity", async ({
expect,
}) => {
const { credentialService, userService, emailProvider } = await setup();
// Create user without credentials (OAuth-only user)
await userService.users().create({
email: "oauth@example.com",
username: "oauthuser",
roles: ["user"],
});
// Should still return a response
const result =
await credentialService.createPasswordResetIntent("oauth@example.com");
expect(result.intentId).toBeDefined();
expect(result.expiresAt).toBeDefined();
// No email should be sent
expect(emailProvider.records.length).toBe(0);
});
it("should set correct expiration time (10 minutes)", async ({
expect,
}) => {
const {
credentialService,
userService,
cryptoProvider,
dateTimeProvider,
} = await setup();
await createUserWithCredentials(
userService,
credentialService,
cryptoProvider,
"expiry@example.com",
"Password123!",
);
const before = dateTimeProvider.now();
const result =
await credentialService.createPasswordResetIntent("expiry@example.com");
const expiresAt = dateTimeProvider.of(result.expiresAt);
const expectedExpiry = before.add(10, "minutes");
// Should expire approximately 10 minutes from now
expect(expiresAt.diff(expectedExpiry, "seconds")).toBeLessThan(5);
});
});
describe("Phase 2: completePasswordReset", () => {
it("should complete password reset with valid code", async ({ expect }) => {
const {
credentialService,
userService,
sessionService,
cryptoProvider,
emailProvider,
} = await setup();
await createUserWithCredentials(
userService,
credentialService,
cryptoProvider,
"complete@example.com",
"OldPassword123!",
);
// Phase 1: Create intent
const intent = await credentialService.createPasswordResetIntent(
"complete@example.com",
);
// Extract code from email
await expect.poll(() => emailProvider.records.length).toBe(1);
const code = extractCode(emailProvider.records[0].body);
// Phase 2: Complete password reset
await credentialService.completePasswordReset({
intentId: intent.intentId,
code,
newPassword: "NewPassword456!",
});
// Verify new password works
const loggedInUser = await sessionService.login(
"credentials",
"complete@example.com",
"NewPassword456!",
);
expect(loggedInUser?.email).toBe("complete@example.com");
});
it("should reject expired intent (410 Gone)", async ({ expect }) => {
const {
credentialService,
userService,
cryptoProvider,
dateTimeProvider,
emailProvider,
} = await setup();
await createUserWithCredentials(
userService,
credentialService,
cryptoProvider,
"expired@example.com",
"OldPassword123!",
);
// Create intent
const intent = await credentialService.createPasswordResetIntent(
"expired@example.com",
);
// Extract code
await expect.poll(() => emailProvider.records.length).toBe(1);
const code = extractCode(emailProvider.records[0].body);
// Travel forward 11 minutes (intent expires at 10)
dateTimeProvider.travel(11, "minutes");
// Attempt to complete
await expect(
credentialService.completePasswordReset({
intentId: intent.intentId,
code,
newPassword: "NewPassword456!",
}),
).rejects.toThrow(HttpError);
});
it("should reject invalid intent ID", async ({ expect }) => {
const { credentialService } = await setup();
await expect(
credentialService.completePasswordReset({
intentId: "550e8400-e29b-41d4-a716-446655440000",
code: "123456",
newPassword: "NewPassword456!",
}),
).rejects.toThrow(HttpError);
});
it("should reject invalid verification code", async ({ expect }) => {
const { credentialService, userService, cryptoProvider, emailProvider } =
await setup();
await createUserWithCredentials(
userService,
credentialService,
cryptoProvider,
"wrongcode@example.com",
"OldPassword123!",
);
const intent = await credentialService.createPasswordResetIntent(
"wrongcode@example.com",
);
// Wait for email
await expect.poll(() => emailProvider.records.length).toBe(1);
await expect(
credentialService.completePasswordReset({
intentId: intent.intentId,
code: "000000", // Wrong code
newPassword: "NewPassword456!",
}),
).rejects.toThrowError(BadRequestError);
});
it("should not allow intent reuse after successful reset", async ({
expect,
}) => {
const { credentialService, userService, cryptoProvider, emailProvider } =
await setup();
await createUserWithCredentials(
userService,
credentialService,
cryptoProvider,
"onetime@example.com",
"OldPassword123!",
);
const intent = await credentialService.createPasswordResetIntent(
"onetime@example.com",
);
// Extract code
await expect.poll(() => emailProvider.records.length).toBe(1);
const code = extractCode(emailProvider.records[0].body);
// First completion should succeed
await credentialService.completePasswordReset({
intentId: intent.intentId,
code,
newPassword: "NewPassword456!",
});
// Second attempt should fail (intent deleted)
await expect(
credentialService.completePasswordReset({
intentId: intent.intentId,
code,
newPassword: "AnotherPassword789!",
}),
).rejects.toThrow(HttpError);
});
it("should invalidate all existing sessions after password reset", async ({
expect,
}) => {
const {
credentialService,
userService,
sessionService,
cryptoProvider,
emailProvider,
} = await setup();
const user = await createUserWithCredentials(
userService,
credentialService,
cryptoProvider,
"sessions@example.com",
"OldPassword123!",
);
// Create a session
await sessionService.sessions().create({
userId: user.id,
refreshToken: crypto.randomUUID(),
expiresAt: new Date(Date.now() + 3600000).toISOString(),
});
// Verify session exists
const sessionsBefore = await sessionService.sessions().findMany({
where: { userId: { eq: user.id } },
});
expect(sessionsBefore).toHaveLength(1);
// Create password reset intent
const intent = await credentialService.createPasswordResetIntent(
"sessions@example.com",
);
// Extract code
await expect.poll(() => emailProvider.records.length).toBe(1);
const code = extractCode(emailProvider.records[0].body);
// Complete password reset
await credentialService.completePasswordReset({
intentId: intent.intentId,
code,
newPassword: "NewPassword456!",
});
// Verify all sessions are invalidated
const sessionsAfter = await sessionService.sessions().findMany({
where: { userId: { eq: user.id } },
});
expect(sessionsAfter).toHaveLength(0);
});
it("should not allow old password after reset", async ({ expect }) => {
const {
credentialService,
userService,
sessionService,
cryptoProvider,
emailProvider,
} = await setup();
await createUserWithCredentials(
userService,
credentialService,
cryptoProvider,
"oldpass@example.com",
"OldPassword123!",
);
// Verify old password works before reset
const beforeReset = await sessionService.login(
"credentials",
"oldpass@example.com",
"OldPassword123!",
);
expect(beforeReset?.email).toBe("oldpass@example.com");
// Create intent and reset password
const intent = await credentialService.createPasswordResetIntent(
"oldpass@example.com",
);
await expect.poll(() => emailProvider.records.length).toBe(1);
const code = extractCode(emailProvider.records[0].body);
await credentialService.completePasswordReset({
intentId: intent.intentId,
code,
newPassword: "NewPassword456!",
});
// Old password should no longer work (login throws error for invalid credentials)
await expect(
sessionService.login(
"credentials",
"oldpass@example.com",
"OldPassword123!",
),
).rejects.toThrow();
});
});
describe("Full password reset flow integration", () => {
it("should complete full password reset flow", async ({ expect }) => {
const {
credentialService,
userService,
sessionService,
cryptoProvider,
emailProvider,
} = await setup();
// Create user
await createUserWithCredentials(
userService,
credentialService,
cryptoProvider,
"fullflow@example.com",
"OldPassword123!",
);
// Phase 1: Request password reset
const intent = await credentialService.createPasswordResetIntent(
"fullflow@example.com",
);
expect(intent.intentId).toBeDefined();
expect(intent.expiresAt).toBeDefined();
// Verify email was sent
await expect.poll(() => emailProvider.records.length).toBe(1);
expect(emailProvider.records[0].to).toBe("fullflow@example.com");
const code = extractCode(emailProvider.records[0].body);
// Phase 2: Complete password reset
await credentialService.completePasswordReset({
intentId: intent.intentId,
code,
newPassword: "NewSecurePassword789!",
});
// Verify new password works
const user = await sessionService.login(
"credentials",
"fullflow@example.com",
"NewSecurePassword789!",
);
expect(user?.email).toBe("fullflow@example.com");
});
});
describe("resetPasswordAllowed=false", () => {
it("should silently return intent when resetPasswordAllowed is false", async ({
expect,
}) => {
const { alepha, credentialService, userService, cryptoProvider } =
await setup();
// Register a separate realm with resetPasswordAllowed disabled
const realmProvider = alepha.inject(RealmProvider);
realmProvider.register("no-reset", {
features: { notifications: true },
settings: { resetPasswordAllowed: false },
});
// Create a user in the no-reset realm
const user = await credentialService.users("no-reset").create({
email: "noreset@example.com",
username: "noresetuser",
roles: ["user"],
});
const hashedPassword = await cryptoProvider.hashPassword("Password123!");
await credentialService.identities("no-reset").create({
userId: user.id,
provider: "credentials",
password: hashedPassword,
});
// Request password reset - should return an intentId (security: doesn't reveal it's disabled)
const result = await credentialService.createPasswordResetIntent(
"noreset@example.com",
"no-reset",
);
expect(result.intentId).toBeDefined();
expect(result.expiresAt).toBeDefined();
// No email should be sent
const emailProvider = alepha.inject(MemoryEmailProvider);
expect(emailProvider.records.length).toBe(0);
// Completing the reset with the returned intentId should fail with 410
// because the intent was never actually stored in the cache
await expect(
credentialService.completePasswordReset({
intentId: result.intentId,
code: "123456",
newPassword: "NewPassword456!",
}),
).rejects.toThrow(HttpError);
});
});
describe("Password policy enforcement during reset", () => {
it("should reject password that violates realm policy", async ({
expect,
}) => {
const { alepha, credentialService, userService, cryptoProvider } =
await setup();
// Register a realm with strict password policy
const realmProvider = alepha.inject(RealmProvider);
realmProvider.register("strict-policy", {
features: { notifications: true },
settings: {
resetPasswordAllowed: true,
passwordPolicy: {
minLength: 8,
requireUppercase: true,
requireLowercase: true,
requireNumbers: true,
requireSpecialCharacters: false,
},
},
});
// Create a user in the strict-policy realm
const user = await credentialService.users("strict-policy").create({
email: "strict@example.com",
username: "strictuser",
roles: ["user"],
});
const hashedPassword =
await cryptoProvider.hashPassword("OldPassword123!");
await credentialService.identities("strict-policy").create({
userId: user.id,
provider: "credentials",
password: hashedPassword,
});
// Create intent
const intent = await credentialService.createPasswordResetIntent(
"strict@example.com",
"strict-policy",
);
// Extract code from email
const emailProvider = alepha.inject(MemoryEmailProvider);
await expect.poll(() => emailProvider.records.length).toBe(1);
const code = extractCode(emailProvider.records[0].body);
// Try to complete with a password that has no uppercase and no numbers
await expect(
credentialService.completePasswordReset({
intentId: intent.intentId,
code,
newPassword: "alllowercase",
}),
).rejects.toThrowError(BadRequestError);
// The verification code should NOT be consumed - can retry with a valid password
await credentialService.completePasswordReset({
intentId: intent.intentId,
code,
newPassword: "ValidRetry123",
});
});
it("should accept password that meets realm policy", async ({ expect }) => {
const { alepha, credentialService, userService, cryptoProvider } =
await setup();
// Register a realm with strict password policy
const realmProvider = alepha.inject(RealmProvider);
realmProvider.register("strict-accept", {
features: { notifications: true },
settings: {
resetPasswordAllowed: true,
passwordPolicy: {
minLength: 8,
requireUppercase: true,
requireLowercase: true,
requireNumbers: true,
requireSpecialCharacters: false,
},
},
});
// Create a user in the strict-accept realm
const user = await credentialService.users("strict-accept").create({
email: "accept@example.com",
username: "acceptuser",
roles: ["user"],
});
const hashedPassword =
await cryptoProvider.hashPassword("OldPassword123!");
await credentialService.identities("strict-accept").create({
userId: user.id,
provider: "credentials",
password: hashedPassword,
});
// Create intent
const intent = await credentialService.createPasswordResetIntent(
"accept@example.com",
"strict-accept",
);
// Extract code from email
const emailProvider = alepha.inject(MemoryEmailProvider);
await expect.poll(() => emailProvider.records.length).toBe(1);
const code = extractCode(emailProvider.records[0].body);
// Complete with a password that meets the policy
await credentialService.completePasswordReset({
intentId: intent.intentId,
code,
newPassword: "ValidPass123",
});
});
});
describe("Legacy methods (backward compatibility)", () => {
it("requestPasswordReset should work as before", async ({ expect }) => {
const { credentialService, userService, cryptoProvider, emailProvider } =
await setup();
await createUserWithCredentials(
userService,
credentialService,
cryptoProvider,
"legacy@example.com",
"OldPassword123!",
);
const result =
await credentialService.requestPasswordReset("legacy@example.com");
expect(result).toBe(true);
await expect.poll(() => emailProvider.records.length).toBe(1);
});
});
});