✨ 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> | ||||
|         { | ||||
|   | ||||
							
								
								
									
										145
									
								
								DysonNetwork.Sphere/Auth/OpenId/AuthCallbackController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								DysonNetwork.Sphere/Auth/OpenId/AuthCallbackController.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,145 @@ | ||||
| using DysonNetwork.Sphere.Account; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
|  | ||||
| namespace DysonNetwork.Sphere.Auth.OpenId; | ||||
|  | ||||
| /// <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) | ||||
|             { | ||||
|                 // Create a new account using the AccountService | ||||
|                 account = await accounts.CreateAccount(userInfo); | ||||
|             } | ||||
|  | ||||
|             // 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); | ||||
|     } | ||||
| } | ||||
| @@ -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); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user