Swarm/DysonNetwork.Sphere/Auth/AuthCallbackController.cs
2025-06-15 13:11:45 +08:00

181 lines
6.4 KiB
C#

using DysonNetwork.Sphere.Account;
using Microsoft.AspNetCore.Mvc;
using DysonNetwork.Sphere.Auth.OpenId;
using Microsoft.EntityFrameworkCore;
using NodaTime;
namespace DysonNetwork.Sphere.Auth;
/// <summary>
/// This controller is designed to handle the OAuth callback.
/// </summary>
[ApiController]
[Route("/auth/callback")]
public class AuthCallbackController(
AppDatabase db,
AccountService accounts,
MagicSpellService spells,
AuthService auth,
IServiceProvider serviceProvider,
Account.AccountUsernameService accountUsernameService
)
: ControllerBase
{
[HttpPost("apple")]
public async Task<ActionResult> AppleCallbackPost(
[FromForm] string code,
[FromForm(Name = "id_token")] string idToken,
[FromForm] string? state = null,
[FromForm] string? user = null)
{
return await ProcessOidcCallback("apple", new OidcCallbackData
{
Code = code,
IdToken = idToken,
State = state,
RawData = user
});
}
private async Task<ActionResult> ProcessOidcCallback(string provider, OidcCallbackData callbackData)
{
try
{
// Get the appropriate provider service
var oidcService = GetOidcService(provider);
// Process the callback
var userInfo = await oidcService.ProcessCallbackAsync(callbackData);
if (string.IsNullOrEmpty(userInfo.Email) || string.IsNullOrEmpty(userInfo.UserId))
{
return BadRequest($"Email or user ID is missing from {provider}'s response");
}
// First, check if we already have a connection with this provider ID
var existingConnection = await db.AccountConnections
.Include(c => c.Account)
.FirstOrDefaultAsync(c => c.Provider == provider && c.ProvidedIdentifier == userInfo.UserId);
if (existingConnection is not null)
return await CreateSessionAndRedirect(
oidcService,
userInfo,
existingConnection.Account,
callbackData.State
);
// If no existing connection, try to find an account by email
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 session for the user
var session = await oidcService.CreateSessionForUserAsync(userInfo, account);
// Generate token
var token = auth.CreateToken(session);
// Determine where to redirect
var redirectUrl = "/";
if (!string.IsNullOrEmpty(callbackData.State))
{
// Use state as redirect URL (should be validated in production)
redirectUrl = callbackData.State;
}
// Set the token as a cookie
Response.Cookies.Append(AuthConstants.TokenQueryParamName, token, new CookieOptions
{
HttpOnly = true,
Secure = true,
SameSite = SameSiteMode.Lax,
Expires = DateTimeOffset.UtcNow.AddDays(30)
});
return Redirect(redirectUrl);
}
catch (Exception ex)
{
return BadRequest($"Error processing {provider} Sign In: {ex.Message}");
}
}
private OidcService GetOidcService(string provider)
{
return provider.ToLower() switch
{
"apple" => serviceProvider.GetRequiredService<AppleOidcService>(),
"google" => serviceProvider.GetRequiredService<GoogleOidcService>(),
// Add more providers as needed
_ => throw new ArgumentException($"Unsupported provider: {provider}")
};
}
/// <summary>
/// Creates a session and redirects the user with a token
/// </summary>
private async Task<ActionResult> CreateSessionAndRedirect(OidcService oidcService, OidcUserInfo userInfo,
Account.Account account, string? state)
{
// Create a session for the user
var session = await oidcService.CreateSessionForUserAsync(userInfo, account);
// Generate token
var token = auth.CreateToken(session);
// Determine where to redirect
var redirectUrl = "/";
if (!string.IsNullOrEmpty(state))
redirectUrl = state;
// Set the token as a cookie
Response.Cookies.Append(AuthConstants.TokenQueryParamName, token, new CookieOptions
{
HttpOnly = true,
Secure = true,
SameSite = SameSiteMode.Lax,
Expires = DateTimeOffset.UtcNow.AddDays(30)
});
return Redirect(redirectUrl);
}
}