Captain Codeman Captain Codeman

Firebase signInWithRedirect, localhost, and SvelteKit

Making it all work together

Contents

Introduction

Have you suddenly started having problems with Firebase Auth? It’s likely due to recent security changes in Chrome that had already been implemented in Safari and Firefox for a while, which have an impact on how Firebase signInWithRedirect operates … or doesn’t.

While there are some recommended best practices to follow to work around the issue, some of them are easier said than done and you may find yourself feeling like a pin-ball bouncing from issue to issue.

So I thought I’d document what worked for me, so I’ll have something to refer to myself. This is aimed at SvelteKit but should apply to any web development based on Vite.

The issue is how to:

  • enable HTTPS on localhost
  • … so that undici doesn’t crash
  • … so that the firebase auth redirect works
  • … so that you can sign in

Of course you’ll need to replace [your-project-id] in the examples with your actual Firebase project ID along with the rest of the config.

Create a certificate for localhost

There are Vite plugins that promise to do this but I found these instructions to create a certificate for localhost worked best for me (and because I hate having to install extra dependencies).

Run this command in the project root:

openssl req -x509 -out certs/localhost.crt -keyout certs/localhost.key \
  -newkey rsa:2048 -nodes -sha256 \
  -subj '/CN=localhost' -extensions EXT -config <( \
   printf "[dn]\nCN=localhost\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:localhost\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth")

Specify the certs in the server block of your vite.config.ts together with a proxy definition for the auth. Fun fact: if you’re trying to enable https and don’t include at least an empty proxy block, you’ll get weird undici errors … fun times.

import { readFileSync } from 'fs'

export default defineConfig({
  // other config ...
  server: {
    https: {
      key: readFileSync('certs/localhost.key'),
      cert: readFileSync('certs/localhost.crt'),
    },
    proxy: {
      '/__/auth': {
        target: 'https://[your-project-id].firebaseapp.com',
        changeOrigin: true,
      },
    },
  },
  // other config ...
})

Configure Firebase

I always like to put my Firebase config into .env files, because in production the infrastructure is automated and the configuratino comes from the environment that is populated using Terraform. This is for development though. Note that this is the config you get from the Firebase console:

.env

PUBLIC_FIREBASE_API_KEY="AIzaSyAxxxxxxxxxxxxxxxxxxxxxxxxx"
PUBLIC_FIREBASE_AUTH_DOMAIN="[your-project-id].firebaseapp.com"
PUBLIC_FIREBASE_DATABASE_URL="https://[your-project-id]-default-rtdb.firebaseio.com"
PUBLIC_FIREBASE_PROJECT_ID="[your-project-id]
PUBLIC_FIREBASE_STORAGE_BUCKET="[your-project-id].appspot.com"
PUBLIC_FIREBASE_MESSAGING_SENDER_ID="999999999999"
PUBLIC_FIREBASE_APP_ID="1:444444444444:web:aaaaaaaaaaaaaaaaaa"
PUBLIC_FIREBASE_MEASUREMENT_ID="G-JSHDFGJKDH"

then to use localhost for auth during development, we can just override that one setting to point to our dev server, so it will be the same host:

.env.development

PUBLIC_FIREBASE_AUTH_DOMAIN="localhost:5173"

Configure the application

SvelteKit makes loading this config easy and you can decide whether it makes sense to use a static env (build time from the files above) or a dynamic env if you have automated infrastructure or want to deploy the same built code to multiple environments, the code will be similar.

import {
  PUBLIC_FIREBASE_API_KEY,
  PUBLIC_FIREBASE_APP_ID,
  PUBLIC_FIREBASE_AUTH_DOMAIN,
  PUBLIC_FIREBASE_MEASUREMENT_ID,
  PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
  PUBLIC_FIREBASE_PROJECT_ID,
  PUBLIC_FIREBASE_STORAGE_BUCKET,
} from '$env/static/public'

export const config = {
  apiKey: PUBLIC_FIREBASE_API_KEY,
  authDomain: PUBLIC_FIREBASE_AUTH_DOMAIN,
  projectId: PUBLIC_FIREBASE_PROJECT_ID,
  storageBucket: PUBLIC_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
  appId: PUBLIC_FIREBASE_APP_ID,
  measurementId: PUBLIC_FIREBASE_MEASUREMENT_ID,
}

However you load the config, you can use it to initialize the Client-Side Firebase SDK:

import { initializeApp } from 'firebase/app'
import { config } from './config'

export const app = initializeApp(config)

Another way to do it would be to hard-code all the config, but use the dev flag to toggle which Firebase authDomain to use.

Configure the Redirect URI

At this point, the app should run using https on localhost, but when you try to sign in you’ll get a error because it’s now no longer using a configured redirect URL (they configure that for you when your Firebase project is created). It will look like this:

access-blocked

The fix for this is to go to the Google Cloud Credentials page for the project which will look like this:

gcloud-credentials

Click the little edit icon to edit the OAuth 2.0 Client IDs (web client) entry in the middle and you’ll get to this page where you can add the localhost version of the existing Authorised Redirect URI:

authorized-redirect-uri

In case you can’t read it, it’s https://localhost:5173/__/auth/handler which is similar to the existing Firebase entry, but on localhost. This is what you also need to configure if you want to use a custom domain in production.

It doesn’t hurt to also add the localhost address as an Authorised JavaScript Origin too.

Production

That should be everything needed to make localhost development work. Whether you need to do anything for production will depend on your hosting setup and whether you use a custom domain, but you can easily add a production proxy in the same way. I normally do that in a server.js file which is the entry point to my app when using the SvelteKit node adapter:

server.js

import http from 'http'
import compression from 'http-compression'
import httpProxy from 'http-proxy'
import { handler } from './build/handler.js'

const compress = compression()
const proxy = httpProxy.createProxy()

http
  .createServer((req, res) => {
    compress(req, res, () => {
      if (req.url?.startsWith('/__/auth/')) {
        proxy.web(req, res, {
          target: 'https://[my-project-id].firebaseapp.com',
          secure: false,
        })
      } else {
        handler(req, res, (err) => {
          if (err) {
            res.writeHead(500)
            res.end(err.toString())
          } else {
            res.writeHead(404)
            res.end()
          }
        })
      }
    })
  })
  .listen(8080)

Hope this helps!