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

Automatic Social Account Merging #4427

Open
techdragon opened this issue Jun 1, 2020 · 36 comments
Open

Automatic Social Account Merging #4427

techdragon opened this issue Jun 1, 2020 · 36 comments
Assignees
Labels
auth Issues tied to the auth category of the CLI feature-request Request a new feature p4

Comments

@techdragon
Copy link

techdragon commented Jun 1, 2020

Is your feature request related to a problem? Please describe.
Automatic Social Account Merging does not occur by default, #4208 is the original report, but the author closed it after finding their own solution.

Describe the solution you'd like
Automatically merge social accounts based on one of two criteria:

Describe alternatives you've considered
I could build this myself with a bunch of lambdas and other stuff.

Additional context
This sort of functionality is the default in the majority of authentication services I have used. It was genuinely shocking to discover this was not the case while I was searching through the repo's GithHub issues for a different problem and came across #4208. While I'm sure this is primarily a case of terrible default behaviour in the underlying Cognito service, there is a workaround, and from what I can tell, Amplify is trying to be a "we do the boilerplate stuff for you" type of tool, so handling this kind of workaround/boilerplate feels like something Amplify should be doing.

@nikhname nikhname added enhancement auth Issues tied to the auth category of the CLI labels Jun 2, 2020
@nikhname
Copy link
Contributor

nikhname commented Jun 2, 2020

Thanks for the feature request @techdragon we will investigate merging social accounts by default

@rahulje9
Copy link

rahulje9 commented Jul 5, 2020

Any update on this, as we are have deployed our app and currently, this is an issue for us and now we are stuck at login with apple issue. we do have a website so this feature is mandatory because the user logged in with apple cannot use that in web or android, so the user should be able to login using the given email without any issue.

@ianmartorell
Copy link

ianmartorell commented Jul 24, 2020

This seems like such a basic problem that I was also kind of shocked to discover it's not supported by default. There are a bunch of other issues and comments on blog posts asking about this as well.

Apparently the workaround is to use AdminLinkProviderForUser in a pre signup trigger to link to an existing user, but it definitely feels like something Amplify should be handling. As it stands, the same person can sign up with email or an identity provider, forget how they signed up, then sign in with a different identity provider and a new account would be created for them, which is definitely not ideal.

@Mersmith
Copy link

How to verify an account with many social networks?

Then start a session with the different social networks?

Here is an example with the Badoo Social Network.

ex

@xitanggg
Copy link

Any update on this issue?

@ianmartorell
Copy link

The way I'm currently handling this is:

In the PreSignup hook, if event.triggerSource is PreSignUp_ExternalProvider, I look for an existing user with email event.request.userAttributes.email using listUsers. Then:

  • If there's an existing user, call adminLinkProviderForUser and throw an error LINKED_EXTERNAL_USER: ${providerName}. In the frontend I catch this error and reinitiate the OAuth flow, which will log the user in. Normally the end user won't even realize, unless they're logging in with Google and they're signed in to multiple Google accounts on that browser. In that case they'll see the account selector twice, but this will only happen the first time they try to sign in with Google. I haven't been able to work around this.
  • If there's no existing user, I create a native user first and then link it with adminLinkProviderForUser. I do this because otherwise there's no way to link an existing user created with an external provider to a new native (email&password) user. So if the user decided to set a password for their account in the future they wouldn't be able to do that. I do this using adminCreateUser with a random temporary password and MessageAction: 'SUPRESS'. After the user is created you need to call adminSetUserPassword with Permanent: true to change the user state from FORCE_CHANGE_PASSWORD to CONFIRMED. Otherwise they won't be able to reset their password with the normal reset password flow (pretty stupid, I know).

@xitanggg
Copy link

xitanggg commented Nov 29, 2020

@ianmartorell Thank you so much for sharing your implementation. It is extremely helpful. I test it and work well. For others who are interested, below is a sample code of mine used for pre sign up lambda based on @ianmartorell's implementation

const aws = require('aws-sdk');
const { v4: uuidv4 } = require('uuid');

exports.handler = async (event, context, callback) => {
        //  ... skip other codes

	// If trigger source is external provider
	if (event.triggerSource === 'PreSignUp_ExternalProvider') {
		const cognitoProvider = new aws.CognitoIdentityServiceProvider({
			apiVersion: '2016-04-18',
		});

		try {
			// Get user based on email
			const listUserParams = {
				UserPoolId: event.userPoolId,
				AttributesToGet: null, //null returns all attributes
				Filter: `email = \"${event.request.userAttributes.email}\"`,
				Limit: 1,
			};
			const listUsersRes = await cognitoProvider
				.listUsers(listUserParams)
				.promise();

			let destinationAttributeValue;
			// If user not found, create user
			if (listUsersRes.Users.length === 0) {
				console.log('User not found');
				const {
					email = '',
					given_name = '',
					family_name = '',
					phone_number = '',
				} = event.request.userAttributes;
				const newPassword = uuidv4(); // or use your own implementation
				const newUserParams = {
					UserPoolId: event.userPoolId,
					Username: email || phone_number,
					MessageAction: 'SUPPRESS',
					TemporaryPassword: newPassword,
					UserAttributes: [
						{
							Name: 'email',
							Value: email,
						},
						{
							Name: 'email_verified',
							Value: String(!!email), //auto verify email if provided
						},
						{
							Name: 'given_name',
							Value: given_name,
						},
						{
							Name: 'family_name',
							Value: family_name,
						},
						{
							Name: 'phone_number',
							Value: phone_number,
						},
						{
							Name: 'phone_number_verified',
							Value: String(!!phone_number),
						},
					],
				};

				const newUser = await cognitoProvider
					.adminCreateUser(newUserParams)
					.promise();

				// Confirm new user
				const setPasswordParams = {
					Password: newPassword,
					UserPoolId: event.userPoolId,
					Username: newUser.User.Username,
					Permanent: true,
				};
				await cognitoProvider.adminSetUserPassword(setPasswordParams).promise();

				destinationAttributeValue = newUser.User.Username;
			}
			// If user found, simply set username
			else {
				console.log('User found');
				destinationAttributeValue = listUsersRes.Users[0].Username;
			}

			// Link User
			console.log('Link user');
			let [sourceProviderName, sourceAttributeValue] = event.userName.split(
				'_'
			);
			sourceProviderName =
				sourceProviderName[0].toUpperCase() + sourceProviderName.slice(1);
			const adminLinkParams = {
				DestinationUser: {
					ProviderAttributeValue: destinationAttributeValue,
					ProviderName: 'Cognito',
				},
				SourceUser: {
					ProviderAttributeName: 'Cognito_Subject',
					ProviderAttributeValue: sourceAttributeValue,
					ProviderName: sourceProviderName,
				},
				UserPoolId: event.userPoolId,
			};
			await cognitoProvider.adminLinkProviderForUser(adminLinkParams).promise();

			// Finish linking, throw error to frontent
			callback(new Error(`LINKED_EXTERNAL_USER_${sourceProviderName}`), event);
		} catch (error) {
			callback(error, event);
		}
	}

	callback(null, event);
};

Also make sure to add permission to your pre sign up lambda for ListUsers and other Admins call

    "lambdaexecutionpolicy": {
      ...
      "Properties": {
        ...
        "PolicyDocument": {
          "Version": "2012-10-17",
          "Statement": [
            {
              ...
            },
            {
              "Effect": "Allow",
              "Action": [
                "cognito-idp:ListUsers",
                "cognito-idp:AdminLinkProviderForUser",
                "cognito-idp:AdminCreateUser",
                "cognito-idp:AdminSetUserPassword"
              ],
              "Resource": {
                "Fn::Sub": [
                  "arn:aws:cognito-idp:${region}:${account}:*",
                  {
                    "region": {
                      "Ref": "AWS::Region"
                    },
                    "account": {
                      "Ref": "AWS::AccountId"
                    }
                  }
                ]
              }
            }

@xitanggg
Copy link

@ianmartorell One of the problem I run into is that calling Auth.federatedSignIn overwrites Cognito native user attributes. I wonder if you encounter similar issue. Basically, when I created the native user for a user logged in with Facebook, I set email_verified to true, but when Auth.federatedSignIn is called again, email_verified is set to false because it reloads the attribues from Facebook to Cognito and Facebook doesn't have the email_verified attribute apparently. I just created an issue at Amplify.js: aws-amplify/amplify-js#7300

@ianmartorell
Copy link

ianmartorell commented Nov 29, 2020

@xitanggg Glad to hear it works for you too! Oh yes, I have that issue with email_verified as well, I forgot to mention it in my comment. What I do is I automatically confirm and set email_verified to true for the user in the PostAuthentication trigger, if they signed in using an external provider. I can check the code I used if that would be helpful.

@xitanggg
Copy link

xitanggg commented Nov 29, 2020

@ianmartorell Thanks for your prompt response. That is actually what I am thinking as well using AdminUpdateUserAttributes. I have tried it and it works but I am hesitated to include it in the code because it is so stupid and inefficient since it gets called for every sign in event.

@ianmartorell
Copy link

Yeah it's pretty stupid... Unfortunately I don't think there's any other way. As email_verified comes as false from external providers for some reason, it gets overwritten on every login. It's quite bothersome. Honestly we have to go through so many hoops and hacks to make social login work with Amplify, it's crazy.

@xitanggg
Copy link

xitanggg commented Dec 1, 2020

I was able to map google's email_verified to cognito and it comes with true. So I only need to do it for facebook. The problem in this case is more in Cognito than Amplify. I am not sure why Cognito is set up to overwrite the attributes https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-specifying-attribute-mapping.html. Sad, but I will just move on

Amazon Cognito must be able to update your mapped user pool attributes when users sign in to your application. When a user signs in through an identity provider, Amazon Cognito updates the mapped attributes with the latest information from the identity provider. Amazon Cognito updates each mapped attribute, even if its current value already matches the latest information. If Amazon Cognito can't update the attribute, it throws an error. To ensure that Amazon Cognito can update the attributes, check the following requirements:

@oahmaro
Copy link

oahmaro commented Feb 2, 2021

When the Pre sign up hook run it will just create the link but then users will have to sign in again in order to complete the process and this run in two steps, how can i sign in user with external provider right after the link is complete?

@ianmartorell
Copy link

When the Pre sign up hook run it will just create the link but then users will have to sign in again in order to complete the process and this run in two steps, how can i sign in user with external provider right after the link is complete?

I ran into the same issue and explained my workaround in the first bullet point here: #4427 (comment).

@oahmaro
Copy link

oahmaro commented Feb 2, 2021

Can you please be more specific on how you are catching this error on the Frontend? since using the Amplify api will force a redirect and the error wont be presented on the function that triggered the sign in.

@xitanggg
Copy link

xitanggg commented Feb 2, 2021

The exact implementation varies based on the front end framework you use and the error message you throw. The high level idea is that

  1. Once the native user is created and linked with the external provider, it throws an error to the front end. The error appears as a query parameter in the url, like this ?error_description=LINKED_EXTERNAL_USER_{providerName}
  2. You can create something that monitor the url, extract the providerName, and reinitiate the auto sign in for that provider.

In React, I simply create a useEffect hook to do so and use the hook at my redirect page.

import { useRef, useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import queryString from 'query-string';
import { Auth } from 'aws-amplify';

const autoExternalSignIn = async (provider) => {
	try {
		await Auth.federatedSignIn({ provider: provider });
	} catch (error) {
		console.log('Error auto sign in: ', error);
	}
};

/**
 * useAutoExternalSignIn hook attempts to auto sign in user after users register
 * an account with Google / Facebook external provider.
 *
 * When user first registers an account using Google / Facebook, a native user is created
 * in AWS Cognito and an error is throwed to the front end when the process is succeeded.
 */

const useAutoExternalSignIn = () => {
	const numRetry = useRef(0);
	const history = useHistory();
	const params = queryString.parse(history.location.search);
	const errorDes = params.error_description;
	useEffect(() => {
		if (numRetry.current < 2 && errorDes) {
			if (errorDes.includes('LINKED_EXTERNAL_USER_Facebook')) {
				autoExternalSignIn('Facebook');
				numRetry.current += 1;
			}
			if (errorDes.includes('LINKED_EXTERNAL_USER_Google')) {
				autoExternalSignIn('Google');
				numRetry.current += 1;
			}
		}
	}, [errorDes]);
};

export default useAutoExternalSignIn;

@oahmaro
Copy link

oahmaro commented Feb 3, 2021

Testing @xitanggg implementation i sometimes get the following error when first signup with google "Invalid SourceUser: Cognito users with a username/password may not be passed in as a SourceUser, only as a DestinationUser"

@xitanggg
Copy link

xitanggg commented Feb 4, 2021

@oahmaro Interesting, this is my implementation and I haven't encountered this issue. I am not sure what might cause it but I would suggest console.log various variables in each stage of the code to trace down what might cause it. Would be interested to see what you find out.

@THPubs
Copy link

THPubs commented Feb 4, 2021

@oahmaro I also faced the same issue. But later found out that I already had an account in Cognito with the Google account I'm trying. I tried to signup before setting up the pre signup hook. You'll have to clear them out before trying this.

@oahmaro
Copy link

oahmaro commented Feb 4, 2021

hi @xitanggg , can you please share your implementation on post authentication trigger to fix facebook signing overriding the email_verified value?

This would help me a lot as i am not familiar with Cognito API

@oahmaro
Copy link

oahmaro commented Feb 4, 2021

@oahmaro I also faced the same issue. But later found out that I already had an account in Cognito with the Google account I'm trying. I tried to signup before setting up the pre signup hook. You'll have to clear them out before trying this.

Well not really, this issue happens to me even if i don't have any account, i am still investigating the cause

@ianmartorell
Copy link

ianmartorell commented Feb 4, 2021

Testing @xitanggg implementation i sometimes get the following error when first signup with google "Invalid SourceUser: Cognito users with a username/password may not be passed in as a SourceUser, only as a DestinationUser"

I was getting this too! It drove me insane for days, I checked the API calls over and over and I was definitely passing the Cognito user as a DestinationUser. In the end it stopped happening, and as weird as it sounds I think it was because I increased the CPU and memory used by the lambda functions. I did it through editing the CloudFormation templates, I can check what I changed exactly in a few hours.

@THPubs
Copy link

THPubs commented Feb 4, 2021

@oahmaro You can verify the email in the pre-signup hook right after linking the accounts like this:

await cognitoProvider
  .adminUpdateUserAttributes({
    UserAttributes: [
      {
        Name: 'email_verified',
        Value: 'true',
      },
    ],
    Username: destinationAttributeValue,
    UserPoolId: event.userPoolId,
  })
  .promise();

To confirm and enable the user account, you can do the following:

await cognitoProvider
  .adminConfirmSignUp({
    Username: destinationAttributeValue,
    UserPoolId: event.userPoolId,
  })
  .promise();

Make sure you set the proper permissions in the cloudformation-template.json:

"Action": [
  "cognito-idp:ListUsers",
  "cognito-idp:AdminLinkProviderForUser",
  "cognito-idp:AdminConfirmSignUp",
  "cognito-idp:AdminUpdateUserAttributes"
],

@oahmaro
Copy link

oahmaro commented Feb 4, 2021

Testing @xitanggg implementation i sometimes get the following error when first signup with google "Invalid SourceUser: Cognito users with a username/password may not be passed in as a SourceUser, only as a DestinationUser"

I was getting this too! It drove me insane for days, I checked the API calls over and over and I was definitely passing the Cognito user as a DestinationUser. In the end it stopped happening, and as weird as it sounds I think it was because I increased the CPU and memory used by the lambda functions. I did it through editing the CloudFormation templates, I can check what I changed exactly in a few hours.

Hi @ianmartorell this would help a lot. Thanks

@ianmartorell
Copy link

Hi @ianmartorell this would help a lot. Thanks

So basically in my PreSignup template I have this:

{
	"Resources": {
		"LambdaFunction": {
			"Properties": {
				"Handler": "index.handler",
				"MemorySize": 2048,   <-- Add this line
			}
		},
	},
}

Note that although this increases memory usage and it sounds like your functions will be more expensive, they actually run a lot faster because CPU speeds are tied to memory size in lambda functions, and the faster they run the cheaper they are. I haven't run actual benchmarks, but the speed increase was reeeally noticeable in my PostAuthentication hook. I used to have to wait a good couple of seconds after logging in, and now it feels instant.

@oahmaro
Copy link

oahmaro commented Feb 4, 2021

@ianmartorell this seems to fix the issue, speaking of speed this whole signup trigger takes 15 seconds to sign in user in the first time, and 8 seconds after account is linked, which seems to be long. anyone tested the time it takes?

@oahmaro
Copy link

oahmaro commented Feb 4, 2021

@ianmartorell this seems to fix the issue, speaking of speed this whole signup trigger takes 15 seconds to sign in user in the first time, and 8 seconds after account is linked, which seems to be long. anyone tested the time it takes?

Actually nevermind my metrics as they are inaccurate, after building my app it actually was much faster.

@xitanggg
Copy link

xitanggg commented Feb 4, 2021

Haha, good catch @ianmartorell! I actually suspect memory might be the cause, but wasn't 100% sure. I always have my lambda memory size up, in this case to 512MB "MemorySize": "512", so I never actually encounter what you two encounter, got lucky.

The default lambda memory size is 128MB, which is stupid, because the aws-sdk is 57.9 MB big. I suspect it might run out of memory using it therefore causing the error. Amplify team should consider changing the default memory for lambda function whenever it uses the aws-sdk.

@oahmaro my users are coming from google provider and they got mapped correctly, so I haven't written the code to overwrite facebook provider yet (at least not now, may be I will in the next month or so). The Cognito API isn't too difficult to understand. You just need to spend some time reading the docs. As mentioned, AdminUpdateUserAttributes would do the trick, make sure to set proper permission as well.

@siegerts siegerts added feature-request Request a new feature and removed enhancement labels Sep 3, 2021
@undefobj undefobj added the p4 label Feb 28, 2022
@undefobj
Copy link
Contributor

@palpatim @elorzafe FYI for Auth tracking

@AsitDixit
Copy link

@xitanggg I'm using forceAliasAuth=true so that I can sign-in user through username(preferred username) or verified email. My only required attribute is email for sign-up a user. Your code is working for 1st sign-up with email then use same email as signin with Google(but I'm not getting error that ) but when I'm trying fresh new sign in with google it is not working(it is not creating an user entry). my code: and one thing username can't be email as forceAliasAuth=true.
`const aws = require('aws-sdk');
const { v4: uuidv4 } = require('uuid');

exports.handler = async (event, context, callback) => {
// ... skip other codes

// If trigger source is external provider
if (event.triggerSource === 'PreSignUp_ExternalProvider') {
	const cognitoProvider = new aws.CognitoIdentityServiceProvider({
		apiVersion: '2016-04-18',
	});

	try {
		// Get user based on email
		const listUserParams = {
			UserPoolId: event.userPoolId,
			AttributesToGet: null, //null returns all attributes
			Filter: `email = \"${event.request.userAttributes.email}\"`,
			Limit: 1,
		};
		const listUsersRes = await cognitoProvider
			.listUsers(listUserParams)
			.promise();

		let destinationAttributeValue;
		// If user not found, create user
		if (listUsersRes.Users.length === 0) {
			console.log('User not found');
			const {
				email = '',
			
			} = event.request.userAttributes;
			const newPassword = uuidv4(); // or use your own implementation
			const newUserParams = {
				UserPoolId: event.userPoolId,
				Username: event.userName,
				MessageAction: 'SUPPRESS',
				TemporaryPassword: newPassword,
				UserAttributes: [
					{
						Name: 'email',
						Value: email,
					},
					{
						Name: 'email_verified',
						Value: String(!!email), //auto verify email if provided
					},
		
				],
			};

			const newUser = await cognitoProvider
				.adminCreateUser(newUserParams)
				.promise();

			// Confirm new user
			const setPasswordParams = {
				Password: newPassword,
				UserPoolId: event.userPoolId,
				Username: newUser.User.Username,
				Permanent: true,
			};
			await cognitoProvider.adminSetUserPassword(setPasswordParams).promise();

			destinationAttributeValue = newUser.User.Username;
		}
		// If user found, simply set username
		else {
			console.log('User found');
			destinationAttributeValue = listUsersRes.Users[0].Username;
		}

		// Link User
		console.log('Link user');
		let [sourceProviderName, sourceAttributeValue] = event.userName.split(
			'_'
		);
		sourceProviderName =
			sourceProviderName[0].toUpperCase() + sourceProviderName.slice(1);
		const adminLinkParams = {
			DestinationUser: {
				ProviderAttributeValue: destinationAttributeValue,
				ProviderName: 'Cognito',
			},
			SourceUser: {
				ProviderAttributeName: 'Cognito_Subject',
				ProviderAttributeValue: sourceAttributeValue,
				ProviderName: sourceProviderName,
			},
			UserPoolId: event.userPoolId,
		};
		await cognitoProvider.adminLinkProviderForUser(adminLinkParams).promise();

		// Finish linking, throw error to frontent
		callback(new Error(`LINKED_EXTERNAL_USER_${sourceProviderName}`), event);
	} catch (error) {
		callback(error, event);
	}
}

callback(null, event);

};my permission:{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents",
"cognito-idp:AdminInitiateAuth",
"cognito-idp:ListUsers",
"cognito-idp:AdminUpdateUserAttributes",
"cognito-idp:AdminLinkProviderForUser",
"cognito-idp:AdminCreateUser",
"cognito-idp:AdminSetUserPassword"
],
"Resource": [
"arn:aws:logs:us-east-1:xxxxxxxxxxxxxx:log-group:/aws/lambda/Auto_Merge_Account:*",
"arn:aws:cognito-idp:us-east-1:xxxxxxx:userpool/us-east-1xxxxxxxxxxxx"
]
}
]
}`

@AsitDixit
Copy link

@ianmartorell @xitanggg this is not working for me when fresh new sign up with google & im using force alias auth and only requried attirbute is email and user can sign up with email & preferred username and second thing I have mapped username in post conformation function when there is fresh signup with with Google It will not trigger post confirmation as we are throwing error.
`if (listUsersRes.Users.length === 0) {
console.log('User not found');
const {
email = '',
} = event.request.userAttributes;
const newPassword = uuidv4(); // or use your own implementation
const newUserParams = {
UserPoolId: event.userPoolId,
Username: email,
MessageAction: 'SUPPRESS',
TemporaryPassword: newPassword,
UserAttributes: [
{
Name: 'email',
Value: email,
},
{
Name: 'email_verified',
Value: String(!!email), //auto verify email if provided
},

				],
			};

			const newUser = await cognitoProvider.adminCreateUser(newUserParams).promise();

			// Confirm new user
			const setPasswordParams = {
				Password: newPassword,
				UserPoolId: event.userPoolId,
				Username: newUser.User.Username,
				Permanent: true,
			};
			await cognitoProvider.adminSetUserPassword(setPasswordParams).promise();

			destinationAttributeValue = newUser.User.Username;
		}`

@AsitDixit
Copy link

@xitanggg @ianmartorell @THPubs specially adminCreateUser showing error: PreSignUp invocation failed due to error TooManyRequestsException. Why I don't know please tell me solution. whether you are facing the same issue or not.

@hanna-becker
Copy link

Hi @ianmartorell this would help a lot. Thanks

So basically in my PreSignup template I have this:

{
	"Resources": {
		"LambdaFunction": {
			"Properties": {
				"Handler": "index.handler",
				"MemorySize": 2048,   <-- Add this line
			}
		},
	},
}

Note that although this increases memory usage and it sounds like your functions will be more expensive, they actually run a lot faster because CPU speeds are tied to memory size in lambda functions, and the faster they run the cheaper they are. I haven't run actual benchmarks, but the speed increase was reeeally noticeable in my PostAuthentication hook. I used to have to wait a good couple of seconds after logging in, and now it feels instant.

This worked initially for mitigating the confusing error. Then I added a function call to set the user's email_verified to true AFTER linking the accounts, and no matter how much I increase memory, we're back at
"Invalid SourceUser: Cognito users with a username/password may not be passed in as a SourceUser, only as a DestinationUser"
for linking the accounts.

@lucasoares
Copy link

Three and a half years later, Cognito doesn't yet support this natively... It's unbelievable

@zackseyun
Copy link

would love to thumbs up this issue to vote support for this feature request

@HaruMiyaGi
Copy link

Amplify, please add multi social account linking. This would take you few weeks, why has it been 4 years?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
auth Issues tied to the auth category of the CLI feature-request Request a new feature p4
Projects
None yet
Development

No branches or pull requests