Skip to content

Commit f350070

Browse files
authored
Extracting the Token Verification Logic into a New Type (#222)
* Refactoring token verification logic * Improved tests and test coverage * Fixing token verification tests * Cleaned up docs, tests and code order * Added some spaing for clarity * Addressing some style issues
1 parent de00d9a commit f350070

File tree

5 files changed

+591
-370
lines changed

5 files changed

+591
-370
lines changed

auth/auth.go

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

15-
// Package auth contains functions for minting custom authentication tokens, and verifying Firebase ID tokens.
15+
// Package auth contains functions for minting custom authentication tokens, verifying Firebase ID tokens,
16+
// and managing users in a Firebase project.
1617
package auth
1718

1819
import (
@@ -23,49 +24,29 @@ import (
2324

2425
"firebase.google.com/go/internal"
2526
"google.golang.org/api/identitytoolkit/v3"
26-
"google.golang.org/api/option"
2727
"google.golang.org/api/transport"
2828
)
2929

3030
const (
3131
firebaseAudience = "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit"
32-
idTokenCertURL = "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com"
33-
issuerPrefix = "https://securetoken.google.com/"
34-
tokenExpSeconds = 3600
35-
clockSkewSeconds = 300
32+
oneHourInSeconds = 3600
3633
)
3734

3835
var reservedClaims = []string{
3936
"acr", "amr", "at_hash", "aud", "auth_time", "azp", "cnf", "c_hash",
4037
"exp", "firebase", "iat", "iss", "jti", "nbf", "nonce", "sub",
4138
}
4239

43-
// Token represents a decoded Firebase ID token.
44-
//
45-
// Token provides typed accessors to the common JWT fields such as Audience (aud) and Expiry (exp).
46-
// Additionally it provides a UID field, which indicates the user ID of the account to which this token
47-
// belongs. Any additional JWT claims can be accessed via the Claims map of Token.
48-
type Token struct {
49-
Issuer string `json:"iss"`
50-
Audience string `json:"aud"`
51-
Expires int64 `json:"exp"`
52-
IssuedAt int64 `json:"iat"`
53-
Subject string `json:"sub,omitempty"`
54-
UID string `json:"uid,omitempty"`
55-
Claims map[string]interface{} `json:"-"`
56-
}
57-
5840
// Client is the interface for the Firebase auth service.
5941
//
6042
// Client facilitates generating custom JWT tokens for Firebase clients, and verifying ID tokens issued
6143
// by Firebase backend services.
6244
type Client struct {
63-
is *identitytoolkit.Service
64-
keySource keySource
65-
projectID string
66-
signer cryptoSigner
67-
version string
68-
clock internal.Clock
45+
is *identitytoolkit.Service
46+
idTokenVerifier *tokenVerifier
47+
signer cryptoSigner
48+
version string
49+
clock internal.Clock
6950
}
7051

7152
// NewClient creates a new instance of the Firebase Auth Client.
@@ -113,17 +94,16 @@ func NewClient(ctx context.Context, conf *internal.AuthConfig) (*Client, error)
11394
return nil, err
11495
}
11596

116-
noAuthHTTPClient, _, err := transport.NewHTTPClient(ctx, option.WithoutAuthentication())
97+
idTokenVerifier, err := newIDTokenVerifier(ctx, conf.ProjectID)
11798
if err != nil {
11899
return nil, err
119100
}
120101
return &Client{
121-
is: is,
122-
keySource: newHTTPKeySource(idTokenCertURL, noAuthHTTPClient),
123-
projectID: conf.ProjectID,
124-
signer: signer,
125-
version: "Go/Admin/" + conf.Version,
126-
clock: internal.SystemClock,
102+
is: is,
103+
idTokenVerifier: idTokenVerifier,
104+
signer: signer,
105+
version: "Go/Admin/" + conf.Version,
106+
clock: internal.SystemClock,
127107
}, nil
128108
}
129109

@@ -183,101 +163,53 @@ func (c *Client) CustomTokenWithClaims(ctx context.Context, uid string, devClaim
183163
Aud: firebaseAudience,
184164
UID: uid,
185165
Iat: now,
186-
Exp: now + tokenExpSeconds,
166+
Exp: now + oneHourInSeconds,
187167
Claims: devClaims,
188168
},
189169
}
190170
return info.Token(ctx, c.signer)
191171
}
192172

173+
// Token represents a decoded Firebase ID token.
174+
//
175+
// Token provides typed accessors to the common JWT fields such as Audience (aud) and Expiry (exp).
176+
// Additionally it provides a UID field, which indicates the user ID of the account to which this token
177+
// belongs. Any additional JWT claims can be accessed via the Claims map of Token.
178+
type Token struct {
179+
Issuer string `json:"iss"`
180+
Audience string `json:"aud"`
181+
Expires int64 `json:"exp"`
182+
IssuedAt int64 `json:"iat"`
183+
Subject string `json:"sub,omitempty"`
184+
UID string `json:"uid,omitempty"`
185+
Claims map[string]interface{} `json:"-"`
186+
}
187+
193188
// VerifyIDToken verifies the signature and payload of the provided ID token.
194189
//
195190
// VerifyIDToken accepts a signed JWT token string, and verifies that it is current, issued for the
196191
// correct Firebase project, and signed by the Google Firebase services in the cloud. It returns
197192
// a Token containing the decoded claims in the input JWT. See
198193
// https://firebase.google.com/docs/auth/admin/verify-id-tokens#retrieve_id_tokens_on_clients for
199194
// more details on how to obtain an ID token in a client app.
200-
// This does not check whether or not the token has been revoked. See `VerifyIDTokenAndCheckRevoked` below.
195+
//
196+
// This function does not make any RPC calls most of the time. The only time it makes an RPC call
197+
// is when Google public keys need to be refreshed. These keys get cached up to 24 hours, and
198+
// therefore the RPC overhead gets amortized over many invocations of this function.
199+
//
200+
// This does not check whether or not the token has been revoked. Use `VerifyIDTokenAndCheckRevoked()`
201+
// when a revocation check is needed.
201202
func (c *Client) VerifyIDToken(ctx context.Context, idToken string) (*Token, error) {
202-
if c.projectID == "" {
203-
return nil, errors.New("project id not available")
204-
}
205-
if idToken == "" {
206-
return nil, fmt.Errorf("id token must be a non-empty string")
207-
}
208-
209-
segments := strings.Split(idToken, ".")
210-
211-
var (
212-
header jwtHeader
213-
payload Token
214-
claims map[string]interface{}
215-
)
216-
if err := decode(segments[0], &header); err != nil {
217-
return nil, err
218-
}
219-
if err := decode(segments[1], &payload); err != nil {
220-
return nil, err
221-
}
222-
if err := decode(segments[1], &claims); err != nil {
223-
return nil, err
224-
}
225-
// Delete standard claims from the custom claims maps.
226-
for _, r := range []string{"iss", "aud", "exp", "iat", "sub", "uid"} {
227-
delete(claims, r)
228-
}
229-
payload.Claims = claims
230-
231-
projectIDMsg := "make sure the ID token comes from the same Firebase project as the credential used to" +
232-
" authenticate this SDK"
233-
verifyTokenMsg := "see https://firebase.google.com/docs/auth/admin/verify-id-tokens for details on how to " +
234-
"retrieve a valid ID token"
235-
issuer := issuerPrefix + c.projectID
236-
237-
var err error
238-
if header.KeyID == "" {
239-
if payload.Audience == firebaseAudience {
240-
err = fmt.Errorf("expected an ID token but got a custom token")
241-
} else {
242-
err = fmt.Errorf("ID token has no 'kid' header")
243-
}
244-
} else if header.Algorithm != "RS256" {
245-
err = fmt.Errorf("ID token has invalid algorithm; expected 'RS256' but got %q; %s",
246-
header.Algorithm, verifyTokenMsg)
247-
} else if payload.Audience != c.projectID {
248-
err = fmt.Errorf("ID token has invalid 'aud' (audience) claim; expected %q but got %q; %s; %s",
249-
c.projectID, payload.Audience, projectIDMsg, verifyTokenMsg)
250-
} else if payload.Issuer != issuer {
251-
err = fmt.Errorf("ID token has invalid 'iss' (issuer) claim; expected %q but got %q; %s; %s",
252-
issuer, payload.Issuer, projectIDMsg, verifyTokenMsg)
253-
} else if (payload.IssuedAt - clockSkewSeconds) > c.clock.Now().Unix() {
254-
err = fmt.Errorf("ID token issued at future timestamp: %d", payload.IssuedAt)
255-
} else if (payload.Expires + clockSkewSeconds) < c.clock.Now().Unix() {
256-
err = fmt.Errorf("ID token has expired at: %d", payload.Expires)
257-
} else if payload.Subject == "" {
258-
err = fmt.Errorf("ID token has empty 'sub' (subject) claim; %s", verifyTokenMsg)
259-
} else if len(payload.Subject) > 128 {
260-
err = fmt.Errorf("ID token has a 'sub' (subject) claim longer than 128 characters; %s", verifyTokenMsg)
261-
}
262-
263-
if err != nil {
264-
return nil, err
265-
}
266-
payload.UID = payload.Subject
267-
268-
// Verifying the signature requires syncronized access to a key store and
269-
// potentially issues a http request. Validating the fields of the token is
270-
// cheaper and invalid tokens will fail faster.
271-
if err := verifyToken(ctx, idToken, c.keySource); err != nil {
272-
return nil, err
273-
}
274-
return &payload, nil
203+
return c.idTokenVerifier.VerifyToken(ctx, idToken)
275204
}
276205

277-
// VerifyIDTokenAndCheckRevoked verifies the provided ID token and checks it has not been revoked.
206+
// VerifyIDTokenAndCheckRevoked verifies the provided ID token, and additionally checks that the
207+
// token has not been revoked.
278208
//
279-
// VerifyIDTokenAndCheckRevoked verifies the signature and payload of the provided ID token and
280-
// checks that it wasn't revoked. Uses VerifyIDToken() internally to verify the ID token JWT.
209+
// This function uses `VerifyIDToken()` internally to verify the ID token JWT. However, unlike
210+
// `VerifyIDToken()` this function must make an RPC call to perform the revocation check.
211+
// Developers are advised to take this additional overhead into consideration when including this
212+
// function in an authorization flow that gets executed often.
281213
func (c *Client) VerifyIDTokenAndCheckRevoked(ctx context.Context, idToken string) (*Token, error) {
282214
p, err := c.VerifyIDToken(ctx, idToken)
283215
if err != nil {

0 commit comments

Comments
 (0)