Skip to content

Commit 3c2cd3d

Browse files
committed
feat: update base FPNV
1 parent 1c9057a commit 3c2cd3d

File tree

3 files changed

+173
-3
lines changed

3 files changed

+173
-3
lines changed

firebase.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import (
3333
"firebase.google.com/go/v4/messaging"
3434
"firebase.google.com/go/v4/remoteconfig"
3535
"firebase.google.com/go/v4/storage"
36+
"firebase.google.com/go/v4/fpnv"
3637
"google.golang.org/api/option"
3738
"google.golang.org/api/transport"
3839
)
@@ -155,6 +156,14 @@ func (a *App) RemoteConfig(ctx context.Context) (*remoteconfig.Client, error) {
155156
return remoteconfig.NewClient(ctx, conf)
156157
}
157158

159+
// Fpnv returns an instance of fpnv.Client.
160+
func (a *App) Fpnv(ctx context.Context) (*fpnv.Client, error) {
161+
conf := &internal.FpnvConfig{
162+
ProjectID: a.projectID,
163+
}
164+
return fpnv.NewClient(ctx, conf)
165+
}
166+
158167
// NewApp creates a new App from the provided config and client options.
159168
//
160169
// If the client options contain a valid credential (a service account file, a refresh token

fpnv/fpnv.go

Lines changed: 159 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,162 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
// Package messaging contains functions for sending messages and managing
16-
// device subscriptions with Firebase Cloud Messaging (FCM).
17-
package fpnv
15+
// Package fpnv provides functionality for Firebase Phone Number Verification (FPNV) tokens.
16+
package fpnv
17+
18+
import (
19+
"context"
20+
"errors"
21+
"slices"
22+
"strings"
23+
"time"
24+
25+
"github.com/MicahParks/keyfunc"
26+
"github.com/golang-jwt/jwt/v4"
27+
28+
"firebase.google.com/go/v4/internal"
29+
)
30+
31+
const (
32+
fpnvJWKSURL = "https://fpnv.googleapis.com/v1beta/jwks"
33+
fpnvIssuer = "https://fpnv.googleapis.com/projects/"
34+
algorithm = "ES256"
35+
headerTyp = "JWT"
36+
)
37+
38+
var (
39+
// ErrTokenHeaderKid is returned when the token has no 'kid' claim.
40+
ErrTokenHeaderKid = errors.New("FPNV token has no 'kid' claim")
41+
// ErrIncorrectAlgorithm is returned when the token is signed with a non-ES256 algorithm.
42+
ErrIncorrectAlgorithm = errors.New("FPNV token has incorrect algorithm")
43+
// ErrTokenType is returned when the token is not a JWT.
44+
ErrTokenType = errors.New("FPNV token has incorrect type")
45+
// ErrTokenClaims is returned when the token claims cannot be decoded.
46+
ErrTokenClaims = errors.New("FPNV token has incorrect claims")
47+
// ErrTokenAudience is returned when the token audience does not match the current project.
48+
ErrTokenAudience = errors.New("FPNV token has incorrect audience")
49+
// ErrTokenIssuer is returned when the token issuer does not match FPNV service.
50+
ErrTokenIssuer = errors.New("FPNV token has incorrect issuer")
51+
// ErrTokenSubject is returned when the token subject is empty or missing.
52+
ErrTokenSubject = errors.New("FPNV token has empty or missing subject")
53+
)
54+
55+
// DecodedFpnvToken represents a verified FPNV token.
56+
//
57+
// DecodedFpnvToken provides typed accessors to the common JWT fields such as Audience (aud)
58+
// and ExpiresAt (exp). Additionally, it provides an PhoneNumber field,
59+
// which is alias for Subject (sub).
60+
// Any additional JWT claims can be accessed via the Claims map of DecodedFpnvToken.
61+
type DecodedFpnvToken struct {
62+
Issuer string
63+
Subject string
64+
Audience []string
65+
ExpiresAt time.Time
66+
IssuedAt time.Time
67+
PhoneNumber string
68+
Claims map[string]interface{}
69+
}
70+
71+
// Client is the client for the Firebase Phone Number Verification service.
72+
type Client struct {
73+
projectID string
74+
jwks *keyfunc.JWKS
75+
}
76+
77+
// NewClient creates a new instance of the Firebase Phone Number Verification Client.
78+
//
79+
// This function can only be invoked from within the SDK. Client applications should access the
80+
// FPNV service through firebase.App.
81+
func NewClient(ctx context.Context, conf *internal.FpnvConfig) (*Client, error) {
82+
jwks, err := keyfunc.Get(fpnvJWKSURL, keyfunc.Options{
83+
Ctx: ctx,
84+
RefreshInterval: 10 * time.Minute,
85+
})
86+
if err != nil {
87+
return nil, err
88+
}
89+
90+
return &Client{
91+
projectID: conf.ProjectID,
92+
jwks: jwks,
93+
}, nil
94+
}
95+
96+
// VerifyToken verifies the given Firebase Phone Number Verification (FPNV) token.
97+
//
98+
// VerifyToken considers a Firebase Phone Number Verification token string to be valid
99+
// if all the following conditions are met:
100+
// - The token string is a valid ES256 JWT.
101+
// - The JWT contains valid issuer (iss) and audience (aud) claims that match the issuerPrefix
102+
// and projectID of the tokenVerifier.
103+
// - The JWT is not expired, and it has been issued some time in the past.
104+
//
105+
// If any of the above conditions are not met, an error is returned.
106+
// Otherwise, a pointer to a decoded FPNV token is returned.
107+
func (c *Client) VerifyToken(token string) (*DecodedFpnvToken, error) {
108+
// The standard JWT parser also validates the expiration of the token
109+
// so we do not need dedicated code for that.
110+
111+
// Header part
112+
decodedToken, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
113+
if t.Header["kid"] == nil {
114+
return nil, ErrTokenHeaderKid
115+
}
116+
if t.Header["alg"] != algorithm {
117+
return nil, ErrIncorrectAlgorithm
118+
}
119+
if t.Header["typ"] != headerTyp {
120+
return nil, ErrTokenType
121+
}
122+
return c.jwks.Keyfunc(t)
123+
})
124+
if err != nil {
125+
return nil, err
126+
}
127+
128+
// Payload part
129+
claims, ok := decodedToken.Claims.(jwt.MapClaims)
130+
if !ok {
131+
return nil, ErrTokenClaims
132+
}
133+
134+
rawAud := claims["aud"].([]interface{})
135+
var aud []string
136+
for _, v := range rawAud {
137+
aud = append(aud, v.(string))
138+
}
139+
140+
if !slices.Contains(aud, fpnvIssuer+c.projectID) {
141+
return nil, ErrTokenAudience
142+
}
143+
144+
// We check the prefix to make sure this token was issued
145+
// by the Firebase Phone Number Verification service, but we do not check the
146+
// Project Number suffix because the Golang SDK only has project ID.
147+
//
148+
// This is consistent with the Firebase Admin Node SDK.
149+
if !strings.HasPrefix(claims["iss"].(string), fpnvIssuer) {
150+
return nil, ErrTokenIssuer
151+
}
152+
153+
if val, ok := claims["sub"].(string); !ok || val == "" {
154+
return nil, ErrTokenSubject
155+
}
156+
157+
decodedFpnvToken := DecodedFpnvToken{
158+
Issuer: claims["iss"].(string),
159+
Subject: claims["sub"].(string),
160+
Audience: aud,
161+
ExpiresAt: time.Unix(int64(claims["exp"].(float64)), 0),
162+
IssuedAt: time.Unix(int64(claims["iat"].(float64)), 0),
163+
PhoneNumber: claims["sub"].(string),
164+
}
165+
166+
// Remove all the claims we've already parsed.
167+
for _, usedClaim := range []string{"iss", "sub", "aud", "exp", "iat", "sub"} {
168+
delete(claims, usedClaim)
169+
}
170+
decodedFpnvToken.Claims = claims
171+
172+
return &decodedFpnvToken, nil
173+
}

internal/internal.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@ type AppCheckConfig struct {
8686
ProjectID string
8787
}
8888

89+
// FpnvConfig represents the configuration of Firebase Phone Number Verification service.
90+
type FpnvConfig struct {
91+
ProjectID string
92+
}
93+
8994
// MockTokenSource is a TokenSource implementation that can be used for testing.
9095
type MockTokenSource struct {
9196
AccessToken string

0 commit comments

Comments
 (0)