Skip to content

Commit 7538ebd

Browse files
committed
Tolerating a 5 minute clock skew when verifying JWTs
1 parent 7b83c59 commit 7538ebd

File tree

3 files changed

+49
-7
lines changed

3 files changed

+49
-7
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 `VerifyIdToken()Async` function fnow tolerates a clock skew of up to
4+
5 minutes when comparing JWT timestamps.
45

56
# v1.2.0
67

FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseTokenVerifierTest.cs

Lines changed: 36 additions & 2 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

@@ -132,25 +134,57 @@ public async Task Expired()
132134
{
133135
var payload = new Dictionary<string, object>()
134136
{
135-
{ "exp", Clock.UnixTimestamp() - 60 },
137+
{ "exp", Clock.UnixTimestamp() - (ClockSkewSeconds + 1) },
136138
};
137139
var idToken = await CreateTestTokenAsync(payloadOverrides: payload);
138140
await Assert.ThrowsAsync<FirebaseException>(
139141
async () => await TokenVerifier.VerifyTokenAsync(idToken));
140142
}
141143

144+
[Fact]
145+
public async Task ExpiryTimeInAcceptableRange()
146+
{
147+
var expiryTimeSeconds = Clock.UnixTimestamp() - ClockSkewSeconds;
148+
var payload = new Dictionary<string, object>()
149+
{
150+
{ "exp", expiryTimeSeconds },
151+
};
152+
var idToken = await CreateTestTokenAsync(payloadOverrides: payload);
153+
154+
var decoded = await TokenVerifier.VerifyTokenAsync(idToken);
155+
156+
Assert.Equal("testuser", decoded.Uid);
157+
Assert.Equal(expiryTimeSeconds, decoded.ExpirationTimeSeconds);
158+
}
159+
142160
[Fact]
143161
public async Task InvalidIssuedAt()
144162
{
145163
var payload = new Dictionary<string, object>()
146164
{
147-
{ "iat", Clock.UnixTimestamp() + 60 },
165+
{ "iat", Clock.UnixTimestamp() + (ClockSkewSeconds + 1) },
148166
};
149167
var idToken = await CreateTestTokenAsync(payloadOverrides: payload);
150168
await Assert.ThrowsAsync<FirebaseException>(
151169
async () => await TokenVerifier.VerifyTokenAsync(idToken));
152170
}
153171

172+
[Fact]
173+
public async Task IssuedAtInAcceptableRange()
174+
{
175+
var issuedAtSeconds = Clock.UnixTimestamp() + ClockSkewSeconds;
176+
var payload = new Dictionary<string, object>()
177+
{
178+
{ "iat", issuedAtSeconds },
179+
};
180+
var idToken = await CreateTestTokenAsync(payloadOverrides: payload);
181+
182+
var decoded = await TokenVerifier.VerifyTokenAsync(idToken);
183+
184+
Assert.Equal("testuser", decoded.Uid);
185+
Assert.Equal(issuedAtSeconds, decoded.IssuedAtTimeSeconds);
186+
}
187+
154188
[Fact]
155189
public async Task InvalidIssuer()
156190
{

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 greater 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 less than ${currentTimeInSeconds}.";
162169
}
163170
else if (string.IsNullOrEmpty(payload.Subject))
164171
{

0 commit comments

Comments
 (0)