Let's implement a Sign up form where users can create an account using their email address and a password.
Install firebase
from npm
$ npm install firebase
Run this from the working directory: fireauth/
Create the route src/app/signup/page.js
src/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.
src/app/signup/SignUpForm.js
"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 a UserCredential
instance 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 the userCredential
object with a message that says
This 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)
displays someObject
as 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.
userCredential
{
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 OPTIONS
request. 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 a POST
request. But before making the POST
request, the browser interjects and says
Whoa there.. before I make that POST
request, I need to make an OPTIONS request. If the server returns the response I expect, then I'll make the POST
request you asked for.
Why does it do this? It's a protection against legacy servers that implemented logic before CORS Cross-Origin Resource Sharing 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 POST
request π
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 POST
request.
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 POST
request, but instead of hitting the accounts:signUp
endpoint, it hits the accounts:lookup
endpoint. 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 π
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
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
.