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

使用node-oidc-provider调用interactionFinished后遇Access Denied问题求助

问题描述

在AdonisJS自定义框架中使用node-oidc-provider时遇到授权失败问题:

  • 访问授权链接 http://localhost:3333/auth?client_id=foo&redirect_uri=http://localhost:3333/launch&response_type=code&scope=openid&nonce=123&state=321 后,会被重定向至 /interaction/:uid 页面完成登录提交。
  • 调用 interactionFinished 后,先跳转至 /auth/:interaction_id 页面,最终重定向到redirect_uri时出现access_denied错误,无具体原因提示。
  • 排查发现调用 interactionFinished 后,Redis中的interaction键被删除(不确定是否与问题相关)。

配置

const provider = new Provider('http://localhost:3333', {
  adapter: RedisAdapter,
  clients: [
    {
      client_id: 'foo',
      client_secret: 'bar',
      redirect_uris: ['http://localhost:3333/launch'],
      scope: 'openid',
      response_types: ['code'],
    },
  ],
  pkce: {
    required: () => false,
    methods: ['plain'],
  },
  async findAccount(ctx, id) {
    return {
      accountId: id,
      async claims(use, scope) {
        return { sub: id }
      },
    }
  },
  claims: {
    openid: ['sub'],
    email: ['email', 'email_verified'],
    phone: ['phone_number', 'phone_number_verified'],
    profile: [
      'birthdate',
      'family_name',
      'gender',
      'given_name',
      'locale',
      'middle_name',
      'name',
      'nickname',
      'picture',
      'preferred_username',
      'profile',
      'updated_at',
      'website',
      'zoneinfo',
    ],
  },
  interactions: {
    policy: basePolicy,
  },
  cookies: {
    keys: ['some secret key', 'and also the old rotated away some time ago', 'and one more'],
  },
  jwks: {
    keys: [
      {
        d: 'VEZOsY07JTFzGTqv6cC2Y32vsfChind2I_TTuvV225_-0zrSej3XLRg8iE_u0-3GSgiGi4WImmTwmEgLo4Qp3uEcxCYbt4NMJC7fwT2i3dfRZjtZ4yJwFl0SIj8TgfQ8ptwZbFZUlcHGXZIr4nL8GXyQT0CK8wy4COfmymHrrUoyfZA154ql_OsoiupSUCRcKVvZj2JHL2KILsq_sh_l7g2dqAN8D7jYfJ58MkqlknBMa2-zi5I0-1JUOwztVNml_zGrp27UbEU60RqV3GHjoqwI6m01U7K0a8Q_SQAKYGqgepbAYOA-P4_TLl5KC4-WWBZu_rVfwgSENwWNEhw8oQ',
        dp: 'E1Y-SN4bQqX7kP-bNgZ_gEv-pixJ5F_EGocHKfS56jtzRqQdTurrk4jIVpI-ZITA88lWAHxjD-OaoJUh9Jupd_lwD5Si80PyVxOMI2xaGQiF0lbKJfD38Sh8frRpgelZVaK_gm834B6SLfxKdNsP04DsJqGKktODF_fZeaGFPH0',
        dq: 'F90JPxevQYOlAgEH0TUt1-3_hyxY6cfPRU2HQBaahyWrtCWpaOzenKZnvGFZdg-BuLVKjCchq3G_70OLE-XDP_ol0UTJmDTT-WyuJQdEMpt_WFF9yJGoeIu8yohfeLatU-67ukjghJ0s9CBzNE_LrGEV6Cup3FXywpSYZAV3iqc',
        e: 'AQAB',
        kty: 'RSA',
        n: 'xwQ72P9z9OYshiQ-ntDYaPnnfwG6u9JAdLMZ5o0dmjlcyrvwQRdoFIKPnO65Q8mh6F_LDSxjxa2Yzo_wdjhbPZLjfUJXgCzm54cClXzT5twzo7lzoAfaJlkTsoZc2HFWqmcri0BuzmTFLZx2Q7wYBm0pXHmQKF0V-C1O6NWfd4mfBhbM-I1tHYSpAMgarSm22WDMDx-WWI7TEzy2QhaBVaENW9BKaKkJklocAZCxk18WhR0fckIGiWiSM5FcU1PY2jfGsTmX505Ub7P5Dz75Ygqrutd5tFrcqyPAtPTFDk8X1InxkkUwpP3nFU5o50DGhwQolGYKPGtQ-ZtmbOfcWQ',
        p: '5wC6nY6Ev5FqcLPCqn9fC6R9KUuBej6NaAVOKW7GXiOJAq2WrileGKfMc9kIny20zW3uWkRLm-O-3Yzze1zFpxmqvsvCxZ5ERVZ6leiNXSu3tez71ZZwp0O9gys4knjrI-9w46l_vFuRtjL6XEeFfHEZFaNJpz-lcnb3w0okrbM',
        q: '3I1qeEDslZFB8iNfpKAdWtz_Wzm6-jayT_V6aIvhvMj5mnU-Xpj75zLPQSGa9wunMlOoZW9w1wDO1FVuDhwzeOJaTm-Ds0MezeC4U6nVGyyDHb4CUA3ml2tzt4yLrqGYMT7XbADSvuWYADHw79OFjEi4T3s3tJymhaBvy1ulv8M',
        qi: 'wSbXte9PcPtr788e713KHQ4waE26CzoXx-JNOgN0iqJMN6C4_XJEX-cSvCZDf4rh7xpXN6SGLVd5ibIyDJi7bbi5EQ5AXjazPbLBjRthcGXsIuZ3AtQyR0CEWNSdM7EyM5TRdyZQ9kftfz9nI03guW3iKKASETqX2vh0Z8XRjyU',
        use: 'sig',
      },
      {
        crv: 'P-256',
        d: 'K9xfPv773dZR22TVUB80xouzdF7qCg5cWjPjkHyv7Ws',
        kty: 'EC',
        use: 'sig',
        x: 'FWZ9rSkLt6Dx9E3pxLybhdM6xgR5obGsj5_pqmnz5J4',
        y: '_n8G69C-A2Xl4xUW2lF0i8ZGZnk_KPYrhv4GbTGu5G4',
      },
    ],
  },
  features: {
    devInteractions: { enabled: true }, // defaults to true

    deviceFlow: { enabled: true }, // defaults to false
    revocation: { enabled: true }, // defaults to false
  },
})

路由

Route.post('/interaction/:uid', async ({ request, response }) => {
  const details = await provider.interactionDetails(request.request, response.response)

  console.log(details)
  console.log(request.input('login'))

  return await provider.interactionFinished(request.request, response.response, {
    login: {
      accountId: request.input('login'),
    },
  })
})

错误截图

Access Denied

解决思路

1. 补充交互完成的授权同意参数

调用interactionFinished时,仅传入login对象不足以完成OIDC授权流程,必须添加consent对象明确同意申请的scope。修改路由代码如下:

Route.post('/interaction/:uid', async ({ request, response }) => {
  const details = await provider.interactionDetails(request.request, response.response)

  return await provider.interactionFinished(request.request, response.response, {
    login: {
      accountId: request.input('login'),
    },
    consent: {
      scope: details.params.scope, // 同意请求的所有scope
      expiresIn: 3600, // 可选:设置授权过期时间
    },
  })
})

缺少consent是导致access_denied的常见原因,node-oidc-provider会默认拒绝未明确同意的授权请求。

2. 开启详细日志定位问题

在provider配置中添加日志配置,输出debug级别的流程日志,便于查看具体错误原因:

const provider = new Provider('http://localhost:3333', {
  // 其他配置保持不变
  logger: {
    level: 'debug',
    stream: process.stdout,
  },
})

运行后查看控制台输出,日志会详细记录授权流程中的每一步,包括参数验证、决策判断等环节的错误信息。

3. 验证Redis适配器的正确性

Redis中的interaction键被删除是正常行为(交互完成后会清理数据),但需确认适配器实现是否符合规范:

  • 检查适配器的upsert、find、destroy方法是否正确处理数据。
  • 临时切换为内存适配器(注释掉adapter: RedisAdapter)测试,如果问题消失,说明Redis适配器存在bug。

4. 检查自定义交互策略

当前配置使用了basePolicy,需确认该策略是否存在限制授权的逻辑。可以临时注释掉策略配置,使用默认策略测试:

interactions: {
  // policy: basePolicy, // 注释此行
},

如果问题解决,说明自定义策略中存在拒绝授权的规则,需调整策略逻辑。

5. 校验请求参数一致性

确保从初始授权请求到交互完成的所有参数完全一致:

  • 确认redirect_uri与客户端配置中的值完全匹配(包括协议、域名、端口、路径)。
  • 检查nonce、state参数在流程中未被篡改或丢失。

内容的提问来源于stack exchange,提问作者Renzo Sunico

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.20 08:15:45