🧱 OAuth login infra
This commit is contained in:
181
DysonNetwork.Sphere/Auth/AuthCallbackController.cs
Normal file
181
DysonNetwork.Sphere/Auth/AuthCallbackController.cs
Normal file
@ -0,0 +1,181 @@
|
||||
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);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user