'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'); });