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+ }
0 commit comments