Skip to content

Commit 36822f1

Browse files
authored
Create Custom Token API (#4)
* Custom token creation API * Updated documentation * Added documentation and code clean up * Improved error handling * new line at eof * Improved error handling: Parsing errors returned by IAM * Reducing the level of indirection in error handling * Removing System.Threading.Tasks qualifier * Minor refactoring
1 parent 53184e8 commit 36822f1

15 files changed

+1201
-10
lines changed

FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseAuthTest.cs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,63 @@ public void GetAuth()
5959
Assert.Throws<InvalidOperationException>(() => FirebaseAuth.GetAuth(app));
6060
}
6161

62+
[Fact]
63+
public async Task UseAfterDelete()
64+
{
65+
var app = FirebaseApp.Create(new AppOptions(){Credential = mockCredential});
66+
FirebaseAuth auth = FirebaseAuth.DefaultInstance;
67+
app.Delete();
68+
await Assert.ThrowsAsync<InvalidOperationException>(
69+
async () => await auth.CreateCustomTokenAsync("user"));
70+
await Assert.ThrowsAsync<InvalidOperationException>(
71+
async () => await auth.VerifyIdTokenAsync("user"));
72+
}
73+
74+
[Fact]
75+
public async Task CreateCustomToken()
76+
{
77+
var cred = GoogleCredential.FromFile("./resources/service_account.json");
78+
FirebaseApp.Create(new AppOptions(){Credential = cred});
79+
var token = await FirebaseAuth.DefaultInstance.CreateCustomTokenAsync("user1");
80+
VerifyCustomToken(token, "user1", null);
81+
}
82+
83+
[Fact]
84+
public async Task CreateCustomTokenWithClaims()
85+
{
86+
var cred = GoogleCredential.FromFile("./resources/service_account.json");
87+
FirebaseApp.Create(new AppOptions(){Credential = cred});
88+
var developerClaims = new Dictionary<string, object>()
89+
{
90+
{"admin", true},
91+
{"package", "gold"},
92+
{"magicNumber", 42L},
93+
};
94+
var token = await FirebaseAuth.DefaultInstance.CreateCustomTokenAsync(
95+
"user2", developerClaims);
96+
VerifyCustomToken(token, "user2", developerClaims);
97+
}
98+
99+
[Fact]
100+
public async Task CreateCustomTokenCancel()
101+
{
102+
var cred = GoogleCredential.FromFile("./resources/service_account.json");
103+
FirebaseApp.Create(new AppOptions(){Credential = cred});
104+
var canceller = new CancellationTokenSource();
105+
canceller.Cancel();
106+
await Assert.ThrowsAsync<OperationCanceledException>(
107+
async () => await FirebaseAuth.DefaultInstance.CreateCustomTokenAsync(
108+
"user1", canceller.Token));
109+
}
110+
111+
[Fact]
112+
public async Task CreateCustomTokenInvalidCredential()
113+
{
114+
FirebaseApp.Create(new AppOptions(){Credential = mockCredential});
115+
await Assert.ThrowsAsync<FirebaseException>(
116+
async () => await FirebaseAuth.DefaultInstance.CreateCustomTokenAsync("user1"));
117+
}
118+
62119
[Fact]
63120
public async Task VerifyIdTokenNoProjectId()
64121
{
@@ -84,6 +141,38 @@ await Assert.ThrowsAsync<OperationCanceledException>(
84141
idToken, canceller.Token));
85142
}
86143

144+
private static void VerifyCustomToken(string token, string uid, Dictionary<string, object> claims)
145+
{
146+
String[] segments = token.Split(".");
147+
Assert.Equal(3, segments.Length);
148+
149+
var payload = JwtUtils.Decode<CustomTokenPayload>(segments[1]);
150+
Assert.Equal("[email protected]", payload.Issuer);
151+
Assert.Equal("[email protected]", payload.Subject);
152+
Assert.Equal(uid, payload.Uid);
153+
if (claims == null)
154+
{
155+
Assert.Null(payload.Claims);
156+
}
157+
else
158+
{
159+
Assert.Equal(claims.Count, payload.Claims.Count);
160+
foreach (var entry in claims)
161+
{
162+
object value;
163+
Assert.True(payload.Claims.TryGetValue(entry.Key, out value));
164+
Assert.Equal(entry.Value, value);
165+
}
166+
}
167+
168+
var x509cert = new X509Certificate2(File.ReadAllBytes("./resources/public_cert.pem"));
169+
var rsa = (RSA) x509cert.PublicKey.Key;
170+
var tokenData = Encoding.UTF8.GetBytes(segments[0] + "." + segments[1]);
171+
var signature = JwtUtils.Base64DecodeToBytes(segments[2]);
172+
var verified = rsa.VerifyData(tokenData, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
173+
Assert.True(verified);
174+
}
175+
87176
public void Dispose()
88177
{
89178
FirebaseApp.DeleteAll();
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
// Copyright 2018, Google Inc. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
using System;
16+
using System.Collections.Generic;
17+
using System.IO;
18+
using System.Security.Cryptography;
19+
using System.Security.Cryptography.X509Certificates;
20+
using System.Text;
21+
using System.Threading;
22+
using System.Threading.Tasks;
23+
using Xunit;
24+
using FirebaseAdmin.Tests;
25+
using FirebaseAdmin.Auth;
26+
using Google.Apis.Auth;
27+
using Google.Apis.Util;
28+
29+
namespace FirebaseAdmin.Auth.Tests
30+
{
31+
public class FirebaseTokenFactoryTest
32+
{
33+
[Fact]
34+
public async Task CreateCustomToken()
35+
{
36+
var clock = new MockClock();
37+
var factory = new FirebaseTokenFactory(new MockSigner(), clock);
38+
var token = await factory.CreateCustomTokenAsync("user1");
39+
VerifyCustomToken(token, "user1", null);
40+
}
41+
42+
[Fact]
43+
public async Task CreateCustomTokenWithEmptyClaims()
44+
{
45+
var clock = new MockClock();
46+
var factory = new FirebaseTokenFactory(new MockSigner(), clock);
47+
var token = await factory.CreateCustomTokenAsync(
48+
"user1", new Dictionary<string, object>());
49+
VerifyCustomToken(token, "user1", null);
50+
}
51+
52+
[Fact]
53+
public async Task CreateCustomTokenWithClaims()
54+
{
55+
var clock = new MockClock();
56+
var factory = new FirebaseTokenFactory(new MockSigner(), clock);
57+
var developerClaims = new Dictionary<string, object>()
58+
{
59+
{"admin", true},
60+
{"package", "gold"},
61+
{"magicNumber", 42L},
62+
};
63+
var token = await factory.CreateCustomTokenAsync("user2", developerClaims);
64+
VerifyCustomToken(token, "user2", developerClaims);
65+
}
66+
67+
[Fact]
68+
public async Task InvalidUid()
69+
{
70+
var factory = new FirebaseTokenFactory(new MockSigner(), new MockClock());
71+
await Assert.ThrowsAsync<ArgumentException>(
72+
async () => await factory.CreateCustomTokenAsync(null));
73+
await Assert.ThrowsAsync<ArgumentException>(
74+
async () => await factory.CreateCustomTokenAsync(""));
75+
await Assert.ThrowsAsync<ArgumentException>(
76+
async () => await factory.CreateCustomTokenAsync(new String('a', 129)));
77+
}
78+
79+
[Fact]
80+
public async Task ReservedClaims()
81+
{
82+
var factory = new FirebaseTokenFactory(new MockSigner(), new MockClock());
83+
foreach(var key in FirebaseTokenFactory.ReservedClaims)
84+
{
85+
var developerClaims = new Dictionary<string, object>(){
86+
{key, "value"},
87+
};
88+
await Assert.ThrowsAsync<ArgumentException>(
89+
async () => await factory.CreateCustomTokenAsync("user", developerClaims));
90+
}
91+
}
92+
93+
private static void VerifyCustomToken(
94+
string token, string uid, Dictionary<string, object> claims)
95+
{
96+
String[] segments = token.Split(".");
97+
Assert.Equal(3, segments.Length);
98+
// verify header
99+
var header = JwtUtils.Decode<GoogleJsonWebSignature.Header>(segments[0]);
100+
Assert.Equal("JWT", header.Type);
101+
Assert.Equal("RS256", header.Algorithm);
102+
103+
// verify payload
104+
var payload = JwtUtils.Decode<CustomTokenPayload>(segments[1]);
105+
Assert.Equal(MockSigner.KeyIdString, payload.Issuer);
106+
Assert.Equal(MockSigner.KeyIdString, payload.Subject);
107+
Assert.Equal(uid, payload.Uid);
108+
Assert.Equal(FirebaseTokenFactory.FirebaseAudience, payload.Audience);
109+
if (claims == null)
110+
{
111+
Assert.Null(payload.Claims);
112+
}
113+
else
114+
{
115+
Assert.Equal(claims.Count, payload.Claims.Count);
116+
foreach (var entry in claims)
117+
{
118+
object value;
119+
Assert.True(payload.Claims.TryGetValue(entry.Key, out value));
120+
Assert.Equal(entry.Value, value);
121+
}
122+
}
123+
124+
// verify mock signature
125+
Assert.Equal(MockSigner.Signature, JwtUtils.Base64Decode(segments[2]));
126+
}
127+
}
128+
129+
internal sealed class MockSigner : ISigner
130+
{
131+
public const string KeyIdString = "mock-key-id";
132+
public const string Signature = "signature";
133+
134+
public Task<string> GetKeyIdAsync(CancellationToken cancellationToken)
135+
{
136+
return Task.FromResult(KeyIdString);
137+
}
138+
139+
public Task<byte[]> SignDataAsync(byte[] data, CancellationToken cancellationToken)
140+
{
141+
return Task.FromResult(Encoding.UTF8.GetBytes(Signature));
142+
}
143+
144+
public void Dispose() {}
145+
}
146+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// Copyright 2018, Google Inc. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
using System;
16+
using System.IO;
17+
using System.Net;
18+
using System.Net.Http;
19+
using System.Net.Http.Headers;
20+
using System.Threading.Tasks;
21+
using Xunit;
22+
using FirebaseAdmin.Tests;
23+
using FirebaseAdmin.Auth;
24+
25+
namespace FirebaseAdmin.Auth.Tests
26+
{
27+
public class HttpPublicKeySourceTest
28+
{
29+
[Fact]
30+
public async Task GetPublicKeysWithoutCaching()
31+
{
32+
var clock = new MockClock();
33+
var handler = new MockMessageHandler()
34+
{
35+
Response = File.ReadAllBytes("./resources/public_keys.json"),
36+
};
37+
var clientFactory = new MockHttpClientFactory(handler);
38+
var keyManager = new HttpPublicKeySource(
39+
"https://example.com/certs", clock, clientFactory);
40+
var keys = await keyManager.GetPublicKeysAsync();
41+
Assert.Equal(2, keys.Count);
42+
Assert.Equal(1, handler.Calls);
43+
44+
var keys2 = await keyManager.GetPublicKeysAsync();
45+
Assert.Equal(2, keys.Count);
46+
Assert.Equal(2, handler.Calls);
47+
Assert.NotSame(keys, keys2);
48+
}
49+
50+
[Fact]
51+
public async Task GetPublicKeysWithCaching()
52+
{
53+
var clock = new MockClock();
54+
var cacheControl = new CacheControlHeaderValue()
55+
{
56+
MaxAge = new TimeSpan(hours: 1, minutes: 0, seconds: 0),
57+
};
58+
var handler = new MockMessageHandler()
59+
{
60+
Response = File.ReadAllBytes("./resources/public_keys.json"),
61+
ApplyHeaders = (header) => header.CacheControl = cacheControl,
62+
};
63+
var clientFactory = new MockHttpClientFactory(handler);
64+
var keyManager = new HttpPublicKeySource(
65+
"https://example.com/certs", clock, clientFactory);
66+
var keys = await keyManager.GetPublicKeysAsync();
67+
Assert.Equal(2, keys.Count);
68+
Assert.Equal(1, handler.Calls);
69+
70+
clock.UtcNow = clock.UtcNow.AddMinutes(50);
71+
var keys2 = await keyManager.GetPublicKeysAsync();
72+
Assert.Equal(2, keys.Count);
73+
Assert.Equal(1, handler.Calls);
74+
Assert.Same(keys, keys2);
75+
76+
clock.UtcNow = clock.UtcNow.AddMinutes(10);
77+
var keys3 = await keyManager.GetPublicKeysAsync();
78+
Assert.Equal(2, keys.Count);
79+
Assert.Equal(2, handler.Calls);
80+
Assert.NotSame(keys, keys3);
81+
}
82+
83+
[Fact]
84+
public void InvalidArguments()
85+
{
86+
var clock = new MockClock();
87+
var handler = new MockMessageHandler()
88+
{
89+
Response = File.ReadAllBytes("./resources/public_keys.json"),
90+
};
91+
var clientFactory = new MockHttpClientFactory(handler);
92+
Assert.Throws<ArgumentException>(
93+
() => new HttpPublicKeySource(null, clock, clientFactory));
94+
Assert.Throws<ArgumentException>(
95+
() => new HttpPublicKeySource("", clock, clientFactory));
96+
Assert.Throws<ArgumentNullException>(
97+
() => new HttpPublicKeySource("https://example.com/certs", null, clientFactory));
98+
Assert.Throws<ArgumentNullException>(
99+
() => new HttpPublicKeySource("https://example.com/certs", clock, null));
100+
}
101+
102+
[Fact]
103+
public async Task HttpError()
104+
{
105+
var clock = new MockClock();
106+
var handler = new MockMessageHandler()
107+
{
108+
StatusCode = HttpStatusCode.InternalServerError,
109+
Response = "test error",
110+
};
111+
var clientFactory = new MockHttpClientFactory(handler);
112+
var keyManager = new HttpPublicKeySource(
113+
"https://example.com/certs", clock, clientFactory);
114+
await Assert.ThrowsAsync<FirebaseException>(
115+
async () => await keyManager.GetPublicKeysAsync());
116+
Assert.Equal(1, handler.Calls);
117+
}
118+
}
119+
}

0 commit comments

Comments
 (0)