Skip to content

Commit 8bfd614

Browse files
andrei-m-codeclaude
andcommitted
Fix JWT encoding, null safety, and other code quality issues
- Fix Base64URL encoding in ApnSender and FirebaseSender (RFC 7617) - Fix potential NullReferenceException in ApnSender error handling - Fix static token cache key collision by including key ID - Add device token validation in ApnSender.SendAsync - Propagate CancellationToken in FirebaseSender.GetJwtTokenAsync - Remove infinite loop in CorePush.Tester - Remove unused System.IdentityModel.Tokens.Jwt dependency Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ef70ca9 commit 8bfd614

4 files changed

Lines changed: 34 additions & 24 deletions

File tree

CorePush.Tester/CorePush.Tester.csproj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
<ItemGroup>
1313
<PackageReference Include="FirebaseAdmin" Version="3.4.0" />
14-
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.15.0" />
1514
</ItemGroup>
1615

1716
</Project>

CorePush.Tester/Program.cs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,13 @@ private static async Task SendApnNotificationAsync()
5050
ServerType = apnServerType,
5151
};
5252

53-
while (true)
54-
{
55-
var apn = new ApnSender(settings, http);
56-
var payload = new AppleNotification(
57-
Guid.NewGuid(),
58-
"Hello World (Message)",
59-
"Hello World (Title)");
60-
var response = await apn.SendAsync(payload, apnDeviceToken);
61-
}
53+
var apn = new ApnSender(settings, http);
54+
var payload = new AppleNotification(
55+
Guid.NewGuid(),
56+
"Hello World (Message)",
57+
"Hello World (Title)");
58+
var response = await apn.SendAsync(payload, apnDeviceToken);
59+
Console.WriteLine($"APN Response: {response.StatusCode} - {response.Message}");
6260
}
6361

6462
private static async Task SendFirebaseNotificationAsync()

CorePush/Apple/ApnSender.cs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ public async Task<PushResult> SendAsync(
7878
ApnPushType apnPushType = ApnPushType.Alert,
7979
CancellationToken cancellationToken = default)
8080
{
81+
ArgumentException.ThrowIfNullOrWhiteSpace(deviceToken);
82+
8183
var path = $"/3/device/{deviceToken}";
8284
var json = serializer.Serialize(notification);
8385

@@ -102,19 +104,20 @@ public async Task<PushResult> SendAsync(
102104
using var response = await http.SendAsync(message, cancellationToken);
103105

104106
var content = await response.Content.ReadAsStringAsync(cancellationToken);
105-
var error = response.IsSuccessStatusCode
106-
? null
107-
: serializer.Deserialize<ApnsError>(content).Reason;
107+
var error = response.IsSuccessStatusCode
108+
? null
109+
: serializer.Deserialize<ApnsError>(content)?.Reason;
108110

109111
return new PushResult((int)response.StatusCode, response.IsSuccessStatusCode, content, error);
110112
}
111113

112114
private string GetJwtToken()
113115
{
114-
var (token, date) = tokens.GetOrAdd(settings.AppBundleIdentifier, _ => new Tuple<string, DateTime>(CreateJwtToken(), DateTime.UtcNow));
116+
var cacheKey = $"{settings.AppBundleIdentifier}:{settings.P8PrivateKeyId}";
117+
var (token, date) = tokens.GetOrAdd(cacheKey, _ => new Tuple<string, DateTime>(CreateJwtToken(), DateTime.UtcNow));
115118
if (date < DateTime.UtcNow.AddMinutes(-tokenExpiresMinutes))
116119
{
117-
tokens.TryRemove(settings.AppBundleIdentifier, out _);
120+
tokens.TryRemove(cacheKey, out _);
118121
return GetJwtToken();
119122
}
120123

@@ -156,5 +159,9 @@ private static string Base64UrlEncode(string str)
156159
return Base64UrlEncode(bytes);
157160
}
158161

159-
private static string Base64UrlEncode(byte[] bytes) => Convert.ToBase64String(bytes);
162+
private static string Base64UrlEncode(byte[] bytes) =>
163+
Convert.ToBase64String(bytes)
164+
.Replace('+', '-')
165+
.Replace('/', '_')
166+
.TrimEnd('=');
160167
}

CorePush/Firebase/FirebaseSender.cs

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ public async Task<PushResult> SendAsync(object payload, CancellationToken cancel
102102
HttpMethod.Post,
103103
$"https://fcm.googleapis.com/v1/projects/{settings.ProjectId}/messages:send");
104104

105-
var token = await GetJwtTokenAsync();
105+
var token = await GetJwtTokenAsync(cancellationToken);
106106

107107
message.Headers.Add("Authorization", $"Bearer {token}");
108108
message.Content = new StringContent(json, Encoding.UTF8, "application/json");
@@ -118,22 +118,22 @@ public async Task<PushResult> SendAsync(object payload, CancellationToken cancel
118118
firebaseResponse.Error?.Status);
119119
}
120120

121-
private async Task<string> GetJwtTokenAsync()
121+
private async Task<string> GetJwtTokenAsync(CancellationToken cancellationToken)
122122
{
123123
if (firebaseToken != null && firebaseTokenExpiration > DateTime.UtcNow)
124124
{
125125
return firebaseToken.AccessToken;
126126
}
127-
127+
128128
using var message = new HttpRequestMessage(HttpMethod.Post, "https://oauth2.googleapis.com/token");
129129
using var form = new MultipartFormDataContent();
130130
var authToken = GetMasterToken();
131131
form.Add(new StringContent(authToken), "assertion");
132132
form.Add(new StringContent("urn:ietf:params:oauth:grant-type:jwt-bearer"), "grant_type");
133133
message.Content = form;
134134

135-
using var response = await http.SendAsync(message);
136-
var content = await response.Content.ReadAsStringAsync();
135+
using var response = await http.SendAsync(message, cancellationToken);
136+
var content = await response.Content.ReadAsStringAsync(cancellationToken);
137137

138138
if (!response.IsSuccessStatusCode)
139139
{
@@ -163,8 +163,8 @@ private string GetMasterToken()
163163
exp = CryptoHelper.GetEpochTimestamp() + 3600 /* has to be short lived */
164164
});
165165

166-
var headerBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(header));
167-
var payloadBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(payload));
166+
var headerBase64 = Base64UrlEncode(Encoding.UTF8.GetBytes(header));
167+
var payloadBase64 = Base64UrlEncode(Encoding.UTF8.GetBytes(payload));
168168
var unsignedJwtData = $"{headerBase64}.{payloadBase64}";
169169
var unsignedJwtBytes = Encoding.UTF8.GetBytes(unsignedJwtData);
170170

@@ -174,11 +174,17 @@ private string GetMasterToken()
174174
signer.BlockUpdate(unsignedJwtBytes, 0, unsignedJwtBytes.Length);
175175

176176
var signature = signer.GenerateSignature();
177-
var signatureBase64 = Convert.ToBase64String(signature);
177+
var signatureBase64 = Base64UrlEncode(signature);
178178

179179
return $"{unsignedJwtData}.{signatureBase64}";
180180
}
181181

182+
private static string Base64UrlEncode(byte[] bytes) =>
183+
Convert.ToBase64String(bytes)
184+
.Replace('+', '-')
185+
.Replace('/', '_')
186+
.TrimEnd('=');
187+
182188
private static AsymmetricKeyParameter ParsePkcs8PrivateKeyPem(string key)
183189
{
184190
using var keyReader = new StringReader(key);

0 commit comments

Comments
 (0)