-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
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
Comments
Maybe related #621 |
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? |
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! |
@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. |
@glenames Hmm, depending on the flow, you could use the 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. |
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:
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. |
@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
It might be wierd but is working for me hahah |
Do you then not have to request the user log in again as the login failed? |
Exactly, its a bad UX.. |
Ok, here's what im going to try which at least provides a decent UX. Let me know if you have thoughts:
This way i create the account (If verified) before login. |
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! |
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! |
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; |
@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. |
@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"? |
@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 |
Same for me! |
Hey @guiaramos, is How would this implementation work for API routes without a custom express server? |
@balazsorban44 hey, the I think you can implement the server part by creating an 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 |
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 |
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.
The text was updated successfully, but these errors were encountered: