How to persist CognitoUser during signIn with CUSTOM_AUTH authentication flow - react-native

My React Native app uses Amplify for a CUSTOM_AUTH authentication flow. The user receives a link via email to satisfy a challengeAnswer request. The process is like this:
User initiatiates sign in:
const cognitoUser = await Auth.signIn(username);
Email is sent to user via lambda.
User leaves app to retrieve email.
User clicks a link in the email which routes user back to the app via the RN Linking api.
The code from the link is processed with:
await Auth.sendCustomChallengeAnswer(
cognitoUser,
authChallengeAnswer
);
Usually this works well, but there is no guarantee that the cognitoUser object will exist after the app has been backgrounded while the user retrieves the email. There is a non-zero chance that iOS could dump the app during this time, and the cognitoUser var would be gone forcing the user to restart the sign in process. I'm looking for a way to persist the cognitoUser object somehow so if iOS decides the app needs to die this var can be retrieved from cache.
I'm able to cache the object into the Amplify cache (AsyncStorage) with
await Cache.setItem("cognitoUser", cognitoUser);
then fetch with
await Cache.getItem("cognitoUser");
which fails with
TypeError: user.sendCustomChallengeAnswer is not a function
because the process of caching it lost all its __proto__ functions. Its just retrieved as a basic object.
I suspect the cause is that I'm not using TypeScript, and the object loses some type information somehow.
Is there a better way of persisting this CognitoUser object so I can guarantee it exists after the user leaves/returns to the app as is needed in a CUSTOM_AUTH flow.

I use the following code to persist CognitoUser during sign in with CUSTOM_AUTH authentication flow:
import Auth from '#aws-amplify/auth'
import { CognitoUser } from 'amazon-cognito-identity-js'
const CUSTOM_AUTH_TTL = 5 * 60 * 1000 // Milliseconds
interface CustomAuthSession {
username: string
session: string
// Milliseconds after epoch
expiresAt: number
}
function clearCustomAuthSession() {
window.localStorage.removeItem('CustomAuthSession')
}
function loadCustomAuthSession(): CognitoUser {
const raw = window.localStorage.getItem('CustomAuthSession')
if (!raw) {
throw new Error('No custom auth session')
}
const storedSession: CustomAuthSession = window.JSON.parse(raw)
if (storedSession.expiresAt < window.Date.now()) {
clearCustomAuthSession()
throw new Error('Stored custom auth session has expired')
}
const username = storedSession.username
// Accessing private method of Auth here which is BAD, but it's still the
// safest way to restore the custom auth session from local storage, as there
// is no interface that lets us do it.
// (If we created a new user pool object here instead to pass to a
// CognitoUser constructor that would likely result in hard to catch bugs,
// as Auth can assume that all CognitoUsers passed to it come from its pool
// object.)
const user: CognitoUser = (Auth as any).createCognitoUser(username)
// Session is not exposed to TypeScript, but it's a public member in the
// JS code.
;(user as any).Session = storedSession.session
return user
}
function storeCustomAuthSession(cognitoUser: CognitoUser) {
// Session isn't exposed to TypeScript, but it's a public member in JS
const session = (cognitoUser as any).Session
const expiresAt = window.Date.now() + CUSTOM_AUTH_TTL
const otpSession: CustomAuthSession = {
session,
expiresAt,
username: cognitoUser.getUsername(),
}
const json = window.JSON.stringify(otpSession)
window.localStorage.setItem('CustomAuthSession', json)
}

You can reconstruct the CognitoUser object manually from your serialized object in localStorage or cache:
import { CognitoUser, CognitoUserPool } from 'amazon-cognito-identity-js';
const pool = new CognitoUserPool({
UserPoolId: cognitoObject.pool.userPoolId,
ClientId: cognitoObject.pool.clientId,
endpoint: cognitoObject.client.endpoint,
Storage: window.localStorage,
AdvancedSecurityDataCollectionFlag: cognitoObject.advancedSecurityDataCollectionFlag,
})
const cognitoUser = new CognitoUser({
Username: cognitoObject.username,
Pool: pool,
Storage: window.localStorage,
})
cognitoUser.Session = cognitoObject.Session
await Auth.completeNewPassword(cognitoUser, newPassword, cognitoObject.challengeParams)
Had to add the import statement myself, but got the general idea here: https://github.com/aws-amplify/amplify-js/issues/1715#issuecomment-800999983

I had this same issue and the simplest solution was to store it as a global variable within the slice.
authSlice.ts:
// we use this to temporarily store CognitoUser for MFA login.
// CognitoUser is not serializable so we cannot store it on Redux.
let cognitoUser = {};
export const doLogin = createAsyncThunk(
"auth/login",
async ({ email, password }: UserCredentials): Promise<Login | MFA> => {
const res = await Auth.signIn(email, password);
if (res.challengeName === "SOFTWARE_TOKEN_MFA") {
// we use this to temporarily store CognitoUser for MFA login.
// CognitoUser is not serializable so we cannot store it on Redux.
cognitoUser = res;
return {
status: "MFA",
user: null,
};
} else {
const user = await getUser();
return { user, status: "OK" };
}
}
);
export const confirmMFA = createAsyncThunk("auth/confirmMFA", async ({ mfa }: UserMFA) => {
if (!cognitoUser) {
throw new Error("Invalid flow?!");
}
await Auth.confirmSignIn(cognitoUser, mfa, "SOFTWARE_TOKEN_MFA");
const user = await getUser();
return { user, status: "OK" };
});
const getUser = async (): Promise<User> => {
const session = await Auth.currentSession();
// #ts-ignore https://github.com/aws-amplify/amplify-js/issues/4927
const { accessToken } = session;
if (!accessToken) {
throw new Error("Missing access token");
}
setCredentials(accessToken.jwtToken);
const user = await Auth.currentAuthenticatedUser();
return user.attributes;
};

Our requirement was also the same and we managed to get the customAuth flow working by creating cognitoUserPool and cognitoUser instance from localStorage/sessionStorage before calling sendCustomChallengeAnswer.
Example:
const userPoolData = {
Attributes: values(from localStorage);
}
const cognitoUserPool = new CognitoUserPool(userPoolData);
const userData = {
Attributes: values(from localStorage);
}
const cognitoUser = new CognitoUser(userData);
Auth.sendCustomChallengeAnswer(cognitoUser, validationCode);

Related

Google email offline access using react native expo app

I am creating one app using react native expo, which allow end user to login by their google account , and then applicaton try to save the access_token so that server based applicatin can use this to send the email on their behalf ,
But when using google sing in , i am not getting refresh token and not able to send the email ,
Here is code example which i am using
I tried below method to get the access request
const [request, response, promptAsync] = Google.useIdTokenAuthRequest({
clientId: "XXXXXXX",
androidClientId:"XXXXXXX",
iosClientId:"XXXXXXX"
});
const [initializing, setInitializing] = useState(true);
const [user, setUser] = useState();
const sendNotification=useNotification()
//console.log(sendNotification)
useEffect(() => {
if (response?.type === "success") {
const { id_token } = response.params;
const auth = getAuth();
const credential = GoogleAuthProvider.credential(id_token);
signInWithCredential(auth, credential);
let decoded = jwt_decode(id_token);
socialLogin(decoded)
}
}, [response]);
And on server using this code to sending email
const { google } = require('googleapis');
const path = require('path');
const fs = require('fs');
const credentials = require('./credentials.json');
// Replace with the code you received from Google
const code = 'XXXXXXX';
//const code="XXXXXXX"
const { client_secret, client_id, redirect_uris } = credentials.installed;
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]);
oAuth2Client.getToken(code).then(({ tokens }) => {
console.log('first')
const tokenPath = path.join(__dirname, 'token.json');
fs.writeFileSync(tokenPath, JSON.stringify(tokens));
console.log('Access token and refresh token stored to token.json');
}).catch(err=>console.log(err));
async function signInWithGoogleAsync() {
try {
const result = await Google.logInAsync({
androidClientId: YOUR_CLIENT_ID_HERE,
scopes: ["profile", "email"],
});
if (result.type === "success") {
onSignIn(result);
return result.accessToken;
} else {
return { cancelled: true };
}
} catch (e) {
return { error: true };
}
}
Well, I tried to create an application with Google login. To use the Google Sign-In method in a React Native Expo app, you will need to perform the following steps:
Set up a project in the Google Cloud Console and obtain a configuration file for your app.
Install the expo-google-sign-in package in your React Native app.
Import the GoogleSignIn object from the expo-google-sign-in package and use the initAsync method to initialize the Google Sign-In process.
Use the GoogleSignIn.askForPlayServicesAsync method to check if the device has the required Google Play Services installed.
Use the GoogleSignIn.signInAsync method to prompt the user to sign in with their Google account.
Once the user has signed in, you can use the accessToken and refreshToken properties of the returned object to make authorized requests to the Google APIs.
The code lines for the above steps are:
import { GoogleSignIn } from 'expo-google-sign-in';
// Initialize the Google Sign-In process
await GoogleSignIn.initAsync({
// Your config. values
});
// Check if the device has the required Google Play Services installed
const isPlayServicesAvailable = await GoogleSignIn.askForPlayServicesAsync();
if (!isPlayServicesAvailable) {
console.error('Google Play services are not available on this device.');
return;
}
// Prompt the user to sign in with their Google account
const { accessToken, refreshToken } = await GoogleSignIn.signInAsync();

Firebase updatePassword removes sign_in_second_factor phone property from token

I do a reautenticate with a user whom is already logged in as a Multifactor user
async reauthenticate(oldPassword: string): Promise<SignInEmailPassword> {
const user = firebase.auth().currentUser;
try {
if (user?.email) {
const cred = firebase.auth.EmailAuthProvider.credential(user.email, oldPassword);
await user.reauthenticateWithCredential(cred);
}
return { exception: '', token: '' };
} catch (reason) {
let phoneNumber = '****';
if ((reason as any).code === 'auth/multi-factor-auth-required') {
// The user is enrolled in MFA, must be verified
this.mfaResolver = (reason as any).resolver;
phoneNumber = (reason as any).resolver.hints[0].phoneNumber;
}
return { exception: 'auth/multi-factor-auth-required', phoneNumber };
}
}
I do the phone verification like
const phoneAuthProvider = new firebase.auth.PhoneAuthProvider();
const phoneOpts = {
multiFactorHint: this.mfaResolver.hints[0],
session: this.mfaResolver.session,
};
try {
this.verificationId = await phoneAuthProvider.verifyPhoneNumber(phoneOpts, recaptcha);
All good so far ( the recaptcha works with some other code, not mentioned here )
Then the actual SMS is verified, like:
const cred = firebase.auth.PhoneAuthProvider.credential(this.verificationId, phoneCode);
const multiFactorAssertion = firebase.auth.PhoneMultiFactorGenerator.assertion(cred);
const user = firebase.auth().currentUser;
try {
if (this.mfaResolver) {
await this.mfaResolver.resolveSignIn(multiFactorAssertion);
}
all good, and then finally
I can update the password with
const user = firebase.app().auth().currentUser;
if (user) {
await user.updatePassword(password);
}
If I console.log the token JUST before the updatePassword, I get my old token, with the
"sign_in_second_factor": "phone" property, but the token AFTER the updatePassword suddenly is without the sign_in_second_factor property, so basically it broke the token.
My solution is now to log out, and force the user to log back in ( again with MFA ), but an unnecessary step.
Is this avoidable,
to me it looks like a firebase bug, as it generates a valid token, WITHOUT a sign_in_second_factor being present, while it is a MFA firebase user.

How to properly store a CognitoUser?

I'm working on a simple React/Typescript application to test Cognito and I'm running into some problems handling the user, I have a function that gets the user like so:
/* CognitoUtils.tsx */
import UserPool from './UserPool';
export const getCognitoUser = (phone: string): CognitoUser => {
const userData: ICognitoUserData = {
Username: phone,
Pool: UserPool,
};
return new CognitoUser(userData);
}
I have the UserPool defined in a separate file like so:
/* UsePool.tsx */
import { ICognitoUserPoolData, CognitoUserPool } from "amazon-cognito-identity-js";
const poolData: ICognitoUserPoolData = {
UserPoolId: 'XXX',
ClientId: 'XXX',
Storage: window.sessionStorage
};
const userPool = new CognitoUserPool(poolData);
export default userPool;
And this is the code I use to sign in:
/* CognitoUtils.tsx */
export const signIn = (phone: string) => {
var authenticationDetails = new AuthenticationDetails({
Username: phone,
Password: 'XXXX',
});
const user = getCognitoUser(phone);
user.authenticateUser(authenticationDetails, {
onSuccess: function(session: CognitoUserSession) {
console.log("Succesfully signed in", session, "current user", user);
},
onFailure: function(error) {
console.log("Error while signing in", error);
},
});
}
This is working fine, the user and session objects are printed without a problem.
I have a button for signing out that calls this function:
/* CognitoUtils.tsx */
export const signOut = (phone: string) => {
var user = getCognitoUser(phone);
user.getSession((err: any, session: CognitoUserSession) => {
console.log("Getting session error", err, "session:", session);
user.globalSignOut({
onSuccess: (msg: string) => {
console.log("Succesfully signed out", msg);
},
onFailure: (err: globalThis.Error) => {
console.log("Error while signing out", err);
}
});
});
}
This causes the following error on user.getSession():
Getting session error Error: Local storage is missing an ID Token, Please authenticate
Despite having an id token in my local storage (which is generated on sign in), I have tried saving the user in a shared variable in the same file, saving it on a redux store and even converting the object into a json and saving using localStorage, the closes I've ever got to have this working is the signOut function working after signIn but the user breaking if I refresh the page in between.
My question is, what is the proper way of storing/retrieving this user?
This is the closest I've found to my error but even after moving the UsePool to a separate file this still happens.
This is the tutorial I've been following.
Any help is greatly appreciated.

setting cookies from GraphQL resolver

I'm learning GraphQL via https://github.com/the-road-to-graphql/fullstack-apollo-express-postgresql-boilerplate
and I'm wondering how to set cookies from a resolver as I'm used to using Express to do so.
signIn: async (
parent,
{ login, password },
{ models, secret },
) => {
const user = await models.User.findByLogin(login);
if (!user) {
throw new UserInputError(
'No user found with this login credentials.',
);
}
const isValid = await user.validatePassword(password);
if (!isValid) {
throw new AuthenticationError('Invalid password.');
}
return { token: createToken(user, secret, '5m') };
},
instead of returning a token obj, how can I access the response object and add a cookie?
You can achieve this using the context object, looking at the example you send. You will need to return the res variable from this function https://github.com/the-road-to-graphql/fullstack-apollo-express-postgresql-boilerplate/blob/master/src/index.js#L55
The context object is located at the 3rd argument to your resolver. The context is created on each request & available to all resolvers.
Example:
const server = new ApolloServer({
context: ({res}) => ({res})
});
function resolver(root, args, context){
context.res// express
}

How to use c8yClient code in the Angular 6 App (typescript file)

For Example:
import { Client } from '#c8y/client';
const baseUrl = 'https://demos.cumulocity.com/';
const tenant = 'demos';
const user = 'user';
const password = 'pw';
(async () => {
const client = await Client.authenticate({
tenant,
user,
password
}), baseUrl);
const { data, paging } = await client.inventory.list();
// data = first page of inventory
const nextPage = await paging.next();
// nextPage.data = second page of inventory
})();
Consider that I have login module in an angular 6 application. How to use this above code and authenticate the user in the login.component.ts file?
Cumulocity has released a demo on Stackblitz how to log in the user. Basically you build a ngForm with username, password and tenant and pass that to the Cumulocity client:
async login() {
const client = new Client(new BasicAuth(
{
user: this.model.user,
password: this.model.password,
tenant: this.model.tenant
}),
`https://${this.model.tenant}.cumulocity.com`
);
try {
let user = await client.user.current();
this.cumulocity.client = client;
} catch (ex) {
this.cumulocity.client = null;
this.error.shown = true;
this.error.msg = ex.message;
}
}
In this case this.model is the data coming from an ngFrom and on button click the login()? function is executed. The this.cumulocity variable contains a service so that you can share the logged in client with other components.
Note: If you run this on a different server (not hosted), then you need to enable CORS in the Cumulocity administration.