feat(auth): remember password securely
This commit is contained in:
1 parent
1c6b004436
commit
990639f770
11 files changed
+530
-17
No files matched your search
@@ -11,6 +11,11 @@ import {
|
||||
storeWorksSquareSession,
|
||||
} from '@electron/services/works-square-session';
|
||||
import { resetManagedWorksSquareRuntimeForTests } from '@electron/services/works-square-runtime';
|
||||
import {
|
||||
getRememberedPasswordState,
|
||||
initializeRememberedPassword,
|
||||
resetRememberedPasswordForTests,
|
||||
} from '@electron/services/remembered-password';
|
||||
|
||||
const providerServiceMock = vi.hoisted(() => ({
|
||||
deleteAccountApiKey: vi.fn(),
|
||||
@@ -58,6 +63,7 @@ describe('auth host api routes', () => {
|
||||
vi.restoreAllMocks();
|
||||
resetWorksSquareSessionForTests();
|
||||
resetManagedWorksSquareRuntimeForTests();
|
||||
resetRememberedPasswordForTests();
|
||||
providerServiceMock.deleteAccountApiKey.mockReset();
|
||||
providerServiceMock.deleteAccountApiKey.mockResolvedValue(true);
|
||||
});
|
||||
@@ -173,6 +179,11 @@ describe('auth host api routes', () => {
|
||||
});
|
||||
|
||||
it('proxies password login through Works and commits a redacted Main session', async () => {
|
||||
const credentialPersistence = {
|
||||
load: vi.fn().mockResolvedValue(null),
|
||||
save: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
await initializeRememberedPassword({ persistence: credentialPersistence });
|
||||
const fetchMock = vi.fn().mockResolvedValueOnce(
|
||||
new Response(JSON.stringify({
|
||||
access_token: 'access-token',
|
||||
@@ -190,6 +201,7 @@ describe('auth host api routes', () => {
|
||||
createRequest('POST', {
|
||||
username: 'zhangsan',
|
||||
password: 'passw0rd',
|
||||
rememberPassword: true,
|
||||
}),
|
||||
response.res,
|
||||
new URL('http://127.0.0.1:13210/api/auth/login'),
|
||||
@@ -226,11 +238,35 @@ describe('auth host api routes', () => {
|
||||
password: 'passw0rd',
|
||||
});
|
||||
expect(JSON.stringify(response.json())).not.toContain('refresh-token');
|
||||
expect(JSON.stringify(response.json())).not.toContain('passw0rd');
|
||||
expect(getWorksSquareSessionSnapshot()).toMatchObject({
|
||||
accessToken: 'access-token',
|
||||
canRefresh: true,
|
||||
});
|
||||
expect(getWorksSquareSessionSnapshot()).not.toHaveProperty('refreshToken');
|
||||
expect(credentialPersistence.save).toHaveBeenCalledWith({
|
||||
username: 'zhangsan',
|
||||
password: 'passw0rd',
|
||||
});
|
||||
await expect(getRememberedPasswordState()).resolves.toEqual({
|
||||
available: true,
|
||||
credentials: { username: 'zhangsan', password: 'passw0rd' },
|
||||
});
|
||||
|
||||
const rememberedResponse = createResponse();
|
||||
await handleAuthRoutes(
|
||||
createRequest('GET'),
|
||||
rememberedResponse.res,
|
||||
new URL('http://127.0.0.1:13210/api/auth/remembered-password'),
|
||||
{} as never,
|
||||
);
|
||||
expect(rememberedResponse.statusCode).toBe(200);
|
||||
expect(rememberedResponse.res.setHeader).toHaveBeenCalledWith('Cache-Control', 'no-store');
|
||||
expect(rememberedResponse.json()).toEqual({
|
||||
success: true,
|
||||
available: true,
|
||||
credentials: { username: 'zhangsan', password: 'passw0rd' },
|
||||
});
|
||||
});
|
||||
|
||||
it('allows explicit reauthorization to replace an unreadable persisted session', async () => {
|
||||
@@ -268,6 +304,44 @@ describe('auth host api routes', () => {
|
||||
}));
|
||||
});
|
||||
|
||||
it('clears an old remembered password after a successful unremembered password login', async () => {
|
||||
const credentialPersistence = {
|
||||
load: vi.fn().mockResolvedValue({
|
||||
username: 'old-user',
|
||||
password: 'old-password',
|
||||
}),
|
||||
save: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
await initializeRememberedPassword({ persistence: credentialPersistence });
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValueOnce(
|
||||
new Response(JSON.stringify({
|
||||
access_token: 'access-token',
|
||||
refresh_token: 'refresh-token',
|
||||
expires_in: 3600,
|
||||
username: 'new-user',
|
||||
}), { status: 200 }),
|
||||
));
|
||||
const response = createResponse();
|
||||
|
||||
await handleAuthRoutes(
|
||||
createRequest('POST', {
|
||||
username: 'new-user',
|
||||
password: 'new-password',
|
||||
rememberPassword: false,
|
||||
}),
|
||||
response.res,
|
||||
new URL('http://127.0.0.1:13210/api/auth/login'),
|
||||
{} as never,
|
||||
);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(credentialPersistence.save).toHaveBeenCalledWith(null);
|
||||
await expect(getRememberedPasswordState()).resolves.toEqual({
|
||||
available: true,
|
||||
credentials: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('passes through SSO credential errors without exposing secrets', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValueOnce(
|
||||
new Response(JSON.stringify({ code: 1, msg: 'Bad credentials', data: null }), { status: 401 }),
|
||||
|
||||
Reference in new issue
Block a user