16
16
package auth
17
17
18
18
import (
19
- "crypto/rsa"
20
19
"encoding/json"
21
20
"errors"
22
21
"fmt"
@@ -58,34 +57,15 @@ type Token struct {
58
57
Claims map [string ]interface {} `json:"-"`
59
58
}
60
59
61
- func (t * Token ) decodeFrom (s string ) error {
62
- // Decode into a regular map to access custom claims.
63
- claims := make (map [string ]interface {})
64
- if err := decode (s , & claims ); err != nil {
65
- return err
66
- }
67
- // Now decode into Token to access the standard claims.
68
- if err := decode (s , t ); err != nil {
69
- return err
70
- }
71
-
72
- // Delete standard claims from the custom claims maps.
73
- for _ , r := range []string {"iss" , "aud" , "exp" , "iat" , "sub" , "uid" } {
74
- delete (claims , r )
75
- }
76
- t .Claims = claims
77
- return nil
78
- }
79
-
80
60
// Client is the interface for the Firebase auth service.
81
61
//
82
62
// Client facilitates generating custom JWT tokens for Firebase clients, and verifying ID tokens issued
83
63
// by Firebase backend services.
84
64
type Client struct {
85
65
is * identitytoolkit.Service
86
- ks keySource
66
+ keySource keySource
87
67
projectID string
88
- snr signer
68
+ signer cryptoSigner
89
69
version string
90
70
}
91
71
@@ -98,40 +78,45 @@ type signer interface {
98
78
//
99
79
// This function can only be invoked from within the SDK. Client applications should access the
100
80
// Auth service through firebase.App.
101
- func NewClient (ctx context.Context , c * internal.AuthConfig ) (* Client , error ) {
81
+ func NewClient (ctx context.Context , conf * internal.AuthConfig ) (* Client , error ) {
102
82
var (
103
- err error
104
- email string
105
- pk * rsa.PrivateKey
83
+ signer cryptoSigner
84
+ err error
106
85
)
107
- if c .Creds != nil && len (c .Creds .JSON ) > 0 {
108
- var svcAcct struct {
109
- ClientEmail string `json:"client_email"`
110
- PrivateKey string `json:"private_key"`
111
- }
112
- if err := json .Unmarshal (c .Creds .JSON , & svcAcct ); err != nil {
86
+ // Initialize a signer by following the go/firebase-admin-sign protocol.
87
+ if conf .Creds != nil && len (conf .Creds .JSON ) > 0 {
88
+ // If the SDK was initialized with a service account, use it to sign bytes.
89
+ var sa serviceAccount
90
+ if err = json .Unmarshal (conf .Creds .JSON , & sa ); err != nil {
113
91
return nil , err
114
92
}
115
- if svcAcct .PrivateKey != "" {
116
- pk , err = parsePrivateKey (svcAcct .PrivateKey )
93
+ if sa .PrivateKey != "" && sa .ClientEmail != "" {
94
+ var err error
95
+ signer , err = newServiceAccountSigner (sa )
117
96
if err != nil {
118
97
return nil , err
119
98
}
120
99
}
121
- email = svcAcct .ClientEmail
122
100
}
123
-
124
- var snr signer
125
- if email != "" && pk != nil {
126
- snr = serviceAcctSigner {email : email , pk : pk }
127
- } else {
128
- snr , err = newSigner (ctx )
129
- if err != nil {
130
- return nil , err
101
+ if signer == nil {
102
+ if conf .ServiceAccountID != "" {
103
+ // If the SDK was initialized with a service account email, use it with the IAM service
104
+ // to sign bytes.
105
+ signer , err = newIAMSigner (ctx , conf )
106
+ if err != nil {
107
+ return nil , err
108
+ }
109
+ } else {
110
+ // Use GAE signing capabilities if available. Otherwise, obtain a service account email
111
+ // from the local Metadata service, and fallback to the IAM service.
112
+ signer , err = newCryptoSigner (ctx , conf )
113
+ if err != nil {
114
+ return nil , err
115
+ }
131
116
}
132
117
}
133
118
134
- hc , _ , err := transport .NewHTTPClient (ctx , c .Opts ... )
119
+ hc , _ , err := transport .NewHTTPClient (ctx , conf .Opts ... )
135
120
if err != nil {
136
121
return nil , err
137
122
}
@@ -143,25 +128,40 @@ func NewClient(ctx context.Context, c *internal.AuthConfig) (*Client, error) {
143
128
144
129
return & Client {
145
130
is : is ,
146
- ks : newHTTPKeySource (idTokenCertURL , hc ),
147
- projectID : c .ProjectID ,
148
- snr : snr ,
149
- version : "Go/Admin/" + c .Version ,
131
+ keySource : newHTTPKeySource (idTokenCertURL , hc ),
132
+ projectID : conf .ProjectID ,
133
+ signer : signer ,
134
+ version : "Go/Admin/" + conf .Version ,
150
135
}, nil
151
136
}
152
137
153
- // CustomToken creates a signed custom authentication token with the specified user ID. The resulting
154
- // JWT can be used in a Firebase client SDK to trigger an authentication flow. See
138
+ // CustomToken creates a signed custom authentication token with the specified user ID.
139
+ //
140
+ // The resulting JWT can be used in a Firebase client SDK to trigger an authentication flow. See
155
141
// https://firebase.google.com/docs/auth/admin/create-custom-tokens#sign_in_using_custom_tokens_on_clients
156
142
// for more details on how to use custom tokens for client authentication.
143
+ //
144
+ // CustomToken follows the protocol outlined below to sign the generated tokens:
145
+ // - If the SDK was initialized with service account credentials, uses the private key present in
146
+ // the credentials to sign tokens locally.
147
+ // - If a service account email was specified during initialization (via firebase.Config struct),
148
+ // calls the IAM service with that email to sign tokens remotely. See
149
+ // https://cloud.google.com/iam/reference/rest/v1/projects.serviceAccounts/signBlob.
150
+ // - If the code is deployed in the Google App Engine standard environment, uses the App Identity
151
+ // service to sign tokens. See https://cloud.google.com/appengine/docs/standard/go/reference#SignBytes.
152
+ // - If the code is deployed in a different GCP-managed environment (e.g. Google Compute Engine),
153
+ // uses the local Metadata server to auto discover a service account email. This is used in
154
+ // conjunction with the IAM service to sign tokens remotely.
155
+ //
156
+ // CustomToken returns an error the SDK fails to discover a viable mechanism for signing tokens.
157
157
func (c * Client ) CustomToken (ctx context.Context , uid string ) (string , error ) {
158
158
return c .CustomTokenWithClaims (ctx , uid , nil )
159
159
}
160
160
161
161
// CustomTokenWithClaims is similar to CustomToken, but in addition to the user ID, it also encodes
162
162
// all the key-value pairs in the provided map as claims in the resulting JWT.
163
163
func (c * Client ) CustomTokenWithClaims (ctx context.Context , uid string , devClaims map [string ]interface {}) (string , error ) {
164
- iss , err := c .snr .Email (ctx )
164
+ iss , err := c .signer .Email (ctx )
165
165
if err != nil {
166
166
return "" , err
167
167
}
@@ -183,29 +183,19 @@ func (c *Client) CustomTokenWithClaims(ctx context.Context, uid string, devClaim
183
183
}
184
184
185
185
now := clk .Now ().Unix ()
186
- header := jwtHeader {Algorithm : "RS256" , Type : "JWT" }
187
- payload := & customToken {
188
- Iss : iss ,
189
- Sub : iss ,
190
- Aud : firebaseAudience ,
191
- UID : uid ,
192
- Iat : now ,
193
- Exp : now + tokenExpSeconds ,
194
- Claims : devClaims ,
186
+ info := & jwtInfo {
187
+ header : jwtHeader {Algorithm : "RS256" , Type : "JWT" },
188
+ payload : & customToken {
189
+ Iss : iss ,
190
+ Sub : iss ,
191
+ Aud : firebaseAudience ,
192
+ UID : uid ,
193
+ Iat : now ,
194
+ Exp : now + tokenExpSeconds ,
195
+ Claims : devClaims ,
196
+ },
195
197
}
196
- return encodeToken (ctx , c .snr , header , payload )
197
- }
198
-
199
- // RevokeRefreshTokens revokes all refresh tokens issued to a user.
200
- //
201
- // RevokeRefreshTokens updates the user's TokensValidAfterMillis to the current UTC second.
202
- // It is important that the server on which this is called has its clock set correctly and synchronized.
203
- //
204
- // While this revokes all sessions for a specified user and disables any new ID tokens for existing sessions
205
- // from getting minted, existing ID tokens may remain active until their natural expiration (one hour).
206
- // To verify that ID tokens are revoked, use `verifyIdTokenAndCheckRevoked(ctx, idToken)`.
207
- func (c * Client ) RevokeRefreshTokens (ctx context.Context , uid string ) error {
208
- return c .updateUser (ctx , uid , (& UserToUpdate {}).revokeRefreshTokens ())
198
+ return info .Token (ctx , c .signer )
209
199
}
210
200
211
201
// VerifyIDToken verifies the signature and payload of the provided ID token.
@@ -224,11 +214,30 @@ func (c *Client) VerifyIDToken(ctx context.Context, idToken string) (*Token, err
224
214
return nil , fmt .Errorf ("id token must be a non-empty string" )
225
215
}
226
216
227
- h := & jwtHeader {}
228
- p := & Token {}
229
- if err := decodeToken (ctx , idToken , c .ks , h , p ); err != nil {
217
+ if err := verifyToken (ctx , idToken , c .keySource ); err != nil {
218
+ return nil , err
219
+ }
220
+ segments := strings .Split (idToken , "." )
221
+
222
+ var (
223
+ header jwtHeader
224
+ payload Token
225
+ claims map [string ]interface {}
226
+ )
227
+ if err := decode (segments [0 ], & header ); err != nil {
228
+ return nil , err
229
+ }
230
+ if err := decode (segments [1 ], & payload ); err != nil {
231
+ return nil , err
232
+ }
233
+ if err := decode (segments [1 ], & claims ); err != nil {
230
234
return nil , err
231
235
}
236
+ // Delete standard claims from the custom claims maps.
237
+ for _ , r := range []string {"iss" , "aud" , "exp" , "iat" , "sub" , "uid" } {
238
+ delete (claims , r )
239
+ }
240
+ payload .Claims = claims
232
241
233
242
projectIDMsg := "make sure the ID token comes from the same Firebase project as the credential used to" +
234
243
" authenticate this SDK"
@@ -237,36 +246,36 @@ func (c *Client) VerifyIDToken(ctx context.Context, idToken string) (*Token, err
237
246
issuer := issuerPrefix + c .projectID
238
247
239
248
var err error
240
- if h .KeyID == "" {
241
- if p .Audience == firebaseAudience {
249
+ if header .KeyID == "" {
250
+ if payload .Audience == firebaseAudience {
242
251
err = fmt .Errorf ("expected an ID token but got a custom token" )
243
252
} else {
244
253
err = fmt .Errorf ("ID token has no 'kid' header" )
245
254
}
246
- } else if h .Algorithm != "RS256" {
255
+ } else if header .Algorithm != "RS256" {
247
256
err = fmt .Errorf ("ID token has invalid algorithm; expected 'RS256' but got %q; %s" ,
248
- h .Algorithm , verifyTokenMsg )
249
- } else if p .Audience != c .projectID {
257
+ header .Algorithm , verifyTokenMsg )
258
+ } else if payload .Audience != c .projectID {
250
259
err = fmt .Errorf ("ID token has invalid 'aud' (audience) claim; expected %q but got %q; %s; %s" ,
251
- c .projectID , p .Audience , projectIDMsg , verifyTokenMsg )
252
- } else if p .Issuer != issuer {
260
+ c .projectID , payload .Audience , projectIDMsg , verifyTokenMsg )
261
+ } else if payload .Issuer != issuer {
253
262
err = fmt .Errorf ("ID token has invalid 'iss' (issuer) claim; expected %q but got %q; %s; %s" ,
254
- issuer , p .Issuer , projectIDMsg , verifyTokenMsg )
255
- } else if p .IssuedAt > clk .Now ().Unix () {
256
- err = fmt .Errorf ("ID token issued at future timestamp: %d" , p .IssuedAt )
257
- } else if p .Expires < clk .Now ().Unix () {
258
- err = fmt .Errorf ("ID token has expired at: %d" , p .Expires )
259
- } else if p .Subject == "" {
263
+ issuer , payload .Issuer , projectIDMsg , verifyTokenMsg )
264
+ } else if payload .IssuedAt > clk .Now ().Unix () {
265
+ err = fmt .Errorf ("ID token issued at future timestamp: %d" , payload .IssuedAt )
266
+ } else if payload .Expires < clk .Now ().Unix () {
267
+ err = fmt .Errorf ("ID token has expired at: %d" , payload .Expires )
268
+ } else if payload .Subject == "" {
260
269
err = fmt .Errorf ("ID token has empty 'sub' (subject) claim; %s" , verifyTokenMsg )
261
- } else if len (p .Subject ) > 128 {
270
+ } else if len (payload .Subject ) > 128 {
262
271
err = fmt .Errorf ("ID token has a 'sub' (subject) claim longer than 128 characters; %s" , verifyTokenMsg )
263
272
}
264
273
265
274
if err != nil {
266
275
return nil , err
267
276
}
268
- p .UID = p .Subject
269
- return p , nil
277
+ payload .UID = payload .Subject
278
+ return & payload , nil
270
279
}
271
280
272
281
// VerifyIDTokenAndCheckRevoked verifies the provided ID token and checks it has not been revoked.
0 commit comments