2

I am trying to build an OAuth2.0 server using https://www.npmjs.com/package/oidc-provider.

The 2 main flows that I need to support are the client credentials one and the authorization code flow. For the latter the idea is to send an authorization code (OTP) to the user's email and exchange that for an access token.

Is the above achievable with this library? If not, are there any alternatives you could recommend, which are in working order and actively maintained?

Here is the request I am making to the /auth endpoint:

enter image description here

The above requests redirects me to the /interaction/:uid endpoint, where I am presented with the following details:

{"iat":1658821468,"exp":1658825068,"returnTo":"http://localhost:3000/oidc/auth/T6W8Cl7_bxYiiYC2AxG3f","prompt":{"name":"login","reasons":["no_session"],"details":{}},"params":{"client_id":"foo","code_challenge":"nELIDtDAvjgo5Hn0eh7mx4JmfFbfWZ166nwXwg89_zs","code_challenge_method":"S256","redirect_uri":"https://oauthdebugger.com/debug","response_mode":"query","response_type":"code"},"kind":"Interaction","jti":"T6W8Cl7_bxYiiYC2AxG3f"}

I may be wrong in how I understand the flow, but I expected to have a code field in the response / code parameter in the redirect url that I can then send to the user's email and (not exactly sure how) exchange it for the access token.

This is my current progress with the POC:

const { Provider } = require('oidc-provider');
const express = require('express');
const cors = require('cors')

const PORT = 3000;
const app = express();

const oidcPrefix = '/oidc';

app.use(cors());

const oidc = new Provider('http://localhost:3000', {
  clients: [{
    client_id: 'foo',
    client_secret: 'bar',
    allowOmittingSingleRegisteredRedirectUri: true,
    redirect_uris: ['https://oauthdebugger.com/debug'],
    rotateRefreshToken: true,
    pkce: {
      required: true
    },
    grant_types: ['authorization_code', 'refresh_token'],
    response_types: ['code'],
    token_endpoint_auth_method: "client_secret_basic"
  }],
  interactions: {
    url: (_, interaction) => `${oidcPrefix}/interaction/${interaction.uid}`
  },
  features: {
    devInteractions: {
      enabled: false,
    },
    introspection: {
      enabled: true
    },
  },
  formats: {
    AccessToken: 'jwt',
  },
  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',
      },
    ],
  },
});

app.get(`${oidcPrefix}/interaction/:id`, async (req, res) => {
  const details = await oidc.interactionDetails(req, res);
  res.send(JSON.stringify(details))
});

app.get(`${oidcPrefix}/auth/:id`, async (req, res) => {
  const details = await oidc.interactionDetails(req, res);

  res.send(JSON.stringify(details))
});

app.use(oidcPrefix, oidc.callback());

app.listen(PORT, () => {
  console.log(`OAUTH Server listening on ${PORT}`);
})

I am open for any suggestions. Also, if you need me to provide additional details, do let me know. Thank you in advance!

Radoslav Naidenov
  • 747
  • 1
  • 5
  • 12

1 Answers1

1

Generally follow the section on User Flows in the documentation.

//The library will send the user to this route for authentication
app.get('/interaction/:uid', async (req, res) => {
  //Send email with OTP and link to `/interaction/:uid/login&otp=xxx`

  //It's common to also provide a from to let the user manually enter their OTP code instead of clicking on the link
  //Display the form here, it should redirect to `/interaction/:uid/login&otp=xxx`
  //otherwise just display a message saying OTP link was sent
});

//This is the route that would normally be used to POST password login forms
app.post('/interaction/:uid/login', async (req, res) => {
  //verify OTP in POST data or query `&otp=xxx`

  //if not valid, display error

  //if valid, call `interactionFinished`
  //this indicates to oidc-provider that the user has successfully authenticated
  return provider.interactionFinished(req, res, { login: { accountId: 'abc123' }});
);

That's about it

Codebling
  • 10,764
  • 2
  • 38
  • 66
  • Hey there! Thank you for the reply. I did in the end manage to get a better understanding of the library (and OAuth2.0!), but I got stuck with a `Session not found` error. Essentially, I didn't want to display a form, but validate the user credentials (email + OTP) based on request params and couldn't find the right way to get the flow / redirects working correctly. Any ideas on this? `/authorize?&email=...&otp=...` -> then, where would I validate without display a form. Also, if anyone stumbles across this, I would suggest taking a look into Ory Hydra! – Radoslav Naidenov Aug 31 '22 at 12:51
  • @RadoslavNaidenov "Session not found" is usually a result of using the default memory store. The app gets restarted, then when reconnecting with an old cookie, the library cannot match the session cookie up with a session it knows about. Ory Hydra looks cool, will check it out, thanks! – Codebling Aug 31 '22 at 16:40
  • 1
    And ya it looks like you maybe I didn't do a good job of explaining myself with the code in my answer. The users will wind up on `/interaction/:uid` when they try to authenticate. You send an email with OTP, and display a message saying "email was sent". Then the email link points to `/interaction/:uid/login&otp=xxx`, you verify `xxx` and then call `interactionFinished`. No need to pass any other oauth/openid params as long as the browser still has the session cookie. I've edited the answer to clarify – Codebling Aug 31 '22 at 16:43
  • 1
    Thank you for the help! I've moved on from the oidc-provider solution, but I really appreciate you stepping in with these ideas . – Radoslav Naidenov Sep 12 '22 at 15:30