I’m working on a React Native app that loads the example.com in a webview. I was wondering if it’s safe to use postMessage to pass username and password to React Native from the React Native webview. Do you think it’s okay or it's not safe?
This is the web app(example.com) sending username and password to React Native
And this is React Native <WebView onMessage={m => this._onMessage(m)} /> to receive that username and password to Authenticate user.
Seems like it will work but not sure if it’s safe. If not, is there a way to authenticate user in React Native from React Native webview?
According to MDN the postMessage method is safe, but please refer to security concern section.
You might send user session instead of username/password pair.
Your code would look like this:
// in webb app
const sendSession = async() => {
const session = await Auth.currentSession()
const data = {
idToken: session.getIdToken().getJwtToken(),
accessToken: session.getAccessToken().getJwtToken(),
refreshToken: session.getRefreshToken().getToken(),
}
window.ReactNativeWebView?.postMessage(JSON.stringify(data))
}
// in mobile app
const _onMessage = (event) => {
const data = JSON.parse(event.nativeEvent.data)
const {
idToken,
accessToken,
refreshToken
} = data
const userPool = new CognitoUserPool({
UserPoolId: 'UserPoolId',
ClientId: 'ClientId',
})
const cognitoIdToken = new CognitoIdToken({
IdToken: idToken,
})
const cognitoAccessToken = new CognitoAccessToken({
AccessToken: accessToken,
})
const cognitoRefreshToken = new CognitoRefreshToken({
RefreshToken: refreshToken,
})
const username = cognitoIdToken.payload['cognito:username'] // or whatever you need to get from playload as a username
const user = new CognitoUser({
Username: username,
Pool: userPool,
})
user.setSignInUserSession(
new CognitoUserSession({
AccessToken: cognitoAccessToken,
IdToken: cognitoIdToken,
RefreshToken: cognitoRefreshToken,
}),
)
}
Related
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();
The auth0 documentation on their credentials manager states
The credentials manager is an easy to use source of Keychain-based
authentication for iOS and Android, and should be usable with
auth.credentialsManager
When trying to use this suggested method
const isLoggedIn = await auth0.credentialsManager.hasValidCredentials();
This error is being thrown
undefined is not an object (evaluating '_$$_REQUIRE(_dependencyMap[10],
"../context/actions/authActions").auth0.credentialsManager.getCredentials')
Here's an overview of our auth0 configuration, and how it works currently
in AuthActions.js
export const auth0 = new Auth0({
domain: Config.AUTH0_DOMAIN,
clientId: Config.AUTH0_CLIENT_ID,
});
export const actionLogin = async (callback) => {
try {
const authState = await auth0.webAuth.authorize({
scope: 'openid profile email offline_access',
audience: Config.AUTH0_AUDIENCE,
prompt: 'login',
});
let response = await getState(authState, callback);
return response
} catch (e) {
console.log('Error Authenticating: ', e)
}
The hasValidCredentials() method mentioned above is called after a user has successfully authenticated with the webAuth, and it should be returning something along the lines of an access token, refresh token, id, and email per the docs
Note that we are trying to use this so that we can stop using the react-native-keychain package and use auth0's implementation of the native keystores by
await auth0.credentialsManager.requireLocalAuthentication();
I'm trying to get my expo AuthSession to work (sign up with google) in my react native app. I have been following this clip (https://www.youtube.com/watch?v=YX7IWOQIKA0) on youtube but get an error as soon as i try to navigate to the login screen.
The error message i get is:
Cannot use the AuthSession proxy because the project full name is not defined. Prefer AuthRequest (with the useProxy option set to false) in combination with an Expo Development Client build of your application. To continue using the AuthSession proxy, specify the project full name (#owner/slug) using the projectNameForProxy option.
Notes:
The app is working overall but breaks completely when i try to navigate to the LoginScreen where the code below exists. I have checked that the "https://auth.expo.io/#owner/slug" in the Google Cloud Credentials is correct. Have i missed to add something that i should've? I should also add that i am aiming to get this to work on both IOS and Android devices.
My relevant code to the problem is in my LoginScreen.tsx and it looks like this:
Imports:
import * as WebBrowser from 'expo-web-browser';
import * as Google from 'expo-auth-session/providers/google';
WebBrowser.maybeCompleteAuthSession();
GoogleAuthRequest:
const [request, response, promptAsync] = Google.useAuthRequest({
expoClientId: '*THE CLIENT ID FROM GOOGLE CLOUD CREDENTIALS IS PASTED HERE*',
iosClientId: 'GOOGLE_GUID.apps.googleusercontent.com',
androidClientId: 'GOOGLE_GUID.apps.googleusercontent.com',
webClientId: 'GOOGLE_GUID.apps.googleusercontent.com',
});
Code that runs when pressing "continue with google":
const signUpGoogleHandler = async () => {
const response = await promptAsync();
if (response.type === 'success') {
const {access_token } = response.params;
console.log('res params ', access_token);
}
console.log('test123');
const provider = new GoogleAuthProvider();
await signInWithRedirect(auth, provider);
getRedirectResult(auth)
.then((result) => {
const credential = GoogleAuthProvider.credentialFromResult(result);
const token = credential.accessToken;
const user = result.user;
}).catch((error) => {
const errorCode = error.code;
const errorMessage = error.message;
const email = error.customData.email;
const credential = GoogleAuthProvider.credentialFromError(error);
})
}
How my Client ID for my Web application is setup in Google Cloud (the Authorized redirect URIs is written as https://auth.expo.io/#myusername/slugname):
Client ID for Web application
I have a react native app that renders a WebView of a Web app
The react native app uses Cognito and Amplify for authentication.
The web app also uses the same Cognito and Amplify for authentication.
I have a login flow built with in the react native that has email/password login and social media federated Oauth logins. Both these login flows successfully work in the react native space and return a
CognitoUserSession {
idToken: CognitoIdToken,
refreshToken: CognitoRefreshToken,
accessToken: CognitoAccessToken,
clockDrift: 0
}
When the react native app renders the WebView the web app is unauthenticated. I am able to pass the CognitoUserSession data into the WebView successfully. Unfortunately, I don't see a way to have Amplify re-authenticate with this session.
this is the mobileLogin function I wrote that works
import Amplify, { Auth } from 'aws-amplify';
import {
CognitoUser,
CognitoUserSession,
CognitoIdToken,
CognitoRefreshToken,
CognitoAccessToken,
} from 'amazon-cognito-identity-js';
window.mobileLogin = async function(mobileSession) {
amplify = Amplify.configure({
...config().amplify,
userPoolWebClientId: '', //switch to mobile client
});
const localSession = new CognitoUserSession({
IdToken: new CognitoIdToken({ IdToken: mobileSession.idToken.jwtToken }),
RefreshToken: new CognitoRefreshToken({ RefreshToken: mobileSession.refreshToken }),
AccessToken: new CognitoAccessToken({ AccessToken: mobileSession.accessToken.jwtToken }),
});
const localUser = new CognitoUser({
Username: mobileSession.accessToken.payload.username,
Pool: Auth.userPool,
Storage: Auth.userPool.storage,
});
localUser.setSignInUserSession(localSession);
// this seems like a hack
Auth.currentCredentials = async () => localSession;
try {
await Auth.currentSession();
console.warn(`mobile login current session!!`);
store.dispatch(silentReloginAction())
} catch (ex) {
console.warn(`mobile login ${ex}`);
}
};
}
For someone who still need this.
First, you need add oauth setting to your Web application's AwsExports.json.
const AwsExports = {
Auth: {
...
oauth: {
domain: 'xxx.auth.us-east-1.amazoncognito.com',
scope:['openid'],
redirectSignIn: 'https://example.com',
redirectSignOut: 'https://example.com',
responseType: 'token'
}
},
};
then you can pass token with uri.
const session = await Auth.currentSession(),
id_token = session.getIdToken().getJwtToken(),
access_token = session.getAccessToken().getJwtToken(),
uri = `https://example.com##id_token=${id_token}&access_token=${access_token}`;
You should actually setup oauth things.
Because webview is opened as part of Oauth flow, oauth loggin out flow can be executed.
so without proper setting of oauth, error comes up
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);