- Added async GetAuthorizationUrlAsync() methods to all OIDC providers - Updated base OidcService with abstract async contract and backward-compatible sync wrapper - Modified OidcController to use async authorization URL generation - Removed sync blocks using .GetAwaiter().GetResult() in Google provider - Maintained backward compatibility with existing sync method calls - Eliminated thread blocking and improved async flow throughout auth pipeline - Enhanced scalability by allowing non-blocking async authorization URL generation
		
			
				
	
	
		
			198 lines
		
	
	
		
			7.3 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			198 lines
		
	
	
		
			7.3 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
using DysonNetwork.Pass.Account;
 | 
						|
using DysonNetwork.Shared.Cache;
 | 
						|
using DysonNetwork.Shared.Models;
 | 
						|
using Microsoft.AspNetCore.Mvc;
 | 
						|
using Microsoft.EntityFrameworkCore;
 | 
						|
using Microsoft.IdentityModel.Tokens;
 | 
						|
using NodaTime;
 | 
						|
 | 
						|
namespace DysonNetwork.Pass.Auth.OpenId;
 | 
						|
 | 
						|
[ApiController]
 | 
						|
[Route("/api/auth/login")]
 | 
						|
public class OidcController(
 | 
						|
    IServiceProvider serviceProvider,
 | 
						|
    AppDatabase db,
 | 
						|
    AccountService accounts,
 | 
						|
    ICacheService cache
 | 
						|
)
 | 
						|
    : ControllerBase
 | 
						|
{
 | 
						|
    private const string StateCachePrefix = "oidc-state:";
 | 
						|
    private static readonly TimeSpan StateExpiration = TimeSpan.FromMinutes(15);
 | 
						|
 | 
						|
    [HttpGet("{provider}")]
 | 
						|
    public async Task<ActionResult> OidcLogin(
 | 
						|
        [FromRoute] string provider,
 | 
						|
        [FromQuery] string? returnUrl = "/",
 | 
						|
        [FromHeader(Name = "X-Device-Id")] string? deviceId = null
 | 
						|
    )
 | 
						|
    {
 | 
						|
        try
 | 
						|
        {
 | 
						|
            var oidcService = GetOidcService(provider);
 | 
						|
 | 
						|
            // If the user is already authenticated, treat as an account connection request
 | 
						|
            if (HttpContext.Items["CurrentUser"] is SnAccount currentUser)
 | 
						|
            {
 | 
						|
                var state = Guid.NewGuid().ToString();
 | 
						|
                var nonce = Guid.NewGuid().ToString();
 | 
						|
 | 
						|
                // Create and store connection state
 | 
						|
                var oidcState = OidcState.ForConnection(currentUser.Id, provider, nonce, deviceId);
 | 
						|
                await cache.SetAsync($"{StateCachePrefix}{state}", oidcState, StateExpiration);
 | 
						|
 | 
						|
                // The state parameter sent to the provider is the GUID key for the cache.
 | 
						|
                var authUrl = await oidcService.GetAuthorizationUrlAsync(state, nonce);
 | 
						|
                return Redirect(authUrl);
 | 
						|
            }
 | 
						|
            else // Otherwise, proceed with the login / registration flow
 | 
						|
            {
 | 
						|
                var nonce = Guid.NewGuid().ToString();
 | 
						|
                var state = Guid.NewGuid().ToString();
 | 
						|
 | 
						|
                // Create login state with return URL and device ID
 | 
						|
                var oidcState = OidcState.ForLogin(returnUrl ?? "/", deviceId);
 | 
						|
                await cache.SetAsync($"{StateCachePrefix}{state}", oidcState, StateExpiration);
 | 
						|
                var authUrl = await oidcService.GetAuthorizationUrlAsync(state, nonce);
 | 
						|
                return Redirect(authUrl);
 | 
						|
            }
 | 
						|
        }
 | 
						|
        catch (Exception ex)
 | 
						|
        {
 | 
						|
            return BadRequest($"Error initiating OpenID Connect flow: {ex.Message}");
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /// <summary>
 | 
						|
    /// Mobile Apple Sign In endpoint
 | 
						|
    /// Handles Apple authentication directly from mobile apps
 | 
						|
    /// </summary>
 | 
						|
    [HttpPost("apple/mobile")]
 | 
						|
    public async Task<ActionResult<SnAuthChallenge>> AppleMobileLogin(
 | 
						|
        [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 challenge = await appleService.CreateChallengeForUserAsync(
 | 
						|
                userInfo,
 | 
						|
                account,
 | 
						|
                HttpContext,
 | 
						|
                request.DeviceId,
 | 
						|
                request.DeviceName
 | 
						|
            );
 | 
						|
 | 
						|
            return Ok(challenge);
 | 
						|
        }
 | 
						|
        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
 | 
						|
        {
 | 
						|
            "apple" => serviceProvider.GetRequiredService<AppleOidcService>(),
 | 
						|
            "google" => serviceProvider.GetRequiredService<GoogleOidcService>(),
 | 
						|
            "microsoft" => serviceProvider.GetRequiredService<MicrosoftOidcService>(),
 | 
						|
            "discord" => serviceProvider.GetRequiredService<DiscordOidcService>(),
 | 
						|
            "github" => serviceProvider.GetRequiredService<GitHubOidcService>(),
 | 
						|
            "afdian" => serviceProvider.GetRequiredService<AfdianOidcService>(),
 | 
						|
            _ => throw new ArgumentException($"Unsupported provider: {provider}")
 | 
						|
        };
 | 
						|
    }
 | 
						|
 | 
						|
    private async Task<SnAccount> FindOrCreateAccount(OidcUserInfo userInfo, string provider)
 | 
						|
    {
 | 
						|
        if (string.IsNullOrEmpty(userInfo.Email))
 | 
						|
            throw new ArgumentException("Email is required for account creation");
 | 
						|
 | 
						|
        // Check if an 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)
 | 
						|
            {
 | 
						|
                await db.AccountConnections
 | 
						|
                    .Where(c => c.AccountId == existingAccount.Id &&
 | 
						|
                                c.Provider == provider &&
 | 
						|
                                c.ProvidedIdentifier == userInfo.UserId)
 | 
						|
                    .ExecuteUpdateAsync(s => s
 | 
						|
                        .SetProperty(c => c.LastUsedAt, SystemClock.Instance.GetCurrentInstant())
 | 
						|
                        .SetProperty(c => c.Meta, userInfo.ToMetadata()));
 | 
						|
 | 
						|
                return existingAccount;
 | 
						|
            }
 | 
						|
 | 
						|
            var connection = new SnAccountConnection
 | 
						|
            {
 | 
						|
                AccountId = existingAccount.Id,
 | 
						|
                Provider = provider,
 | 
						|
                ProvidedIdentifier = userInfo.UserId!,
 | 
						|
                AccessToken = userInfo.AccessToken,
 | 
						|
                RefreshToken = userInfo.RefreshToken,
 | 
						|
                LastUsedAt = SystemClock.Instance.GetCurrentInstant(),
 | 
						|
                Meta = userInfo.ToMetadata()
 | 
						|
            };
 | 
						|
 | 
						|
            await db.AccountConnections.AddAsync(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 SnAccountConnection
 | 
						|
        {
 | 
						|
            AccountId = newAccount.Id,
 | 
						|
            Provider = provider,
 | 
						|
            ProvidedIdentifier = userInfo.UserId!,
 | 
						|
            AccessToken = userInfo.AccessToken,
 | 
						|
            RefreshToken = userInfo.RefreshToken,
 | 
						|
            LastUsedAt = SystemClock.Instance.GetCurrentInstant(),
 | 
						|
            Meta = userInfo.ToMetadata()
 | 
						|
        };
 | 
						|
 | 
						|
        db.AccountConnections.Add(newConnection);
 | 
						|
        await db.SaveChangesAsync();
 | 
						|
 | 
						|
        return newAccount;
 | 
						|
    }
 | 
						|
}
 |