You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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}&amp;redirect_uri=${redirectUri}&amp;code_verifier=${codeVerifier}&amp;grant_type=authorization_code&amp;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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 12:57:02