Open aldarund opened 6 years ago
@aldarund for now there is no way to get a user object which is in the middle state of signing in. We will mark this as feature request.
Maybe you can try a work around which is to construct the user object your self like:
const userPoolData = {
UserPoolId: userPoolId,
ClientId: userPoolWebClientId,
Storage: // by default is localStorage
};
const userPool = new CognitoUserPool(userPoolData);
const userData = {
UserName: 'username'
Pool: userPool,
Storage: the_storage_object_for_caching // by default is local storage
}
const user = new CognitoUser(userData);
Here is a way that works for me.
The mandatory argument of Auth.signIn()
is username. Let say the link is constructed as /:username/:challengeAnswer
and it's possible to extract the values as props.
Then sign-in can be done by executing the following when a component is created (or page is visited). (This example is based on Vue)
created () {
Auth.signIn(this.username)
.then(user => {
Auth.sendCustomChallengeAnswer(user, this.challengeAnswer)
.then(user => {
this.$router.push('/')
})
.catch(err => {
this.$router.push('/unauthorized')
})
})
.catch(err => {
this.$router.push('/error')
})
}
Hope this helps.
@jaehyeon-kim hm, isnt it will create a new session so a challengeAnswer will be different too?
@aldarund
You mentioned you've already created all lambda triggers + code, which is a challenge answer. I consider only username is missing in this case.
See the relevant section of the Amplify document - https://aws-amplify.github.io/amplify-js/media/authentication_guide#defining-a-custom-challenge (actually it's in Verify Challenge Response section just below it, which cannot be tagged.)
@jaehyeon-kim not what i mean. I mean challenge answer created during login and by logic should be tied to session. When u do Auth.signIn(this.username) it will create new challenge code with new session, so i dont see how your solution could work
@aldarund
I see. Yes, a different session will be created. However it's up to you which to return as a challenge answer or whether to return at all. For example, as far as I've checked, it's possible to fake the create trigger because eventually what you need is whether event.response.answerCorrect
is true or false in the verify trigger.
For more example, the following will always result in successful authentication. It's up to you how to verify.
export const handler = async (event, context) => {
if (event.request.privateChallengeParameters.answer === event.request.challengeAnswer) {
event.response.answerCorrect = true;
} else {
event.response.answerCorrect = false;
}
return event;
};
// change into
export const handler = async (event, context) => {
event.response.answerCorrect = true;
return event;
};
@jaehyeon-kim yes, but that still will send user new email on second login call, so it endup with two email for login flow which is bad.
@aldarund
I'm building a portal that supports single sign on. In the portal, a user signs in and then a list of apps that the user has access are shown. If the user clicks one of them, it opens the app while passing the username (email) and a challenge answer that can be independently verified (guess which may be a good challenge answer in this case?) - here I faked the create trigger.
Possibly the key difference is whether a user is already signed in or not. In my app, it's serving signed-in users while yours doesn't seem to be. Event the latter case, however, I don't think it's a big deal as long as it's possible to verify the challenge answer reliably.
As far as I understand, it endup with two email for login flow
seems to be due to misunderstanding.
@jaehyeon-kim i just need sign in via mail. E.g. user enter his email on site -> cognito send mail to him -> user click email and get logged in. So with your solution one email would be send when he tried to login, and second email will be send when he click link from first mail to actually login.
@aldarund
user enter his email on site -> cognito send mail to him
- Why Cognito? Isn't it possible to send a username and a verifiable challenge answer without relying on any of the auth challenge triggers? As far as I've checked, the create trigger can be faked and verification can be done independently but reliably.
yes, it can be, just tried to implement it as a cognito since it does support all this triggers :)
When the user enters their email address invoke a Lambda via the API GW to send the sign-in link. After the user clicks the link and reloads the app then use Auth.signIn()
with a custom auth flow to complete the process. You might need to include the email address in the sign-in link.
Has there been any improvement to the process mentioned by @powerful23 to access a CognitoUser
object? I have a similar use case in my React Native app for a passwordless flow:
await Auth.signUp({username, tmpPassword})
const cognitoUser = await Auth.signIn(username);
The user then receives an email linking them back to the app, but at that point I no longer have access to cognitoUser
and must re-create it. I've tried using the Amplify Cache
to store it, but the functions on the object are not persisted (namely sendCustomChallengeAnswer
). For now I'm doing this but looking for a better way.
const CognitoUserPool = require('amazon-cognito-identity-js').CognitoUserPool;
getCognitoUser = function(email) {
const poolData = {
UserPoolId : COGNITO_USER_POOL_ID,
ClientId : COGNITO_CLIENT_ID
};
const userPool = new CognitoUserPool(poolData);
const userData = {
Username : email,
Pool : userPool
};
return new CognitoUser(userData);
};
edit: Also trying this
import { Auth } from "aws-amplify";
const cognitoUser = Auth.createCognitoUser(username);
await Auth.sendCustomChallengeAnswer(cognitoUser, authChallengeAnswer);
but cognitoUser.Session
is null
causing an error:
{"code":"InvalidParameterException","name":"InvalidParameterException","message":"Missing required parameter Session"}
Hi,
The login flow is a bit different but I thought it might be useful to link this AWS blog article here (as I found it after this issue): https://aws.amazon.com/blogs/mobile/implementing-passwordless-email-authentication-with-amazon-cognito/
The context is not lost because the user enters the code he has received on the same page that did the request. I think it's even better in a browser as it avoids the user ending with an extra open tab (I agree that clicking a link is better for native apps).
@apuyou That is a good example of how to create a passwordless authentication by sending user a code via email.
Is there any better solution/examples for sending user a magic link via email that would open the app and sign them in?
I am also very interested in this. I've gotten 95% of the way there to a "magic link" login, but now I'm stuck at sendCustomChallengeAnswer... going to try and recreate the CognitoUser somehow which might work.
The custom challenge flow was a dead end for my purposes. However, I just posted a workaround that might help: https://github.com/aws-amplify/amplify-js/issues/3500
It took me a while but I figured out how to do it.
import { Auth } from 'aws-amplify'
import awsConfig from '@configs/aws-config'
import * as AmazonCognitoIdentity from 'amazon-cognito-identity-js'
async function signIn(emailAddress: string) {
const user = await Auth.signIn(emailAddress)
// the main issue is that the user session needs to be stored and hydrated later.
localStorage.setItem(
'cognitoUser',
JSON.stringify({
username: emailAddress,
session: user.Session,
})
)
},
async function answerCustomChallenge(answer: string) {
const user = JSON.parse(localStorage.getItem('cognitoUser'))
// rehydrate the CognitoUser
const authenticationData = {
Username: user.username,
}
const authenticationDetails = new AmazonCognitoIdentity.AuthenticationDetails(
authenticationData
)
const poolData = {
UserPoolId: awsConfig.Auth.userPoolId as string,
ClientId: awsConfig.Auth.userPoolWebClientId as string,
}
const userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData)
const userData = {
Username: user.username,
Pool: userPool,
}
const cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData)
// After this set the session to the previously stored user session
cognitoUser.Session = user.session
// rehydrating the user and sending the auth challenge answer directly will not
// trigger a new email
cognitoUser.setAuthenticationFlowType('CUSTOM_AUTH')
cognitoUser.sendCustomChallengeAnswer(answer, {
async onSuccess(success: any) {
// If we get here, the answer was sent successfully,
// but it might have been wrong (1st or 2nd time)
// So we should test if the user is authenticated now
try {
// This will throw an error if the user is not yet authenticated:
const user = await Auth.currentSession()
} catch {
console.error('Apparently the user did not enter the right code')
}
},
onFailure(failure: any) {
console.error(failure)
},
})
},
This solution works with https://aws.amazon.com/blogs/mobile/implementing-passwordless-email-authentication-with-amazon-cognito/
Thanks @waydelyle for your proposal. It would actually make sense for me to store/retrieve the session information using the local storage.
When I run your code, then the call of sendCustomChallengeAnswer() with the correct code (and session) does not trigger onSuccess or onFailure, but the customChallenge callback. Therefore it seems, it does not work for me. When I try to retrieve the session anyway, I get the following response: 'Session: Error: Local storage is missing an ID Token, Please authenticate.
Any ideas?
It seems to do that if signIn
and answerCustomChallenge
are triggered within the same session. In my case, I'm sending a magic link to the user for login which triggers answerCustomChallenge
separately. If signIn is triggered within the same session you can just reference user.sendCustomChallengeAnswer
instead.
We are actually using also magic link mechanism and having a new browser window (i.e. new session), but I messed up with providing the code. Now the solution works fine for me too. Thanks @waydelyle
No problem!
The only problem I see with this approach over my suggestion with #3500 is that access to localStorage is required, as are Lambdas, which could become a maintenance headache.
I was able to implement this following @waydelyle approach but rather than use local storage I'm using DynamoDB, this way it works for every browser, no matter where you ask the code. It looks like:
export function sendAuthCode(email) {
const loginDetails = new AuthenticationDetails({ Username: email });
const preAuthCognitoUser = createCognitoUser(email);
preAuthCognitoUser.setAuthenticationFlowType("CUSTOM_AUTH");
return new Promise((resolve, reject) =>
preAuthCognitoUser.initiateAuth(loginDetails, {
onSuccess: resolve,
onFailure: reject,
customChallenge: async function() {
await savePreAuthSession({
email: preAuthCognitoUser.username,
session: preAuthCognitoUser.Session
}).catch(e =>
console.warn(
"Could not store session remotely. Access from an other browser tab won't be allowed",
e
)
);
resolve();
}
})
);
}
export async function checkAuthCode(code, email) {
const sessionData = await getPreAuthSession(email);
const preAuthCognitoUser = createCognitoUser(email, sessionData.session);
return new Promise((resolve, reject) =>
preAuthCognitoUser.sendCustomChallengeAnswer(code, {
onSuccess: resolve,
onFailure: reject,
customChallenge: async () => {
await savePreAuthSession({
email: preAuthCognitoUser.username,
session: preAuthCognitoUser.session
});
const error = new Error("Code not valid, try again");
error.code = "CodeValidationFail";
reject(error);
}
})
);
}
function createCognitoUser(email, session) {
const cognitoUser = new CognitoUser({ Username: email, Pool: userPool });
if (session) {
cognitoUser.Session = session;
}
return cognitoUser;
}
Notice that savePreAuthSession
and getPreAuthSession
are just rest call to save and fetch the session.
@aldarund for now there is no way to get a user object which is in the middle state of signing in. We will mark this as feature request.
Maybe you can try a work around which is to construct the user object your self like:
const userPoolData = { UserPoolId: userPoolId, ClientId: userPoolWebClientId, Storage: // by default is localStorage }; const userPool = new CognitoUserPool(userPoolData); const userData = { UserName: 'username' Pool: userPool, Storage: the_storage_object_for_caching // by default is local storage } const user = new CognitoUser(userData);
@powerful23 , Can you please tell me what you mean by "Storage: // by default is localStorage" in both userPoolData and userData? Is it the object which Auth.signIn returns? I'm getting error : " AuthClass - Failed to get the signed in user No current user "
Another approach is to persist the challenge code as a custom property on the user object in Cognito. This means you can separate the link creation from the sing in session. For detailed implementation check this post. The only point I would add to that post is that you properly should hash the challenge code so that you don't have them available in plain text to people with access to the user pool.
Hey @robertomoreno I know this is quite a while ago now but can you share more of your implementation above? I am hitting cognitoUser Session issues with it being null for some reason.
@kazinayem2011 i am getting the same AuthClass - Failed to get the signed in user No current user
error when sending the corect challenge anwser.
anyone has overcome this issue?
have same issue, I will be using suggested solutions here but also feel like this should have been addressed a while ago
Building on the solution from @waydelyle I found this worked well. The main difference is UniversalStorage
is used to support withSSRContext
in your app, meaning you can get the authed user on your server.
Use the signIn
method to capture the users email and initiate a login.
Use the answerCustomChallenge
method to take the value from your magic link and send back to Cognito for authentication.
import { UniversalStorage } from '@aws-amplify/core';
import { Auth } from '@aws-amplify/auth';
import { CognitoUser, CognitoUserPool } from 'amazon-cognito-identity-js';
async function signIn(emailAddress: string) {
const user = await Auth.signIn(emailAddress)
// the main issue is that the user session needs to be stored and hydrated later.
sessionStorage.setItem(
'cognitoUser',
JSON.stringify({
username: emailAddress,
session: user.Session,
})
)
}
// Do all this when your magic link mounts in the client
async function answerCustomChallenge(answer: string) {
const cognitoUserFromSession = JSON.parse(sessionStorage.getItem('cognitoUser'));
if (!cognitoUserFromSession) {
// Handle error behaviour in your app
} else {
const cognitoUser = new CognitoUser({
Username: cognitoUserFromSession.username,
Pool: new CognitoUserPool({
UserPoolId: // <Your user pool id>,
ClientId: // <Your client id>,
}),
Storage: new UniversalStorage(), // This is important for SSR contexts when signed in
});
// TypeScript will probably error here. Ignore it.
cognitoUser.Session = cognitoUserFromSession.session;
cognitoUser.setAuthenticationFlowType('CUSTOM_AUTH');
try {
await Auth.sendCustomChallengeAnswer(cognitoUser, answer);
// If we get here, the answer was sent successfully,
// but we should test if the user is authenticated now.
// This will throw an error if the user is not yet authenticated.
const authedUser = await Auth.currentAuthenticatedUser();
// Handle success behaviour here
} catch {
// Handle error behaviour in your app
}
}
}
I'm trying to implement auth via link without password. E.g. user enter his email -> cognito send an email with link that user can click and login. It
It is supported by cognito via custom challenge. E.g. https://aws-amplify.github.io/amplify-js/media/authentication_guide#using-a-custom-challenge
I have created DefineAuthChallenge, CreateAuthChallenge, VerifyAuthChallenge lambda function that associated with my cognito pool. CreateAuthChallenge generate code that is send to user email with link to site.
Next i was planning to grab that code from url on site and login user via Auth.sendCustomChallengeAnswer
But here is the problem. Auth.sendCustomChallengeAnswer require user object that is passed from Auth.signIn. But if user just click link from email there wont be any user object at all. And amplify lib dont save that middle session user object from Auth.Signin, so on page reload its lost. It is saving only if auth completed to its storage https://github.com/aws-amplify/amplify-js/blob/master/packages/amazon-cognito-identity-js/src/CognitoUser.js#L175
So the question is how can i save & reconstruct user object from Auth.signIn function on page reload to be able to call sendCustomChallengeAnswer. Or if there a better approach to sign in via link without password?
Which Category is your question related to? Authentification
What AWS Services are you utilizing? Cognito
Provide additional details e.g. code snippets