'use strict'; const assert = require('node:assert/strict'); const test = require('node:test'); const jwt = require('jsonwebtoken'); const { THIRD_PARTY_FLOW, buildCodeChallenge, buildUserInfo, completeThirdPartyAuthorization, createSsoClient, exchangeAuthorizationCode, getThirdPartySsoCallbackBaseUrl, hashValue, revokeAccessToken, rotateSsoClientSecret, validateAuthorizationRequest, verifyThirdPartyAccessToken, } = require('../src/services/thirdPartySsoService'); const ORIGINAL_SECRET = process.env.CUBEJS_SECRET; process.env.CUBEJS_SECRET = 'third-party-sso-test-secret'; function resolvedUser() { return { id: 42, username: 'operator', name: '陆南亭', email: 'operator@example.com', role: 'user', status: 'active', auth_source: 'dingtalk', departmentId: 8, departmentName: '运营中心', allowedPageKeys: ['planning_upload'], }; } function createFakeStore(clientOverrides = {}) { let client = { id: 1, clientId: 'sso_test_client', clientName: '测试应用', clientType: 'confidential', clientSecretHash: hashValue('client-secret'), redirectUris: ['https://partner.example.com/oauth/callback'], allowedScopes: ['profile', 'email', 'department', 'permissions'], status: 'active', ...clientOverrides, }; const codes = new Map(); const revoked = new Set(); const audits = []; return { client, codes, revoked, audits, async findClient(clientId) { return client.clientId === clientId ? client : null; }, async listClients() { return [client]; }, async createClient(record) { client = { id: 2, status: 'active', ...record }; this.client = client; return client; }, async updateClient(clientId, changes) { if (client.clientId !== clientId) return null; client = { ...client, ...changes }; this.client = client; return client; }, async insertAuthorizationCode(record) { codes.set(record.codeHash, { ...record, consumedAt: null }); }, async findAuthorizationCode(codeHash, clientId) { const record = codes.get(codeHash); return record?.clientId === clientId ? record : null; }, async consumeAuthorizationCode(codeHash) { const record = codes.get(codeHash); if (!record || record.consumedAt) return 0; record.consumedAt = new Date(); return 1; }, async touchClient() {}, async isTokenRevoked(jti) { return revoked.has(jti); }, async revokeToken(record) { revoked.add(record.jti); }, async audit(record) { audits.push(record); }, async cleanupExpired() {}, }; } async function issueToken(store, overrides = {}) { const verifier = overrides.verifier || 'A'.repeat(64); const authorization = await validateAuthorizationRequest({ response_type: 'code', client_id: store.client.clientId, redirect_uri: store.client.redirectUris[0], state: 'partner-state', scope: 'profile email department permissions', code_challenge: buildCodeChallenge(verifier), code_challenge_method: 'S256', }, { store }); assert.equal(authorization.stateClaims.flow, THIRD_PARTY_FLOW); const completed = await completeThirdPartyAuthorization({ stateClaims: authorization.stateClaims, userId: 42, }, { store }); const code = new URL(completed.redirectUrl).searchParams.get('code'); const token = await exchangeAuthorizationCode({ grant_type: 'authorization_code', code, redirect_uri: store.client.redirectUris[0], code_verifier: verifier, }, { clientId: store.client.clientId, clientSecret: overrides.clientSecret === undefined ? 'client-secret' : overrides.clientSecret, }, { store, resolveUser: async () => resolvedUser(), }); return { code, token }; } test.after(() => { if (ORIGINAL_SECRET === undefined) delete process.env.CUBEJS_SECRET; else process.env.CUBEJS_SECRET = ORIGINAL_SECRET; }); test('third-party SSO callback stays on the configured issuer origin', () => { assert.equal(getThirdPartySsoCallbackBaseUrl(), 'https://pddg.jiansuntuzhi.cn'); }); test('third-party SSO completes authorization-code + PKCE and returns scoped user info', async () => { const store = createFakeStore(); const { token } = await issueToken(store); assert.equal(token.token_type, 'Bearer'); assert.equal(token.scope, 'profile email department permissions'); const verified = await verifyThirdPartyAccessToken(token.access_token, { store, resolveUser: async () => resolvedUser(), }); const claims = jwt.decode(token.access_token); assert.equal(claims.tokenType, 'third-party-sso'); assert.equal(claims.aud, store.client.clientId); assert.deepEqual(buildUserInfo(verified), { sub: '42', username: 'operator', name: '陆南亭', picture: '', auth_source: 'dingtalk', email: 'operator@example.com', department: { id: 8, name: '运营中心' }, permissions: { role: 'user', allowed_page_keys: ['planning_upload'] }, }); }); test('authorization code is one-time and rejects replay', async () => { const store = createFakeStore(); const { code } = await issueToken(store); await assert.rejects( exchangeAuthorizationCode({ grant_type: 'authorization_code', code, redirect_uri: store.client.redirectUris[0], code_verifier: 'A'.repeat(64), }, { clientId: store.client.clientId, clientSecret: 'client-secret', }, { store, resolveUser: async () => resolvedUser() }), (error) => error.code === 'invalid_grant' ); }); test('authorization request rejects unregistered redirect URI and unsupported scopes', async () => { const store = createFakeStore(); const base = { response_type: 'code', client_id: store.client.clientId, state: 'partner-state', code_challenge: buildCodeChallenge('B'.repeat(64)), code_challenge_method: 'S256', }; await assert.rejects( validateAuthorizationRequest({ ...base, redirect_uri: 'https://evil.example.com/callback', }, { store }), /redirect_uri/ ); await assert.rejects( validateAuthorizationRequest({ ...base, redirect_uri: store.client.redirectUris[0], scope: 'profile admin', }, { store }), (error) => error.code === 'invalid_scope' ); }); test('public clients use PKCE without a client secret', async () => { const store = createFakeStore({ clientType: 'public', clientSecretHash: '', }); const { token } = await issueToken(store, { clientSecret: '' }); assert.ok(token.access_token); }); test('revoked token cannot access userinfo', async () => { const store = createFakeStore(); const { token } = await issueToken(store); await revokeAccessToken(token.access_token, { clientId: store.client.clientId, clientSecret: 'client-secret', }, { store }); await assert.rejects( verifyThirdPartyAccessToken(token.access_token, { store, resolveUser: async () => resolvedUser(), }), (error) => error.code === 'invalid_token' ); }); test('internal application JWT is rejected by third-party SSO verification', async () => { const store = createFakeStore(); const internalToken = jwt.sign({ sub: '42', clientId: store.client.clientId, }, process.env.CUBEJS_SECRET, { expiresIn: '1h' }); await assert.rejects( verifyThirdPartyAccessToken(internalToken, { store, resolveUser: async () => resolvedUser(), }), (error) => error.code === 'invalid_token' ); }); test('confidential client creation returns its secret once and rotation invalidates the old hash', async () => { const store = createFakeStore(); const created = await createSsoClient({ clientName: '新建第三方应用', clientType: 'confidential', redirectUris: ['https://new-partner.example.com/callback'], allowedScopes: ['profile', 'department'], }, { sub: 1 }, { store }); assert.ok(created.clientSecret); assert.equal(store.client.clientSecretHash, hashValue(created.clientSecret)); assert.equal(created.clientSecretHash, undefined); const oldHash = store.client.clientSecretHash; const rotated = await rotateSsoClientSecret(created.clientId, { sub: 1 }, { store }); assert.ok(rotated.clientSecret); assert.notEqual(store.client.clientSecretHash, oldHash); assert.equal(store.client.clientSecretHash, hashValue(rotated.clientSecret)); });