Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
22 changes: 21 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
# Unreleased
-

-

# v2.5.0

- [changed] Import context from `golang.org/x/net` for 1.6 compatibility

### Cloud Messaging

- [added] Added the `messaging` package for sending Firebase notifications
and managing topic subscriptions.

### Authentication

- [added] A new [`VerifyIDTokenAndCheckRevoked()`](https://godoc.org/firebase.google.com/go/auth#Client.VerifyIDToken)
function has been added to check for revoked ID tokens.
- [added] A new [`RevokeRefreshTokens()`](https://godoc.org/firebase.google.com/go/auth#Client.RevokeRefreshTokens)
function has been added to invalidate all refresh tokens issued to a user.
- [added] A new property `TokensValidAfterMillis` has been added to the
['UserRecord'](https://godoc.org/firebase.google.com/go/auth#UserRecord)
type, which stores the time of the revocation truncated to 1 second accuracy.

# v2.4.0

Expand Down
34 changes: 34 additions & 0 deletions auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,13 +177,26 @@ func (c *Client) CustomTokenWithClaims(uid string, devClaims map[string]interfac
return encodeToken(c.snr, defaultHeader(), payload)
}

// RevokeRefreshTokens revokes all refresh tokens issued to a user.
//
// RevokeRefreshTokens updates the user's TokensValidAfterMillis to the current UTC second.
// It is important that the server on which this is called has its clock set correctly and synchronized.
//
// While this revokes all sessions for a specified user and disables any new ID tokens for existing sessions
// from getting minted, existing ID tokens may remain active until their natural expiration (one hour).
// To verify that ID tokens are revoked, use `verifyIdTokenAndCheckRevoked(ctx, idToken)`.
func (c *Client) RevokeRefreshTokens(ctx context.Context, uid string) error {
return c.updateUser(ctx, uid, (&UserToUpdate{}).revokeRefreshTokens())
}

// VerifyIDToken verifies the signature and payload of the provided ID token.
//
// VerifyIDToken accepts a signed JWT token string, and verifies that it is current, issued for the
// correct Firebase project, and signed by the Google Firebase services in the cloud. It returns
// a Token containing the decoded claims in the input JWT. See
// https://firebase.google.com/docs/auth/admin/verify-id-tokens#retrieve_id_tokens_on_clients for
// more details on how to obtain an ID token in a client app.
// This does not check whether or not the token has been revoked. See `VerifyIDTokenAndCheckRevoked` below.
func (c *Client) VerifyIDToken(idToken string) (*Token, error) {
if c.projectID == "" {
return nil, errors.New("project id not available")
Expand Down Expand Up @@ -237,6 +250,27 @@ func (c *Client) VerifyIDToken(idToken string) (*Token, error) {
return p, nil
}

// VerifyIDTokenAndCheckRevoked verifies the provided ID token and checks it has not been revoked.
//
// VerifyIDTokenAndCheckRevoked verifies the signature and payload of the provided ID token and
// checks that it wasn't revoked. Uses VerifyIDToken() internally to verify the ID token JWT.
func (c *Client) VerifyIDTokenAndCheckRevoked(ctx context.Context, idToken string) (*Token, error) {
p, err := c.VerifyIDToken(idToken)
if err != nil {
return nil, err
}

user, err := c.GetUser(ctx, p.UID)
if err != nil {
return nil, err
}

if p.IssuedAt*1000 < user.TokensValidAfterMillis {
return nil, fmt.Errorf("ID token has been revoked")
}
return p, nil
}

func parseKey(key string) (*rsa.PrivateKey, error) {
block, _ := pem.Decode([]byte(key))
if block == nil {
Expand Down
2 changes: 1 addition & 1 deletion auth/auth_std.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

package auth

import "context"
import "golang.org/x/net/context"

func newSigner(ctx context.Context) (signer, error) {
return serviceAcctSigner{}, nil
Expand Down
49 changes: 47 additions & 2 deletions auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ import (
)

var client *Client
var ctx context.Context
var testIDToken string
var testGetUserResponse []byte
var testListUsersResponse []byte

var defaultTestOpts = []option.ClientOption{
option.WithCredentialsFile("../testdata/service_account.json"),
}
Expand All @@ -49,7 +49,6 @@ func TestMain(m *testing.M) {
var (
err error
ks keySource
ctx context.Context
creds *google.DefaultCredentials
opts []option.ClientOption
)
Expand Down Expand Up @@ -193,6 +192,52 @@ func TestCustomTokenInvalidCredential(t *testing.T) {
}
}

func TestVerifyIDTokenAndCheckRevokedValid(t *testing.T) {
s := echoServer(testGetUserResponse, t)
defer s.Close()

ft, err := s.Client.VerifyIDTokenAndCheckRevoked(ctx, testIDToken)
if err != nil {
t.Error(err)
}
if ft.Claims["admin"] != true {
t.Errorf("Claims['admin'] = %v; want = true", ft.Claims["admin"])
}
if ft.UID != ft.Subject {
t.Errorf("UID = %q; Sub = %q; want UID = Sub", ft.UID, ft.Subject)
}
}

func TestVerifyIDTokenAndCheckRevokedDoNotCheck(t *testing.T) {
s := echoServer(testGetUserResponse, t)
defer s.Close()
tok := getIDToken(mockIDTokenPayload{"uid": "uid", "iat": 1970}) // old token

ft, err := s.Client.VerifyIDToken(tok)
if err != nil {
t.Fatal(err)
}
if ft.Claims["admin"] != true {
t.Errorf("Claims['admin'] = %v; want = true", ft.Claims["admin"])
}
if ft.UID != ft.Subject {
t.Errorf("UID = %q; Sub = %q; want UID = Sub", ft.UID, ft.Subject)
}
}

func TestVerifyIDTokenAndCheckRevokedInvalidated(t *testing.T) {
s := echoServer(testGetUserResponse, t)
defer s.Close()
tok := getIDToken(mockIDTokenPayload{"uid": "uid", "iat": 1970}) // old token

p, err := s.Client.VerifyIDTokenAndCheckRevoked(ctx, tok)
we := "ID token has been revoked"
if p != nil || err == nil || err.Error() != we {
t.Errorf("VerifyIDTokenAndCheckRevoked(ctx, token) =(%v, %v); want = (%v, %v)",
p, err, nil, we)
}
}

func TestVerifyIDToken(t *testing.T) {
ft, err := client.VerifyIDToken(testIDToken)
if err != nil {
Expand Down
69 changes: 27 additions & 42 deletions auth/user_mgt.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"reflect"
"regexp"
"strings"
"time"

"golang.org/x/net/context"
"google.golang.org/api/identitytoolkit/v3"
Expand All @@ -39,6 +40,7 @@ var commonValidators = map[string]func(interface{}) error{
"password": validatePassword,
"photoUrl": validatePhotoURL,
"localId": validateUID,
"validSince": func(interface{}) error { return nil }, // Needed for preparePayload.
}

// Create a new interface
Expand All @@ -65,6 +67,7 @@ type UserInfo struct {
}

// UserMetadata contains additional metadata associated with a user account.
// Timestamps are in milliseconds since epoch.
type UserMetadata struct {
CreationTimestamp int64
LastLogInTimestamp int64
Expand All @@ -73,11 +76,12 @@ type UserMetadata struct {
// UserRecord contains metadata associated with a Firebase user account.
type UserRecord struct {
*UserInfo
CustomClaims map[string]interface{}
Disabled bool
EmailVerified bool
ProviderUserInfo []*UserInfo
UserMetadata *UserMetadata
CustomClaims map[string]interface{}
Disabled bool
EmailVerified bool
ProviderUserInfo []*UserInfo
TokensValidAfterMillis int64 // milliseconds since epoch.
UserMetadata *UserMetadata
}

// ExportedUserRecord is the returned user value used when listing all the users.
Expand Down Expand Up @@ -173,6 +177,13 @@ func (u *UserToUpdate) PhoneNumber(phone string) *UserToUpdate { u.set("phoneNum
// PhotoURL setter.
func (u *UserToUpdate) PhotoURL(url string) *UserToUpdate { u.set("photoUrl", url); return u }

// revokeRefreshTokens revokes all refresh tokens for a user by setting the validSince property
// to the present in epoch seconds.
func (u *UserToUpdate) revokeRefreshTokens() *UserToUpdate {
u.set("validSince", time.Now().Unix())
return u
}

// CreateUser creates a new user with the specified properties.
func (c *Client) CreateUser(ctx context.Context, user *UserToCreate) (*UserRecord, error) {
uid, err := c.createUser(ctx, user)
Expand Down Expand Up @@ -471,7 +482,12 @@ func (u *UserToUpdate) preparePayload(user *identitytoolkit.IdentitytoolkitRelyi
if err := validate(v); err != nil {
return err
}
reflect.ValueOf(user).Elem().FieldByName(strings.Title(key)).SetString(params[key].(string))
f := reflect.ValueOf(user).Elem().FieldByName(strings.Title(key))
if f.Kind() == reflect.String {
f.SetString(params[key].(string))
} else if f.Kind() == reflect.Int64 {
f.SetInt(params[key].(int64))
}
}
}
if params["disableUser"] != nil {
Expand All @@ -498,37 +514,6 @@ func (u *UserToUpdate) preparePayload(user *identitytoolkit.IdentitytoolkitRelyi

// End of validators

// Response Types -------------------------------

type getUserResponse struct {
RequestType string
Users []responseUserRecord
}

type responseUserRecord struct {
UID string
DisplayName string
Email string
PhoneNumber string
PhotoURL string
CreationTimestamp int64
LastLogInTimestamp int64
ProviderID string
CustomClaims string
Disabled bool
EmailVerified bool
ProviderUserInfo []*UserInfo
PasswordHash string
PasswordSalt string
ValidSince int64
}

type listUsersResponse struct {
RequestType string
Users []responseUserRecord
NextPage string
}

// Helper functions for retrieval and HTTP calls.

func (c *Client) createUser(ctx context.Context, user *UserToCreate) (string, error) {
Expand Down Expand Up @@ -559,7 +544,6 @@ func (c *Client) updateUser(ctx context.Context, uid string, user *UserToUpdate)
if user == nil || user.params == nil {
return fmt.Errorf("update parameters must not be nil or empty")
}

request := &identitytoolkit.IdentitytoolkitRelyingpartySetAccountInfoRequest{
LocalId: uid,
}
Expand Down Expand Up @@ -628,10 +612,11 @@ func makeExportedUser(r *identitytoolkit.UserInfo) (*ExportedUserRecord, error)
ProviderID: defaultProviderID,
UID: r.LocalId,
},
CustomClaims: cc,
Disabled: r.Disabled,
EmailVerified: r.EmailVerified,
ProviderUserInfo: providerUserInfo,
CustomClaims: cc,
Disabled: r.Disabled,
EmailVerified: r.EmailVerified,
ProviderUserInfo: providerUserInfo,
TokensValidAfterMillis: r.ValidSince * 1000,
UserMetadata: &UserMetadata{
LastLogInTimestamp: r.LastLoginAt,
CreationTimestamp: r.CreatedAt,
Expand Down
Loading