itpropro / nuxt-oidc-auth

OIDC (OpenID connect) focused auth module for Nuxt
https://nuxt.com/modules/nuxt-oidc-auth
MIT License
68 stars 13 forks source link

Feide OpenID provider #9

Closed espensgr closed 5 months ago

espensgr commented 7 months ago

I'm trying to implement Feide OpendID but i just cant get past the Invalid JWT token. It's my first time working on an OpenId provider so the errors just dont give me enough info, and maybe there is some settings that i dont see that should be there from the config/spec.

I've setup the playground folder locally with this config:

oidc: {
    providers: {
      oidc: {
        redirectUri: "http://localhost:3000/auth/oidc/callback",
        clientId: "",
        clientSecret: "",
        authorizationUrl: "https://auth.dataporten.no/oauth/authorization",
        tokenUrl: "https://auth.dataporten.no/oauth/token",
        userinfoUrl: "https://auth.dataporten.no/openid/userinfo",
        logoutUrl: "https://auth.dataporten.no/openid/endsession",
        responseType: "code",
        scope: ["openid"],
      },
    },
    middleware: {
      globalMiddlewareEnabled: false,
      customLoginPage: false,
    },
  },

With NUXT_OIDC_PROVIDERS_OIDC_CLIENT_ID and NUXT_OIDC_PROVIDERS_OIDC_CLIENT_SECRET in .env.

The callback is: /auth/oidc/callback?code=[uuid]&state=[hash]

If you have some tips or some help with the config it would be must appreciated

itpropro commented 7 months ago

Hey, Invalid JWT Token normally only appears when the token cannot be parsed or validated. This is often times because of missing audience fields, signatures or because it’s not a parsable JWT (which could happen as there is no spec that dictates parsable tokens). Try to disable token validation or add the missing audience fields. In general, it the token doesn’t have your app as the audience, it should not be validated.

espensgr commented 7 months ago

Hey, thanks for the reply. you mean like this in the config?:

validateAccessToken: false,
validateIdToken: false,

Either of them give me invalid JWT.

I have no idea what should be in the audience field, this is all the info on the connection of the provider i have, and i dont know how to translate that into the config files 😢

espensgr commented 7 months ago

I tried with the skipAccessTokenParsing: true then the callback worked and was bounced back to the login page, but dont get logged in of course. So it has to be something with the parsing of the token then, from the parseJwtToken(token, skipParsing)?

We are able to get the response in Postman from https://auth.dataporten.no/oauth/token, it looks like this:

{
    "access_token": "[uuid]",
    "token_type": "Bearer",
    "expires_in": 28799,
    "scope": "openid userid-feide",
    "id_token": "[JWT]"
}

If we use the access_token further it works, and the id_token contains user id that we need, so parsing that would be enough, just dont know if that is possible here.

itpropro commented 6 months ago

I tried with the skipAccessTokenParsing: true then the callback worked and was bounced back to the login page, but dont get logged in of course. So it has to be something with the parsing of the token then, from the parseJwtToken(token, skipParsing)?

We are able to get the response in Postman from https://auth.dataporten.no/oauth/token, it looks like this:

{
    "access_token": "[uuid]",
    "token_type": "Bearer",
    "expires_in": 28799,
    "scope": "openid userid-feide",
    "id_token": "[JWT]"
}

If we use the access_token further it works, and the id_token contains user id that we need, so parsing that would be enough, just dont know if that is possible here.

Do you have some instruction's on how to reproduce the error (docker image etc. with your version or config)? From your object description it looks like the access_token is not a valid JWT structure but instead a uuid, so token parsing cannot work with that. SkipAccessTokenParsing is probably your only choice if Feide doesn’t provide JWT access tokens. The interesting part is why you are not logged in if that is configured, which is what I would like to investigate with a reproduction.

espensgr commented 6 months ago

The config we have is here, and according to this there should be an accesstoken. It may be that we are missing something in the config, i just cant see what.

When i do skipAccessTokenParsing the accesstoken that is returned is just an empty object {}, dont know if that is a bug or what it is suppose to do.

Would be nice with some more console logging to be able to see what is going on in dev mode.

espensgr commented 6 months ago

So i did some console logging in the oidc.mjs file to get som context. This is the config:

oidc: {
    redirectUri: "http://localhost:3000/auth/oidc/callback",
    clientId: "",
    clientSecret: "",
    authorizationUrl: "https://auth.dataporten.no/oauth/authorization",
    tokenUrl: "https://auth.dataporten.no/oauth/token",
    userinfoUrl: "https://auth.dataporten.no/openid/userinfo",
    logoutUrl: "https://auth.dataporten.no/openid/endsession",
    responseType: "code",
    scope: ["openid", "profile", "email"],
    skipAccessTokenParsing: true,
},

Here is the tokenRespone:

{
  access_token: '[uuid],
  token_type: 'Bearer',
  expires_in: 28799,
  scope: 'openid',
  id_token: '[jwt]'
}

Here is the tokens object:

 {
  accessToken: {},
  idToken: {
    iss: 'https://auth.dataporten.no',
    jti: '[uuid]',
    aud: '[uuid]',
    sub: '[uuid]',
    iat: 1709113464,
    exp: 1709142264,
    auth_time: 1709111252
  }
}

At last the user object:

{
  canRefresh: false,
  loggedInAt: 1709113464,
  updatedAt: 1709113464,
  expireAt: 1709199864,
  provider: 'oidc',
  providerInfo: {
    aud: '[uuid]',
    sub: '[uuid]',
    'connect-userid_sec': [],
    'dataporten-userid_sec': [],
    'https://n.feide.no/claims/userid_sec': []
  }
}

What i have found so far is that it don't get a refresh_token, the rest seem fine. Feide do have a openidconfig, but for that to be validated i do have to have an aud in the accessToken, which we dont.

Hope there is some insights to be read from this.

espensgr commented 6 months ago

Just tried to dismantle the code and tried stuff and came up with this. If i rewrite the refresh_token if logic, remove the tokenResponse.refresh_token and do this:

const persistentSession = {
  exp: accessToken.exp,
  iat: accessToken.iat,
  accessToken: await encryptToken(tokenResponse.access_token, tokenKey),
  refreshToken: tokenResponse.refresh_token ? await encryptToken(tokenResponse.refresh_token, tokenKey) : null
};

Then i get logged in, i just can't refresh the session of course.

espensgr commented 6 months ago

Contacted the developers for the api and they don't use the refresh_token, so i guess the plugin should account for that and still set the session?

espensgr commented 5 months ago

Do you want me to make a forked repo with our settings to try to debug this? I would love to get this working as the project requires openid and we haven't found any other implementations of this.

itpropro commented 5 months ago

I can unfortunately only help you to debug this, if there is some way for me to reproduce the issue locally or with a online provider. Is Feide a closed system or is there any possibility for me to get a client id/secret for debugging by registering? If there is no refresh token the only thing I can image why your session fails to be recognized as logged in is that you haven't turned session -> expirationCheck off (set to false). As this functionality requires a refresh token to refresh and to rely on server side persistent session handling for security reasons.

espensgr commented 5 months ago

That worked, thanks! is that something that should be a setting for each provider? If some has a refresh token and others dont? Or maybe check if the provider provides a refresh token in the request, as i see there is an canRefresh added?

itpropro commented 5 months ago

That worked, thanks! is that something that should be a setting for each provider? If some has a refresh token and others dont? Or maybe check if the provider provides a refresh token in the request, as i see there is an canRefresh added?

The idea is that this module provides high compatibility with OIDC providers. With a provider that is OIDC compliant, there should be a refresh token. It could make sense though to change this for the generic OIDC provider just so that users can incrementally enable features that the specific provider supports. For all built-in providers it is configured in a way that worked when it was tested on the respective platform. If your problem is solved, feel free to close the issue :)

espensgr commented 5 months ago

Should is the key word there, not all has that, so that can't be required for it to be an OIDC provider, but i dont know the spec on this 😄

If i setup this Provider (Feide) with the generic OIDC provider and add the Github provider, that needs the expirationCheck as it has a refresh token, the generic OIDC will fail right? Thats why i suggested this should be a per provider setting, or override.

itpropro commented 5 months ago

Should is the key word there, not all has that, so that can't be required for it to be an OIDC provider, but i dont know the spec on this 😄

If i setup this Provider (Feide) with the generic OIDC provider and add the Github provider, that needs the expirationCheck as it has a refresh token, the generic OIDC will fail right? Thats why i suggested this should be a per provider setting, or override.

Ah ok got you, yes multi-provider expiration settings is definitely planned, it just requires some internal refactoring. The module was initially built to very strictly comply to OIDC, so there wasn't even the possibility to turn expirationCheck and it's implications off. As I added some providers that are not strictly OIDC, the things became a little bit more muddy on the configuration side. Great feedback though, I will use the GitHub provider combined with a generic as a test case, as this was not a scenario I realized someone would actually do so far :)