cangku/backend/test/thirdPartySsoDirectService.test.js

203 lines
7.2 KiB
JavaScript

'use strict';
const assert = require('node:assert/strict');
const test = require('node:test');
const {
beginDirectLogin,
completeDirectLogin,
createDirectApiKey,
createDirectLoginSession,
parseDirectApiKey,
pollDirectLoginSession,
updateDirectReturnUrls,
} = require('../src/services/thirdPartySsoDirectService');
const { hashValue } = require('../src/services/thirdPartySsoService');
function resolvedUser() {
return {
id: 42,
username: 'operator',
name: '陆南亭',
email: 'operator@example.com',
role: 'user',
status: 'active',
auth_source: 'dingtalk',
departmentId: 8,
departmentName: '运营中心',
allowedPageKeys: [],
};
}
function createStores() {
let client = null;
const sessions = new Map();
const clientStore = {
async findClient(clientId) {
return client?.clientId === clientId ? client : null;
},
async createClient(record) {
client = { id: 1, status: 'active', ...record };
return client;
},
async updateClient(clientId, changes) {
if (client?.clientId !== clientId) return null;
client = { ...client, ...changes };
return client;
},
async audit() {},
};
const directStore = {
sessions,
async insertDirectSession(record) {
sessions.set(record.sessionHash, {
...record,
status: 'pending',
userId: null,
});
},
async findDirectSession(sessionHash, clientId = '') {
const session = sessions.get(sessionHash);
if (!session || (clientId && session.clientId !== clientId)) return null;
return session;
},
async completeDirectSession(sessionHash, userId) {
const session = sessions.get(sessionHash);
if (!session || session.status !== 'pending' || session.expiresAt <= Date.now()) return 0;
session.status = 'completed';
session.userId = userId;
return 1;
},
async consumeDirectSession(sessionHash, clientId) {
const session = sessions.get(sessionHash);
if (!session || session.clientId !== clientId || session.status !== 'completed') return 0;
session.status = 'consumed';
return 1;
},
async cleanupExpiredDirectSessions() {},
};
return { clientStore, directStore };
}
async function createCredential(clientStore, overrides = {}) {
return createDirectApiKey({
clientName: '第三方直连应用',
allowedScopes: ['profile', 'email', 'department'],
...overrides,
}, { sub: 1 }, { store: clientStore });
}
test('direct API Key login completes without a redirect URI and returns user once', async () => {
const { clientStore, directStore } = createStores();
const credential = await createCredential(clientStore);
assert.match(credential.apiKey, /^ddak_sso_/u);
assert.equal(parseDirectApiKey(credential.apiKey).clientId, credential.clientId);
const created = await createDirectLoginSession({
scope: 'profile email department',
}, credential.apiKey, { store: clientStore, directStore });
assert.equal(created.status, 'pending');
assert.match(created.login_url, /\/api\/sso\/direct\/login\?request_id=/u);
const begun = await beginDirectLogin(created.request_id, { directStore });
await completeDirectLogin({
stateClaims: begun.stateClaims,
userId: 42,
}, { directStore });
const completed = await pollDirectLoginSession(created.request_id, credential.apiKey, {
store: clientStore,
directStore,
resolveUser: async () => resolvedUser(),
});
assert.equal(completed.status, 'completed');
assert.deepEqual(completed.user, {
sub: '42',
username: 'operator',
name: '陆南亭',
picture: '',
auth_source: 'dingtalk',
email: 'operator@example.com',
department: { id: 8, name: '运营中心' },
});
const consumed = await pollDirectLoginSession(created.request_id, credential.apiKey, {
store: clientStore,
directStore,
resolveUser: async () => resolvedUser(),
});
assert.equal(consumed.status, 'consumed');
});
test('direct login rejects a wrong API Key and expired request', async () => {
const { clientStore, directStore } = createStores();
const credential = await createCredential(clientStore);
await assert.rejects(
createDirectLoginSession({}, `${credential.apiKey}x`, { store: clientStore, directStore }),
(error) => error.code === 'invalid_client'
);
const created = await createDirectLoginSession({}, credential.apiKey, {
store: clientStore,
directStore,
});
directStore.sessions.get(hashValue(created.request_id)).expiresAt = Date.now() - 1;
await assert.rejects(
beginDirectLogin(created.request_id, { directStore }),
(error) => error.status === 410
);
});
test('direct login returns to an allowlisted third-party URL after DingTalk completes', async () => {
const { clientStore, directStore } = createStores();
const credential = await createCredential(clientStore);
const configured = await updateDirectReturnUrls(credential.clientId, {
allowedReturnUrls: ['https://partner.example.com/auth/complete'],
}, { sub: 1 }, { store: clientStore });
assert.deepEqual(configured.allowedReturnUrls, ['https://partner.example.com/auth/complete']);
const created = await createDirectLoginSession({
scope: 'profile',
return_url: 'https://partner.example.com/auth/complete',
}, credential.apiKey, { store: clientStore, directStore });
assert.equal(created.return_url, 'https://partner.example.com/auth/complete');
const begun = await beginDirectLogin(created.request_id, { directStore });
const completed = await completeDirectLogin({
stateClaims: begun.stateClaims,
userId: 42,
}, { directStore });
const redirect = new URL(completed.completionRedirect);
assert.equal(`${redirect.origin}${redirect.pathname}`, 'https://partner.example.com/auth/complete');
assert.equal(redirect.searchParams.get('sso_request_id'), created.request_id);
assert.equal(redirect.searchParams.get('sso_status'), 'completed');
await assert.rejects(
createDirectLoginSession({
return_url: 'https://evil.example.com/callback',
}, credential.apiKey, { store: clientStore, directStore }),
(error) => error.code === 'invalid_request' && /白名单/u.test(error.message)
);
});
test('hosted direct login supports an exactly allowlisted HTTP partner URL', async () => {
const { clientStore, directStore } = createStores();
const credential = await createCredential(clientStore);
const configured = await updateDirectReturnUrls(credential.clientId, {
allowedReturnUrls: ['http://47.98.122.179'],
}, { sub: 1 }, { store: clientStore });
assert.deepEqual(configured.allowedReturnUrls, ['http://47.98.122.179/']);
const created = await createDirectLoginSession({
return_url: 'http://47.98.122.179',
}, credential.apiKey, { store: clientStore, directStore });
const begun = await beginDirectLogin(created.request_id, { directStore });
const completed = await completeDirectLogin({
stateClaims: begun.stateClaims,
userId: 42,
}, { directStore });
const redirect = new URL(completed.completionRedirect);
assert.equal(redirect.origin, 'http://47.98.122.179');
assert.equal(redirect.searchParams.get('sso_request_id'), created.request_id);
assert.equal(redirect.searchParams.get('sso_status'), 'completed');
});