Firebase updatePassword removes sign_in_second_factor phone property from token - firebase-authentication

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.

Related

OAuth 2Client: Invalid token signature

I wanted to handle my user auth by google.
async verify(token) {
try {
const ticket = await client.verifyIdToken({
idToken:token,
audience: '245409008225-isc00em81fk0vs423pm4jmgc2hcma5jj.apps.googleusercontent.com',
});
const payload = ticket.getPayload();
return payload
} catch (error) {
console.log(error)
}
this code works fine, only for first time to create user in DB. And i save this token to localstorage and retrieve it every time to validate that user is authentificated. Here is my code:
async isAuth(token) {
if (!token) {
false
}
const userData = tokenService.verify(token);
const tokenFromDb = await tokenService.findToken(token);
if (!userData || !tokenFromDb) {
throw ApiError.UnAuthorizedError();
}
const user = await User.findOne({where: {email: userData.email}});
await tokenService.saveToken(token);
return true;
}
I did google, and i supposed to define jwk key for google auth api? But I can't find real solution. So, hope you guys can help me. I never used before google auth. For now I have this solution by making request to this api https://www.googleapis.com/oauth2/v1/tokeninfo?id_token=token and getting from there my user email

Is NextAuth Credentials safe?

I use next-auth Credentials (v3) to allow my users to register and sign in with good old email and password in my NextJS website. I use MongoDB as my database.
This is my [...nextauth].js file:
export default NextAuth({
session: {
jwt: true
},
providers: [
Providers.Credentials({
async authorize(credentials) {
await dbConnect();
// Check if a user with the email exists
const user = await UserModel.findOne({ email: credentials.email });
if (!user) throw new Error("Emailen is not in use");
// Check if the password is correct
const correctPassword = await bcrypt.compare(
credentials.password,
user.password
);
if (!correctPassword) throw new Error("Wrong password");
return {
userid: user._id,
email: user.email,
};
},
}),
],
callbacks: {
// Add userid to token
async jwt(token, user, account, profile, isNewUser) {
if (user) {
token.id = user.userid;
}
return token
},
// Add userid to session returned to front-end
async session(session, token) {
session.user.id = token.id;
return session
}
}
});
Before fetching data in my NextJS API endpoints, I check if the user is authenticated like this:
const session = await getSession({ req });
const user = await UserModel.findById(session?.user?.id);
if (!session || !user)
return res.status(400).json({ success: false });
But I'm worried that if a person gets the id of another user, they can just edit their JWT session.user.id and access any API endpoint pretending to be another user?
Is that true? Would the users be able to fake their id's in my code?
If so, what can I do to avoid that?

How to switch accounts in react native

I'm trying to build react native app in which user will have option to switch multiple accounts, similar to Instagram. My problem is, How to change user data on when user switches account.
You can use react-native-keychain to store the Active User and User List. This will store the data in Device.
const StoreData = async () => {
let activeUser = 'User1'
let userList = [
{ ID: 'User1' },
{ ID: 'User2' }
]
userList = JSON.stringify(userList)
await Keychain.setGenericPassword(activeUser, userList); // Note: activeUser and userList should be string.
}
const RetrieveData = async () => {
try {
const credentials = await Keychain.getGenericPassword();
if (!credentials)
return console.log('No credentials stored')
let { username: activeUser, password: userList } = credentials
userList = JSON.parse(userList)
return console.log({ activeUser, userList })
} catch (error) {
return console.log("Keychain couldn't be accessed!", error);
}
}
If you are using library similar to Redux, MobX, ... You can use them to refresh the APP with the selected user.
One way you could do this is by having the token of the other users and then retrieve all the user data everytime you switch users.

cognito custom auth (CUSTOM_CHALLENGE) ignore retry because session expiration

my goal is to implement otp by sending a sms to user mobile. im able to achieve this using cognito custom auth flow, but, only works if the user success in the firts attemp, if the user enter a bad code, the session is gonna expire and a new code is required to be sent again, bad ux. i do need at least 3 attemps, which in theory are 3 sessions across this cognito auth flow.
im gonna share the four cognito lambdas (cognito triggers) i used for this: preSignUp, defineAuthChallenge, createAuthChallenge and verifyChanllenge
// preSignUp lambda
exports.handler = async (event) => {
event.response.autoConfirmUser = true;
event.response.autoVerifyPhone = true;
return event;
};
// defineAuthChallenge
exports.handler = async (event, context, callback) => {
if (event.request.session.length >= 3 && event.request.session.slice(-1)[0].challengeResult === false) {
// wrong OTP even After 3 sessions? FINISH auth, dont send token
event.response.issueToken = false;
event.response.failAuthentication = true;
} else if (event.request.session.length > 0 && event.request.session.slice(-1)[0].challengeResult === true) {
// Last answer was Correct! send token and FINISH auth
event.response.issueTokens = true;
event.response.failAuthentication = false;
} else {
// INIT flow - OR - not yet received correct OTP
event.response.issueTokens = false;
event.response.failAuthentication = false;
event.response.challengeName = 'CUSTOM_CHALLENGE';
}
return event;
};
// createAuthChallenge
exports.handler = async (event, context) => {
if (!event.request.session || event.request.session.length === 0) {
// create only once the otp, send over sms only once
var otp = generateOtp();
const phone = event.request.userAttributes.phone_number;
sendSMS(phone, otp);
} else {
// get previous challenge answer
const previousChallenge = event.request.session.slice(-1)[0];
otp = previousChallenge.challengeMetadata;
}
event.response = {
...event.response,
privateChallengeParameters: {
answer: otp
},
challengeMetadata: otp // save it here to use across sessions
};
return event
}
// verifyChanllenge
exports.handler = async (event, context) => {
event.response.answerCorrect = event.request.privateChallengeParameters.answer === event.request.challengeAnswer;
return event
}
for the client, which is a RN app, im using amplify, this is the flow in the app:
// SignIn form screen
import { Auth } from "aws-amplify";
const signUp = (phone) => {
Auth.signUp({
username: phone,
/** dummy pass since its required but unused for OTP */
password: "12345678"
}).then(() => {
// after signup, go an automatically login (which trigger sms to be sent)
otpSignIn(phone);
}).catch(({code}) => {
// signup fail because user already exists, ok, just try login it
if (code === SignUpErrCode.USER_EXISTS) {
otpSignIn(phone)
} else {
...
}
})
}
const otpSignIn = async (phoneNumber) => {
const cognitoUser = await Auth.signIn(phoneNumber)
setCognitoUser(cognitoUser);
navigate("ConfirmNumber", {phoneNumber});
}
import { Auth } from "aws-amplify";
let cognitoUser;
export function setCognitoUser(user) {
console.log('setCognitoUser', user)
cognitoUser = user;
}
export function sendChallenge(challengeResponse) {
return Auth.sendCustomChallengeAnswer(cognitoUser, challengeResponse)
}
// Confirm number screen
const onChangeText = (value) => {
if (value.length === 4) {
try {
const user = await sendChallenge(value)
// WEIRD THING NUMBER 1
// when the user send the second attempt, no error is raised, this promise is resolve!
// even when the trigger *verifyChanllenge* is returning false.
} catch (err) {
// WEIRD THING NUMBER 2
// from the trigger *createAuthChallenge* if i define the anser in the if block,
// and not store such answer for future use (i do that in else block), then,
// for the second..third attempt the error raised here is that *Invalid session for user* which mean session has expired,
// what i need is to persist session until third attempt
}
}
}
// this is amplify config: try 1
const awsExports = {
Auth: {
region: ...,
userPoolId: ...,
userPoolWebClientId: ...,
authenticationFlowType: 'CUSTOM_AUTH',
},
...
}
Amplify.configure(awsExports);
// this is amplify config: try 2
import {Auth} from "aws-amplify"
Auth.configure({
authenticationFlowType: 'CUSTOM_AUTH'
});
everything is correct in the code above, and the config for amplify authenticationFlowType: 'CUSTOM_AUTH' is not necessary.
the problem is that Auth.sendCustomChallengeAnswer(cognitoUser, challengeResponse) is not raising an error when the trigger defineAuthChallenge set this combination:
event.response.issueTokens = false;
event.response.failAuthentication = false;
event.response.challengeName = 'CUSTOM_CHALLENGE';
which presents the next attempt.
so i found a way to check the error when the user fail the otp:
const sendCode = async (value) => {
try {
// send the answer to the User Pool
// this will throw an error if it's the 3rd wrong answer
const user = await sendChallenge(value);
// the answer was sent successfully, but it doesnt mean it is the right one
// so we should test if the user is authenticated now
// this will throw an error if the user is not yet authenticated:
await Auth.currentSession();
} catch (err) {
setError(true);
}
}

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.