Skip to content

Commit d2ac9c6

Browse files
authored
feat(auth): Added revocation check support for VerifyIdTokenAsync() (#172)
* feat(auth): Added revocation check support for VerifyIdTokenAsync() * Fixed a typo in internal method name * Moving the new error code to the end of the enum * Updated copyright year
1 parent 6c51b93 commit d2ac9c6

File tree

7 files changed

+586
-338
lines changed

7 files changed

+586
-338
lines changed

FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseAuthTest.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ public async Task CreateCustomTokenInvalidCredential()
127127
public async Task VerifyIdTokenNoProjectId()
128128
{
129129
FirebaseApp.Create(new AppOptions() { Credential = MockCredential });
130-
var idToken = await FirebaseTokenVerifierTest.CreateTestTokenAsync();
130+
var idToken = await IdTokenVerificationTest.CreateTestTokenAsync();
131131
await Assert.ThrowsAsync<ArgumentException>(
132132
async () => await FirebaseAuth.DefaultInstance.VerifyIdTokenAsync(idToken));
133133
}
@@ -142,7 +142,7 @@ public async Task VerifyIdTokenCancel()
142142
});
143143
var canceller = new CancellationTokenSource();
144144
canceller.Cancel();
145-
var idToken = await FirebaseTokenVerifierTest.CreateTestTokenAsync();
145+
var idToken = await IdTokenVerificationTest.CreateTestTokenAsync();
146146
await Assert.ThrowsAnyAsync<OperationCanceledException>(
147147
async () => await FirebaseAuth.DefaultInstance.VerifyIdTokenAsync(
148148
idToken, canceller.Token));

FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseTokenVerifierTest.cs

Lines changed: 5 additions & 316 deletions
Original file line numberDiff line numberDiff line change
@@ -13,256 +13,24 @@
1313
// limitations under the License.
1414

1515
using System;
16-
using System.Collections.Generic;
17-
using System.Collections.Immutable;
18-
using System.IO;
19-
using System.Security.Cryptography;
20-
using System.Security.Cryptography.X509Certificates;
21-
using System.Threading;
22-
using System.Threading.Tasks;
23-
using FirebaseAdmin.Tests;
2416
using Google.Apis.Auth.OAuth2;
25-
using Google.Apis.Util;
2617
using Xunit;
2718

2819
namespace FirebaseAdmin.Auth.Tests
2920
{
3021
public class FirebaseTokenVerifierTest : IDisposable
3122
{
32-
private const long ClockSkewSeconds = 5 * 60;
33-
34-
private static readonly IPublicKeySource KeySource = new FileSystemPublicKeySource(
35-
"./resources/public_cert.pem");
36-
37-
private static readonly IClock Clock = new MockClock();
38-
39-
private static readonly ISigner Signer = CreateTestSigner();
40-
41-
private static readonly FirebaseTokenVerifier TokenVerifier = new FirebaseTokenVerifier(
42-
new FirebaseTokenVerifierArgs()
43-
{
44-
ProjectId = "test-project",
45-
ShortName = "ID token",
46-
Operation = "VerifyIdTokenAsync()",
47-
Url = "https://firebase.google.com/docs/auth/admin/verify-id-tokens",
48-
Issuer = "https://securetoken.google.com/",
49-
Clock = Clock,
50-
PublicKeySource = KeySource,
51-
});
52-
5323
private static readonly GoogleCredential MockCredential =
5424
GoogleCredential.FromAccessToken("test-token");
5525

5626
[Fact]
57-
public async Task ValidToken()
58-
{
59-
var payload = new Dictionary<string, object>()
60-
{
61-
{ "foo", "bar" },
62-
};
63-
var idToken = await CreateTestTokenAsync(payloadOverrides: payload);
64-
var decoded = await TokenVerifier.VerifyTokenAsync(idToken);
65-
Assert.Equal("testuser", decoded.Uid);
66-
Assert.Equal("test-project", decoded.Audience);
67-
Assert.Equal("testuser", decoded.Subject);
68-
69-
// The default test token created by CreateTestTokenAsync has an issue time 10 minutes
70-
// ago, and an expiry time 50 minutes in the future.
71-
Assert.Equal(Clock.UnixTimestamp() - (60 * 10), decoded.IssuedAtTimeSeconds);
72-
Assert.Equal(Clock.UnixTimestamp() + (60 * 50), decoded.ExpirationTimeSeconds);
73-
Assert.Single(decoded.Claims);
74-
object value;
75-
Assert.True(decoded.Claims.TryGetValue("foo", out value));
76-
Assert.Equal("bar", value);
77-
}
78-
79-
[Fact]
80-
public async Task InvalidArgument()
81-
{
82-
await Assert.ThrowsAsync<ArgumentException>(
83-
async () => await TokenVerifier.VerifyTokenAsync(null));
84-
await Assert.ThrowsAsync<ArgumentException>(
85-
async () => await TokenVerifier.VerifyTokenAsync(string.Empty));
86-
}
87-
88-
[Fact]
89-
public async Task MalformedToken()
90-
{
91-
var exception = await Assert.ThrowsAsync<FirebaseAuthException>(
92-
async () => await TokenVerifier.VerifyTokenAsync("not-a-token"));
93-
94-
this.CheckException(exception, "Incorrect number of segments in ID token.");
95-
}
96-
97-
[Fact]
98-
public async Task NoKid()
99-
{
100-
var header = new Dictionary<string, object>()
101-
{
102-
{ "kid", string.Empty },
103-
};
104-
var idToken = await CreateTestTokenAsync(headerOverrides: header);
105-
var exception = await Assert.ThrowsAsync<FirebaseAuthException>(
106-
async () => await TokenVerifier.VerifyTokenAsync(idToken));
107-
108-
this.CheckException(exception, "Firebase ID token has no 'kid' claim.");
109-
}
110-
111-
[Fact]
112-
public async Task IncorrectKid()
113-
{
114-
var header = new Dictionary<string, object>()
115-
{
116-
{ "kid", "incorrect-key-id" },
117-
};
118-
var idToken = await CreateTestTokenAsync(headerOverrides: header);
119-
var exception = await Assert.ThrowsAsync<FirebaseAuthException>(
120-
async () => await TokenVerifier.VerifyTokenAsync(idToken));
121-
122-
this.CheckException(exception, "Failed to verify ID token signature.");
123-
}
124-
125-
[Fact]
126-
public async Task IncorrectAlgorithm()
127-
{
128-
var header = new Dictionary<string, object>()
129-
{
130-
{ "alg", "HS256" },
131-
};
132-
var idToken = await CreateTestTokenAsync(headerOverrides: header);
133-
var exception = await Assert.ThrowsAsync<FirebaseAuthException>(
134-
async () => await TokenVerifier.VerifyTokenAsync(idToken));
135-
136-
var expectedMessage = "Firebase ID token has incorrect algorithm."
137-
+ " Expected RS256 but got HS256.";
138-
this.CheckException(exception, expectedMessage);
139-
}
140-
141-
[Fact]
142-
public async Task Expired()
143-
{
144-
var expiryTime = Clock.UnixTimestamp() - (ClockSkewSeconds + 1);
145-
var payload = new Dictionary<string, object>()
146-
{
147-
{ "exp", expiryTime },
148-
};
149-
var idToken = await CreateTestTokenAsync(payloadOverrides: payload);
150-
var exception = await Assert.ThrowsAsync<FirebaseAuthException>(
151-
async () => await TokenVerifier.VerifyTokenAsync(idToken));
152-
153-
var expectedMessage = $"Firebase ID token expired at {expiryTime}. "
154-
+ $"Expected to be greater than {Clock.UnixTimestamp()}.";
155-
this.CheckException(exception, expectedMessage, AuthErrorCode.ExpiredIdToken);
156-
}
157-
158-
[Fact]
159-
public async Task ExpiryTimeInAcceptableRange()
160-
{
161-
var expiryTimeSeconds = Clock.UnixTimestamp() - ClockSkewSeconds;
162-
var payload = new Dictionary<string, object>()
163-
{
164-
{ "exp", expiryTimeSeconds },
165-
};
166-
var idToken = await CreateTestTokenAsync(payloadOverrides: payload);
167-
168-
var decoded = await TokenVerifier.VerifyTokenAsync(idToken);
169-
170-
Assert.Equal("testuser", decoded.Uid);
171-
Assert.Equal(expiryTimeSeconds, decoded.ExpirationTimeSeconds);
172-
}
173-
174-
[Fact]
175-
public async Task InvalidIssuedAt()
176-
{
177-
var issuedAt = Clock.UnixTimestamp() + (ClockSkewSeconds + 1);
178-
var payload = new Dictionary<string, object>()
179-
{
180-
{ "iat", issuedAt },
181-
};
182-
var idToken = await CreateTestTokenAsync(payloadOverrides: payload);
183-
var exception = await Assert.ThrowsAsync<FirebaseAuthException>(
184-
async () => await TokenVerifier.VerifyTokenAsync(idToken));
185-
186-
var expectedMessage = $"Firebase ID token issued at future timestamp {issuedAt}.";
187-
this.CheckException(exception, expectedMessage);
188-
}
189-
190-
[Fact]
191-
public async Task IssuedAtInAcceptableRange()
27+
public void NoProjectId()
19228
{
193-
var issuedAtSeconds = Clock.UnixTimestamp() + ClockSkewSeconds;
194-
var payload = new Dictionary<string, object>()
195-
{
196-
{ "iat", issuedAtSeconds },
197-
};
198-
var idToken = await CreateTestTokenAsync(payloadOverrides: payload);
199-
200-
var decoded = await TokenVerifier.VerifyTokenAsync(idToken);
201-
202-
Assert.Equal("testuser", decoded.Uid);
203-
Assert.Equal(issuedAtSeconds, decoded.IssuedAtTimeSeconds);
204-
}
205-
206-
[Fact]
207-
public async Task InvalidIssuer()
208-
{
209-
var payload = new Dictionary<string, object>()
210-
{
211-
{ "iss", "wrong-issuer" },
212-
};
213-
var idToken = await CreateTestTokenAsync(payloadOverrides: payload);
214-
var exception = await Assert.ThrowsAsync<FirebaseAuthException>(
215-
async () => await TokenVerifier.VerifyTokenAsync(idToken));
216-
217-
var expectedMessage = "ID token has incorrect issuer (iss) claim.";
218-
this.CheckException(exception, expectedMessage);
219-
}
220-
221-
[Fact]
222-
public async Task InvalidAudience()
223-
{
224-
var payload = new Dictionary<string, object>()
225-
{
226-
{ "aud", "wrong-audience" },
227-
};
228-
var idToken = await CreateTestTokenAsync(payloadOverrides: payload);
229-
var exception = await Assert.ThrowsAsync<FirebaseAuthException>(
230-
async () => await TokenVerifier.VerifyTokenAsync(idToken));
231-
232-
var expectedMessage = "ID token has incorrect audience (aud) claim."
233-
+ " Expected test-project but got wrong-audience";
234-
this.CheckException(exception, expectedMessage);
235-
}
236-
237-
[Fact]
238-
public async Task EmptySubject()
239-
{
240-
var payload = new Dictionary<string, object>()
241-
{
242-
{ "sub", string.Empty },
243-
};
244-
var idToken = await CreateTestTokenAsync(payloadOverrides: payload);
245-
var exception = await Assert.ThrowsAsync<FirebaseAuthException>(
246-
async () => await TokenVerifier.VerifyTokenAsync(idToken));
247-
248-
var expectedMessage = "Firebase ID token has no or empty subject (sub) claim.";
249-
this.CheckException(exception, expectedMessage);
250-
}
251-
252-
[Fact]
253-
public async Task LongSubject()
254-
{
255-
var payload = new Dictionary<string, object>()
29+
var app = FirebaseApp.Create(new AppOptions()
25630
{
257-
{ "sub", new string('a', 129) },
258-
};
259-
var idToken = await CreateTestTokenAsync(payloadOverrides: payload);
260-
var exception = await Assert.ThrowsAsync<FirebaseAuthException>(
261-
async () => await TokenVerifier.VerifyTokenAsync(idToken));
262-
263-
var expectedMessage = "Firebase ID token has a subject claim longer than"
264-
+ " 128 characters.";
265-
this.CheckException(exception, expectedMessage);
31+
Credential = MockCredential,
32+
});
33+
Assert.Throws<ArgumentException>(() => FirebaseTokenVerifier.CreateIDTokenVerifier(app));
26634
}
26735

26836
[Fact]
@@ -311,84 +79,5 @@ public void Dispose()
31179
{
31280
FirebaseApp.DeleteAll();
31381
}
314-
315-
/// <summary>
316-
/// Creates a mock ID token for testing purposes. By default the created token has an issue
317-
/// time 10 minutes ago, and an expirty time 50 minutes into the future. All header and
318-
/// payload claims can be overridden if needed.
319-
/// </summary>
320-
internal static async Task<string> CreateTestTokenAsync(
321-
Dictionary<string, object> headerOverrides = null,
322-
Dictionary<string, object> payloadOverrides = null)
323-
{
324-
var header = new Dictionary<string, object>()
325-
{
326-
{ "alg", "RS256" },
327-
{ "typ", "jwt" },
328-
{ "kid", "test-key-id" },
329-
};
330-
if (headerOverrides != null)
331-
{
332-
foreach (var entry in headerOverrides)
333-
{
334-
header[entry.Key] = entry.Value;
335-
}
336-
}
337-
338-
var payload = new Dictionary<string, object>()
339-
{
340-
{ "sub", "testuser" },
341-
{ "iss", "https://securetoken.google.com/test-project" },
342-
{ "aud", "test-project" },
343-
{ "iat", Clock.UnixTimestamp() - (60 * 10) },
344-
{ "exp", Clock.UnixTimestamp() + (60 * 50) },
345-
};
346-
if (payloadOverrides != null)
347-
{
348-
foreach (var entry in payloadOverrides)
349-
{
350-
payload[entry.Key] = entry.Value;
351-
}
352-
}
353-
354-
return await JwtUtils.CreateSignedJwtAsync(header, payload, Signer);
355-
}
356-
357-
private static ISigner CreateTestSigner()
358-
{
359-
var credential = GoogleCredential.FromFile("./resources/service_account.json");
360-
var serviceAccount = (ServiceAccountCredential)credential.UnderlyingCredential;
361-
return new ServiceAccountSigner(serviceAccount);
362-
}
363-
364-
private void CheckException(
365-
FirebaseAuthException exception,
366-
string prefix,
367-
AuthErrorCode errorCode = AuthErrorCode.InvalidIdToken)
368-
{
369-
Assert.Equal(ErrorCode.InvalidArgument, exception.ErrorCode);
370-
Assert.StartsWith(prefix, exception.Message);
371-
Assert.Equal(errorCode, exception.AuthErrorCode);
372-
Assert.Null(exception.InnerException);
373-
Assert.Null(exception.HttpResponse);
374-
}
375-
}
376-
377-
internal class FileSystemPublicKeySource : IPublicKeySource
378-
{
379-
private IReadOnlyList<PublicKey> rsa;
380-
381-
public FileSystemPublicKeySource(string file)
382-
{
383-
var x509cert = new X509Certificate2(File.ReadAllBytes(file));
384-
var rsa = (RSA)x509cert.PublicKey.Key;
385-
this.rsa = ImmutableList.Create(new PublicKey("test-key-id", rsa));
386-
}
387-
388-
public Task<IReadOnlyList<PublicKey>> GetPublicKeysAsync(
389-
CancellationToken cancellationToken)
390-
{
391-
return Task.FromResult(this.rsa);
392-
}
39382
}
39483
}

0 commit comments

Comments
 (0)