Skip to content

Commit f21c952

Browse files
authored
Enable Apple Sign In (#140)
* Enable Apple Sign In authentication via OAuth credential * New test added for Apple Sign In * Apple Sign In test must use an ID token
1 parent 8fcb3be commit f21c952

File tree

5 files changed

+90
-50
lines changed

5 files changed

+90
-50
lines changed

src/Firebase.Auth.Tests/IntegrationTests.cs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public class IntegrationTests
2121
private const string FirebaseEmail = "<TEST USER EMAIL>";
2222
private const string FirebasePassword = "<TEST USER PASSWORD>";
2323

24+
private const string AppleIDToken = "<APPLE USER ID TOKEN>";
2425
private const string FirebaseTenant = "<TEST USER TENANT ID>";
2526

2627
[TestMethod]
@@ -45,6 +46,16 @@ public void GoogleTest()
4546
auth.FirebaseToken.Should().NotBeNullOrWhiteSpace();
4647
}
4748

49+
[TestMethod]
50+
public void AppleTest()
51+
{
52+
var authProvider = new FirebaseAuthProvider(new FirebaseConfig(ApiKey));
53+
54+
var auth = authProvider.SignInWithOAuthAsync(FirebaseAuthType.Apple, AppleIDToken).Result;
55+
56+
auth.FirebaseToken.Should().NotBeNullOrWhiteSpace();
57+
}
58+
4859
[TestMethod]
4960
public void EmailTest()
5061
{
@@ -79,7 +90,7 @@ public void Unknown_email_address_should_be_reflected_by_failure_reason()
7990
}
8091
catch (Exception e)
8192
{
82-
var exception = (FirebaseAuthException) e.InnerException;
93+
var exception = (FirebaseAuthException)e.InnerException;
8394
exception.Reason.Should().Be(AuthErrorReason.UnknownEmailAddress);
8495
}
8596
}
@@ -127,7 +138,7 @@ public void Invalid_password_should_be_reflected_by_failure_reason()
127138
public void CreateUserTest()
128139
{
129140
var authProvider = new FirebaseAuthProvider(new FirebaseConfig(ApiKey));
130-
var email = $"abcd{new Random().Next()}@test.com";
141+
var email = $"abcd{new Random().Next()}@test.com";
131142

132143
var auth = authProvider.SignInWithEmailAndPasswordAsync(email, "test1234").Result;
133144

@@ -182,10 +193,10 @@ public void RefreshAccessToken()
182193

183194
var auth = authProvider.SignInWithOAuthAsync(FirebaseAuthType.Facebook, FacebookAccessToken).Result;
184195
var originalToken = auth.FirebaseToken;
185-
196+
186197
// simulate the token already expired
187198
auth.Created = DateTime.MinValue;
188-
199+
189200
var freshAuth = auth.GetFreshAuthAsync().Result;
190201

191202
freshAuth.FirebaseToken.Should().NotBe(originalToken);

src/Firebase.Auth/AuthErrorReason.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,14 @@ public enum AuthErrorReason
9393
/// <summary>
9494
/// Linked accounts: account to link has already been linked.
9595
/// </summary>
96-
AlreadyLinked
96+
AlreadyLinked,
97+
/// <summary>
98+
/// Third-party Auth Providers: PostBody contains an Id Token string obtained from Auth Provider which is actually stale.
99+
/// </summary>
100+
StaleIDToken,
101+
/// <summary>
102+
/// Third-party Auth Providers: PostBody contains an Id Token string obtained from Auth Provider which has already been received.
103+
/// </summary>
104+
DuplicateCredentialUse
97105
}
98106
}
Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,21 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
2-
3-
<PropertyGroup>
4-
<TargetFramework>netstandard1.1</TargetFramework>
5-
<PackageId>FirebaseAuthentication.net</PackageId>
6-
<PackageVersion>3.6.0</PackageVersion>
7-
<Authors>Step Up Labs, Inc.</Authors>
8-
<Description>Firebase authentication library. It can generate Firebase auth token based on given OAuth token (issued by Google, Facebook...). This Firebase token can then be used with REST queries against Firebase endpoints. </Description>
9-
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
10-
<PackageIconUrl>https://github.com/step-up-labs/firebase-authentication-dotnet/raw/master/art/FirebaseLogo.128x128.png</PackageIconUrl>
11-
<PackageProjectUrl>https://github.com/step-up-labs/firebase-authentication-dotnet</PackageProjectUrl>
12-
<PackageLicenseUrl>https://github.com/step-up-labs/firebase-authentication-dotnet/blob/master/LICENSE</PackageLicenseUrl>
13-
<IncludeSource>True</IncludeSource>
14-
<IncludeSymbols>True</IncludeSymbols>
15-
<Copyright>Step Up Labs, Inc. 2016</Copyright>
16-
<PackageTags>Firebase Auth Authentication Google Facebook Github Twitter</PackageTags>
17-
</PropertyGroup>
18-
19-
<ItemGroup>
20-
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
21-
</ItemGroup>
22-
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Project Sdk="Microsoft.NET.Sdk">
3+
<PropertyGroup>
4+
<TargetFramework>netstandard1.1</TargetFramework>
5+
<PackageId>FirebaseAuthentication.net</PackageId>
6+
<PackageVersion>3.7.0</PackageVersion>
7+
<Authors>Step Up Labs, Inc.</Authors>
8+
<Description>Firebase authentication library. It can generate Firebase auth token based on given OAuth token (issued by Google, Facebook...). This Firebase token can then be used with REST queries against Firebase endpoints. </Description>
9+
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
10+
<PackageIconUrl>https://github.com/step-up-labs/firebase-authentication-dotnet/raw/master/art/FirebaseLogo.128x128.png</PackageIconUrl>
11+
<PackageProjectUrl>https://github.com/step-up-labs/firebase-authentication-dotnet</PackageProjectUrl>
12+
<PackageLicenseUrl>https://github.com/step-up-labs/firebase-authentication-dotnet/blob/master/LICENSE</PackageLicenseUrl>
13+
<IncludeSource>True</IncludeSource>
14+
<IncludeSymbols>True</IncludeSymbols>
15+
<Copyright>Step Up Labs, Inc. 2016</Copyright>
16+
<PackageTags>Firebase Auth Authentication Google Facebook Github Twitter Apple</PackageTags>
17+
</PropertyGroup>
18+
<ItemGroup>
19+
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
20+
</ItemGroup>
2321
</Project>

src/Firebase.Auth/FirebaseAuthProvider.cs

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ public class FirebaseAuthProvider : IDisposable, IFirebaseAuthProvider
2525
private const string GoogleSetAccountUrl = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/setAccountInfo?key={0}";
2626
private const string GoogleCreateAuthUrl = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/createAuthUri?key={0}";
2727
private const string GoogleUpdateUserPassword = "https://identitytoolkit.googleapis.com/v1/accounts:update?key={0}";
28-
29-
28+
29+
3030
private const string ProfileDeleteDisplayName = "DISPLAY_NAME";
3131
private const string ProfileDeletePhotoUrl = "PHOTO_URL";
3232

@@ -55,7 +55,7 @@ public async Task<FirebaseAuthLink> SignInWithCustomTokenAsync(string customToke
5555
firebaseAuthLink.User = await this.GetUserAsync(firebaseAuthLink.FirebaseToken).ConfigureAwait(false);
5656
return firebaseAuthLink;
5757
}
58-
58+
5959
/// <summary>
6060
/// Using the idToken of an authenticated user, get the details of the user's account
6161
/// </summary>
@@ -66,14 +66,14 @@ public async Task<User> GetUserAsync(string firebaseToken)
6666
var content = $"{{\"idToken\":\"{firebaseToken}\"}}";
6767
var responseData = "N/A";
6868
try
69-
{
69+
{
7070
var response = await this.client.PostAsync(new Uri(string.Format(GoogleGetUser, this.authConfig.ApiKey)), new StringContent(content, Encoding.UTF8, "application/json")).ConfigureAwait(false);
7171
responseData = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
7272
response.EnsureSuccessStatusCode();
7373

7474
var resultJson = JObject.Parse(responseData);
7575
var user = JsonConvert.DeserializeObject<User>(resultJson["users"].First().ToString());
76-
return user;
76+
return user;
7777
}
7878
catch (Exception ex)
7979
{
@@ -92,15 +92,26 @@ public async Task<User> GetUserAsync(FirebaseAuth auth)
9292
}
9393

9494
/// <summary>
95-
/// Using the provided access token from third party auth provider (google, facebook...), get the firebase auth with token and basic user credentials.
95+
/// Using the provided access token from third party auth provider (google, facebook...), or ID token (apple), get the firebase auth with token and basic user credentials.
9696
/// </summary>
9797
/// <param name="authType"> The auth type. </param>
98-
/// <param name="oauthAccessToken"> The access token retrieved from login provider of your choice. </param>
98+
/// <param name="oauthToken"> The access token or ID token retrieved from login provider of your choice. </param>
9999
/// <returns> The <see cref="FirebaseAuth"/>. </returns>
100-
public async Task<FirebaseAuthLink> SignInWithOAuthAsync(FirebaseAuthType authType, string oauthAccessToken)
100+
public async Task<FirebaseAuthLink> SignInWithOAuthAsync(FirebaseAuthType authType, string oauthToken)
101101
{
102102
var providerId = this.GetProviderId(authType);
103-
var content = $"{{\"postBody\":\"access_token={oauthAccessToken}&providerId={providerId}\",\"requestUri\":\"http://localhost\",\"returnSecureToken\":true}}";
103+
104+
string content;
105+
106+
switch (authType)
107+
{
108+
case FirebaseAuthType.Apple:
109+
content = $"{{\"postBody\":\"id_token={oauthToken}&providerId={providerId}\",\"requestUri\":\"http://localhost\",\"returnSecureToken\":true}}";
110+
break;
111+
default:
112+
content = $"{{\"postBody\":\"access_token={oauthToken}&providerId={providerId}\",\"requestUri\":\"http://localhost\",\"returnSecureToken\":true}}";
113+
break;
114+
}
104115

105116
FirebaseAuthLink firebaseAuthLink = await this.ExecuteWithPostContentAsync(GoogleIdentityUrl, content).ConfigureAwait(false);
106117
firebaseAuthLink.User = await this.GetUserAsync(firebaseAuthLink.FirebaseToken).ConfigureAwait(false);
@@ -174,7 +185,7 @@ public async Task<FirebaseAuthLink> SignInWithEmailAndPasswordAsync(string email
174185
firebaseAuthLink.User = await this.GetUserAsync(firebaseAuthLink.FirebaseToken).ConfigureAwait(false);
175186
return firebaseAuthLink;
176187
}
177-
188+
178189
/// <summary>
179190
/// Change a password from an user with his token.
180191
/// </summary>
@@ -187,8 +198,8 @@ public async Task<FirebaseAuthLink> ChangeUserPassword(string idToken, string pa
187198

188199
return await this.ExecuteWithPostContentAsync(GoogleUpdateUserPassword, content).ConfigureAwait(false);
189200
}
190-
191-
201+
202+
192203
/// <summary>
193204
/// Creates new user with given credentials.
194205
/// </summary>
@@ -273,15 +284,15 @@ public async Task DeleteUserAsync(string firebaseToken)
273284
{
274285
var content = $"{{ \"idToken\": \"{firebaseToken}\" }}";
275286
var responseData = "N/A";
276-
277-
try
287+
288+
try
278289
{
279290
var response = await this.client.PostAsync(new Uri(string.Format(GoogleDeleteUserUrl, this.authConfig.ApiKey)), new StringContent(content, Encoding.UTF8, "application/json")).ConfigureAwait(false);
280291
responseData = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
281-
292+
282293
response.EnsureSuccessStatusCode();
283294
}
284-
catch(Exception ex)
295+
catch (Exception ex)
285296
{
286297
AuthErrorReason errorReason = GetFailureReason(responseData);
287298
throw new FirebaseAuthException(GoogleDeleteUserUrl, content, responseData, ex, errorReason);
@@ -296,12 +307,12 @@ public async Task SendPasswordResetEmailAsync(string email)
296307
{
297308
var content = $"{{\"requestType\":\"PASSWORD_RESET\",\"email\":\"{email}\"}}";
298309
var responseData = "N/A";
299-
310+
300311
try
301312
{
302313
var response = await this.client.PostAsync(new Uri(string.Format(GoogleGetConfirmationCodeUrl, this.authConfig.ApiKey)), new StringContent(content, Encoding.UTF8, "application/json")).ConfigureAwait(false);
303314
responseData = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
304-
315+
305316
response.EnsureSuccessStatusCode();
306317
}
307318
catch (Exception ex)
@@ -550,6 +561,9 @@ private static AuthErrorReason GetFailureReason(string responseData)
550561
case "A system error has occurred - missing or invalid postBody":
551562
failureReason = AuthErrorReason.SystemError;
552563
break;
564+
case "MISSING_OR_INVALID_NONCE : Duplicate credential received. Please try again with a new credential.":
565+
failureReason = AuthErrorReason.DuplicateCredentialUse;
566+
break;
553567

554568
//possible errors from Email/Password Account Signup (via signupNewUser or setAccountInfo) or Signin
555569
case "INVALID_EMAIL":
@@ -563,7 +577,7 @@ private static AuthErrorReason GetFailureReason(string responseData)
563577
case "EMAIL_EXISTS":
564578
failureReason = AuthErrorReason.EmailExists;
565579
break;
566-
580+
567581
//possible errors from Account Delete
568582
case "USER_NOT_FOUND":
569583
failureReason = AuthErrorReason.UserNotFound;
@@ -610,12 +624,14 @@ private static AuthErrorReason GetFailureReason(string responseData)
610624
break;
611625
}
612626

613-
if(failureReason == AuthErrorReason.Undefined)
614-
{
627+
if (failureReason == AuthErrorReason.Undefined)
628+
{
615629
//possible errors from Email/Password Account Signup (via signupNewUser or setAccountInfo)
616-
if(errorData?.error?.message?.StartsWith("WEAK_PASSWORD :") ?? false) failureReason = AuthErrorReason.WeakPassword;
630+
if (errorData?.error?.message?.StartsWith("WEAK_PASSWORD :") ?? false) failureReason = AuthErrorReason.WeakPassword;
617631
//possible errors from Email/Password Signin
618632
else if (errorData?.error?.message?.StartsWith("TOO_MANY_ATTEMPTS_TRY_LATER :") ?? false) failureReason = AuthErrorReason.TooManyAttemptsTryLater;
633+
//ID Token issued is stale to sign-in (e.g. with Apple)
634+
else if (errorData?.error?.message?.StartsWith("ERROR_INVALID_CREDENTIAL") ?? false) failureReason = AuthErrorReason.StaleIDToken;
619635
}
620636
}
621637
}
@@ -638,6 +654,7 @@ private string GetProviderId(FirebaseAuthType authType)
638654
{
639655
case FirebaseAuthType.Facebook:
640656
case FirebaseAuthType.Google:
657+
case FirebaseAuthType.Apple:
641658
case FirebaseAuthType.Github:
642659
case FirebaseAuthType.Twitter:
643660
return authType.ToEnumString();

src/Firebase.Auth/FirebaseAuthType.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ public enum FirebaseAuthType
2222
[EnumMember(Value = "google.com")]
2323
Google,
2424

25+
/// <summary>
26+
/// The apple auth.
27+
/// </summary>
28+
[EnumMember(Value = "apple.com")]
29+
Apple,
30+
2531
/// <summary>
2632
/// The github auth.
2733
/// </summary>
@@ -39,5 +45,5 @@ public enum FirebaseAuthType
3945
/// </summary>
4046
[EnumMember(Value = "password")]
4147
EmailAndPassword
42-
}
48+
}
4349
}

0 commit comments

Comments
 (0)