Does AngularFire2 support update of user credentials? - firebase

I'm using AngularFire2 (2.0.0-beta.2) incombination with angular2 (2.0.0-rc.4). Using Angularfire I can programatically create a user (email/password) with
angularFire.auth.createUser({email : this.email, password; this.password})
That part works as expected. Subsequently, I would like to update either the email address or password. I've examined the AngularFire source and there doesn't seem to be a mechanism to do this. Am I correct in this assessment? And if I'm correct, should I expect to see a mechanism in upcoming releases or should I just use the native Firebase mechanisms?

You are looking to use $firebaseAuth(). Just inject it in your controller and go with
$firebaseAuth().$updateEmail("email#email.com");
$firebaseAuth().$updatePassword("newpass123");

I think my answer will be helpful, Password can be changed by firebase-admin using cloud function you will just have to pass email and new password from the client-side(Angular, ionic, ...)
Cloud function:
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
const cors = require('cors')({ origin: true });
export const resetUserPassword = functions.https.onRequest( async (req, res) => {
return cors( req, res, async () => {
const email = req.body.data.email;
const password = req.body.data.password;
admin.auth().getUserByEmail(email)
.then((getUserRecord) => {
admin.auth().updateUser(getUserRecord.uid, {
email,
password
})
.then(function(userRecord) {
res.send({
status: 200,
data: userRecord
})
})
.catch(function(error) {
res.send({
status: 500,
data: error
})
});
})
.catch((error) => {
console.log('Error fetching user data:', error);
});
})
});
Client side code(IONIC):
import { AngularFireFunctions } from '#angular/fire/functions';
export class AuthService {
constructor(
private functions: AngularFireFunctions
) {}
resetUserPassword() {
const userNewCredential = {email: 'user-email', password: 'your-new-password'};
this.functions.httpsCallable('resetUserPassword')
(userNewCredential).toPromise().then((updatedUser) => {
console.log(updatedUser);
}).catch((error) => console.log(error));
}

I'm going to try an answer my own question. I think that the AngularFire2 public API's are missing some functionality related to firebase authentication. For example I don't think the present version of AngularFire (2.0.0-beta2) has the ability to update the email address or password, or send the password reset email. I think the solution to this present shortcoming is to get the native firebase objet and just use the native firebase methods to resolve. However, I haven't been able to figure out how to get access to the native FireBase object using AngularFire2. I've posted this question to see if anyone can help me do that.

Related

listUsers is not a function error in Firebase Authentication

I'm trying to fetch a list of all the users in my web app, but I keep receiving this error:
"TypeError: utils_firebase_WEBPACK_IMPORTED_MODULE_2_.auth.listUsers is not a function"
I copied exactly from Firebase documentation, and below is my code.
auth prints out "AuthImpl {app: FirebaseAppImpl, heartbeatServiceProvider: Provider, config: {…}, currentUser: null, emulatorConfig: null, …}", so I know that auth exists.
import { useEffect } from "react";
import { auth } from "../utils/firebase";
function users() {
const listAllUsers = (nextPageToken) => {
console.log(auth);
auth
.listUsers(1000, nextPageToken)
.then((listUsersResult) => {
listUsersResult.users.forEach((userRecord) => {
console.log("user", userRecord);
});
if (listUsersResult.pageToken) {
listAllUsers(listUsersResult.pageToken);
}
})
.catch((error) => {
console.log("Error listing users:", error);
});
};
useEffect(() => {
listAllUsers();
}, []);
return <div>users</div>;
}
export default users;
Can someone help me with this? Thanks!
I tried to look at the documentation from Firebase, but with no luck
listUsers() is a method from the Admin SDK and not from the JS SDK. The page you copied the code from documents the Admin SDK methods for the Authentication Service.
There isn't any corresponding method in the JS SDK because, for security reasons, it is not possible to let a user directly listing all users of a Firebase project from a front-end.
If you want to list all users of your Firebase project from your front-end you can write a Callable Cloud Function that uses the listUsers() Admin SDK's method. It's then up to you to verify who can call this Cloud Function.

How do you send a Firebase verification email (so that a user can verify their email) to a user in React Native?

I am making a React Native app where you can create accounts via Firebase authentication. I am try to set up a way to verify your email. I thought the code below would work, but it doesn't work. Anyone know the solution to this:
Code:
async function handleSignUp() {
await createUserWithEmailAndPassword(auth, email, password)
.then((userCredentials) => {
let user = auth.currentUser;
const actionCodeSettings = {
url: `${configData.BASE_URL}/sign-in/?email=${user.email}`,
};
auth.currentUser.sendEmailVerification(actionCodeSettings).then(() => {
alert("Verification link has been sent to your email");
});
})
.catch((error) => alert(error.message));
}
You're using the v9 or later SDK, which uses a modular syntax like you see here: await createUserWithEmailAndPassword(auth, email, password).
Similar to createUserWithEmailAndPassword, sendEmailVerification is a top-level function too in this API, so the syntax is:
sendEmailVerification(auth.currentUser, actionCodeSettings).then(() => {
alert("Verification link has been sent to your email");
});
This API is quite well covered in the Firebase documentation on sending an email verification link, so I recommend keeping that handy while coding.

How to disable account creation in firebase authentication

I've a project in which I used to authenticate the users with firebase-auth.In my project users can not create their accounts on their own.Only admin have the privilege to add the user accounts.
In order to use onAuthStateChanged() function I must use firebase-auth in my page.But the issue is because of using firebase-auth on client side one can esaily create accounts by running createUserWithEmailAndPassword() function on the console without having the admin privilege.
Now how can I restrict the people from using createUserWithEmailAndPassword() function on client side?
The only way you can stop clients apps from creating accounts is to disable all authentication providers for your project in the Firebase console. You could write an auth onCreate Cloud Function that attempts to figure out if a new account was created by client or admin code if you want to try to delete it immediately.
I think you can add a claim once the user is added, via a cloud function, which requires authorization, so that if the user doesn't have that claim he can't use the app or can't login.
In 2022 with Firebase Auth with Identity Platform and blocking functions, we can accomplish that the following way:
Create an HTTP function that receives email, password and displayName, and creates user using firebase-admin:
import { https } from 'firebase-functions';
import { getAuth } from 'firebase-admin/auth';
import cors from 'cors';
const auth = getAuth();
// Register an HTTP function with the Functions Framework
export const signupUser = https.onRequest((req, res) => {
const options = {
origin: 'http://localhost:3000'
};
cors(options)(req, res, () => {
console.log('all good');
auth
.createUser({
email: 'example#email.com',
emailVerified: false,
password: 'secretPassword',
displayName: 'John Doe',
disabled: false,
})
.then((userRecord) => {
// See the UserRecord reference doc for the contents of userRecord.
console.log('Successfully created new user:', userRecord.uid);
})
.catch((error) => {
console.log('Error creating new user:', error);
});
// Send an HTTP response
res.send('OK');
});
});
Modify response and origin in CORS as you need.
Now create a blocking beforeCreate function and check for user's display name, if there is no display name, throw an error:
import { auth } from "firebase-functions";
import { initializeApp, applicationDefault } from 'firebase-admin/app';
import { getAuth } from 'firebase-admin/auth';
import postmark from 'postmark';
const app = initializeApp({
credential: applicationDefault(),
projectId: 'your_project_id',
});
const tnc = getAuth(app);
export const signUp = auth
.user().beforeCreate((user, context) => {
if (!user.displayName) {
throw new auth.HttpsError('permission-denied');
}
});
This will work because there is no way to include "display name" when signing up via client side
So you, in short, point is to create a Cloud Function that will register users and make sure to add the check to beforeCreate for something that you know is only possible to do on server-side via firebase-admin sdk.
EDIT: CORRECTION
Just found out you can now disable client side signup from Firebase Console if you have Auth + Identity Platform

Get current users access token from Firebase in React Native

I am trying to get the Firebase authentication access token within a React Native application so that I can authenticate my API calls to a custom server. The Firebase documentation says I should get this token by using auth().currentUser.getIdToken(); however currentUser returns null.
I've tried to use getIdToken() in multiple areas of the application. I know the access token is generated as I can see it in the logs while using expo (user.stsTokenManager.accessToken).
Why is currentUser returning null and how can I get the accessToken?
You need to wrap user.getIdToken() inside of firebase.auth().onAuthStateChanged for user to be available. You can then use jwtToken in your header to authenticate your API calls. You need to import your Firebase configuration file for this to work.
let jwtToken = firebase.auth().onAuthStateChanged(function(user) {
if (user) {
user.getIdToken().then(function(idToken) { // <------ Check this line
alert(idToken); // It shows the Firebase token now
return idToken;
});
}
});
Just putting await before will work too just like this:
await auth().currentUser.getIdToken();
getIdToken returns a promise
firebase.auth()
.signInWithCredential(credential)
.then(async data => {
const jwtToken = await data.user?.getIdToken();
console.log(jwtToken);
})
Hook example
Unfortunately, its not reliable to directly get the token. You first have to listen to the authentication state change event which fires upon initialization since its asynchronous.
import {auth} from '../utils/firebase'
import {useState, useEffect} from 'react'
export default function useToken() {
const [token, setToken] = useState('')
useEffect(() => {
return auth().onAuthStateChanged(user => {
if (user) {
user.getIdToken(true)
.then(latestToken => setToken(latestToken))
.catch(err => console.log(err))
}
})
}, [])
return token
}
then use like so in your functional component
const token = useToken()
useEffect(() => {
if (token) {
// go wild
}
}, [token])

Is it possible to use Cypress e2e testing with a firebase auth project?

I am exploring Cypress for e2e testing, looks like great software.
The problem is Authentication, the Cypress documentation explains why using the UI is very bad here.
So I tried looking at the network tap of my application, to see if I could create a POST request to the firebase API, and authenticate without using the GUI. But I can see that there at least 2 request fired, and token saved to application storage.
So what approach should I use?
Authenticate with the UI of my application, and instruct Cypress not to touch the local storage
Keep experimenting with a way of sending the correct POST requests, and save the values to local storage.
Make Cypress run custom JS code, and then use the Firebase SDK to login.
I am really looking for some advice here :)
When doing this myself I made custom commands (like cy.login for auth then cy.callRtdb and cy.callFirestore for verifying data). After getting tired of repeating the logic it took to build them, I wrapped it up into a library called cypress-firebase. It includes custom commands and a cli to generate a custom auth token.
Setup mostly just consists of adding the custom commands in cypress/support/commands.js:
import firebase from 'firebase/app';
import 'firebase/auth';
import 'firebase/database';
import 'firebase/firestore';
import { attachCustomCommands } from 'cypress-firebase';
const fbConfig = {
// Your config from Firebase Console
};
window.fbInstance = firebase.initializeApp(fbConfig);
attachCustomCommands({ Cypress, cy, firebase })
And adding the plugin to cypress/plugins/index.js:
const cypressFirebasePlugin = require('cypress-firebase').plugin
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
// Return extended config (with settings from .firebaserc)
return cypressFirebasePlugin(config)
}
But there full details on setup are available in the setup docs.
Disclosure, I am the author of cypress-firebase, which is the whole answer.
I took the approach of using automated UI to obtain the contents of localStorage used by Firebase JS SDK. I also wanted to do this only once per whole Cypress run so I did it before the Cypress start.
Obtain Firebase SDK localStorage entry via pupeteer
Store the contents in the tmp file (problems passing it via env var to Cypress)
Pass the file location to Cypress via env var and let it read the contents and set the localStorage to setup the session
Helper script which obtains contents of localStorage:
const puppeteer = require('puppeteer')
const invokeLogin = async page => {
await page.goto('http://localhost:3000/login')
await page.waitForSelector('.btn-googleplus')
await page.evaluate(() =>
document.querySelector('.btn-googleplus').click())
}
const doLogin = async (page, {username, password}) => {
// Username step
await page.waitForSelector('#identifierId')
await page.evaluate((username) => {
document.querySelector('#identifierId').value = username
document.querySelector('#identifierNext').click()
}, username)
// Password step
await page.waitForSelector('#passwordNext')
await page.evaluate(password =>
setTimeout(() => {
document.querySelector('input[type=password]').value = password
document.querySelector('#passwordNext').click()
}, 3000) // Wait 3 second to next phase to init (couldn't find better way)
, password)
}
const extractStorageEntry = async page =>
page.evaluate(() => {
for (let key in localStorage) {
if (key.startsWith('firebase'))
return {key, value: localStorage[key]}
}
})
const waitForApp = async page => {
await page.waitForSelector('#app')
}
const main = async (credentials, cfg) => {
const browser = await puppeteer.launch(cfg)
const page = await browser.newPage()
await invokeLogin(page)
await doLogin(page, credentials)
await waitForApp(page)
const entry = await extractStorageEntry(page)
console.log(JSON.stringify(entry))
await browser.close()
}
const username = process.argv[2]
const password = process.argv[3]
main({username, password}, {
headless: true // Set to false for debugging
})
Since there were problem with sending JSON as environment variables to Cypress I use tmp file to pass the data between the script and the Cypress process.
node test/getFbAuthEntry ${USER} ${PASSWORD} > test/tmp/fbAuth.json
cypress open --env FB_AUTH_FILE=test/tmp/fbAuth.json
In Cypress I read it from the file system and set it to the localStorage
const setFbAuth = () =>
cy.readFile(Cypress.env('FB_AUTH_FILE'))
.then(fbAuth => {
const {key, value} = fbAuth
localStorage[key] = value
})
describe('an app something', () => {
it('does stuff', () => {
setFbAuth()
cy.viewport(1300, 800)
...
This is certainly a hack but to get around the login part for the app I am working on I use the beforeEach hook to login to the application.
beforeEach(() => {
cy.resetTestDatabase().then(() => {
cy.setupTestDatabase();
});
});
Which is derived from my helper functions.
Cypress.Commands.add('login', () => {
return firebase
.auth()
.signInWithEmailAndPassword(Cypress.env('USER_EMAIL'), Cypress.env('USER_PASSWORD'));
});
Cypress.Commands.add('resetTestDatabase', () => {
return cy.login().then(() => {
firebase
.database()
.ref(DEFAULT_CATEGORIES_PATH)
.once('value')
.then(snapshot => {
const defaultCategories = snapshot.val();
const updates = {};
updates[TEST_CATEGORIES_PATH] = defaultCategories;
updates[TEST_EVENTS_PATH] = null;
updates[TEST_STATE_PATH] = null;
updates[TEST_EXPORT_PATH] = null;
return firebase
.database()
.ref()
.update(updates);
});
});
});
What I would like to know is how the information coming back from firebase ultimately gets saved to localStorage. I don't really have an answer to this but it works. Also, the app uses .signInWithPopup(new firebase.auth.GoogleAuthProvider()) whereas above it signs in with email and password. So I am kind of shortcutting the signin process only because cypress has the CORS limitation.
This is becoming way easier with the upcoming Auth emulator. This has become easier with the Firebase Auth Emulator (firebase-tools >= 8.1.4).
cypress/support/signAs.js:
Cypress.Commands.add('signAs', (uid, opt) => {
cy.visit('/')
cy.window().its('firebase').then( fb => {
cy.wrap( (async _ => {
// Create a user based on the provided token (only '.uid' is used by Firebase)
await fb.auth().signInWithCustomToken( JSON.stringify({ uid }) );
// Set '.displayName', '.photoURL'; for email and password, other functions exist (not implemented)
await fb.auth().currentUser.updateProfile(opt);
})() )
})
})
Use it as:
cy.signAs('joe', { displayName: 'Joe D.', photoURL: 'http://some' });
If you need to set .email or .password, there are similar functions for those, but this was sufficient for my tests. I can now impersonate any user ad-hoc, as part of the test. The approach does not need users to be created in the emulator; you can just claim to be one, with the particular uid. Works well for me.
Note:
Firebase authentication is in IndexedDB (as mentioned in other answers) and Cypress does not clear it, between the tests. There is discussion about this in cypress #1208.
At the time writing, I've examined these approaches
stubbing firebase network requests - really difficult. A bunch of firebase requests is sent continuously. There are so many request params & large payload and they're unreadable.
localStorage injection - as same as request stubbing. It requires an internally thorough understanding of both firebase SDK and data structure.
cypress-firebase plugin - it's not matured enough and lack of documentation. I skipped this option because it needs a service account (admin key). The project I'm working on is opensource and there are many contributors. It's hard to share the key without including it in the source control.
Eventually, I implemented it on my own which is quite simple. Most importantly, it doesn't require any confidential firebase credentials. Basically, it's done by
initialize another firebase instance within Cypress
use that firebase instance to build a Cypress custom command to login
const fbConfig = {
apiKey: `your api key`, // AIzaSyDAxS_7M780mI3_tlwnAvpbaqRsQPlmp64
authDomain: `your auth domain`, // onearmy-test-ci.firebaseapp.com
projectId: `your project id`, // onearmy-test-ci
}
firebase.initializeApp(fbConfig)
const attachCustomCommands = (
Cypress,
{ auth, firestore }: typeof firebase,
) => {
let currentUser: null | firebase.User = null
auth().onAuthStateChanged(user => {
currentUser = user
})
Cypress.Commands.add('login', (email, password) => {
Cypress.log({
displayName: 'login',
consoleProps: () => {
return { email, password }
},
})
return auth().signInWithEmailAndPassword(email, password)
})
Cypress.Commands.add('logout', () => {
const userInfo = currentUser ? currentUser.email : 'Not login yet - Skipped'
Cypress.log({
displayName: 'logout',
consoleProps: () => {
return { currentUser: userInfo }
},
})
return auth().signOut()
})
}
attachCustomCommands(Cypress, firebase)
Here is the commit that has all integration code https://github.com/ONEARMY/community-platform/commit/b441699c856c6aeedb8b73464c05fce542e9ead1
Ok after much trial and error, I tried solution path 2 and it worked.
So my auth flow looks like this:
Send POST request (using cybress.request) to
https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword,
and parse the response. Create an object: response1 = response.body
Send POST request (using cybress.request) to
https://www.googleapis.com/identitytoolkit/v3/relyingparty/getAccountInfo,
use the idToken from the prev request. Create an object: user = response2.body.users[0];
Combine the response in an object, with the following properties:
const authObject = {
uid: response1.localId,
displayName: response1.displayName,
photoURL: null,
email: response1.email,
phoneNumber: null,
isAnonymous: false,
providerData: [
{
uid: response1.email,
displayName: response1.displayName,
photoURL: null,
email: body.email,
phoneNumber: null,
providerId: 'password'
}
],
'apiKey': apiKey,
'appName': '[DEFAULT]',
'authDomain': '<name of firebase domain>',
'stsTokenManager': {
'apiKey': apiKey,
'refreshToken': response1.refreshToken,
'accessToken': response1.idToken,
'expirationTime': user.lastLoginAt + Number(response1.expiresIn)
},
'redirectEventId': null,
'lastLoginAt': user.lastLoginAt,
'createdAt': user.createdAt
};
Then in cybress, I simply save this object in local storag, in the before hook: localStorage.setItem(firebase:authUser:${apiKey}:[DEFAULT], authObject);
Maybe not perfect, but it solves the problem. Let me know if you interested in the code, and if you have any knowledge about how to build the "authObject", or solve this problem in another way.

Resources