修复钉钉工作通知免登凭证混用
This commit is contained in:
parent
bca088c96d
commit
e2d3314e62
@ -16,7 +16,9 @@ export async function POST(request: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
const response = await loginDingtalkUserByH5AuthCode(request, code)
|
||||
const response = await loginDingtalkUserByH5AuthCode(request, code, {
|
||||
appSource: 'workNotice',
|
||||
})
|
||||
response.headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
|
||||
return response
|
||||
} catch (error: any) {
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { loginDingtalkUserByOAuthCode } from '@/lib/dingtalk-login'
|
||||
import {
|
||||
DINGTALK_DIRECT_LOGIN_SOURCE,
|
||||
parseDingtalkOAuthState,
|
||||
} from '@/lib/dingtalk-oauth'
|
||||
|
||||
/**
|
||||
* R25修复:移除硬编码的钉钉密钥,强制使用环境变量
|
||||
@ -11,7 +15,7 @@ import { loginDingtalkUserByOAuthCode } from '@/lib/dingtalk-login'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { code } = await request.json()
|
||||
const { code, state } = await request.json()
|
||||
|
||||
if (!code) {
|
||||
return NextResponse.json(
|
||||
@ -20,7 +24,10 @@ export async function POST(request: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
return loginDingtalkUserByOAuthCode(request, String(code))
|
||||
const { source } = parseDingtalkOAuthState(state)
|
||||
return loginDingtalkUserByOAuthCode(request, String(code), {
|
||||
appSource: source === DINGTALK_DIRECT_LOGIN_SOURCE ? 'workNotice' : 'login',
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error('钉钉登录失败:', error)
|
||||
return NextResponse.json(
|
||||
|
||||
@ -7,8 +7,8 @@ import {
|
||||
import {
|
||||
DINGTALK_DIRECT_LOGIN_SOURCE,
|
||||
buildDingtalkOAuthUrl,
|
||||
getDingtalkOAuthClientId,
|
||||
} from '@/lib/dingtalk-oauth'
|
||||
import { getDingtalkWorkNoticeClientId } from '@/lib/dingtalk-client'
|
||||
import {
|
||||
getConfiguredDingtalkCorpId,
|
||||
normalizeDingtalkCorpId,
|
||||
@ -45,10 +45,12 @@ function stringifyForInlineScript(value: string) {
|
||||
function createH5LoginPage({
|
||||
redirectPath,
|
||||
corpId,
|
||||
clientId,
|
||||
fallbackOAuthUrl,
|
||||
}: {
|
||||
redirectPath: string
|
||||
corpId: string
|
||||
clientId: string
|
||||
fallbackOAuthUrl: string
|
||||
}) {
|
||||
return new NextResponse(
|
||||
@ -217,7 +219,7 @@ function createH5LoginPage({
|
||||
try {
|
||||
dd.requestAuthCode({
|
||||
corpId: corpId,
|
||||
clientId: ${stringifyForInlineScript(getDingtalkOAuthClientId())},
|
||||
clientId: ${stringifyForInlineScript(clientId)},
|
||||
onSuccess: function (result) {
|
||||
var code = resolveAuthCode(result);
|
||||
if (!code) {
|
||||
@ -372,6 +374,7 @@ export async function GET(request: NextRequest) {
|
||||
const fallbackOAuthUrl = buildDingtalkOAuthUrl({
|
||||
redirectPath,
|
||||
callbackPath: '/api/dingtalk/callback',
|
||||
clientId: getDingtalkWorkNoticeClientId(),
|
||||
source: DINGTALK_DIRECT_LOGIN_SOURCE,
|
||||
})
|
||||
|
||||
@ -391,6 +394,7 @@ export async function GET(request: NextRequest) {
|
||||
return createH5LoginPage({
|
||||
redirectPath,
|
||||
corpId,
|
||||
clientId: getDingtalkWorkNoticeClientId(),
|
||||
fallbackOAuthUrl,
|
||||
})
|
||||
}
|
||||
|
||||
@ -76,6 +76,17 @@ describe('dingtalk client', () => {
|
||||
}))
|
||||
})
|
||||
|
||||
test('工作通知免登入口优先使用工作通知应用 clientId', async () => {
|
||||
process.env.DINGTALK_CLIENT_ID = 'login-client-id'
|
||||
process.env.DINGTALK_CLIENT_SECRET = 'login-client-secret'
|
||||
process.env.DINGTALK_WORK_NOTICE_CLIENT_ID = 'notice-client-id'
|
||||
process.env.DINGTALK_WORK_NOTICE_CLIENT_SECRET = 'notice-client-secret'
|
||||
|
||||
const { getDingtalkWorkNoticeClientId } = await import('@/lib/dingtalk-client')
|
||||
|
||||
expect(getDingtalkWorkNoticeClientId()).toBe('notice-client-id')
|
||||
})
|
||||
|
||||
test('带跳转地址的工作通知优先使用钉钉应用内打开链接', async () => {
|
||||
process.env.DINGTALK_CLIENT_ID = 'login-client-id'
|
||||
process.env.DINGTALK_CLIENT_SECRET = 'login-client-secret'
|
||||
|
||||
32
lib/__tests__/dingtalk-oauth.test.ts
Normal file
32
lib/__tests__/dingtalk-oauth.test.ts
Normal file
@ -0,0 +1,32 @@
|
||||
/** @jest-environment node */
|
||||
|
||||
describe('dingtalk oauth helpers', () => {
|
||||
const originalEnv = process.env
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules()
|
||||
process.env = {
|
||||
...originalEnv,
|
||||
NEXT_PUBLIC_APP_URL: 'https://sc.jiansun.vip',
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
test('buildDingtalkOAuthUrl 支持覆盖 client_id', async () => {
|
||||
process.env.DINGTALK_CLIENT_ID = 'login-client-id'
|
||||
|
||||
const { buildDingtalkOAuthUrl } = await import('@/lib/dingtalk-oauth')
|
||||
const url = new URL(buildDingtalkOAuthUrl({
|
||||
redirectPath: '/performance-survey',
|
||||
clientId: 'notice-client-id',
|
||||
source: 'work_notice_direct',
|
||||
}))
|
||||
|
||||
expect(url.searchParams.get('client_id')).toBe('notice-client-id')
|
||||
expect(url.searchParams.get('client_id')).not.toBe('login-client-id')
|
||||
})
|
||||
})
|
||||
@ -139,6 +139,20 @@ async function getDingtalkWorkNoticeAccessToken() {
|
||||
return getDingtalkAccessToken(getDingtalkWorkNoticeAppConfig())
|
||||
}
|
||||
|
||||
export function getDingtalkWorkNoticeClientId() {
|
||||
return (
|
||||
process.env.DINGTALK_WORK_NOTICE_CLIENT_ID ||
|
||||
process.env.DINGTALK_WORK_NOTICE_APP_KEY ||
|
||||
process.env.DINGTALK_CLIENT_ID ||
|
||||
process.env.DINGTALK_APP_KEY ||
|
||||
''
|
||||
)
|
||||
}
|
||||
|
||||
export async function getDingtalkWorkNoticeAppAccessToken() {
|
||||
return getDingtalkWorkNoticeAccessToken()
|
||||
}
|
||||
|
||||
async function getDingtalkAccessToken(config: DingtalkAppConfig) {
|
||||
const cacheKey = `${config.source}:${config.appKey}:${config.appSecret.slice(-8)}`
|
||||
const cachedAppToken = cachedAppTokens.get(cacheKey)
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { getDingtalkAppAccessToken } from '@/lib/dingtalk-client'
|
||||
import {
|
||||
getDingtalkAppAccessToken,
|
||||
getDingtalkWorkNoticeAppAccessToken,
|
||||
} from '@/lib/dingtalk-client'
|
||||
import { ensurePerformanceSurveyEmployeeProfileForUser } from '@/lib/performance-survey-report-sync'
|
||||
import {
|
||||
getAuthSessionCookieName,
|
||||
@ -17,16 +20,32 @@ type DingtalkUserInfo = {
|
||||
email?: string | null
|
||||
}
|
||||
|
||||
type DingtalkLoginAppSource = 'login' | 'workNotice'
|
||||
|
||||
let configValidated = false
|
||||
|
||||
function validateDingtalkConfig(): { clientId: string; clientSecret: string } | null {
|
||||
const clientId = process.env.DINGTALK_CLIENT_ID
|
||||
const clientSecret = process.env.DINGTALK_CLIENT_SECRET
|
||||
function validateDingtalkConfig(
|
||||
appSource: DingtalkLoginAppSource = 'login',
|
||||
): { clientId: string; clientSecret: string } | null {
|
||||
const clientId = appSource === 'workNotice'
|
||||
? process.env.DINGTALK_WORK_NOTICE_CLIENT_ID ||
|
||||
process.env.DINGTALK_WORK_NOTICE_APP_KEY ||
|
||||
process.env.DINGTALK_CLIENT_ID
|
||||
: process.env.DINGTALK_CLIENT_ID
|
||||
const clientSecret = appSource === 'workNotice'
|
||||
? process.env.DINGTALK_WORK_NOTICE_CLIENT_SECRET ||
|
||||
process.env.DINGTALK_WORK_NOTICE_APP_SECRET ||
|
||||
process.env.DINGTALK_CLIENT_SECRET
|
||||
: process.env.DINGTALK_CLIENT_SECRET
|
||||
|
||||
if (!clientId || !clientSecret) {
|
||||
if (!configValidated) {
|
||||
console.error('[Dingtalk] 配置错误:缺少必需的环境变量')
|
||||
console.error('[Dingtalk] 请配置 DINGTALK_CLIENT_ID 和 DINGTALK_CLIENT_SECRET')
|
||||
console.error(
|
||||
appSource === 'workNotice'
|
||||
? '[Dingtalk] 请配置 DINGTALK_WORK_NOTICE_CLIENT_ID 和 DINGTALK_WORK_NOTICE_CLIENT_SECRET'
|
||||
: '[Dingtalk] 请配置 DINGTALK_CLIENT_ID 和 DINGTALK_CLIENT_SECRET',
|
||||
)
|
||||
configValidated = true
|
||||
}
|
||||
return null
|
||||
@ -40,16 +59,22 @@ function validateDingtalkConfig(): { clientId: string; clientSecret: string } |
|
||||
return { clientId, clientSecret }
|
||||
}
|
||||
|
||||
function getDingtalkConfig(): { clientId: string; clientSecret: string } {
|
||||
const config = validateDingtalkConfig()
|
||||
function getDingtalkConfig(
|
||||
appSource: DingtalkLoginAppSource = 'login',
|
||||
): { clientId: string; clientSecret: string } {
|
||||
const config = validateDingtalkConfig(appSource)
|
||||
if (!config) {
|
||||
throw new Error('钉钉登录未配置,请联系管理员配置 DINGTALK_CLIENT_ID 和 DINGTALK_CLIENT_SECRET')
|
||||
throw new Error(
|
||||
appSource === 'workNotice'
|
||||
? '钉钉工作通知免登未配置,请联系管理员配置 DINGTALK_WORK_NOTICE_CLIENT_ID 和 DINGTALK_WORK_NOTICE_CLIENT_SECRET'
|
||||
: '钉钉登录未配置,请联系管理员配置 DINGTALK_CLIENT_ID 和 DINGTALK_CLIENT_SECRET',
|
||||
)
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
async function getUserAccessToken(code: string) {
|
||||
const config = getDingtalkConfig()
|
||||
async function getUserAccessToken(code: string, appSource: DingtalkLoginAppSource = 'login') {
|
||||
const config = getDingtalkConfig(appSource)
|
||||
const response = await fetch('https://api.dingtalk.com/v1.0/oauth2/userAccessToken', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@ -422,10 +447,21 @@ async function createOrUpdateUserSession(
|
||||
})
|
||||
}
|
||||
|
||||
export async function loginDingtalkUserByOAuthCode(request: NextRequest, code: string) {
|
||||
console.log('开始钉钉OAuth登录流程')
|
||||
function getAppAccessToken(appSource: DingtalkLoginAppSource) {
|
||||
return appSource === 'workNotice'
|
||||
? getDingtalkWorkNoticeAppAccessToken()
|
||||
: getDingtalkAppAccessToken()
|
||||
}
|
||||
|
||||
const tokenData = await getUserAccessToken(code)
|
||||
export async function loginDingtalkUserByOAuthCode(
|
||||
request: NextRequest,
|
||||
code: string,
|
||||
options?: { appSource?: DingtalkLoginAppSource },
|
||||
) {
|
||||
console.log('开始钉钉OAuth登录流程')
|
||||
const appSource = options?.appSource || 'login'
|
||||
|
||||
const tokenData = await getUserAccessToken(code, appSource)
|
||||
if (!tokenData?.accessToken) {
|
||||
console.error('获取用户访问令牌返回数据异常:', tokenData)
|
||||
return NextResponse.json(
|
||||
@ -435,7 +471,7 @@ export async function loginDingtalkUserByOAuthCode(request: NextRequest, code: s
|
||||
}
|
||||
|
||||
console.log('获取用户访问令牌成功')
|
||||
const appAccessToken = await getDingtalkAppAccessToken()
|
||||
const appAccessToken = await getAppAccessToken(appSource)
|
||||
console.log('获取应用访问令牌成功')
|
||||
|
||||
const unionId = await getUnionId(tokenData.accessToken)
|
||||
@ -450,10 +486,15 @@ export async function loginDingtalkUserByOAuthCode(request: NextRequest, code: s
|
||||
return createOrUpdateUserSession(request, userId, userInfo)
|
||||
}
|
||||
|
||||
export async function loginDingtalkUserByH5AuthCode(request: NextRequest, code: string) {
|
||||
export async function loginDingtalkUserByH5AuthCode(
|
||||
request: NextRequest,
|
||||
code: string,
|
||||
options?: { appSource?: DingtalkLoginAppSource },
|
||||
) {
|
||||
console.log('开始钉钉H5免登流程')
|
||||
const appSource = options?.appSource || 'login'
|
||||
|
||||
const appAccessToken = await getDingtalkAppAccessToken()
|
||||
const appAccessToken = await getAppAccessToken(appSource)
|
||||
console.log('获取应用访问令牌成功')
|
||||
|
||||
const userId = await getUserIdByH5AuthCode(code, appAccessToken)
|
||||
|
||||
@ -65,11 +65,13 @@ export function parseDingtalkOAuthState(value: unknown) {
|
||||
export function buildDingtalkOAuthUrl({
|
||||
redirectPath,
|
||||
callbackPath = '/api/dingtalk/callback',
|
||||
clientId,
|
||||
prompt,
|
||||
source,
|
||||
}: {
|
||||
redirectPath: unknown
|
||||
callbackPath?: string
|
||||
clientId?: string
|
||||
prompt?: string
|
||||
source?: string
|
||||
}) {
|
||||
@ -77,7 +79,7 @@ export function buildDingtalkOAuthUrl({
|
||||
const redirectUrl = `${publicOrigin}${callbackPath}`
|
||||
const params = new URLSearchParams({
|
||||
redirect_uri: redirectUrl,
|
||||
client_id: getDingtalkOAuthClientId(),
|
||||
client_id: clientId || getDingtalkOAuthClientId(),
|
||||
scope: 'openid',
|
||||
response_type: 'code',
|
||||
state: encodeDingtalkOAuthState(redirectPath, publicOrigin, { source }),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user