Skip to content

Commit a8cdedb

Browse files
authored
Migrating CreateCustomTokenAsync() APIs to the new error handling scheme (#105)
1 parent e9c8cad commit a8cdedb

File tree

6 files changed

+113
-88
lines changed

6 files changed

+113
-88
lines changed

FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseAuthTest.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@
2020
using System.Text;
2121
using System.Threading;
2222
using System.Threading.Tasks;
23-
using FirebaseAdmin.Auth;
24-
using Google.Apis.Auth;
2523
using Google.Apis.Auth.OAuth2;
2624
using Xunit;
2725

@@ -114,8 +112,15 @@ await Assert.ThrowsAsync<OperationCanceledException>(
114112
public async Task CreateCustomTokenInvalidCredential()
115113
{
116114
FirebaseApp.Create(new AppOptions() { Credential = MockCredential });
117-
await Assert.ThrowsAsync<FirebaseException>(
115+
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
118116
async () => await FirebaseAuth.DefaultInstance.CreateCustomTokenAsync("user1"));
117+
118+
var errorMessage = "Failed to determine service account ID. Make sure to initialize the SDK "
119+
+ "with service account credentials or specify a service account "
120+
+ "ID with iam.serviceAccounts.signBlob permission. Please refer to "
121+
+ "https://firebase.google.com/docs/auth/admin/create-custom-tokens for "
122+
+ "more details on creating custom tokens.";
123+
Assert.Equal(errorMessage, ex.Message);
119124
}
120125

121126
[Fact]

FirebaseAdmin/FirebaseAdmin.Tests/Auth/IAMSignerTest.cs

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,10 @@
1515
using System;
1616
using System.Net;
1717
using System.Net.Http;
18-
using System.Net.Http.Headers;
1918
using System.Text;
20-
using System.Threading;
2119
using System.Threading.Tasks;
2220
using FirebaseAdmin.Tests;
2321
using Google.Apis.Auth.OAuth2;
24-
using Google.Apis.Http;
2522
using Google.Apis.Json;
2623
using Xunit;
2724

@@ -69,12 +66,23 @@ public async Task AccountDiscoveryError()
6966
};
7067
var factory = new MockHttpClientFactory(handler);
7168
var signer = new IAMSigner(factory, GoogleCredential.FromAccessToken("token"));
72-
await Assert.ThrowsAsync<FirebaseException>(
69+
var errorMessage = "Failed to determine service account ID. Make sure to initialize the SDK "
70+
+ "with service account credentials or specify a service account "
71+
+ "ID with iam.serviceAccounts.signBlob permission. Please refer to "
72+
+ "https://firebase.google.com/docs/auth/admin/create-custom-tokens for "
73+
+ "more details on creating custom tokens.";
74+
75+
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
7376
async () => await signer.GetKeyIdAsync());
7477
Assert.Equal(1, handler.Calls);
75-
await Assert.ThrowsAsync<FirebaseException>(
78+
Assert.Equal(errorMessage, ex.Message);
79+
Assert.IsType<HttpRequestException>(ex.InnerException);
80+
81+
ex = await Assert.ThrowsAsync<InvalidOperationException>(
7682
async () => await signer.GetKeyIdAsync());
7783
Assert.Equal(1, handler.Calls);
84+
Assert.Equal(errorMessage, ex.Message);
85+
Assert.IsType<HttpRequestException>(ex.InnerException);
7886
}
7987
}
8088

@@ -117,9 +125,37 @@ public async Task WelformedSignError()
117125
factory, GoogleCredential.FromAccessToken("token"), "test-service-account");
118126
Assert.Equal("test-service-account", await signer.GetKeyIdAsync());
119127
byte[] data = Encoding.UTF8.GetBytes("Hello world");
120-
var ex = await Assert.ThrowsAsync<FirebaseException>(
128+
var ex = await Assert.ThrowsAsync<FirebaseAuthException>(
129+
async () => await signer.SignDataAsync(data));
130+
131+
Assert.Equal(ErrorCode.Internal, ex.ErrorCode);
132+
Assert.Equal("test reason", ex.Message);
133+
Assert.Null(ex.AuthErrorCode);
134+
Assert.NotNull(ex.HttpResponse);
135+
Assert.Null(ex.InnerException);
136+
}
137+
138+
[Fact]
139+
public async Task WelformedSignErrorWithCode()
140+
{
141+
var handler = new MockMessageHandler()
142+
{
143+
StatusCode = HttpStatusCode.InternalServerError,
144+
Response = @"{""error"": {""message"": ""test reason"", ""status"": ""UNAVAILABLE""}}",
145+
};
146+
var factory = new MockHttpClientFactory(handler);
147+
var signer = new FixedAccountIAMSigner(
148+
factory, GoogleCredential.FromAccessToken("token"), "test-service-account");
149+
Assert.Equal("test-service-account", await signer.GetKeyIdAsync());
150+
byte[] data = Encoding.UTF8.GetBytes("Hello world");
151+
var ex = await Assert.ThrowsAsync<FirebaseAuthException>(
121152
async () => await signer.SignDataAsync(data));
153+
154+
Assert.Equal(ErrorCode.Unavailable, ex.ErrorCode);
122155
Assert.Equal("test reason", ex.Message);
156+
Assert.Null(ex.AuthErrorCode);
157+
Assert.NotNull(ex.HttpResponse);
158+
Assert.Null(ex.InnerException);
123159
}
124160

125161
[Fact]
@@ -135,9 +171,16 @@ public async Task UnexpectedSignError()
135171
factory, GoogleCredential.FromAccessToken("token"), "test-service-account");
136172
Assert.Equal("test-service-account", await signer.GetKeyIdAsync());
137173
byte[] data = Encoding.UTF8.GetBytes("Hello world");
138-
var ex = await Assert.ThrowsAsync<FirebaseException>(
174+
var ex = await Assert.ThrowsAsync<FirebaseAuthException>(
139175
async () => await signer.SignDataAsync(data));
140-
Assert.Contains("not json", ex.Message);
176+
177+
Assert.Equal(ErrorCode.Internal, ex.ErrorCode);
178+
Assert.Equal(
179+
$"Unexpected HTTP response with status: 500 (InternalServerError)\nnot json",
180+
ex.Message);
181+
Assert.Null(ex.AuthErrorCode);
182+
Assert.NotNull(ex.HttpResponse);
183+
Assert.Null(ex.InnerException);
141184
}
142185
}
143186
}

FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,9 @@ public static FirebaseAuth GetAuth(FirebaseApp app)
105105
/// <returns>A task that completes with a Firebase custom token.</returns>
106106
/// <exception cref="ArgumentException">If <paramref name="uid"/> is null, empty or longer
107107
/// than 128 characters.</exception>
108-
/// <exception cref="FirebaseException">If an error occurs while creating a custom
108+
/// <exception cref="InvalidOperationException">If no service account can be discovered
109+
/// from either the <see cref="AppOptions"/> or the deployment environment.</exception>
110+
/// <exception cref="FirebaseAuthException">If an error occurs while creating a custom
109111
/// token.</exception>
110112
/// <param name="uid">The UID to store in the token. This identifies the user to other
111113
/// Firebase services (Realtime Database, Firebase Auth, etc.). Must not be longer than
@@ -142,7 +144,9 @@ public async Task<string> CreateCustomTokenAsync(string uid)
142144
/// <returns>A task that completes with a Firebase custom token.</returns>
143145
/// <exception cref="ArgumentException">If <paramref name="uid"/> is null, empty or longer
144146
/// than 128 characters.</exception>
145-
/// <exception cref="FirebaseException">If an error occurs while creating a custom
147+
/// <exception cref="InvalidOperationException">If no service account can be discovered
148+
/// from either the <see cref="AppOptions"/> or the deployment environment.</exception>
149+
/// <exception cref="FirebaseAuthException">If an error occurs while creating a custom
146150
/// token.</exception>
147151
/// <param name="uid">The UID to store in the token. This identifies the user to other
148152
/// Firebase services (Realtime Database, Firebase Auth, etc.). Must not be longer than
@@ -169,7 +173,9 @@ public async Task<string> CreateCustomTokenAsync(
169173
/// <exception cref="ArgumentException">If <paramref name="uid"/> is null, empty or longer
170174
/// than 128 characters. Or, if <paramref name="developerClaims"/> contains any standard
171175
/// JWT claims.</exception>
172-
/// <exception cref="FirebaseException">If an error occurs while creating a custom
176+
/// <exception cref="InvalidOperationException">If no service account can be discovered
177+
/// from either the <see cref="AppOptions"/> or the deployment environment.</exception>
178+
/// <exception cref="FirebaseAuthException">If an error occurs while creating a custom
173179
/// token.</exception>
174180
/// <param name="uid">The UID to store in the token. This identifies the user to other
175181
/// Firebase services (Realtime Database, Firebase Auth, etc.). Must not be longer than
@@ -197,7 +203,9 @@ public async Task<string> CreateCustomTokenAsync(
197203
/// <exception cref="ArgumentException">If <paramref name="uid"/> is null, empty or longer
198204
/// than 128 characters. Or, if <paramref name="developerClaims"/> contains any standard
199205
/// JWT claims.</exception>
200-
/// <exception cref="FirebaseException">If an error occurs while creating a custom
206+
/// <exception cref="InvalidOperationException">If no service account can be discovered
207+
/// from either the <see cref="AppOptions"/> or the deployment environment.</exception>
208+
/// <exception cref="FirebaseAuthException">If an error occurs while creating a custom
201209
/// token.</exception>
202210
/// <param name="uid">The UID to store in the token. This identifies the user to other
203211
/// Firebase services (Realtime Database, Firebase Auth, etc.). Must not be longer than

FirebaseAdmin/FirebaseAdmin/Auth/IAMSigner.cs

Lines changed: 35 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,13 @@
1313
// limitations under the License.
1414

1515
using System;
16-
using System.Net;
1716
using System.Net.Http;
1817
using System.Threading;
1918
using System.Threading.Tasks;
19+
using FirebaseAdmin.Util;
2020
using Google.Apis.Auth.OAuth2;
2121
using Google.Apis.Http;
2222
using Google.Apis.Json;
23-
using Google.Apis.Util;
2423

2524
namespace FirebaseAdmin.Auth
2625
{
@@ -38,14 +37,21 @@ internal class IAMSigner : ISigner
3837
"https://iam.googleapis.com/v1/projects/-/serviceAccounts/{0}:signBlob";
3938

4039
private const string MetadataServerUrl =
41-
"http://metadata/computeMetadata/v1/instance/service-accounts/default/email";
40+
"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/email";
4241

43-
private readonly ConfigurableHttpClient httpClient;
42+
private readonly ErrorHandlingHttpClient<FirebaseAuthException> httpClient;
4443
private readonly Lazy<Task<string>> keyId;
4544

4645
public IAMSigner(HttpClientFactory clientFactory, GoogleCredential credential)
4746
{
48-
this.httpClient = clientFactory.CreateAuthorizedHttpClient(credential);
47+
this.httpClient = new ErrorHandlingHttpClient<FirebaseAuthException>(
48+
new ErrorHandlingHttpClientArgs<FirebaseAuthException>()
49+
{
50+
HttpClientFactory = clientFactory,
51+
ErrorResponseHandler = IAMSignerErrorHandler.Instance,
52+
RequestExceptionHandler = AuthErrorHandler.Instance,
53+
DeserializeExceptionHandler = AuthErrorHandler.Instance,
54+
});
4955
this.keyId = new Lazy<Task<string>>(
5056
async () => await DiscoverServiceAccountIdAsync(clientFactory)
5157
.ConfigureAwait(false), true);
@@ -55,25 +61,21 @@ public async Task<byte[]> SignDataAsync(
5561
byte[] data, CancellationToken cancellationToken = default(CancellationToken))
5662
{
5763
var keyId = await this.GetKeyIdAsync(cancellationToken).ConfigureAwait(false);
58-
var url = string.Format(SignBlobUrl, keyId);
59-
var request = new SignBlobRequest
64+
var body = new SignBlobRequest
6065
{
6166
BytesToSign = Convert.ToBase64String(data),
6267
};
63-
64-
try
65-
{
66-
var response = await this.httpClient.PostJsonAsync(url, request, cancellationToken)
67-
.ConfigureAwait(false);
68-
var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
69-
this.ThrowIfError(response, json);
70-
var parsed = NewtonsoftJsonSerializer.Instance.Deserialize<SignBlobResponse>(json);
71-
return Convert.FromBase64String(parsed.Signature);
72-
}
73-
catch (HttpRequestException e)
68+
var request = new HttpRequestMessage()
7469
{
75-
throw new FirebaseException("Error while calling the IAM service.", e);
76-
}
70+
Method = HttpMethod.Post,
71+
RequestUri = new Uri(string.Format(SignBlobUrl, keyId)),
72+
Content = NewtonsoftJsonSerializer.Instance.CreateJsonHttpContent(body),
73+
};
74+
75+
var response = await this.httpClient
76+
.SendAndDeserializeAsync<SignBlobResponse>(request, cancellationToken)
77+
.ConfigureAwait(false);
78+
return Convert.FromBase64String(response.Result.Signature);
7779
}
7880

7981
public virtual async Task<string> GetKeyIdAsync(
@@ -85,7 +87,8 @@ public virtual async Task<string> GetKeyIdAsync(
8587
}
8688
catch (Exception e)
8789
{
88-
throw new FirebaseException(
90+
// Invalid configuration or environment error.
91+
throw new InvalidOperationException(
8992
"Failed to determine service account ID. Make sure to initialize the SDK "
9093
+ "with service account credentials or specify a service account "
9194
+ "ID with iam.serviceAccounts.signBlob permission. Please refer to "
@@ -105,38 +108,12 @@ private static async Task<string> DiscoverServiceAccountIdAsync(
105108
using (var client = clientFactory.CreateDefaultHttpClient())
106109
{
107110
client.DefaultRequestHeaders.Add("Metadata-Flavor", "Google");
108-
return await client.GetStringAsync(MetadataServerUrl).ConfigureAwait(false);
111+
var resp = await client.GetAsync(MetadataServerUrl).ConfigureAwait(false);
112+
resp.EnsureSuccessStatusCode();
113+
return await resp.Content.ReadAsStringAsync().ConfigureAwait(false);
109114
}
110115
}
111116

112-
private void ThrowIfError(HttpResponseMessage response, string content)
113-
{
114-
if (response.IsSuccessStatusCode)
115-
{
116-
return;
117-
}
118-
119-
string error = null;
120-
try
121-
{
122-
var result = NewtonsoftJsonSerializer.Instance.Deserialize<SignBlobError>(content);
123-
error = result?.Error.Message;
124-
}
125-
catch (Exception)
126-
{
127-
// Ignore any errors encountered while parsing the originl error.
128-
}
129-
130-
if (string.IsNullOrEmpty(error))
131-
{
132-
error = "Response status code does not indicate success: "
133-
+ $"{(int)response.StatusCode} ({response.StatusCode})"
134-
+ $"{Environment.NewLine}{content}";
135-
}
136-
137-
throw new FirebaseException(error);
138-
}
139-
140117
/// <summary>
141118
/// Represents the sign request sent to the remote IAM service.
142119
/// </summary>
@@ -155,22 +132,16 @@ internal class SignBlobResponse
155132
public string Signature { get; set; }
156133
}
157134

158-
/// <summary>
159-
/// Represents an error response sent by the remote IAM service.
160-
/// </summary>
161-
private class SignBlobError
135+
private class IAMSignerErrorHandler : PlatformErrorHandler<FirebaseAuthException>
162136
{
163-
[Newtonsoft.Json.JsonProperty("error")]
164-
public SignBlobErrorDetail Error { get; set; }
165-
}
137+
internal static readonly IAMSignerErrorHandler Instance = new IAMSignerErrorHandler();
166138

167-
/// <summary>
168-
/// Represents the error details embedded in an IAM error response.
169-
/// </summary>
170-
private class SignBlobErrorDetail
171-
{
172-
[Newtonsoft.Json.JsonProperty("message")]
173-
public string Message { get; set; }
139+
private IAMSignerErrorHandler() { }
140+
141+
protected override FirebaseAuthException CreateException(FirebaseExceptionArgs args)
142+
{
143+
return new FirebaseAuthException(args.Code, args.Message, response: args.HttpResponse);
144+
}
174145
}
175146
}
176147
}

FirebaseAdmin/FirebaseAdmin/FirebaseException.cs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,6 @@ namespace FirebaseAdmin
2222
/// </summary>
2323
public class FirebaseException : Exception
2424
{
25-
internal FirebaseException(string message)
26-
: base(message) { } // TODO: Remove this constructor
27-
28-
internal FirebaseException(string message, Exception inner)
29-
: base(message, inner) { } // TODO: Remove this constructor
30-
3125
internal FirebaseException(
3226
ErrorCode code,
3327
string message,
@@ -42,8 +36,12 @@ internal FirebaseException(
4236
/// <summary>
4337
/// Gets the platform-wide error code associated with this exception.
4438
/// </summary>
45-
internal ErrorCode ErrorCode { get; private set; } // TODO: Expose this as public
39+
public ErrorCode ErrorCode { get; private set; }
4640

47-
internal HttpResponseMessage HttpResponse { get; private set; }
41+
/// <summary>
42+
/// Gets the HTTP response that resulted in this exception. Null, if the exception was not
43+
/// caused by an HTTP error response.
44+
/// </summary>
45+
public HttpResponseMessage HttpResponse { get; private set; }
4846
}
4947
}

FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ private string DirectionString
164164
this.Direction = Messaging.Direction.RightToLeft;
165165
return;
166166
default:
167-
throw new FirebaseException(
167+
throw new ArgumentException(
168168
$"Invalid direction value: {value}. Only 'auto', 'rtl' and 'ltr' "
169169
+ "are allowed.");
170170
}

0 commit comments

Comments
 (0)