Skip to content

Commit fcc5d17

Browse files
authored
feat(auth): Added support for creating tenant-scoped custom tokens (#228)
* feat(auth): Added support for creating tenant-scoped custom tokens * fix: Cleaned up the test code
1 parent 4b4049c commit fcc5d17

File tree

11 files changed

+643
-339
lines changed

11 files changed

+643
-339
lines changed
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// Copyright 2020, 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.Threading;
18+
using System.Threading.Tasks;
19+
using FirebaseAdmin.Auth.Jwt.Tests;
20+
using Google.Apis.Auth.OAuth2;
21+
using Xunit;
22+
23+
namespace FirebaseAdmin.Auth
24+
{
25+
public class CustomTokenTest : IDisposable
26+
{
27+
public static readonly IEnumerable<object[]> TestConfigs = new List<object[]>()
28+
{
29+
new object[] { FirebaseAuthTestConfig.DefaultInstance },
30+
new object[] { TenantAwareFirebaseAuthTestConfig.DefaultInstance },
31+
};
32+
33+
[Theory]
34+
[MemberData(nameof(TestConfigs))]
35+
public async Task CreateCustomToken(TestConfig config)
36+
{
37+
var token = await config.CreateAuth().CreateCustomTokenAsync("user1");
38+
39+
config.AssertCustomToken(token, "user1");
40+
}
41+
42+
[Theory]
43+
[MemberData(nameof(TestConfigs))]
44+
public async Task CreateCustomTokenWithClaims(TestConfig config)
45+
{
46+
var developerClaims = new Dictionary<string, object>()
47+
{
48+
{ "admin", true },
49+
{ "package", "gold" },
50+
{ "magicNumber", 42L },
51+
};
52+
53+
var token = await config.CreateAuth().CreateCustomTokenAsync("user2", developerClaims);
54+
55+
config.AssertCustomToken(token, "user2", developerClaims);
56+
}
57+
58+
[Theory]
59+
[MemberData(nameof(TestConfigs))]
60+
public async Task CreateCustomTokenCancel(TestConfig config)
61+
{
62+
var canceller = new CancellationTokenSource();
63+
canceller.Cancel();
64+
var auth = config.CreateAuth();
65+
66+
await Assert.ThrowsAsync<OperationCanceledException>(
67+
() => auth.CreateCustomTokenAsync("user1", canceller.Token));
68+
}
69+
70+
[Theory]
71+
[MemberData(nameof(TestConfigs))]
72+
public async Task CreateCustomTokenInvalidCredential(TestConfig config)
73+
{
74+
var options = new AppOptions
75+
{
76+
Credential = GoogleCredential.FromAccessToken("test-token"),
77+
ProjectId = "project1",
78+
};
79+
var auth = config.CreateAuth(options);
80+
81+
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
82+
() => auth.CreateCustomTokenAsync("user1"));
83+
84+
var errorMessage = "Failed to determine service account ID. Make sure to initialize the SDK "
85+
+ "with service account credentials or specify a service account "
86+
+ "ID with iam.serviceAccounts.signBlob permission. Please refer to "
87+
+ "https://firebase.google.com/docs/auth/admin/create-custom-tokens for "
88+
+ "more details on creating custom tokens.";
89+
Assert.Equal(errorMessage, ex.Message);
90+
}
91+
92+
public void Dispose()
93+
{
94+
FirebaseApp.DeleteAll();
95+
}
96+
97+
public abstract class TestConfig
98+
{
99+
protected static readonly AppOptions DefaultOptions = new AppOptions
100+
{
101+
Credential = GoogleCredential.FromFile("./resources/service_account.json"),
102+
};
103+
104+
internal abstract CustomTokenVerifier TokenVerifier { get; }
105+
106+
internal abstract AbstractFirebaseAuth CreateAuth(AppOptions options = null);
107+
108+
internal void AssertCustomToken(
109+
string token, string uid, Dictionary<string, object> claims = null)
110+
{
111+
this.TokenVerifier.Verify(token, uid, claims);
112+
}
113+
}
114+
115+
private sealed class FirebaseAuthTestConfig : TestConfig
116+
{
117+
internal static readonly FirebaseAuthTestConfig DefaultInstance =
118+
new FirebaseAuthTestConfig();
119+
120+
internal override CustomTokenVerifier TokenVerifier =>
121+
CustomTokenVerifier.FromDefaultServiceAccount();
122+
123+
internal override AbstractFirebaseAuth CreateAuth(AppOptions options = null)
124+
{
125+
FirebaseApp.Create(options ?? DefaultOptions);
126+
return FirebaseAuth.DefaultInstance;
127+
}
128+
}
129+
130+
private sealed class TenantAwareFirebaseAuthTestConfig : TestConfig
131+
{
132+
internal static readonly TenantAwareFirebaseAuthTestConfig DefaultInstance =
133+
new TenantAwareFirebaseAuthTestConfig();
134+
135+
internal override CustomTokenVerifier TokenVerifier =>
136+
CustomTokenVerifier.FromDefaultServiceAccount("tenant1");
137+
138+
internal override AbstractFirebaseAuth CreateAuth(AppOptions options = null)
139+
{
140+
FirebaseApp.Create(options ?? DefaultOptions);
141+
return FirebaseAuth.DefaultInstance.TenantManager.AuthForTenant("tenant1");
142+
}
143+
}
144+
}
145+
}

FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseAuthTest.cs

Lines changed: 8 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,8 @@
1414

1515
using System;
1616
using System.Collections.Generic;
17-
using System.IO;
18-
using System.Security.Cryptography;
19-
using System.Security.Cryptography.X509Certificates;
2017
using System.Text;
21-
using System.Threading;
2218
using System.Threading.Tasks;
23-
using FirebaseAdmin.Auth.Jwt;
2419
using Google.Apis.Auth.OAuth2;
2520
using Xunit;
2621

@@ -62,10 +57,11 @@ public void GetAuth()
6257
public async Task UseAfterDelete()
6358
{
6459
var app = FirebaseApp.Create(new AppOptions() { Credential = MockCredential });
65-
FirebaseAuth auth = FirebaseAuth.DefaultInstance;
60+
var auth = FirebaseAuth.DefaultInstance;
61+
6662
app.Delete();
67-
await Assert.ThrowsAsync<InvalidOperationException>(
68-
async () => await auth.CreateCustomTokenAsync("user"));
63+
64+
Assert.Throws<InvalidOperationException>(() => auth.TokenFactory);
6965
await Assert.ThrowsAsync<InvalidOperationException>(
7066
async () => await auth.VerifyIdTokenAsync("user"));
7167
await Assert.ThrowsAsync<InvalidOperationException>(
@@ -76,55 +72,13 @@ await Assert.ThrowsAsync<InvalidOperationException>(
7672
}
7773

7874
[Fact]
79-
public async Task CreateCustomToken()
80-
{
81-
var cred = GoogleCredential.FromFile("./resources/service_account.json");
82-
FirebaseApp.Create(new AppOptions() { Credential = cred });
83-
var token = await FirebaseAuth.DefaultInstance.CreateCustomTokenAsync("user1");
84-
VerifyCustomToken(token, "user1", null);
85-
}
86-
87-
[Fact]
88-
public async Task CreateCustomTokenWithClaims()
89-
{
90-
var cred = GoogleCredential.FromFile("./resources/service_account.json");
91-
FirebaseApp.Create(new AppOptions() { Credential = cred });
92-
var developerClaims = new Dictionary<string, object>()
93-
{
94-
{ "admin", true },
95-
{ "package", "gold" },
96-
{ "magicNumber", 42L },
97-
};
98-
var token = await FirebaseAuth.DefaultInstance.CreateCustomTokenAsync(
99-
"user2", developerClaims);
100-
VerifyCustomToken(token, "user2", developerClaims);
101-
}
102-
103-
[Fact]
104-
public async Task CreateCustomTokenCancel()
75+
public void NoTenantId()
10576
{
106-
var cred = GoogleCredential.FromFile("./resources/service_account.json");
107-
FirebaseApp.Create(new AppOptions() { Credential = cred });
108-
var canceller = new CancellationTokenSource();
109-
canceller.Cancel();
110-
await Assert.ThrowsAsync<OperationCanceledException>(
111-
async () => await FirebaseAuth.DefaultInstance.CreateCustomTokenAsync(
112-
"user1", canceller.Token));
113-
}
77+
var app = FirebaseApp.Create(new AppOptions() { Credential = MockCredential });
11478

115-
[Fact]
116-
public async Task CreateCustomTokenInvalidCredential()
117-
{
118-
FirebaseApp.Create(new AppOptions() { Credential = MockCredential });
119-
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
120-
async () => await FirebaseAuth.DefaultInstance.CreateCustomTokenAsync("user1"));
79+
FirebaseAuth auth = FirebaseAuth.DefaultInstance;
12180

122-
var errorMessage = "Failed to determine service account ID. Make sure to initialize the SDK "
123-
+ "with service account credentials or specify a service account "
124-
+ "ID with iam.serviceAccounts.signBlob permission. Please refer to "
125-
+ "https://firebase.google.com/docs/auth/admin/create-custom-tokens for "
126-
+ "more details on creating custom tokens.";
127-
Assert.Equal(errorMessage, ex.Message);
81+
Assert.Null(auth.TokenFactory.TenantId);
12882
}
12983

13084
[Fact]
@@ -162,37 +116,5 @@ public void Dispose()
162116
{
163117
FirebaseApp.DeleteAll();
164118
}
165-
166-
private static void VerifyCustomToken(string token, string uid, Dictionary<string, object> claims)
167-
{
168-
string[] segments = token.Split(".");
169-
Assert.Equal(3, segments.Length);
170-
171-
var payload = JwtUtils.Decode<FirebaseTokenFactory.CustomTokenPayload>(segments[1]);
172-
Assert.Equal("[email protected]", payload.Issuer);
173-
Assert.Equal("[email protected]", payload.Subject);
174-
Assert.Equal(uid, payload.Uid);
175-
if (claims == null)
176-
{
177-
Assert.Null(payload.Claims);
178-
}
179-
else
180-
{
181-
Assert.Equal(claims.Count, payload.Claims.Count);
182-
foreach (var entry in claims)
183-
{
184-
object value;
185-
Assert.True(payload.Claims.TryGetValue(entry.Key, out value));
186-
Assert.Equal(entry.Value, value);
187-
}
188-
}
189-
190-
var x509cert = new X509Certificate2(File.ReadAllBytes("./resources/public_cert.pem"));
191-
var rsa = (RSA)x509cert.PublicKey.Key;
192-
var tokenData = Encoding.UTF8.GetBytes(segments[0] + "." + segments[1]);
193-
var signature = JwtUtils.Base64DecodeToBytes(segments[2]);
194-
var verified = rsa.VerifyData(tokenData, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
195-
Assert.True(verified);
196-
}
197119
}
198120
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// Copyright 2020, 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.Collections.Generic;
16+
using System.IO;
17+
using System.Security.Cryptography;
18+
using System.Security.Cryptography.X509Certificates;
19+
using System.Text;
20+
using Xunit;
21+
22+
namespace FirebaseAdmin.Auth.Jwt.Tests
23+
{
24+
internal abstract class CustomTokenVerifier
25+
{
26+
private const string ClientEmail = "[email protected]";
27+
28+
private static readonly byte[] PublicKey =
29+
File.ReadAllBytes("./resources/public_cert.pem");
30+
31+
private readonly string issuer;
32+
private readonly string tenantId;
33+
34+
internal CustomTokenVerifier(string issuer, string tenantId = null)
35+
{
36+
this.issuer = issuer;
37+
this.tenantId = tenantId;
38+
}
39+
40+
internal static CustomTokenVerifier FromDefaultServiceAccount(string tenantId = null)
41+
{
42+
return new RSACustomTokenVerifier(ClientEmail, PublicKey, tenantId);
43+
}
44+
45+
internal void Verify(string token, string uid, IDictionary<string, object> claims = null)
46+
{
47+
string[] segments = token.Split(".");
48+
Assert.Equal(3, segments.Length);
49+
50+
var payload = JwtUtils.Decode<FirebaseTokenFactory.CustomTokenPayload>(segments[1]);
51+
Assert.Equal(this.issuer, payload.Issuer);
52+
Assert.Equal(this.issuer, payload.Subject);
53+
Assert.Equal(uid, payload.Uid);
54+
if (claims == null)
55+
{
56+
Assert.Null(payload.Claims);
57+
}
58+
else
59+
{
60+
Assert.Equal(claims.Count, payload.Claims.Count);
61+
foreach (var entry in claims)
62+
{
63+
object value;
64+
Assert.True(payload.Claims.TryGetValue(entry.Key, out value));
65+
Assert.Equal(entry.Value, value);
66+
}
67+
}
68+
69+
if (this.tenantId == null)
70+
{
71+
Assert.Null(payload.TenantId);
72+
}
73+
else
74+
{
75+
Assert.Equal(this.tenantId, payload.TenantId);
76+
}
77+
78+
this.AssertSignature($"{segments[0]}.{segments[1]}", segments[2]);
79+
}
80+
81+
protected abstract void AssertSignature(string tokenData, string signature);
82+
83+
private sealed class RSACustomTokenVerifier : CustomTokenVerifier
84+
{
85+
private readonly RSA rsa;
86+
87+
internal RSACustomTokenVerifier(string issuer, byte[] publicKey, string tenantId)
88+
: base(issuer, tenantId)
89+
{
90+
var x509cert = new X509Certificate2(publicKey);
91+
this.rsa = (RSA)x509cert.PublicKey.Key;
92+
}
93+
94+
protected override void AssertSignature(string tokenData, string signature)
95+
{
96+
var tokenDataBytes = Encoding.UTF8.GetBytes(tokenData);
97+
var signatureBytes = JwtUtils.Base64DecodeToBytes(signature);
98+
var verified = this.rsa.VerifyData(
99+
tokenDataBytes,
100+
signatureBytes,
101+
HashAlgorithmName.SHA256,
102+
RSASignaturePadding.Pkcs1);
103+
Assert.True(verified);
104+
}
105+
}
106+
}
107+
}

0 commit comments

Comments
 (0)