Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Organization Invitation to be handled in the login endpoint #701

Closed
KevinEdry opened this issue Jun 1, 2022 · 10 comments
Closed

Organization Invitation to be handled in the login endpoint #701

KevinEdry opened this issue Jun 1, 2022 · 10 comments

Comments

@KevinEdry
Copy link

Describe the problem you'd like to have solved

The organization feature has been rolled out for quite some time now and there isn't any native support for it in the /api/auth/login endpoint.

This means that there is an inherent difference between the organization invitation you can generate from the Auth0 dashboard and the expected implementation in this SDK.

Describe the ideal solution

The SDK should append the invitation, organization and organization_name to the /authorize endpoint when provided to the /api/auth/login endpoint.

Alternatives and current work-arounds

We currently can create a new endpoint /api/invite and append those query params via the handleLogin method.
As described here:

* // pages/api/invite.js
* import { handleLogin } from '@auth0/nextjs-auth0';
*
* export default async function invite(req, res) {
* try {
* const { invitation, organization } = req.query;
* if (!invitation) {
* res.status(400).end('Missing "invitation" parameter');
* }
* await handleLogin(req, res, {
* authorizationParams: {
* invitation,
* organization
* }
* });
* } catch (error) {
* res.status(error.status || 500).end(error.message);
* }
* } ;

Additional information, if any

@adamjmcgrath
Copy link
Contributor

Hi @KevinEdry - thanks for raising this

Customising the login handler or adding your own handler for invites is the current suggested way of implementing Organization invites in this SDK.

There's currently no desire to add this to the login handler, since many of the users of the login handler wont be using Organizations and it's fairly trivial to add this yourself.

@KevinEdry
Copy link
Author

KevinEdry commented Jun 1, 2022

@adamjmcgrath The PR I submitted enables both the use cases seamlessly for organization invitations and for the regular users that don't use organizations.
This only has a positive effect on the SDK and the code is already PR'ed and ready to use, mind to reconsider it?

The PR: #703

@adamjmcgrath
Copy link
Contributor

adamjmcgrath commented Jun 1, 2022

Hi @KevinEdry - thanks for your PR, but we don't want to add these params to the API surface area. There may be use cases where users don't want unexpected parameters to be passed on to the authorization server and we feel the best way to support extra parameters (either organization or otherwise) is through the existing extensibility options we provide.

@nickzelei
Copy link

nickzelei commented Sep 22, 2022

I know this issue is closed, but I also hit this a while ago when looking to use organizations and wasted a bunch of time figuring out how to get this to work and am frankly surprised the login flow doesn't handle this.

This is the function I came up with:

import { NextApiRequest, NextApiResponse } from 'next';
import { URLSearchParams } from 'url';

export default async function Login(req: NextApiRequest, res: NextApiResponse) {
  const params = new URLSearchParams(toUrlSearchParams(req.query));

  const scopes = process.env.AUTH0_SCOPE
    ? process.env.AUTH0_SCOPE
    : 'openid profile';
  const audience = process.env.AUTH0_AUDIENCE
    ? process.env.AUTH0_AUDIENCE
    : '<default audience>';

  const clientId = process.env.AUTH0_CLIENT_ID ?? '';
  const redirectUri = process.env.AUTH0_BASE_URL ?? '';
  const issuerUrl = process.env.AUTH0_ISSUER_BASE_URL ?? '';

  params.append('client_id', clientId);
  params.append('response_type', 'code');
  params.append('scope', scopes);
  params.append('audience', audience);
  params.append('redirect_uri', redirectUri);
  return res.redirect(`${issuerUrl}/authorize?` + params.toString());
}

function* toUrlSearchParams(
  query: NextApiRequest['query']
): Iterable<[string, string]> {
  for (const [key, valOrVals] of Object.entries(query)) {
    if (!valOrVals) {
      continue;
    }
    if (Array.isArray(valOrVals)) {
      for (const val of valOrVals) {
        yield [key, val];
      }
    } else {
      yield [key, valOrVals];
    }
  }
}

Then, for the Application I'm having people log in to, I had to set the link to this function as the Application Login URI.
This gets weird though if you have orgs and non-orgs logging in through the same client. I haven't finished testing that flow yet, but I know this function definitely works when signing up and logging in as an organization.

Hopefully this helps someone in the future.

@KevinEdry
Copy link
Author

I know this issue is closed, but I also hit this a while ago when looking to use organizations and wasted a bunch of time figuring out how to get this to work and am frankly surprised the login flow doesn't handle this.

This is the function I came up with:

import { NextApiRequest, NextApiResponse } from 'next';
import { URLSearchParams } from 'url';

export default async function Login(req: NextApiRequest, res: NextApiResponse) {
  const params = new URLSearchParams(toUrlSearchParams(req.query));

  const scopes = process.env.AUTH0_SCOPE
    ? process.env.AUTH0_SCOPE
    : 'openid profile';
  const audience = process.env.AUTH0_AUDIENCE
    ? process.env.AUTH0_AUDIENCE
    : '<default audience>';

  const clientId = process.env.AUTH0_CLIENT_ID ?? '';
  const redirectUri = process.env.AUTH0_BASE_URL ?? '';
  const issuerUrl = process.env.AUTH0_ISSUER_BASE_URL ?? '';

  params.append('client_id', clientId);
  params.append('response_type', 'code');
  params.append('scope', scopes);
  params.append('audience', audience);
  params.append('redirect_uri', redirectUri);
  return res.redirect(`${issuerUrl}/authorize?` + params.toString());
}

function* toUrlSearchParams(
  query: NextApiRequest['query']
): Iterable<[string, string]> {
  for (const [key, valOrVals] of Object.entries(query)) {
    if (!valOrVals) {
      continue;
    }
    if (Array.isArray(valOrVals)) {
      for (const val of valOrVals) {
        yield [key, val];
      }
    } else {
      yield [key, valOrVals];
    }
  }
}

Then, for the Application I'm having people log in to, I had to set the link to this function as the Application Login URI. This gets weird though if you have orgs and non-orgs logging in through the same client. I haven't finished testing that flow yet, but I know this function definitely works when signing up and logging in as an organization.

Hopefully this helps someone in the future.

I had a PR ready to take care of it, and they turned it down for some reason. If they have no intentions of making this lib better, the least they could do is review the PR and approve it.

@nickzelei
Copy link

I had a PR ready to take care of it, and they turned it down for some reason. If they have no intentions of making this lib better, the least they could do is review the PR and approve it.

That's too bad. It's not intuitive the way it is written now, especially when the documentation just says to "implement it yourself". At least providing an example of what to implement would go a long way, even if it can't land directly in this library.

Separately- you just indirectly helped me with issue I was having!

I looked again at your original code snippet and saw you were calling the handleLogin function. I updated some code i was using to call that instead of doing what I posted above, and the thing I've been struggling with started working for me! 😄

Have been trying to figure out how to redirect users to auto-log them in to an organization. And calling the handleLogin worked!!

So, thank you for indirectly helping me. 😄

@adamjmcgrath
Copy link
Contributor

Hi @nickzelei @KevinEdry - thanks for the feedback

There's an example of passing custom parameters to the authorize url here https://github.com/auth0/nextjs-auth0/blob/main/EXAMPLES.md#customize-handlers-behavior

So for the invitations use case, that would be something like:

// pages/api/auth/[...auth0].js
import { handleAuth, handleLogin } from '@auth0/nextjs-auth0';

export default handleAuth({
  async login(req, res) {
    try {
      await handleLogin(req, res, {
        authorizationParams: {
          invitation: req.query.invitation,
          organization: req.query.organization,
        }
      });
    } catch (error) {
      res.status(error.status || 500).end(error.message);
    }
  }
});

I'm sorry we didn't accept your PR @KevinEdry - hopefully there's no hard feelings 😄

Also, we have some changes in the works for v2 that should simplify this example, will share with this thread when it's available to try.

@nickzelei
Copy link

Hi @nickzelei @KevinEdry - thanks for the feedback

There's an example of passing custom parameters to the authorize url here https://github.com/auth0/nextjs-auth0/blob/main/EXAMPLES.md#customize-handlers-behavior

So for the invitations use case, that would be something like:

// pages/api/auth/[...auth0].js
import { handleAuth, handleLogin } from '@auth0/nextjs-auth0';

export default handleAuth({
  async login(req, res) {
    try {
      await handleLogin(req, res, {
        authorizationParams: {
          invitation: req.query.invitation,
          organization: req.query.organization,
        }
      });
    } catch (error) {
      res.status(error.status || 500).end(error.message);
    }
  }
});

I'm sorry we didn't accept your PR @KevinEdry - hopefully there's no hard feelings 😄

Also, we have some changes in the works for v2 that should simplify this example, will share with this thread when it's available to try.

Awesome, thanks @adamjmcgrath ! I'll give that a try the next time I have a sec to circle back to the org invites flow. That is very helpful. I like how much cleaner that is by just overriding the basic login function. One less route to have floating around. 😄

@jamesarosen
Copy link

Customising the login handler or adding your own handler for invites is the current suggested way of implementing Organization invites in this SDK.

There's currently no desire to add this to the login handler, since many of the users of the login handler wont be using Organizations and it's fairly trivial to add this yourself.

The docs for this are buried in AuthorizationParams.invitation. It would be nice to have a more prominent example or recipe to support this commonly-used Auth0 feature.

@FlooflesDev
Copy link

Want to add my solution to this, since most suggested methods (from the docs) flat out didn't work or seemed tedious to me. Most solutions suggest getting the parameters from req.query but this was always undefined for me.
I also did not like the suggested solution of adding a new /invite route and changing the invite link to point to this route.

Solution

The code in #703 seemed the cleanest to me, but it also did not seem to work. It required one small tweak, which finally landed me on the following solution:

function getInvitationParameters(req) {
  // req.query does not work, since this is undefined (not forwarded?).
  // new URLSearchParams(req.url) also does not work, since this delimits only on '&' (see comment below).
  const searchParams = new URL(req.url).searchParams;
  return {
    invitation: searchParams.get("invitation"),
    organization: searchParams.get("organization"),
    organization_name: searchParams.get("organization_name"),
  };
}

export const GET = auth0.handleAuth({
  login: auth0.handleLogin((req) => {
    return { authorizationParams: { ...getInvitationParameters(req) } };
  }),
});

Problem with URLSearchParams

I found that the code used in the closed merge request incorrectly split the search params. The resulting code would return the following object:

URLSearchParams {
  'http://localhost:3001/api/auth/login?invitation' => '9evhYvksfcRCFkANyBVNcJdLWOHSrtfQ',
  'organization' => 'org_id,
  'organization_name' => 'org' 
}

Making it very difficult to forward the invitation.

Hope that this helps anyone running into the same issue in the future!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants