Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
54b8114
Merge dev into master
google-oss-bot May 21, 2020
cef91ac
Merge dev into master
google-oss-bot Jun 16, 2020
77177c7
Merge dev into master
google-oss-bot Oct 22, 2020
a957589
Merge dev into master
google-oss-bot Jan 28, 2021
eb0d2a0
Merge dev into master
google-oss-bot Mar 24, 2021
05378ef
Merge dev into master
google-oss-bot Mar 29, 2021
4121c50
Merge dev into master
google-oss-bot Apr 14, 2021
928b104
Merge dev into master
google-oss-bot Jun 2, 2021
02cde4f
Merge dev into master
google-oss-bot Nov 4, 2021
6b40682
Merge dev into master
google-oss-bot Dec 15, 2021
e60757f
Merge dev into master
google-oss-bot Jan 20, 2022
bb055ed
Merge dev into master
google-oss-bot Apr 6, 2022
23a1f17
Merge dev into master
google-oss-bot Oct 6, 2022
1d24577
Merge dev into master
google-oss-bot Nov 10, 2022
61c6c04
Merge dev into master
google-oss-bot Apr 6, 2023
32af2b8
[chore] Release 4.12.0 (#561)
lahirumaramba Jun 22, 2023
02300a8
Revert "[chore] Release 4.12.0 (#561)" (#565)
lahirumaramba Jul 11, 2023
74c9bd5
Merge dev into master
google-oss-bot Jul 12, 2023
37c7936
Merge dev into master
google-oss-bot Sep 25, 2023
b04387e
Merge dev into master
google-oss-bot Nov 23, 2023
87b867c
Merge dev into master
google-oss-bot Apr 10, 2024
6a28190
Merge dev into master
google-oss-bot May 30, 2024
c3be6f2
Merge dev into master
google-oss-bot Oct 24, 2024
afeaa15
Merge dev into master
google-oss-bot Dec 5, 2024
570427a
Merge dev into master
google-oss-bot Feb 13, 2025
fe866a0
Merge dev into master
google-oss-bot Jun 5, 2025
db240e4
Merge dev into master
google-oss-bot Jun 11, 2025
d515faf
Merge dev into master
google-oss-bot Jul 17, 2025
26dec0b
Merge dev into master
google-oss-bot Jul 31, 2025
4f7026f
[chore] Release v4.19.0 (#747)
github-actions[bot] Jan 21, 2026
1c9057a
feat: add base FPNV
boikoa-gl Jan 26, 2026
3c2cd3d
feat: update base FPNV
boikoa-gl Jan 26, 2026
a8315d0
chore: update fpnv
boikoa-gl Jan 26, 2026
05f105a
chore: update tests
boikoa-gl Jan 27, 2026
cd5403d
chore: update tests
boikoa-gl Jan 27, 2026
769f423
chore: resolve comments
boikoa-gl Jan 27, 2026
cd3edca
chore: resolve robot comments
boikoa-gl Jan 27, 2026
2ffc303
Merge branch 'dev' into fpnv
boikoa-gl Jan 28, 2026
c1afebb
chore: resolve gofmt linting
boikoa-gl Jan 28, 2026
7243936
chore: resolve gofmt linting
boikoa-gl Jan 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions firebase.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"firebase.google.com/go/v4/appcheck"
"firebase.google.com/go/v4/auth"
"firebase.google.com/go/v4/db"
"firebase.google.com/go/v4/fpnv"
"firebase.google.com/go/v4/iid"
"firebase.google.com/go/v4/internal"
"firebase.google.com/go/v4/messaging"
Expand Down Expand Up @@ -155,6 +156,14 @@ func (a *App) RemoteConfig(ctx context.Context) (*remoteconfig.Client, error) {
return remoteconfig.NewClient(ctx, conf)
}

// Fpnv returns an instance of fpnv.Client.
func (a *App) Fpnv(ctx context.Context) (*fpnv.Client, error) {
conf := &internal.FpnvConfig{
ProjectID: a.projectID,
}
return fpnv.NewClient(ctx, conf)
}

// NewApp creates a new App from the provided config and client options.
//
// If the client options contain a valid credential (a service account file, a refresh token
Expand Down
12 changes: 12 additions & 0 deletions firebase_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,18 @@ func TestAutoInit(t *testing.T) {
}
}

func TestFpnv(t *testing.T) {
ctx := context.Background()
app, err := NewApp(ctx, nil, option.WithCredentialsFile("testdata/service_account.json"))
if err != nil {
t.Fatal(err)
}

if c, err := app.Fpnv(ctx); c == nil || err != nil {
t.Errorf("Fpnv() = (%v, %v); want (fpnv, nil)", c, err)
}
}

func TestAutoInitInvalidFiles(t *testing.T) {
tests := []struct {
name string
Expand Down
209 changes: 209 additions & 0 deletions fpnv/fpnv.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
// Copyright 2026 Google LLC All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package fpnv provides functionality for Firebase Phone Number Verification (FPNV) tokens.
package fpnv

import (
"context"
"errors"
"slices"
"strings"
"time"

"github.com/MicahParks/keyfunc"
"github.com/golang-jwt/jwt/v4"

"firebase.google.com/go/v4/internal"
)

const (
fpnvJWKSURL = "https://fpnv.googleapis.com/v1beta/jwks"
fpnvIssuer = "https://fpnv.googleapis.com/projects/"
algorithm = "ES256"
headerTyp = "JWT"
)

var (
// ErrTokenHeaderKid is returned when the token has no 'kid' claim.
ErrTokenHeaderKid = errors.New("FPNV token has no 'kid' claim")
// ErrIncorrectAlgorithm is returned when the token is signed with a non-ES256 algorithm.
ErrIncorrectAlgorithm = errors.New("FPNV token has incorrect algorithm")
// ErrTokenType is returned when the token is not a JWT.
ErrTokenType = errors.New("FPNV token has incorrect type")
// ErrTokenClaims is returned when the token claims cannot be decoded.
ErrTokenClaims = errors.New("FPNV token has incorrect claims")
// ErrTokenEmptyAudience is returned when the token audience has no audience.
ErrTokenEmptyAudience = errors.New("FPNV token has no 'aud' claim")
// ErrTokenAudience is returned when the token audience does not match the current project.
ErrTokenAudience = errors.New("FPNV token has incorrect audience")
// ErrTokenIssuer is returned when the token issuer does not match FPNV service.
ErrTokenIssuer = errors.New("FPNV token has incorrect issuer")
// ErrTokenSubject is returned when the token subject is empty or missing.
ErrTokenSubject = errors.New("FPNV token has empty or missing subject")
// ErrTokenExpiresAt is returned when the token has issue with expiresAt.
ErrTokenExpiresAt = errors.New("FPNV token has incorrect expiresAt")
// ErrTokenIssuedAt is returned when the token has issue with issuedAt.
ErrTokenIssuedAt = errors.New("FPNV token has incorrect issuedAt")
)

// DecodedFpnvToken represents a verified FPNV token.
//
// DecodedFpnvToken provides typed accessors to the common JWT fields such as Audience (aud)
// and ExpiresAt (exp). Additionally, it provides an PhoneNumber field,
// which is alias for Subject (sub).
// Any additional JWT claims can be accessed via the Claims map of DecodedFpnvToken.
type DecodedFpnvToken struct {
Issuer string
Subject string
Audience []string
ExpiresAt time.Time
IssuedAt time.Time
PhoneNumber string
Claims map[string]interface{}
}

// Client is the client for the Firebase Phone Number Verification service.
type Client struct {
projectID string
jwks *keyfunc.JWKS
}

// NewClient creates a new instance of the Firebase Phone Number Verification Client.
//
// This function can only be invoked from within the SDK. Client applications should access the
// FPNV service through firebase.App.
func NewClient(ctx context.Context, conf *internal.FpnvConfig) (*Client, error) {
jwks, err := keyfunc.Get(fpnvJWKSURL, keyfunc.Options{
Ctx: ctx,
RefreshInterval: 10 * time.Minute,
})
if err != nil {
return nil, err
}

return &Client{
projectID: conf.ProjectID,
jwks: jwks,
}, nil
}

// VerifyToken verifies the given Firebase Phone Number Verification (FPNV) token.
//
// VerifyToken considers a Firebase Phone Number Verification token string to be valid
// if all the following conditions are met:
// - The token string is a valid ES256 JWT.
// - The JWT contains valid issuer (iss) and audience (aud) claims that match the issuerPrefix
// and projectID of the tokenVerifier.
// - The JWT is not expired, and it has been issued some time in the past.
//
// If any of the above conditions are not met, an error is returned.
// Otherwise, a pointer to a decoded FPNV token is returned.
func (c *Client) VerifyToken(token string) (*DecodedFpnvToken, error) {
if c.projectID == "" {
return nil, errors.New("project ID is required to access Fpnv client")
}

if token == "" {
return nil, errors.New("token must be not empty")
}

// The standard JWT parser also validates the expiration of the token
// so we do not need dedicated code for that.

// Header part
decodedToken, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
if t.Header["kid"] == nil {
return nil, ErrTokenHeaderKid
}
if t.Header["alg"] != algorithm {
return nil, ErrIncorrectAlgorithm
}
if t.Header["typ"] != headerTyp {
return nil, ErrTokenType
}
return c.jwks.Keyfunc(t)
})

if err != nil {
return nil, err
}

// Payload part
claims, ok := decodedToken.Claims.(jwt.MapClaims)
if !ok {
return nil, ErrTokenClaims
}

_, okAud := claims["aud"]
if !okAud {
return nil, ErrTokenEmptyAudience
}

var aud []string
switch v := claims["aud"].(type) {
case string:
aud = []string{v}
case []interface{}:
for _, s := range v {
if str, ok := s.(string); ok {
aud = append(aud, str)
}
}
}

if !slices.Contains(aud, fpnvIssuer+c.projectID) {
return nil, ErrTokenAudience
}

// Prepare claims for DecodedFpnvToken
sub, ok := claims["sub"].(string)
if !ok || sub == "" {
return nil, ErrTokenSubject
}
iss, ok := claims["iss"].(string)
// We check the prefix to make sure this token was issued
// by the Firebase Phone Number Verification service, but we do not check the
// Project Number suffix because the Golang SDK only has project ID.
//
// This is consistent with the Firebase Admin Node SDK.
if !ok || !strings.HasPrefix(iss, fpnvIssuer) {
return nil, ErrTokenIssuer
}
exp, ok := claims["exp"].(float64)
if !ok || exp == 0 {
return nil, ErrTokenExpiresAt
}
iat, ok := claims["iat"].(float64)
if !ok || iat == 0 {
return nil, ErrTokenIssuedAt
}

decodedFpnvToken := DecodedFpnvToken{
Issuer: iss,
Subject: sub,
Audience: aud,
ExpiresAt: time.Unix(int64(exp), 0),
IssuedAt: time.Unix(int64(iat), 0),
PhoneNumber: sub,
}

// Remove all the claims we've already parsed.
for _, usedClaim := range []string{"iss", "sub", "aud", "exp", "iat"} {
delete(claims, usedClaim)
}
decodedFpnvToken.Claims = claims

return &decodedFpnvToken, nil
}
Loading