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?
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'),
},
})
})
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With