Skip to content

Commit 4f7020b

Browse files
authored
Merge pull request #29 from firebase/hkj-clock-skew
Tolerating a 5 minute clock skew when verifying JWTs
2 parents 7b83c59 + 083ca43 commit 4f7020b

File tree

3 files changed

+59
-9
lines changed

3 files changed

+59
-9
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Unreleased
22

3-
-
3+
- [fixed] The `VerifyIdTokenAsync()` function now tolerates a clock skew of up
4+
to 5 minutes when comparing JWT timestamps.
45

56
# v1.2.0
67

FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseTokenVerifierTest.cs

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ namespace FirebaseAdmin.Auth.Tests
3030
{
3131
public class FirebaseTokenVerifierTest : IDisposable
3232
{
33+
private const long ClockSkewSeconds = 5 * 60;
34+
3335
private static readonly IPublicKeySource KeySource = new FileSystemPublicKeySource(
3436
"./resources/public_cert.pem");
3537

@@ -130,25 +132,65 @@ await Assert.ThrowsAsync<FirebaseException>(
130132
[Fact]
131133
public async Task Expired()
132134
{
135+
var expiryTime = Clock.UnixTimestamp() - (ClockSkewSeconds + 1);
133136
var payload = new Dictionary<string, object>()
134137
{
135-
{ "exp", Clock.UnixTimestamp() - 60 },
138+
{ "exp", expiryTime },
136139
};
137140
var idToken = await CreateTestTokenAsync(payloadOverrides: payload);
138-
await Assert.ThrowsAsync<FirebaseException>(
141+
var exception = await Assert.ThrowsAsync<FirebaseException>(
139142
async () => await TokenVerifier.VerifyTokenAsync(idToken));
143+
var expectedMessage = $"Firebase ID token expired at {expiryTime}. "
144+
+ $"Expected to be greater than {Clock.UnixTimestamp()}.";
145+
Assert.Equal(expectedMessage, exception.Message);
146+
}
147+
148+
[Fact]
149+
public async Task ExpiryTimeInAcceptableRange()
150+
{
151+
var expiryTimeSeconds = Clock.UnixTimestamp() - ClockSkewSeconds;
152+
var payload = new Dictionary<string, object>()
153+
{
154+
{ "exp", expiryTimeSeconds },
155+
};
156+
var idToken = await CreateTestTokenAsync(payloadOverrides: payload);
157+
158+
var decoded = await TokenVerifier.VerifyTokenAsync(idToken);
159+
160+
Assert.Equal("testuser", decoded.Uid);
161+
Assert.Equal(expiryTimeSeconds, decoded.ExpirationTimeSeconds);
140162
}
141163

142164
[Fact]
143165
public async Task InvalidIssuedAt()
144166
{
167+
var issuedAt = Clock.UnixTimestamp() + (ClockSkewSeconds + 1);
145168
var payload = new Dictionary<string, object>()
146169
{
147-
{ "iat", Clock.UnixTimestamp() + 60 },
170+
{ "iat", issuedAt },
148171
};
149172
var idToken = await CreateTestTokenAsync(payloadOverrides: payload);
150-
await Assert.ThrowsAsync<FirebaseException>(
173+
var exception = await Assert.ThrowsAsync<FirebaseException>(
151174
async () => await TokenVerifier.VerifyTokenAsync(idToken));
175+
var expectedMessage = $"Firebase ID token issued at future timestamp {issuedAt}. "
176+
+ $"Expected to be less than {Clock.UnixTimestamp()}.";
177+
Assert.Equal(expectedMessage, exception.Message);
178+
}
179+
180+
[Fact]
181+
public async Task IssuedAtInAcceptableRange()
182+
{
183+
var issuedAtSeconds = Clock.UnixTimestamp() + ClockSkewSeconds;
184+
var payload = new Dictionary<string, object>()
185+
{
186+
{ "iat", issuedAtSeconds },
187+
};
188+
var idToken = await CreateTestTokenAsync(payloadOverrides: payload);
189+
190+
var decoded = await TokenVerifier.VerifyTokenAsync(idToken);
191+
192+
Assert.Equal("testuser", decoded.Uid);
193+
Assert.Equal(issuedAtSeconds, decoded.IssuedAtTimeSeconds);
152194
}
153195

154196
[Fact]

FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenVerifier.cs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ internal sealed class FirebaseTokenVerifier
3838
private const string FirebaseAudience = "https://identitytoolkit.googleapis.com/"
3939
+ "google.identity.identitytoolkit.v1.IdentityToolkit";
4040

41+
private const long ClockSkewSeconds = 5 * 60;
42+
4143
// See http://oid-info.com/get/2.16.840.1.101.3.4.2.1
4244
private const string Sha256Oid = "2.16.840.1.101.3.4.2.1";
4345

@@ -120,6 +122,8 @@ internal async Task<FirebaseToken> VerifyTokenAsync(
120122
+ $"{this.shortName}.";
121123
var issuer = this.issuer + this.ProjectId;
122124
string error = null;
125+
var currentTimeInSeconds = this.clock.UnixTimestamp();
126+
123127
if (string.IsNullOrEmpty(header.KeyId))
124128
{
125129
if (payload.Audience == FirebaseAudience)
@@ -152,13 +156,16 @@ internal async Task<FirebaseToken> VerifyTokenAsync(
152156
error = $"{this.shortName} has incorrect issuer (iss) claim. Expected {issuer} but "
153157
+ $"got {payload.Issuer}. {projectIdMessage} {verifyTokenMessage}";
154158
}
155-
else if (payload.IssuedAtTimeSeconds > this.clock.UnixTimestamp())
159+
else if (payload.IssuedAtTimeSeconds - ClockSkewSeconds > currentTimeInSeconds)
156160
{
157-
error = $"Firebase {this.shortName} issued at future timestamp";
161+
error = $"Firebase {this.shortName} issued at future timestamp "
162+
+ $"{payload.IssuedAtTimeSeconds}. Expected to be less than "
163+
+ $"{currentTimeInSeconds}.";
158164
}
159-
else if (payload.ExpirationTimeSeconds < this.clock.UnixTimestamp())
165+
else if (payload.ExpirationTimeSeconds + ClockSkewSeconds < currentTimeInSeconds)
160166
{
161-
error = $"Firebase {this.shortName} expired at {payload.ExpirationTimeSeconds}";
167+
error = $"Firebase {this.shortName} expired at {payload.ExpirationTimeSeconds}. "
168+
+ $"Expected to be greater than {currentTimeInSeconds}.";
162169
}
163170
else if (string.IsNullOrEmpty(payload.Subject))
164171
{

0 commit comments

Comments
 (0)