Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
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
73 changes: 40 additions & 33 deletions auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const (

// SDK-generated error codes
idTokenRevoked = "ID_TOKEN_REVOKED"
userDisabled = "USER_DISABLED"
sessionCookieRevoked = "SESSION_COOKIE_REVOKED"
tenantIDMismatch = "TENANT_ID_MISMATCH"
)
Expand Down Expand Up @@ -288,14 +289,14 @@ func (c *baseClient) withTenantID(tenantID string) *baseClient {
// These keys get cached up to 24 hours, and therefore the RPC overhead gets amortized
// over many invocations of this function.
//
// This does not check whether or not the token has been revoked. Use `VerifyIDTokenAndCheckRevoked()`
// This does not check whether or not the token has been revoked or disabled. Use `VerifyIDTokenAndCheckRevoked()`
// when a revocation check is needed.
func (c *baseClient) VerifyIDToken(ctx context.Context, idToken string) (*Token, error) {
return c.verifyIDToken(ctx, idToken, false)
}

// VerifyIDTokenAndCheckRevoked verifies the provided ID token, and additionally checks that the
// token has not been revoked.
// token has not been revoked or disabled.
//
// Unlike `VerifyIDToken()`, this function must make an RPC call to perform the revocation check.
// Developers are advised to take this additional overhead into consideration when including this
Expand All @@ -304,7 +305,7 @@ func (c *baseClient) VerifyIDTokenAndCheckRevoked(ctx context.Context, idToken s
return c.verifyIDToken(ctx, idToken, true)
}

func (c *baseClient) verifyIDToken(ctx context.Context, idToken string, checkRevoked bool) (*Token, error) {
func (c *baseClient) verifyIDToken(ctx context.Context, idToken string, checkRevokedOrDisabled bool) (*Token, error) {
decoded, err := c.idTokenVerifier.VerifyToken(ctx, idToken, c.isEmulator)
if err != nil {
return nil, err
Expand All @@ -320,21 +321,11 @@ func (c *baseClient) verifyIDToken(ctx context.Context, idToken string, checkRev
}
}

if c.isEmulator || checkRevoked {
revoked, err := c.checkRevoked(ctx, decoded)
if c.isEmulator || checkRevokedOrDisabled {
err = c.checkRevokedOrDisabled(ctx, decoded, idTokenRevoked, "ID token has been revoked")
if err != nil {
return nil, err
}

if revoked {
return nil, &internal.FirebaseError{
ErrorCode: internal.InvalidArgument,
String: "ID token has been revoked",
Ext: map[string]interface{}{
authErrorCode: idTokenRevoked,
},
}
}
}

return decoded, nil
Expand All @@ -347,11 +338,18 @@ func IsTenantIDMismatch(err error) bool {

// IsIDTokenRevoked checks if the given error was due to a revoked ID token.
//
// When IsIDTokenRevoked returns true, IsIDTokenInvalid is guranteed to return true.
// When IsIDTokenRevoked returns true, IsIDTokenInvalid is guaranteed to return true.
func IsIDTokenRevoked(err error) bool {
return hasAuthErrorCode(err, idTokenRevoked)
}

// IsUserDisabled checks if the given error was due to a disabled ID token
//
// When IsUserDisabled returns true, IsIDTokenInvalid is guaranteed to return true.
func IsUserDisabled(err error) bool {
return hasAuthErrorCode(err, userDisabled)
}

// VerifySessionCookie verifies the signature and payload of the provided Firebase session cookie.
//
// VerifySessionCookie accepts a signed JWT token string, and verifies that it is current, issued for the
Expand Down Expand Up @@ -380,46 +378,55 @@ func (c *Client) VerifySessionCookieAndCheckRevoked(ctx context.Context, session
return c.verifySessionCookie(ctx, sessionCookie, true)
}

func (c *Client) verifySessionCookie(ctx context.Context, sessionCookie string, checkRevoked bool) (*Token, error) {
func (c *Client) verifySessionCookie(ctx context.Context, sessionCookie string, checkRevokedOrDisabled bool) (*Token, error) {
decoded, err := c.cookieVerifier.VerifyToken(ctx, sessionCookie, c.isEmulator)
if err != nil {
return nil, err
}

if c.isEmulator || checkRevoked {
revoked, err := c.checkRevoked(ctx, decoded)
if c.isEmulator || checkRevokedOrDisabled {
err := c.checkRevokedOrDisabled(ctx, decoded, sessionCookieRevoked, "session cookie has been revoked")
if err != nil {
return nil, err
}

if revoked {
return nil, &internal.FirebaseError{
ErrorCode: internal.InvalidArgument,
String: "session cookie has been revoked",
Ext: map[string]interface{}{
authErrorCode: sessionCookieRevoked,
},
}
}
}

return decoded, nil
}

// IsSessionCookieRevoked checks if the given error was due to a revoked session cookie.
//
// When IsSessionCookieRevoked returns true, IsSessionCookieInvalid is guranteed to return true.
// When IsSessionCookieRevoked returns true, IsSessionCookieInvalid is guaranteed to return true.
func IsSessionCookieRevoked(err error) bool {
return hasAuthErrorCode(err, sessionCookieRevoked)
}

func (c *baseClient) checkRevoked(ctx context.Context, token *Token) (bool, error) {
// checkRevokedOrDisabled checks whether the input token has been revoked or disabled.
func (c *baseClient) checkRevokedOrDisabled(ctx context.Context, token *Token, errCode string, errMessage string) error {
user, err := c.GetUser(ctx, token.UID)
if err != nil {
return false, err
return err
}
if user.Disabled {
return &internal.FirebaseError{
ErrorCode: internal.InvalidArgument,
String: "user has been disabled",
Ext: map[string]interface{}{
authErrorCode: userDisabled,
},
}

return token.IssuedAt*1000 < user.TokensValidAfterMillis, nil
}
if token.IssuedAt*1000 < user.TokensValidAfterMillis {
return &internal.FirebaseError{
ErrorCode: internal.InvalidArgument,
String: errMessage,
Ext: map[string]interface{}{
authErrorCode: errCode,
},
}
}
return nil
}

func hasAuthErrorCode(err error, code string) bool {
Expand Down
34 changes: 26 additions & 8 deletions auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,13 @@ const (
)

var (
testGetUserResponse []byte
testIDToken string
testSessionCookie string
testSigner cryptoSigner
testIDTokenVerifier *tokenVerifier
testCookieVerifier *tokenVerifier
testGetUserResponse []byte
testGetDisabledUserResponse []byte
testIDToken string
testSessionCookie string
testSigner cryptoSigner
testIDTokenVerifier *tokenVerifier
testCookieVerifier *tokenVerifier

optsWithServiceAcct = []option.ClientOption{
option.WithCredentialsFile("../testdata/service_account.json"),
Expand All @@ -76,6 +77,9 @@ func TestMain(m *testing.M) {
testGetUserResponse, err = ioutil.ReadFile("../testdata/get_user.json")
logFatal(err)

testGetDisabledUserResponse, err = ioutil.ReadFile("../testdata/get_disabled_user.json")
logFatal(err)

testIDToken = getIDToken(nil)
testSessionCookie = getSessionCookie(nil)
os.Exit(m.Run())
Expand Down Expand Up @@ -852,13 +856,13 @@ func TestVerifyIDTokenDoesNotCheckRevoked(t *testing.T) {
}
}

func TestInvalidTokenDoesNotCheckRevoked(t *testing.T) {
func TestInvalidTokenDoesNotCheckRevokedOrDisabled(t *testing.T) {
s := echoServer(testGetUserResponse, t)
defer s.Close()
s.Client.idTokenVerifier = testIDTokenVerifier

ft, err := s.Client.VerifyIDTokenAndCheckRevoked(context.Background(), "")
if ft != nil || !IsIDTokenInvalid(err) || IsIDTokenRevoked(err) {
if ft != nil || !IsIDTokenInvalid(err) || IsIDTokenRevoked(err) || IsUserDisabled(err) {
t.Errorf("VerifyIDTokenAndCheckRevoked() = (%v, %v); want = (nil, IDTokenInvalid)", ft, err)
}
if len(s.Req) != 0 {
Expand All @@ -880,6 +884,20 @@ func TestVerifyIDTokenAndCheckRevokedError(t *testing.T) {
}
}

func TestVerifyIDTokenAndCheckDisabledError(t *testing.T) {
s := echoServer(testGetDisabledUserResponse, t)
defer s.Close()
revokedToken := getIDToken(mockIDTokenPayload{"uid": "uid", "iat": 1970})
s.Client.idTokenVerifier = testIDTokenVerifier

p, err := s.Client.VerifyIDTokenAndCheckRevoked(context.Background(), revokedToken)
we := "user has been disabled"
if p != nil || !IsUserDisabled(err) || !IsIDTokenInvalid(err) || err.Error() != we {
t.Errorf("VerifyIDTokenAndCheckRevoked(ctx, token) =(%v, %v); want = (%v, %v)",
p, err, nil, we)
}
}

func TestIDTokenRevocationCheckUserMgtError(t *testing.T) {
resp := `{
"kind" : "identitytoolkit#GetAccountInfoResponse",
Expand Down
2 changes: 1 addition & 1 deletion auth/token_verifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func IsIDTokenExpired(err error) bool {
// An ID token is considered invalid when it is malformed (i.e. contains incorrect data), expired
// or revoked.
func IsIDTokenInvalid(err error) bool {
return hasAuthErrorCode(err, idTokenInvalid) || IsIDTokenExpired(err) || IsIDTokenRevoked(err)
return hasAuthErrorCode(err, idTokenInvalid) || IsIDTokenExpired(err) || IsIDTokenRevoked(err) || IsUserDisabled(err)
}

// IsSessionCookieExpired checks if the given error was due to an expired session cookie.
Expand Down
36 changes: 36 additions & 0 deletions integration/auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,42 @@ func TestRevokeRefreshTokens(t *testing.T) {
}
}

func TestIDTokenForDisabledUser(t *testing.T) {
uid := "user_disabled"
ct, err := client.CustomToken(context.Background(), uid)
if err != nil {
t.Fatal(err)
}
idt, err := signInWithCustomToken(ct)
if err != nil {
t.Fatal(err)
}
defer deleteUser(uid)

vt, err := client.VerifyIDTokenAndCheckRevoked(context.Background(), idt)
if err != nil {
t.Fatal(err)
}
if vt.UID != uid {
t.Errorf("UID = %q; want UID = %q", vt.UID, uid)
}

// Disable the user
updates := auth.UserToUpdate{}
updates.Disabled(true)
_, err = client.UpdateUser(context.Background(), uid, &updates)
if err != nil {
t.Fatalf("failed to disable user with UpdateUser: %v", err)
}

vt, err = client.VerifyIDTokenAndCheckRevoked(context.Background(), idt)
we := "user has been disabled"
if vt != nil || err == nil || !auth.IsUserDisabled(err) || err.Error() != we {
t.Errorf("tok, err := VerifyIDTokenAndCheckRevoked(); got (%v, %s) ; want (%v, %v)",
vt, err, nil, we)
}
}

// verifyCustomToken verifies the given custom token by signing into a Firebase project with it.
//
// A successful sign in creates the user account in the Firebase back-end. This method ensures that
Expand Down
2 changes: 2 additions & 0 deletions snippets/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -1425,6 +1425,8 @@ func verifyIDTokenAndCheckRevokedTenant(ctx context.Context, tenantClient *auth.
if err != nil {
if auth.IsIDTokenRevoked(err) {
// Token is revoked. Inform the user to reauthenticate or signOut() the user.
} else if auth.IsUserDisabled(err) {
// Token is disabled.
} else {
// Token is invalid
}
Expand Down
2 changes: 1 addition & 1 deletion snippets/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import (
"fmt"
"log"

"firebase.google.com/go/v4"
firebase "firebase.google.com/go/v4"
"firebase.google.com/go/v4/db"
"google.golang.org/api/option"
)
Expand Down
22 changes: 22 additions & 0 deletions testdata/get_disabled_user.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"kind": "identitytoolkit#GetAccountInfoResponse",
"users": [
{
"localId": "testuser",
"email": "[email protected]",
"phoneNumber": "+1234567890",
"emailVerified": true,
"displayName": "Test User",
"photoUrl": "http://www.example.com/testuser/photo.png",
"passwordHash": "passwordhash",
"salt": "salt===",
"passwordUpdatedAt": 1.494364393E+12,
"validSince": "1494364393",
"disabled": true,
"createdAt": "1234567890000",
"lastLoginAt": "1233211232000",
"customAttributes": "{\"admin\": true, \"package\": \"gold\"}",
"tenantId": "testTenant"
}
]
}