OAuth2 PKCE授权码换令牌失败:客户端ID不匹配问题求助
OAuth2/OIDC令牌换取失败问题排查
问题描述
- 环境:企业内部不可直接访问的IBM OAuth2服务器,Angular应用已注册合法重定向URI
- 核心现象:能正常获取授权码,但换取Access Token/ID Token时持续失败,错误提示:
The authenticated client id: [username] does not match the client id in the request body,其中[username]为企业账号(来自智能卡证书认证环节,浏览器自动触发加载,需输入PIN码完成) - 已尝试的无效操作:
- 清除浏览器Cookie、会话存储
- 新建HttpClient实例以排除默认请求配置干扰
- 使用angular-oauth2-oidc库复现流程
- 关键反差:将浏览器中令牌换取请求复制为CURL命令在终端执行,能成功获取令牌
用户调试代码
//preparation const codeVerifier = await this.generateRandomString(43); const codeChallenge = await this.generateCodeChallenge(codeVerifier); const clientId = 'my_client_id'; // given to me by the OAuth2 endpoint team const redirectUri = encodeURIComponent('https://MY_REGISTERED_DOMAIN'); const scope = encodeURIComponent('openid'); const responseType = 'code'; let state = 'state'; // Optional, for CSRF protection let code = ''; const headers = new HttpHeaders({ 'Content-Type': 'application/x-www-form-urlencoded' }); const authorizationUrl = `https://sso-endpoint/mga/sps/oauth/oauth20/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&scope=${scope}&code_challenge=${codeChallenge}&code_verifier=${codeVerifier}&response_type=${responseType}&state=${state}&code_challenge_method=S256`; let code = ''; this.http.get(authorizationUrl, { headers: headers, observe: 'response', responseType: 'text' }) .subscribe((response: HttpResponse<string>) => { // Get the value of the `Location` header from the response const locationHeader = response.headers.get('Location'); if (!locationHeader) { console.error('Location header not found in response.'); return; } // Extract the value of the `code` query parameter from the URL const codeParam = new URL(locationHeader).searchParams.get('code'); if (!codeParam) { console.error('Code parameter not found in response URL.'); return; } // Handle the returned data with the extracted code parameter console.log('Code:', codeParam); code = codeParam; }); const httpClient2 = new HttpClient(new HttpXhrBackend({ build: () => new XMLHttpRequest() })); // At this point, I have extracted an authorization code, which I now want to exchange for an access token -> this last request fails with the above mentioned error const aTokenUrl = `https://sso-endpoint/mga/sps/oauth/oauth20/token?client_id=${clientId}&redirect_uri=${redirectUri}&code_verifier=${codeVerifier}&grant_type=authorization_code&code=${code}`; httpClient2.post(aTokenUrl,{headers: headers,responseType: 'text'}) .subscribe(data => { // Handle returned data -> if the request worked, I would use the result elsewise console.log(data); }); private async generateRandomString(length: number): Promise<string> { const allowedChars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-._~'; let result = ''; for (let i = 0; i < length; i++) { result += allowedChars.charAt(Math.floor(Math.random() * allowedChars.length)); } return result; } private async generateCodeChallenge(codeVerifier: string): Promise<string> { const encodedVerifier = new TextEncoder().encode(codeVerifier); const hashedVerifier = await crypto.subtle.digest('SHA-256', encodedVerifier); const base64EncodedVerifier = btoa(String.fromCharCode(...new Uint8Array(hashedVerifier))); const codeChallenge = base64EncodedVerifier.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); return codeChallenge; }
核心原因
这是IBM Security Verify Access(原ISAM/MGA)的专属行为:当令牌端点收到带有客户端证书认证的请求时,会优先将证书中的身份标记为client_id,直接忽略请求体/URL参数里的合法client_id,导致和授权码绑定的客户端ID不匹配,触发报错。
浏览器请求会自动带上智能卡的客户端证书(因为之前的授权认证环节已建立会话关联),而CURL请求未携带证书,服务器会正常使用URL/请求体中的client_id验证,因此成功。
解决步骤
1. 修正令牌请求的参数传递方式
IBM令牌端点要求client_id必须放在请求体中,禁止通过URL参数传递,同时确保请求体为application/x-www-form-urlencoded格式。
2. 禁用浏览器自动发送客户端证书
在Angular中通过自定义XMLHttpRequest配置,阻止自动发送智能卡证书:
const httpClient2 = new HttpClient(new HttpXhrBackend({ build: () => { const xhr = new XMLHttpRequest(); // 禁止自动发送凭据(含证书) xhr.withCredentials = false; // Chrome专属配置:禁用头部检查,阻止证书自动发送 if ((xhr as any).setDisableHeaderCheck) { (xhr as any).setDisableHeaderCheck(true); } // Firefox专属配置:标记为后台请求,不触发证书选择 if ((xhr as any).mozBackgroundRequest) { (xhr as any).mozBackgroundRequest = true; } return xhr; } }));
3. 调整令牌请求的参数格式
将所有参数放入请求体,而非URL拼接:
const tokenBody = new HttpParams() .set('client_id', clientId) .set('redirect_uri', 'https://MY_REGISTERED_DOMAIN') // 无需编码,表单会自动处理 .set('code_verifier', codeVerifier) .set('grant_type', 'authorization_code') .set('code', code); // code为从重定向URL中提取的授权码 httpClient2.post('https://sso-endpoint/mga/sps/oauth/oauth20/token', tokenBody, { headers: new HttpHeaders({ 'Content-Type': 'application/x-www-form-urlencoded' }), responseType: 'text' }).subscribe({ next: data => console.log(data), error: err => console.error(err) });
4. 修复授权请求的PKCE错误
授权URL中错误地传递了code_verifier,PKCE流程仅需在授权请求中传递code_challenge和code_challenge_method,code_verifier仅在令牌请求中使用,需删除授权URL中的code_verifier参数。
5. 修正授权流程的触发方式
不能通过HttpClient的GET请求触发授权流程,必须通过浏览器跳转完成(否则无法触发智能卡证书认证):
// 构造正确的授权URL(已移除code_verifier) const authorizationUrl = `https://sso-endpoint/mga/sps/oauth/oauth20/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(scope)}&code_challenge=${codeChallenge}&response_type=${responseType}&state=${state}&code_challenge_method=S256`; // 触发浏览器跳转 window.location.href = authorizationUrl;
内容的提问来源于stack exchange,提问作者LeBoy
相关产品推荐
相关产品推荐

