Firebase Auth authorized domains
- 6 minutes read - 1148 wordsI’m using Firebase Authentication in a project to authenticate users of various OAuth2 identity systems. Firebase Authentication requires a set of Authorized Domains.
The (web) app that interacts with Firebase Authentication is deployed to Cloud Run. The Authorized Domains list must include the app’s Cloud Run service URL.
Cloud Run service URLs vary by Project (ID). They are a combination of the service name, a hash (?) of the Project (ID) and .a.run.app
.
As I’m testing the app, I want to be able to programmatically update the list of Firebase Auth’s authorized domains.
curl
The solution (more on this below) is to curl
Google’s Identity Platform (?) endpoint: identitytoolkit.googleapis.com/admin/v2
. The flow is:
- getConfig
- extract the list of
authorizedDomains
from the current (!)Config
- add the Cloud Run service URL and then updateConfig
- field masking the
Config
body so that I need only provideauthorizedDomains
:
PROJECT="..." # Firebase Authentication project ID
URL="https://identitytoolkit.googleapis.com/v2/projects/${PROJECT}"
MASK="authorizedDomains"
FILTER=".${MASK} | . += [\"${HOST}\"] | { \"${MASK}\": . }"
DATA=$(curl \
--request GET \
--header "Authorization: Bearer ${TOKEN}" \
--header "Accept: application/json" \
--silent \
--compressed \
${URL}/config \
| jq "${FILTER}")
So the jq
FILTER
is very likely sub-optimal, but it works.
Assuming Config
contains authorizedDomains
:
{
"authorizedDomnains": [
"foo",
"bar",
]
}
We grab the array ([...]
) and add an array (!) to it containing the URL (${HOST}
) of a Cloud Run Service.
NOTE This is one way to grab the service URL, given the service name and project is. This returns only the fully-qualified host (i.e.
some-service-xxxxxxxxxx-xx.a.run.app
) from the URL:HOST=$(gcloud run services describe ${NAME} \ --project=${PROJECT} \ --platform=managed \ --region=${REGION} \ --format="value(status.address.url)") && \ HOST=${HOST#https://} && \ echo ${HOST}
Then – and there’s very likely a way to do this without extracting authorizedDomains
from the JSON only to put it back – is to take the result of list append and return:
{
"authorizedDomains": [...]
}
This is all that’s needed for the Config
we give to updateConfig
because we can use a field mask to tell the service we’re only updating authorizedDomains
:
curl \
--request PATCH \
--header "Authorization: Bearer ${TOKEN}" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--data "${DATA}" \
--silent \
--compressed \
${URL}/config?updateMask=${MASK}
That should work!
One important consideration is that you won’t be able to perform this using your regular human credentials obtained using gcloud auth print-access-token
. This is because the project that backs gcloud
does not have the Identity Platform service enabled.
You’ll want to:
- Enable
identitytoolkit
service - Create a Service Account in Firebase Auth project
- Activate the Service Account in
gcloud
- Generate an access token using it
Something along the lines of:
PROJECT="..." # Firebase Auth project ID
# Enable the Identity Platform service
SERVICE="identitytoolkit"
gcloud services enable ${SERVICE}.googleapis.com \
--project=${PROJECT}
ACCOUNT="..." # Your preference
EMAIL="${ACCOUNT}@${PROJECT}.iam.gserviceaccount.com"
# Create Service Account in Firebase Auth project
gcloud iam service-accounts create ${ACCOUNT} \
--project=${PROJECT}
# Grant it sufficient permissions
# Unclear whether firebaseauth.admin is correct
# https://cloud.google.com/identity-platform/docs/access-control
gcloud projects add-iam-policy-binding ${PROJECT} \
--member=serviceAccount:${EMAIL} \
--role=roles/firebaseauth.admin
# Create a key
gcloud iam service-accounts keys create ${PWD}/${ACCOUNT}.json \
--iam-account=${EMAIL} \
--project=${PROJECT}
# Retain current active account
DEFAULT="$(gcloud config get-value account)"
# Activate Service Account
gcloud auth activate-service-account \
--key-file=${PWD}/${ACCOUNT}.json
# Revert to previously active account
gcloud config set account ${DEFAULT}
# Generate access token using the Service Account
TOKEN=$(gcloud auth print-access-token ${EMAIL})
When you’re done, you may want to revoke the credentials of the Service Account and delete it:
gcloud auth revoke ${EMAIL}
gcloud iam service-accounts delete ${EMAIL} \
--project=${PROJECT}
SDKs… or not
I’m familiar with Google’s myriad language SDKs but I had problems with Identity Toolkit|Platform v2|v3.
I’m confident there aren’t Cloud Client Libraries for it for Golang or Python.
There should (!) be API Client Libraries because these are machine|auto-generated.
However, starting with APIs Explorer, Identity Toolkit API redirects instead of listing the APIs methods.
The getConfig and updateConfig that I referenced previously are anchored around this page:
https://cloud.google.com/identity-platform/docs/reference/rest
Which is called “Identity Platform” not “Identity Toolkit”.
OK…
There are 2 discovery documents associated with the api: v1 and v2. We may return to those.
Golang
The only API I was able to find for the Golang API Client Library is v3 (!)
https://pkg.go.dev/google.golang.org/api@v0.59.0/identitytoolkit/v3
This was last updated to v0.59.0 on 22-October-2021. So it’s current.
A type called RelyingpartyService includes methods:
Which certainly appear to match getConfig
and updateConfig
and indeed:
package main
import (
"context"
"log"
"google.golang.org/api/identitytoolkit/v3"
"google.golang.org/api/option"
)
type Client struct {
svc *identitytoolkit.Service
}
func NewClient() (*Client, error) {
ctx := context.Background()
opts := []option.ClientOption{
option.WithScopes("https://www.googleapis.com/auth/cloud-platform"),
}
svc, err := identitytoolkit.NewService(
ctx,
opts...,
)
if err != nil {
return &Client{}, err
}
return &Client{
svc: svc,
}, nil
}
func (c *Client) GetDomains() ([]string, error) {
resp, err := c.svc.Relyingparty.GetProjectConfig().Fields("authorizedDomains").Do()
return resp.AuthorizedDomains, err
}
func main() {
client := NewClient()
authorizedDomains, err := client.GetDomains()
if err != nil {
log.Fatal(err)
}
log.Println(authorizedDomains)
}
As expected logs something of the form:
2021/10/26 13:22:10 [
localhost
${PROJECT}.firebaseapp.com
${PROJECT}.web.app
]
NOTE
*.firebaseapp.com
and*.web.app
are default authorized domains.
But, I’m unable to get SetProjectConfig
to work:
package main
import (
"context"
"log"
"google.golang.org/api/identitytoolkit/v3"
"google.golang.org/api/option"
)
type Client struct {
svc *identitytoolkit.Service
}
func NewClient() (*Client, error) {
ctx := context.Background()
opts := []option.ClientOption{
option.WithScopes("https://www.googleapis.com/auth/cloud-platform"),
}
svc, err := identitytoolkit.NewService(
ctx,
opts...,
)
if err != nil {
return &Client{}, err
}
return &Client{
svc: svc,
}, nil
}
func (c *Client) GetDomains() ([]string, error) {
resp, err := c.svc.Relyingparty.GetProjectConfig().Fields("authorizedDomains").Do()
return resp.AuthorizedDomains, err
}
func (c *Client) AddDomain(domain string) error {
domains, err := c.GetDomains()
if err != nil {
return err
}
domains = append(domains, domain)
return c.SetDomains(domains)
}
func (c *Client) SetDomains(domains []string) error {
rqst := &identitytoolkit.IdentitytoolkitRelyingpartySetProjectConfigRequest{}
rqst.AuthorizedDomains = domains
_, err := c.svc.Relyingparty.SetProjectConfig(rqst).Fields("authorizedDomains").Do()
return err
}
func main() {
client := NewClient()
authorizedDomains, err := client.GetDomains()
if err != nil {
log.Fatal(err)
}
log.Println(authorizedDomains)
if err := client.AddDomain("xxx.com"); err != nil {
log.Fatal(err)
}
}
Instead I receive:
The requested URL <code>/identitytoolkit/v3/relyingparty/setProjectConfig?alt=json&fields=authorizedDomains&prettyPrint=false</code> was not found on this server.
Python
It appears – though I’ve yet not tried this – that Python’s SDK is the same:
The documentation is:
https://googleapis.github.io/google-api-python-client/docs/dyn/identitytoolkit_v3.html
And:
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
import google.auth
SCOPES = [
'https://www.googleapis.com/auth/cloud-platform'
]
class Client:
def __init__(self,creds):
self.svc = build("identitytoolkit","v3",credentials=creds)
def get_domains(self):
rqst = self.svc.relyingparty().getProjectConfig()
try:
resp = rqst.execute()
return resp["authorizedDomains"]
except HttpError as e:
print("[{code}] {message}".format(code=e.resp.status,message=e.resp.reason))
def add_domain(self,domain):
domains=self.get_domains()
domains.append(domain)
try:
self.set_domains(domains)
except HttpError as e:
print("[{code}] {message}".format(code=e.resp.status,message=e.resp.reason))
def set_domains(self,domains):
rqst = self.svc.relyingparty().setProjectConfig()
try:
resp=rqst.execute()
except HttpError as e:
print("[{code}] {message}".format(code=e.resp.status,message=e.resp.reason))
def main():
creds, project_id = google.auth.default(scopes=SCOPES)
client = Client(creds)
domains=client.get_domains()
print(domains)
client.add_domain("baz")
domains=client.get_domains()
print(domains)
if __name__ == "__main__":
main()
It’s noteworthy that Python’s error includes the URL:
<HttpError 404 when requesting https://www.googleapis.com/identitytoolkit/v3/relyingparty/setProjectConfig?alt=json returned "Not Found"
Discovery
The Discovery document for “v2” includes getConfig
and updateConfig
but I’ve been unable to generate a client from the document for this version to try it out.