Firebase Authentication sessions are long lived. Every time a user signs in, the user credentials are sent to the Firebase Authentication backend and exchanged for a Firebase ID token (a JWT) and refresh token. Firebase ID tokens are short lived and last for an hour; the refresh token can be used to retrieve new ID tokens. Refresh tokens expire only when one of the following occurs:
- The user is deleted
- The user is disabled
- A major account change is detected for the user. This includes events like password or email address updates.
The Firebase Admin SDK provides the ability to revoke refresh tokens for a specified user. In addition, an API to check for ID token revocation is also made available. With these capabilities, you have more control over user sessions. The SDK provides the ability to add restrictions to prevent sessions from being used in suspicious circumstances, as well as a mechanism for recovery from potential token theft.
Revoke refresh tokens
You might revoke a user's existing refresh token when a user reports a lost or
stolen device. Similarly, if you discover a general vulnerability or suspect a
wide-scale leak of active tokens, you can use the listUsers
API to look up all users and revoke their tokens for the specified project.
Password resets also revoke a user's existing tokens; however, the Firebase Authentication backend handles the revocation automatically in that case. On revocation, the user is signed out and prompted to reauthenticate.
Here is an example implementation that uses the Admin SDK to revoke the refresh token of a given user. To initialize the Admin SDK follow the instructions on the setup page .
Node.js
// Revoke all refresh tokens for a specified user for whatever reason.
// Retrieve the timestamp of the revocation, in seconds since the epoch.
getAuth
()
.
revokeRefreshTokens
(
uid
)
.
then
(()
=
>
{
return
getAuth
().
getUser
(
uid
);
})
.
then
((
userRecord
)
=
>
{
return
new
Date
(
userRecord
.
tokensValidAfterTime
).
getTime
()
/
1000
;
})
.
then
((
timestamp
)
=
>
{
console
.
log
(
`Tokens revoked at:
${
timestamp
}
`
);
});
Java
FirebaseAuth
.
getInstance
().
revokeRefreshTokens
(
uid
);
UserRecord
user
=
FirebaseAuth
.
getInstance
().
getUser
(
uid
);
// Convert to seconds as the auth_time in the token claims is in seconds too.
long
revocationSecond
=
user
.
getTokensValidAfterTimestamp
()
/
1000
;
System
.
out
.
println
(
"Tokens revoked at: "
+
revocationSecond
);
Python
# Revoke tokens on the backend.
auth
.
revoke_refresh_tokens
(
uid
)
user
=
auth
.
get_user
(
uid
)
# Convert to seconds as the auth_time in the token claims is in seconds.
revocation_second
=
user
.
tokens_valid_after_timestamp
/
1000
print
(
f
'Tokens revoked at:
{
revocation_second
}
'
)
Go
client
,
err
:=
app
.
Auth
(
ctx
)
if
err
!=
nil
{
log
.
Fatalf
(
"error getting Auth client: %v\n"
,
err
)
}
if
err
:=
client
.
RevokeRefreshTokens
(
ctx
,
uid
);
err
!=
nil
{
log
.
Fatalf
(
"error revoking tokens for user: %v, %v\n"
,
uid
,
err
)
}
// accessing the user's TokenValidAfter
u
,
err
:=
client
.
GetUser
(
ctx
,
uid
)
if
err
!=
nil
{
log
.
Fatalf
(
"error getting user %s: %v\n"
,
uid
,
err
)
}
timestamp
:=
u
.
TokensValidAfterMillis
/
1000
log
.
Printf
(
"the refresh tokens were revoked at: %d (UTC seconds) "
,
timestamp
)
C#
await
FirebaseAuth
.
DefaultInstance
.
RevokeRefreshTokensAsync
(
uid
);
var
user
=
await
FirebaseAuth
.
DefaultInstance
.
GetUserAsync
(
uid
);
Console
.
WriteLine
(
"Tokens revoked at: "
+
user
.
TokensValidAfterTimestamp
);
Detect ID token revocation
Because Firebase ID tokens are stateless JWTs, you can determine a token has been revoked only by requesting the token's status from the Firebase Authentication backend. For this reason, performing this check on your server is an expensive operation, requiring an extra network round trip. You can avoid making this network request by setting up Firebase Security Rules that check for revocation rather than using the Admin SDK to make the check.
Detect ID token revocation in Firebase Security Rules
To be able to detect the ID token revocation using Security Rules, we must first store some user-specific metadata.
Update user-specific metadata in Firebase Realtime Database .
Save the refresh token revocation timestamp. This is needed to track ID token revocation via Firebase Security Rules . This allows for efficient checks within the database. In the code samples below, use the uid and the revocation time obtained in the previous section .
Node.js
const
metadataRef
=
getDatabase
().
ref
(
'metadata/'
+
uid
);
metadataRef
.
set
({
revokeTime
:
utcRevocationTimeSecs
}).
then
(()
=
>
{
console
.
log
(
'Database updated successfully.'
);
});
Java
DatabaseReference
ref
=
FirebaseDatabase
.
getInstance
().
getReference
(
"metadata/"
+
uid
);
Map<String
,
Object
>
userData
=
new
HashMap
<> ();
userData
.
put
(
"revokeTime"
,
revocationSecond
);
ref
.
setValueAsync
(
userData
);
Python
metadata_ref
=
firebase_admin
.
db
.
reference
(
"metadata/"
+
uid
)
metadata_ref
.
set
({
'revokeTime'
:
revocation_second
})
Add a check to Firebase Security Rules
To enforce this check, set up a rule with no client write access to store the revocation time per user. This can be updated with the UTC timestamp of the last revocation time as shown in the previous examples:
{
"rules": {
"metadata": {
"$user_id": {
// this could be false as it is only accessed from backend or rules.
".read": "$user_id === auth.uid",
".write": "false",
}
}
}
}
Any data that requires authenticated access must have the following rule configured. This logic only allows authenticated users with unrevoked ID tokens to access the protected data:
{
"rules": {
"users": {
"$user_id": {
".read": "auth != null && $user_id === auth.uid && (
!root.child('metadata').child(auth.uid).child('revokeTime').exists()
|| auth.token.auth_time > root.child('metadata').child(auth.uid).child('revokeTime').val()
)",
".write": "auth != null && $user_id === auth.uid && (
!root.child('metadata').child(auth.uid).child('revokeTime').exists()
|| auth.token.auth_time > root.child('metadata').child(auth.uid).child('revokeTime').val()
)",
}
}
}
}
Detect ID token revocation in the SDK.
In your server, implement the following logic for refresh token revocation and ID token validation:
When a user's ID token is to be verified, the additional checkRevoked
boolean flag has to be passed to verifyIdToken
. If the user's token is
revoked, the user should be signed out on the client or asked to reauthenticate
using reauthentication APIs provided by the Firebase Authentication
client SDKs.
To initialize the Admin SDK for your platform, follow the instructions on the setup page
. Examples of retrieving the ID
token are in the verifyIdToken
section.
Node.js
// Verify the ID token while checking if the token is revoked by passing
// checkRevoked true.
let
checkRevoked
=
true
;
getAuth
()
.
verifyIdToken
(
idToken
,
checkRevoked
)
.
then
((
payload
)
=
>
{
// Token is valid.
})
.
catch
((
error
)
=
>
{
if
(
error
.
code
==
'auth/id-token-revoked'
)
{
// Token has been revoked. Inform the user to reauthenticate or signOut() the user.
}
else
{
// Token is invalid.
}
});
Java
try
{
// Verify the ID token while checking if the token is revoked by passing checkRevoked
// as true.
boolean
checkRevoked
=
true
;
FirebaseToken
decodedToken
=
FirebaseAuth
.
getInstance
()
.
verifyIdToken
(
idToken
,
checkRevoked
);
// Token is valid and not revoked.
String
uid
=
decodedToken
.
getUid
();
}
catch
(
FirebaseAuthException
e
)
{
if
(
e
.
getAuthErrorCode
()
==
AuthErrorCode
.
REVOKED_ID_TOKEN
)
{
// Token has been revoked. Inform the user to re-authenticate or signOut() the user.
}
else
{
// Token is invalid.
}
}
Python
try
:
# Verify the ID token while checking if the token is revoked by
# passing check_revoked=True.
decoded_token
=
auth
.
verify_id_token
(
id_token
,
check_revoked
=
True
)
# Token is valid and not revoked.
uid
=
decoded_token
[
'uid'
]
except
auth
.
RevokedIdTokenError
:
# Token revoked, inform the user to reauthenticate or signOut().
pass
except
auth
.
UserDisabledError
:
# Token belongs to a disabled user record.
pass
except
auth
.
InvalidIdTokenError
:
# Token is invalid
pass
Go
client
,
err
:=
app
.
Auth
(
ctx
)
if
err
!=
nil
{
log
.
Fatalf
(
"error getting Auth client: %v\n"
,
err
)
}
token
,
err
:=
client
.
VerifyIDTokenAndCheckRevoked
(
ctx
,
idToken
)
if
err
!=
nil
{
if
err
.
Error
()
==
"ID token has been revoked"
{
// Token is revoked. Inform the user to reauthenticate or signOut() the user.
}
else
{
// Token is invalid
}
}
log
.
Printf
(
"Verified ID token: %v\n"
,
token
)
C#
try
{
// Verify the ID token while checking if the token is revoked by passing checkRevoked
// as true.
bool
checkRevoked
=
true
;
var
decodedToken
=
await
FirebaseAuth
.
DefaultInstance
.
VerifyIdTokenAsync
(
idToken
,
checkRevoked
);
// Token is valid and not revoked.
string
uid
=
decodedToken
.
Uid
;
}
catch
(
FirebaseAuthException
ex
)
{
if
(
ex
.
AuthErrorCode
==
AuthErrorCode
.
RevokedIdToken
)
{
// Token has been revoked. Inform the user to re-authenticate or signOut() the user.
}
else
{
// Token is invalid.
}
}
Respond to token revocation on the client
If the token is revoked via the Admin SDK, the client is informed of the revocation and the user is expected to reauthenticate or is signed out:
function onIdTokenRevocation() {
// For an email/password user. Prompt the user for the password again.
let password = prompt('Please provide your password for reauthentication');
let credential = firebase.auth.EmailAuthProvider.credential(
firebase.auth().currentUser.email, password);
firebase.auth().currentUser.reauthenticateWithCredential(credential)
.then(result => {
// User successfully reauthenticated. New ID tokens should be valid.
})
.catch(error => {
// An error occurred.
});
}
Advanced Security: Enforce IP address restrictions
A common security mechanism for detecting token theft is to keep track of request IP address origins. For example, if requests are always coming from the same IP address (server making the call), single IP address sessions can be enforced. Or, you might revoke a user's token if you detect that the user's IP address suddenly changed geolocation or you receive a request from a suspicious origin.
To perform security checks based on IP address, for every authenticated request inspect the ID token and check if the request's IP address matches previous trusted IP addresses or is within a trusted range before allowing access to restricted data. For example:
app
.
post
(
'/getRestrictedData'
,
(
req
,
res
)
=
>
{
//
Get
the
ID
token
passed
.
const
idToken
=
req
.
body
.
idToken
;
//
Verify
the
ID
token
,
check
if
revoked
and
decode
its
payload
.
admin
.
auth
()
.
verifyIdToken
(
idToken
,
true
)
.
then
((
claims
)
=
>
{
//
Get
the
user
's previous IP addresses, previously saved.
return
getPreviousUserIpAddresses
(
claims
.
sub
);
})
.
then
(
previousIpAddresses
=
>
{
//
Get
the
request
IP
address
.
const
requestIpAddress
=
req
.
connection
.
remoteAddress
;
//
Check
if
the
request
IP
address
origin
is
suspicious
relative
to
previous
//
IP
addresses
.
The
current
request
timestamp
and
the
auth_time
of
the
ID
//
token
can
provide
additional
signals
of
abuse
especially
if
the
IP
address
//
suddenly
changed
.
If
there
was
a
sudden
location
change
in
a
//
short
period
of
time
,
then
it
will
give
stronger
signals
of
possible
abuse
.
if
(
!
isValidIpAddress
(
previousIpAddresses
,
requestIpAddress
))
{
//
Invalid
IP
address
,
take
action
quickly
and
revoke
all
user
's refresh tokens.
revokeUserTokens
(
claims
.
uid
)
.
then
(()
=
>
{
res
.
status
(
401
)
.
send
({
error
:
'Unauthorized access. Please login again!'
});
},
error
=
>
{
res
.
status
(
401
)
.
send
({
error
:
'Unauthorized access. Please login again!'
});
});
}
else
{
//
Access
is
valid
.
Try
to
return
data
.
getData
(
claims
)
.
then
(
data
=
>
{
res
.
end
(
JSON
.
stringify
(
data
);
},
error
=
>
{
res
.
status
(
500
)
.
send
({
error
:
'Server error!'
})
});
}
});
});

