2

I'm using node-oidc-provider in a custom framework, adonisjs to be specific.

At this point, I can visit http://localhost:3333/auth?client_id=foo&redirect_uri=http://localhost:3333/launch&response_type=code&scope=openid&nonce=123&state=321 and be redirected to the /interaction/:uid page where I am asked to log in.

When I hit submit, I do call interactionFinished as stated in the documentation. I'll then get redirected to the /auth/:interaction_id page and eventually redirected to my redirect_uri with access_denied error.

There's no hint as to why I'm getting access denied. Upon further digging though, I noticed that when interactionFinished is called, the interaction key is deleted in Redis. (Not sure if it's relevant.)

Any ideas?

Configuration

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
  },
})

Routes

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

Renzo Sunico
  • 141
  • 2
  • 6

1 Answers1

2

Okay, I found the solution to my own problem.

What I have missed to mention is that I disabled the consent prompt of the provider.

The problem is it doesn't generate a "Grant" automatically. As a result, the middleware "authorization/interaction.js" fails.

    if (
      !oidc.grant.getOIDCScopeFiltered(oidc.requestParamOIDCScopes)
      && Object.keys(ctx.oidc.resourceServers)
        .every(
          (resource) => !oidc.grant.getResourceScopeFiltered(resource, oidc.requestParamScopes),
        )
    ) {

      throw new errors.AccessDenied(undefined, 'authorization request resolved without requesting interactions but no scope was granted');
    }

In order to resolve this, I have to override the loadExistingGrant and create a new instance of grant if it doesn't exist as explained in the documentation as well.

You may be required to skip (silently accept) some of the consent checks, while it is discouraged there are valid reasons to do that, for instance in some first-party scenarios or going with pre-existing, previously granted, consents. To simply silenty "accept" first-party/resource indicated scopes or pre-agreed upon claims use the loadExistingGrant configuration helper function, in there you may just instantiate (and save!) a grant for the current clientId and accountId values.

  async loadExistingGrant(ctx) {
    const grantId =
      (ctx.oidc.result && ctx.oidc.result.consent && ctx.oidc.result.consent.grantId) ||
      ctx.oidc.session.grantIdFor(ctx.oidc.client.clientId)

    if (grantId) {
      return ctx.oidc.provider.Grant.find(grantId)
    }

    const grant = new ctx.oidc.provider.Grant({
      clientId: ctx.oidc.client.clientId,
      accountId: ctx.oidc.session?.accountId,
    })

    const scopes = ctx.oidc.params.scope.split(' ')

    for (let scope of scopes) {
      // TODO: Confirm if this is correct
      grant.addOIDCScope(scope)
    }

    // TODO: Use ttl.Grant
    await grant.save(3600)

    return grant
  }

** Disclaimer: I'm still testing the code above but this is the gist of what I did to fix the problem.

As a side tip, implement the renderError config ASAP so that you can see the full stacktrace instead of relying on the UI.

Renzo Sunico
  • 141
  • 2
  • 6