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

Allow approved users only #699

Closed
1 of 5 tasks
glenarama opened this issue Sep 21, 2020 · 21 comments
Closed
1 of 5 tasks

Allow approved users only #699

glenarama opened this issue Sep 21, 2020 · 21 comments
Labels
question Ask how to do something or how something works stale Did not receive any activity for 60 days

Comments

@glenarama
Copy link

Using an OAuth provider (Google) I want to only allow access to users that have been pre-approved, i.e: Signup is restricted.
If a user exists in the users table (Even if they do not have any account/provider linked) i receive a message to sign in with the original account.

I could create a separate approved user table - but id have to manage that and add a fair bit of logic. Is there a easier customisation to allow a provider to "claim" an empty user.

Feedback
Documentation refers to searching through online documentation, code comments and issue history. The example project refers to next-auth-example.

  • Found the documentation helpful
  • Found documentation but was incomplete
  • Could not find relevant documentation
  • Found the example project helpful
  • Did not find the example project helpful
@glenarama glenarama added the question Ask how to do something or how something works label Sep 21, 2020
@guiaramos
Copy link

Maybe related #621

@glenarama
Copy link
Author

Thanks @guiaramos - it does appear that is the correct answer. I had seen that but wondered given its not already linked whether there was a solution. To be honest i'm not really sure how to go about linking these manually. Any pointers?

@guiaramos
Copy link

guiaramos commented Sep 22, 2020

Hmm I am on same page as you, as we think link the accounts should be safe and good.

this issue is also related #519

on callback-handler.js we get this:

....
 const userByEmail = profile.email ? await getUserByEmail(profile.email) : null
        if (userByEmail) {
          // We end up here when we don't have an account with the same [provider].id *BUT*
          // we do already have an account with the same email address as the one in the
          // oAuth profile the user has just tried to sign in with.
          //
          // We don't want to have two accounts with the same email address, and we don't
          // want to link them in case it's not safe to do so, so instead we prompt the user
          // to sign in via email to verify their identity and then link the accounts.
          throw new AccountNotLinkedError()
 } else {
...
}

@iaincollins do you know why its not safe to link the accounts and if there is anyway we can do that?

thanks!

@iaincollins
Copy link
Member

@guiaramos the tl;dr is basically it can be used by bad actors to hijack accounts, but there is still scope for NextAuth.js to handle it more gracefully.

I've raised a PR to add a question to address account linking to the FAQ as it's come up before but it doesn't look like we address it in the documentation anywhere.


When I sign in with another account with the same email address, why are accounts not linked automatically?

Automatic account linking on sign in is not secure between arbitrary providers - with the exception of allowing users to sign in via an email addresses as a fallback (as they must verify their email address as part of the flow).

When an email address is associated with an OAuth account it does not necessarily mean that it has been verified as belonging to account holder — how email address verification is handled is not part of the OAuth specification and varies between providers (e.g. some do not verify first, some do verify first, others return metadata indicating the verification status).

With automatic account linking on sign in, this can be exploited by bad actors to hijack accounts by creating an OAuth account associated with the email address of another user.

For this reason it is not secure to automatically link accounts between abitrary providers on sign in, which is why this feature is generally not provided by authentication service and is not provided by NextAuth.js.

Automatic acccount linking is seen on some sites, sometimes insecurely. It can be technically possible to do automatic account linking securely if you trust all the providers involved to ensure they have securely verified the email address associated with the account, but requires placing trust (and transferring the risk) to those providers to handle the process securely.

Examples of scenarios where this is secure include with an OAuth provider you control (e.g. that only authorizes users internal to your organization) or with a provider you explicitly trust to have verified the users email address.

Automatic account linking is not a planned feature of NextAuth.js, however there is scope to improve the user experience of account linking and of handling this flow, in a secure way. Typically this involves providing a fallback option to sign in via email, which is already possible (and recommended), but the current implementation of this flow could be improved on.

Providing support for secure account linking and unlinking of additional providers - which can only be done if a user is already signed in already - was originally a feature in v1.x but has not been present since v2.0, is planned to return in a future release.

@iaincollins
Copy link
Member

@glenames Hmm, depending on the flow, you could use the signin callback to check if a users email address with Google is (a) flagged by Google as verified (Google explicitly sets a flag to confirm this, as per this example in the documentation) and (b) in your table of allowed users.

The caveat to this is it would not work well if users can sign in via email OR Google, in practice you'd want to pick one - otherwise they will conflict if a user tries to sign up via email, THEN tries to use Google later because there is no automatic account linking of OAuth accounts.

While automatic account linking isn't planned the UX could be better for this and is something I'd like to look at improving in future. With NextAuth.js right now, probably your best option is to pick ether Google or Email as the sign in option. If you create a custom sign in page, you could promote Google and choose to let people use email only for account recovery (or as fallback).

We should have secure, manual account linking in soon (e.g. where you sign in, then can select any configured providers and link / unlink any accounts with them you want with you account, then can them to sign in in future) which might help with the edge case where someone signs in via email the first time then later decides they want to use their Google account.

@glenarama
Copy link
Author

Thanks @iaincollins I think i actually need linking even though i'm not linking. I'd prefer to have a single Users table rather than a separate approved users table so i don't have to synch and manage two duplicate lists. There's also certain required information in that table which the authenticating user needs (Company id for example)

The process id like is:

  • Admin creates a user within their organization {id: 123, company_id: 456, email: [email protected]}
  • User can then log in using Google auth (Or email) both is not a requirement.
  • Any user who tries to log into the system but isn't registered is rejected

This seems common for b2b applications - rather than a free for all auto enrolment, but the google workflow won't work with an existing user in the users table even if it doesn't have an alternate provider.

@glenarama
Copy link
Author

@guiaramos Were you able to work out how to "manually" link accounts? Im a bit stuck with this currently and can't move forward. Im only using email and Google so im comfortable to trust with a verified account from google.

@guiaramos
Copy link

@guiaramos Were you able to work out how to "manually" link accounts? Im a bit stuck with this currently and can't move forward. Im only using email and Google so im comfortable to trust with a verified account from google.

Hey, yeah, I did the following

  • I created an resolver (api) that manually link the accounts table with users table on my nodejs server
  • Once we render the error page with the query error I check the error query
  • If the error is OAuthAccountNotLinked we render the loading component and call that API the manually connect the both table on our Nodejs server

It might be wierd but is working for me hahah

@glenarama
Copy link
Author

Do you then not have to request the user log in again as the login failed?

@guiaramos
Copy link

Do you then not have to request the user log in again as the login failed?

Exactly, its a bad UX..

@glenarama
Copy link
Author

glenarama commented Sep 26, 2020

Ok, here's what im going to try which at least provides a decent UX. Let me know if you have thoughts:
In the signin Callback i will:

  dbuser = findOne(User, {profile.email})  
  if(!account.verified_email || !dbuser) resolve(false)  
  dbaccount = findOne(Account, {account.providerId, account.providerAccountId})  
  if(!dbaccount)  // Create new account   
  resolve(true)  

This way i create the account (If verified) before login.

@stale
Copy link

stale bot commented Dec 5, 2020

Hi there! It looks like this issue hasn't had any activity for a while. It will be closed if no further activity occurs. If you think your issue is still relevant, feel free to comment on it to keep ot open. Thanks!

@stale stale bot added the stale Did not receive any activity for 60 days label Dec 5, 2020
@stale
Copy link

stale bot commented Dec 12, 2020

Hi there! It looks like this issue hasn't had any activity for a while. To keep things tidy, I am going to close this issue for now. If you think your issue is still relevant, just leave a comment and I will reopen it. (Read more at #912) Thanks!

@stale stale bot closed this as completed Dec 12, 2020
@guiaramos
Copy link

This is my implementation with @glenames strategy:

Frontend

[...nextauth].ts

import NextAuth, { InitOptions } from 'next-auth';
import {
  signInCallback,
  getProviders,
  getDatabase,
  getPages,
  getSession,
  getCookies
} from '@config/auth';

const IsDev = process.env.NODE_ENV === 'development';

const options: (protocol: string) => InitOptions = (protocol) => {
  return {
    providers: getProviders(protocol),
    database: getDatabase(),
    pages: getPages(),
    session: getSession(),
    cookies: getCookies(),
    callbacks: {
      signIn: signInCallback
    },
    debug: process.env.NODE_ENV !== 'production'
  };
};

export default (req, res) => {
  return NextAuth(req, res, options(IsDev ? 'http' : 'https'));
};

signInCallback.ts

import Axios, { AxiosRequestConfig } from 'axios';
import { ENDPOINTS } from '@libraries/routes';
import { InitOptions } from 'next-auth';

const signInCallback: NonNullable<InitOptions['callbacks']>['signIn'] = async (
  user,
  account,
  profile
) => {
  if (account.type === 'email') return Promise.resolve(true);

  try {
    const params: AxiosRequestConfig['params'] = {
      email: user.email,
      verifiedEmail: profile.verified_email,
      providerId: account.provider,
      providerAccountId: account.id,
      providerType: account.type,
      refreshToken: account.refreshToken,
      accessToken: account.accessToken,
      accessTokenExpires: account.accessTokenExpires
    };

    const { status } = await Axios.get(ENDPOINTS.AUTH, { params });

    if (status !== 200) {
      return Promise.resolve(false);
    }

    return Promise.resolve(true);
  } catch (error) {
    console.error(error);
    return Promise.resolve(false);
  }
};

export default signInCallback;

Backend

signInCallback.ts

import { RequestHandler } from 'express';
import { createHash } from 'crypto';
import { getConnection } from 'typeorm';
import Users from 'src/entities/Postgres/User/Users.postgres';
import Accounts from 'src/entities/Postgres/Account/Accounts.postgres';

const signInHandler: RequestHandler = async (req, res) => {
  const {
    query: { email, verifiedEmail, providerId, providerAccountId, providerType, refreshToken, accessToken, accessTokenExpires }
  } = req;

  if (!verifiedEmail) return res.status(401).json({ error: 'email is not verified' });
  if (typeof email !== 'string') return res.status(401).json({ error: 'email should be string' });
  if (typeof providerId !== 'string') return res.status(401).json({ error: 'providerId should be string' });
  if (typeof providerAccountId !== 'string') return res.status(401).json({ error: 'providerAccountId should be string' });
  if (typeof providerType !== 'string') return res.status(401).json({ error: 'providerType should be string' });
  if (typeof refreshToken !== 'string') return res.status(401).json({ error: 'refresh_token should be string' });
  if (typeof accessToken !== 'string') return res.status(401).json({ error: 'refresh_token should be string' });

  const userRepo = getConnection().getRepository(Users);
  const accountsRepo = getConnection().getRepository(Accounts);

  let user;

  try {
    user = await userRepo.findOne({ email });
  } catch (error) {
    return res.status(401).json({ error: 'could not find user' });
  }

  if (!user) return res.status(200).json({ error: '' });

  const account = await accountsRepo.findOne({ provider_id: providerId, provider_account_id: providerAccountId });

  if (!account) {
    try {
      const compoundId = createHash('sha256').update(`${providerId}:${providerAccountId}`).digest('hex');
      await accountsRepo
        .create({
          user_id: user.id,
          compound_id: compoundId,
          provider_id: providerId,
          provider_type: providerType,
          provider_account_id: providerAccountId,
          refresh_token: refreshToken,
          access_token: accessToken,
          access_token_expires: accessTokenExpires
        })
        .save();
    } catch (error) {
      return res.status(401).json({ error });
    }
  }

  return res.status(200).json({ error: '' });
};

export default signInHandler;

@LeunensMichiel
Copy link

@glenames @guiaramos thank you! I was wondering how I could fix this in our B2B scenario. I'm gonna give this a shot as it looks very promising.

@TimPietrusky
Copy link

@LeunensMichiel how did this turn out for you? Were you able to use it?

@guiaramos Are you still using this implementation? Or are there any "lessons learned"?

@guiaramos
Copy link

@TimPietrusky yeah we still using it, nothing changed since then. Recently we had many users that linked their acc and everything worked fine with this implementation

@LeunensMichiel
Copy link

@LeunensMichiel how did this turn out for you? Were you able to use it?

@guiaramos Are you still using this implementation? Or are there any "lessons learned"?

Same for me!

@blazephoenix
Copy link

Hey @guiaramos, is ENDPOINTS.AUTH here all the endpoints for nextAuth or something else?

How would this implementation work for API routes without a custom express server?

@guiaramos
Copy link

guiaramos commented Jun 28, 2021

@balazsorban44 hey, the ENDPOINTS.AUTH is just the url for my callback on nodejs server http://localhost:4000/api/auth...

I think you can implement the server part by creating an index.ts file on pages/api/... and then you can have the following:

import type { NextApiRequest, NextApiResponse } from 'next'

export default (req: NextApiRequest, res: NextApiResponse) => {
 // implement the callback server in here...
  res.status(200).json({ name: 'John Doe' })
}

ref: https://nextjs.org/docs/basic-features/typescript#api-routes

and then you change ENDPOINTS.AUTH to /api/...

@n1ghtmare
Copy link

n1ghtmare commented Oct 15, 2021

Is there an api to manually link accounts once the user has logged in? If not, then how would you go about doing this?

EDIT:

Never mind, I think (?) I've figured it out, for reference if a user is signed in all you have to do to link another account is to simply use signIn("your_unlinked_provider"). Now that I'm thinking about it, I'm not sure what would happen if the email of the OAuth provider doesn't match?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Ask how to do something or how something works stale Did not receive any activity for 60 days
Projects
None yet
Development

No branches or pull requests

7 participants