Let's implement a Sign up form where users can create an account using their email address and a password.
-
Install
firebasefrom npm$ npm install firebaseRun this from the working directory: fireauth/ -
Create the route
src/app/signup/page.jssrc/app/signup/page.js export default function SignupPage() { return <div>Signup page</div> } -
Create the signup form
src/app/signup/SignUpForm.js "use client" import { useState } from "react" export function SignUpForm() { const [email, setEmail] = useState("") const [password, setPassword] = useState("") const handleFormSubmit = (event) => { event.preventDefault() console.log("email", email) console.log("password", password) // We'll implement the rest of this in soon... } return ( <form onSubmit={handleFormSubmit}> <label>Email:</label> <input type="text" value={email} onChange={(event) => setEmail(event.target.value)} /> <label>Password:</label> <input type="password" value={password} onChange={(event) => setPassword(event.target.value)} /> <button type="submit">Sign up</button> </form> ) }
Now, a visit to http://localhost:3000/signup should display the butt-ugly signup form. π«£ Let's test it out.
Cool. Now let's incorporate the createUserWithEmailAndPassword() Firebase function in the onSubmit form handler.
"use client"
import { auth } from "@/firebase/firebase"
import { createUserWithEmailAndPassword } from "firebase/auth"
import { useState } from "react"
export function SignUpForm() {
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
const handleFormSubmit = (event) => {
event.preventDefault()
createUserWithEmailAndPassword(auth, email, password)
.then((userCredential) => {
console.log("userCredential", userCredential)
})
.catch((error) => {
console.log("Error:", error)
})
}
return (
<form onSubmit={handleFormSubmit}>
<label>Email:</label>
<input
type="text"
value={email}
onChange={(event) => setEmail(event.target.value)}
/>
<label>Password:</label>
<input
type="password"
value={password}
onChange={(event) => setPassword(event.target.value)}
/>
<button type="submit">Sign up</button>
</form>
)
}
Before I explain how it works, let's see it in action.
Notes
-
The
createUserWithEmailAndPassword()returns aUserCredentialinstance with a lot of stuff inside it. We'll review that soon. -
The Authentication tab of the Emulator Suite shows the created user account.
-
In the browser console, there's a little info
icon next to theuserCredentialobject with a message that saysThis value was evaluated upon first expanding. It may have changed since then.
DO NOT IGNORE THIS NOTE! It means that the value you see right now might not be the value that existed 10 seconds ago or 10 seconds in the future. This behavior has caused me a lot of pain and suffering. To avoid this, I find it's best to use
console.log("someObject", JSON.stringify(someObject))rather than
console.log("someObject", someObject)JSON.stringify(someObject)displayssomeObjectas it existed at the time it was logged.
userCredentialΒΆ
A successful invocation of the createUserWithEmailAndPassword() function returns a UserCredential instance. Here's the latest userCredential value returned to me after creating a new user.
{
user: {
uid: "jTzMOkkkEunEdXK9x29lsjVRu7bJ",
email: "[email protected]",
emailVerified: false,
isAnonymous: false,
providerData: [
{
providerId: "password",
uid: "[email protected]",
displayName: null,
email: "[email protected]",
phoneNumber: null,
photoURL: null,
},
],
stsTokenManager: {
refreshToken:
"eyJfQXV0aEVtdWxhdG9yUmVmcmVzaFRva2VuIjoiRE8gTk9UIE1PRElGWSIsImxvY2FsSWQiOiJqVHpNT2tra0V1bkVkWEs5eDI5bHNqVlJ1N2JKIiwicHJvdmlkZXIiOiJwYXNzd29yZCIsImV4dHJhQ2xhaW1zIjp7fSwicHJvamVjdElkIjoiZmlyZWF1dGg1NSJ9",
accessToken:
"eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJlbWFpbCI6InJpY2tAZ21haWwuY29tIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJhdXRoX3RpbWUiOjE3MjM4MzY1NTcsInVzZXJfaWQiOiJqVHpNT2tra0V1bkVkWEs5eDI5bHNqVlJ1N2JKIiwiZmlyZWJhc2UiOnsiaWRlbnRpdGllcyI6eyJlbWFpbCI6WyJyaWNrQGdtYWlsLmNvbSJdfSwic2lnbl9pbl9wcm92aWRlciI6InBhc3N3b3JkIn0sImlhdCI6MTcyMzgzNjU1NywiZXhwIjoxNzIzODQwMTU3LCJhdWQiOiJmaXJlYXV0aDU1IiwiaXNzIjoiaHR0cHM6Ly9zZWN1cmV0b2tlbi5nb29nbGUuY29tL2ZpcmVhdXRoNTUiLCJzdWIiOiJqVHpNT2tra0V1bkVkWEs5eDI5bHNqVlJ1N2JKIn0.",
expirationTime: 1723840157813,
},
createdAt: "1723836557810",
lastLoginAt: "1723836557810",
apiKey: "AIzaSyRUHG9ZP-vUWBqkPU5I",
appName: "[DEFAULT]",
},
providerId: null,
_tokenResponse: {
kind: "identitytoolkit#SignupNewUserResponse",
localId: "jTzMOkkkEunEdXK9x29lsjVRu7bJ",
email: "[email protected]",
idToken:
"eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJlbWFpbCI6InJpY2tAZ21haWwuY29tIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJhdXRoX3RpbWUiOjE3MjM4MzY1NTcsInVzZXJfaWQiOiJqVHpNT2tra0V1bkVkWEs5eDI5bHNqVlJ1N2JKIiwiZmlyZWJhc2UiOnsiaWRlbnRpdGllcyI6eyJlbWFpbCI6WyJyaWNrQGdtYWlsLmNvbSJdfSwic2lnbl9pbl9wcm92aWRlciI6InBhc3N3b3JkIn0sImlhdCI6MTcyMzgzNjU1NywiZXhwIjoxNzIzODQwMTU3LCJhdWQiOiJmaXJlYXV0aDU1IiwiaXNzIjoiaHR0cHM6Ly9zZWN1cmV0b2tlbi5nb29nbGUuY29tL2ZpcmVhdXRoNTUiLCJzdWIiOiJqVHpNT2tra0V1bkVkWEs5eDI5bHNqVlJ1N2JKIn0.",
refreshToken:
"eyJfQXV0aEVtdWxhdG9yUmVmcmVzaFRva2VuIjoiRE8gTk9UIE1PRElGWSIsImxvY2FsSWQiOiJqVHpNT2tra0V1bkVkWEs5eDI5bHNqVlJ1N2JKIiwicHJvdmlkZXIiOiJwYXNzd29yZCIsImV4dHJhQ2xhaW1zIjp7fSwicHJvamVjdElkIjoiZmlyZWF1dGg1NSJ9",
expiresIn: "3600",
},
operationType: "signIn",
}
Notice the user object and other details embedded inside this userCredential.
Network ActivityΒΆ
Now let's see what happens in the Network tab when we create a user...
Interestingly, there are four different requests. Why? π€
Breakdown
-
The first request is an
OPTIONSrequest. It looks like this πfetch("http://127.0.0.1:9099/identitytoolkit.googleapis.com/v1/accounts:signUp?key=AIzaSyRUHG9ZP-vUWBqkPU5I", { "headers": { "accept": "*/*", "accept-language": "en-US,en;q=0.9", "cache-control": "no-cache", "pragma": "no-cache", "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "cross-site" }, "referrerPolicy": "no-referrer", "body": null, "method": "OPTIONS", "mode": "cors", "credentials": "omit" });This is what's called a preflight request. The
createUserWithEmailAndPassword()function didn't tell your browser to make this request. It told your browser to make aPOSTrequest. But before making thePOSTrequest, the browser interjects and saysWhoa there.. before I make that
POSTrequest, I need to make an OPTIONS request. If the server returns the response I expect, then I'll make thePOSTrequest you asked for.Why does it do this? It's a protection against legacy servers that implemented logic before CORS was a thing. Frankly, you can ignore these preflight requests. But if you're curious, this is a nice StackOverflow thread on the matter.
-
The second request is a
POSTrequest πfetch("http://127.0.0.1:9099/identitytoolkit.googleapis.com/v1/accounts:signUp?key=AIzaSyRUHG9ZP-vUWBqkPU5I", { "headers": { "accept": "*/*", "accept-language": "en-US,en;q=0.9", "cache-control": "no-cache", "content-type": "application/json", "pragma": "no-cache", "sec-ch-ua": "\"Not)A;Brand\";v=\"99\", \"Google Chrome\";v=\"127\", \"Chromium\";v=\"127\"", "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": "\"macOS\"", "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "cross-site", "x-client-version": "Chrome/JsCore/10.13.0/FirebaseCore-web", "x-firebase-gmpid": "1:1569792077:web:7263700fbcb18fa02564" }, "referrerPolicy": "no-referrer", "body": "{\"returnSecureToken\":true,\"email\":\"[email protected]\",\"password\":\"hunter1\",\"clientType\":\"CLIENT_TYPE_WEB\"}", "method": "POST", "mode": "cors", "credentials": "omit" });This is where things get juicy π. Here, Firebase makes a request to the endpoint
identitytoolkit.googleapis.com/v1/accounts:signUp, passing along the email and password in the request body.The identity toolkit creates the user account and returns the following response π
{ "kind": "identitytoolkit#SignupNewUserResponse", "localId": "rw989AAMzeEiULUS0guvTbL4uaLH", "email": "[email protected]", "idToken": "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJlbWFpbCI6Imp1bGlhbkBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImF1dGhfdGltZSI6MTcyMzg2MDM0OSwidXNlcl9pZCI6InJ3OTg5QUFNemVFaVVMVVMwZ3V2VGJMNHVhTEgiLCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7ImVtYWlsIjpbImp1bGlhbkBnbWFpbC5jb20iXX0sInNpZ25faW5fcHJvdmlkZXIiOiJwYXNzd29yZCJ9LCJpYXQiOjE3MjM4NjAzNDksImV4cCI6MTcyMzg2Mzk0OSwiYXVkIjoiZmlyZWF1dGg1NSIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9maXJlYXV0aDU1Iiwic3ViIjoicnc5ODlBQU16ZUVpVUxVUzBndXZUYkw0dWFMSCJ9.", "refreshToken": "eyJfQXV0aEVtdWxhdG9yUmVmcmVzaFRva2VuIjoiRE8gTk9UIE1PRElGWSIsImxvY2FsSWQiOiJydzk4OUFBTXplRWlVTFVTMGd1dlRiTDR1YUxIIiwicHJvdmlkZXIiOiJwYXNzd29yZCIsImV4dHJhQ2xhaW1zIjp7fSwicHJvamVjdElkIjoiZmlyZWF1dGg1NSJ9", "expiresIn": "3600" } -
The third request is another CORS preflight request. (Boring!)
-
The fourth request is a another
POSTrequest.fetch("http://127.0.0.1:9099/identitytoolkit.googleapis.com/v1/accounts:lookup?key=AIzaSyRUHG9ZP-vUWBqkPU5I", { "headers": { "accept": "*/*", "accept-language": "en-US,en;q=0.9", "cache-control": "no-cache", "content-type": "application/json", "pragma": "no-cache", "sec-ch-ua": "\"Not)A;Brand\";v=\"99\", \"Google Chrome\";v=\"127\", \"Chromium\";v=\"127\"", "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": "\"macOS\"", "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "cross-site", "x-client-version": "Chrome/JsCore/10.13.0/FirebaseCore-web", "x-firebase-gmpid": "1:1569792077:web:7263700fbcb18fa02564" }, "referrerPolicy": "no-referrer", "body": "{\"idToken\":\"eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJlbWFpbCI6Imp1bGlhbkBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImF1dGhfdGltZSI6MTcyMzg2MDM0OSwidXNlcl9pZCI6InJ3OTg5QUFNemVFaVVMVVMwZ3V2VGJMNHVhTEgiLCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7ImVtYWlsIjpbImp1bGlhbkBnbWFpbC5jb20iXX0sInNpZ25faW5fcHJvdmlkZXIiOiJwYXNzd29yZCJ9LCJpYXQiOjE3MjM4NjAzNDksImV4cCI6MTcyMzg2Mzk0OSwiYXVkIjoiZmlyZWF1dGg1NSIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9maXJlYXV0aDU1Iiwic3ViIjoicnc5ODlBQU16ZUVpVUxVUzBndXZUYkw0dWFMSCJ9.\"}", "method": "POST", "mode": "cors", "credentials": "omit" });This request looks just like the previous
POSTrequest, but instead of hitting theaccounts:signUpendpoint, it hits theaccounts:lookupendpoint. Furthermore, the response is a bit different π{ "kind": "identitytoolkit#GetAccountInfoResponse", "users": [ { "localId": "rw989AAMzeEiULUS0guvTbL4uaLH", "lastLoginAt": "1723860349194", "emailVerified": false, "email": "[email protected]", "salt": "fakeSaltpQlj5gIt73DfCwHad8Pg", "passwordHash": "fakeHash:salt=fakeSaltpQlj5gIt73DfCwHad8Pg:password=hunter1", "passwordUpdatedAt": 1723860349194, "validSince": "1723860349", "createdAt": "1723860349194", "providerUserInfo": [ { "providerId": "password", "email": "[email protected]", "federatedId": "[email protected]", "rawId": "[email protected]" } ], "lastRefreshAt": "2024-08-17T02:05:49.197Z" } ] }
Let's try that again, except this time let's use a mangled email address so that we get an error.
In this case, the first POST request returns the following response headers π
HTTP/1.1 400 Bad Request
X-Powered-By: Express
Access-Control-Allow-Origin: http://localhost:3000
Vary: Origin
Content-Type: application/json; charset=utf-8
Content-Length: 205
ETag: W/"cd-0JYghHL+cORBSUZggCdEJkfW1z8"
Date: Sat, 17 Aug 2024 02:14:39 GMT
Connection: keep-alive
Keep-Alive: timeout=5
and response body
{
"error": {
"code": 400,
"message": "MISSING_PASSWORD",
"errors": [
{
"message": "MISSING_PASSWORD",
"reason": "invalid",
"domain": "global"
}
]
}
}
Furthermore, the second POST request we saw earlier (the one that hits the accounts:lookup endpoint) is not generated.
Application StorageΒΆ
The Application tab in the Chrome DevTools window shows you all the data that an application stores inside your browser (i.e. client-side storage).
Upon creating a new user, Firebase writes information about the user to an indexed database named firebaseLocalStorage.