✨ Login with Apple
This commit is contained in:
13
DysonNetwork.Sphere/Auth/OpenId/AppleMobileSignInRequest.cs
Normal file
13
DysonNetwork.Sphere/Auth/OpenId/AppleMobileSignInRequest.cs
Normal file
@ -0,0 +1,13 @@
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace DysonNetwork.Sphere.Auth.OpenId;
|
||||
|
||||
public class AppleMobileSignInRequest
|
||||
{
|
||||
[Required]
|
||||
public required string IdentityToken { get; set; }
|
||||
[Required]
|
||||
public required string AuthorizationCode { get; set; }
|
||||
}
|
@ -102,8 +102,11 @@ public class AppleOidcService(
|
||||
return ValidateAndExtractIdToken(idToken, validationParameters);
|
||||
}
|
||||
|
||||
protected override Dictionary<string, string> BuildTokenRequestParameters(string code, ProviderConfiguration config,
|
||||
string? codeVerifier)
|
||||
protected override Dictionary<string, string> BuildTokenRequestParameters(
|
||||
string code,
|
||||
ProviderConfiguration config,
|
||||
string? codeVerifier
|
||||
)
|
||||
{
|
||||
var parameters = new Dictionary<string, string>
|
||||
{
|
||||
|
@ -1,10 +1,9 @@
|
||||
using DysonNetwork.Sphere.Account;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using DysonNetwork.Sphere.Auth.OpenId;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Auth;
|
||||
namespace DysonNetwork.Sphere.Auth.OpenId;
|
||||
|
||||
/// <summary>
|
||||
/// This controller is designed to handle the OAuth callback.
|
||||
@ -69,43 +68,8 @@ public class AuthCallbackController(
|
||||
var account = await accounts.LookupAccount(userInfo.Email);
|
||||
if (account == null)
|
||||
{
|
||||
// Generate username and display name from email
|
||||
var username = await accountUsernameService.GenerateUsernameFromEmailAsync(userInfo.Email);
|
||||
var displayName = await accountUsernameService.GenerateDisplayNameFromEmailAsync(userInfo.Email);
|
||||
|
||||
// Create a new account
|
||||
account = new Account.Account
|
||||
{
|
||||
Name = username,
|
||||
Nick = displayName,
|
||||
Contacts = new List<AccountContact>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Type = AccountContactType.Email,
|
||||
Content = userInfo.Email,
|
||||
VerifiedAt = userInfo.EmailVerified ? SystemClock.Instance.GetCurrentInstant() : null,
|
||||
IsPrimary = true
|
||||
}
|
||||
},
|
||||
Profile = new Profile()
|
||||
};
|
||||
|
||||
// Save the account
|
||||
await db.Accounts.AddAsync(account);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
// Do the usual steps
|
||||
var spell = await spells.CreateMagicSpell(
|
||||
account,
|
||||
MagicSpellType.AccountActivation,
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ "contact_method", account.Contacts.First().Content }
|
||||
},
|
||||
expiredAt: SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromDays(7))
|
||||
);
|
||||
await spells.NotifyMagicSpell(spell, true);
|
||||
// Create a new account using the AccountService
|
||||
account = await accounts.CreateAccount(userInfo);
|
||||
}
|
||||
|
||||
// Create a session for the user
|
@ -1,5 +1,8 @@
|
||||
using DysonNetwork.Sphere.Account;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Auth.OpenId;
|
||||
|
||||
@ -8,7 +11,7 @@ namespace DysonNetwork.Sphere.Auth.OpenId;
|
||||
public class OidcController(
|
||||
IServiceProvider serviceProvider,
|
||||
AppDatabase db,
|
||||
Account.AccountService accountService,
|
||||
AccountService accounts,
|
||||
AuthService authService
|
||||
)
|
||||
: ControllerBase
|
||||
@ -35,6 +38,51 @@ public class OidcController(
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mobile Apple Sign In endpoint
|
||||
/// Handles Apple authentication directly from mobile apps
|
||||
/// </summary>
|
||||
[HttpPost("apple/mobile")]
|
||||
public async Task<ActionResult<AuthController.TokenExchangeResponse>> AppleMobileSignIn([FromBody] AppleMobileSignInRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get Apple OIDC service
|
||||
if (GetOidcService("apple") is not AppleOidcService appleService)
|
||||
return StatusCode(503, "Apple OIDC service not available");
|
||||
|
||||
// Prepare callback data for processing
|
||||
var callbackData = new OidcCallbackData
|
||||
{
|
||||
IdToken = request.IdentityToken,
|
||||
Code = request.AuthorizationCode,
|
||||
};
|
||||
|
||||
// Process the authentication
|
||||
var userInfo = await appleService.ProcessCallbackAsync(callbackData);
|
||||
|
||||
// Find or create user account using existing logic
|
||||
var account = await FindOrCreateAccount(userInfo, "apple");
|
||||
|
||||
// Create session using the OIDC service
|
||||
var session = await appleService.CreateSessionForUserAsync(userInfo, account);
|
||||
|
||||
// Generate token using existing auth service
|
||||
var token = authService.CreateToken(session);
|
||||
|
||||
return Ok(new AuthController.TokenExchangeResponse { Token = token });
|
||||
}
|
||||
catch (SecurityTokenValidationException ex)
|
||||
{
|
||||
return Unauthorized($"Invalid identity token: {ex.Message}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log the error
|
||||
return StatusCode(500, $"Authentication failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private OidcService GetOidcService(string provider)
|
||||
{
|
||||
return provider.ToLower() switch
|
||||
@ -45,4 +93,51 @@ public class OidcController(
|
||||
_ => throw new ArgumentException($"Unsupported provider: {provider}")
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<Account.Account> FindOrCreateAccount(OidcUserInfo userInfo, string provider)
|
||||
{
|
||||
if (string.IsNullOrEmpty(userInfo.Email))
|
||||
throw new ArgumentException("Email is required for account creation");
|
||||
|
||||
// Check if account exists by email
|
||||
var existingAccount = await accounts.LookupAccount(userInfo.Email);
|
||||
if (existingAccount != null)
|
||||
{
|
||||
// Check if this provider connection already exists
|
||||
var existingConnection = await db.AccountConnections
|
||||
.FirstOrDefaultAsync(c => c.AccountId == existingAccount.Id &&
|
||||
c.Provider == provider &&
|
||||
c.ProvidedIdentifier == userInfo.UserId);
|
||||
|
||||
// If no connection exists, create one
|
||||
if (existingConnection != null) return existingAccount;
|
||||
var connection = new AccountConnection
|
||||
{
|
||||
AccountId = existingAccount.Id,
|
||||
Provider = provider,
|
||||
ProvidedIdentifier = userInfo.UserId!,
|
||||
};
|
||||
|
||||
db.AccountConnections.Add(connection);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return existingAccount;
|
||||
}
|
||||
|
||||
// Create new account using the AccountService
|
||||
var newAccount = await accounts.CreateAccount(userInfo);
|
||||
|
||||
// Create the provider connection
|
||||
var newConnection = new AccountConnection
|
||||
{
|
||||
AccountId = newAccount.Id,
|
||||
Provider = provider,
|
||||
ProvidedIdentifier = userInfo.UserId!,
|
||||
};
|
||||
|
||||
db.AccountConnections.Add(newConnection);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return newAccount;
|
||||
}
|
||||
}
|
@ -49,7 +49,7 @@ public abstract class OidcService(IConfiguration configuration, IHttpClientFacto
|
||||
{
|
||||
ClientId = configuration[$"Oidc:{ConfigSectionName}:ClientId"] ?? "",
|
||||
ClientSecret = configuration[$"Oidc:{ConfigSectionName}:ClientSecret"] ?? "",
|
||||
RedirectUri = configuration[$"Oidc:{ConfigSectionName}:RedirectUri"] ?? ""
|
||||
RedirectUri = configuration["BaseUrl"] + "/auth/callback/" + ProviderName
|
||||
};
|
||||
}
|
||||
|
||||
@ -177,7 +177,7 @@ public abstract class OidcService(IConfiguration configuration, IHttpClientFacto
|
||||
ProvidedIdentifier = userInfo.UserId ?? "",
|
||||
AccessToken = userInfo.AccessToken,
|
||||
RefreshToken = userInfo.RefreshToken,
|
||||
LastUsedAt = NodaTime.SystemClock.Instance.GetCurrentInstant(),
|
||||
LastUsedAt = SystemClock.Instance.GetCurrentInstant(),
|
||||
AccountId = account.Id
|
||||
};
|
||||
await db.AccountConnections.AddAsync(connection);
|
||||
@ -190,6 +190,7 @@ public abstract class OidcService(IConfiguration configuration, IHttpClientFacto
|
||||
ExpiredAt = now.Plus(Duration.FromHours(1)),
|
||||
StepTotal = 1,
|
||||
StepRemain = 0, // Already verified by provider
|
||||
Type = ChallengeType.Oidc,
|
||||
Platform = ChallengePlatform.Unidentified,
|
||||
Audiences = [ProviderName],
|
||||
Scopes = ["*"],
|
||||
@ -202,8 +203,8 @@ public abstract class OidcService(IConfiguration configuration, IHttpClientFacto
|
||||
var session = new Session
|
||||
{
|
||||
LastGrantedAt = now,
|
||||
Account = account,
|
||||
Challenge = challenge,
|
||||
AccountId = account.Id,
|
||||
ChallengeId = challenge.Id,
|
||||
};
|
||||
|
||||
await db.AuthSessions.AddAsync(session);
|
||||
|
@ -22,7 +22,8 @@ public class Session : ModelBase
|
||||
public enum ChallengeType
|
||||
{
|
||||
Login,
|
||||
OAuth
|
||||
OAuth, // Trying to authorize other platforms
|
||||
Oidc // Trying to connect other platforms
|
||||
}
|
||||
|
||||
public enum ChallengePlatform
|
||||
|
Reference in New Issue
Block a user