✨ Manual setup account connections
🐛 Fix infinite oauth token reconnect websocket due to missing device id 🐛 Fix IP forwarded headers didn't work
This commit is contained in:
parent
16ff5588b9
commit
44ff09c119
@ -2,8 +2,8 @@ using System.Globalization;
|
|||||||
using DysonNetwork.Sphere.Auth;
|
using DysonNetwork.Sphere.Auth;
|
||||||
using DysonNetwork.Sphere.Auth.OpenId;
|
using DysonNetwork.Sphere.Auth.OpenId;
|
||||||
using DysonNetwork.Sphere.Email;
|
using DysonNetwork.Sphere.Email;
|
||||||
|
|
||||||
using DysonNetwork.Sphere.Localization;
|
using DysonNetwork.Sphere.Localization;
|
||||||
using DysonNetwork.Sphere.Pages.Emails;
|
|
||||||
using DysonNetwork.Sphere.Permission;
|
using DysonNetwork.Sphere.Permission;
|
||||||
using DysonNetwork.Sphere.Storage;
|
using DysonNetwork.Sphere.Storage;
|
||||||
using EFCore.BulkExtensions;
|
using EFCore.BulkExtensions;
|
||||||
@ -376,7 +376,7 @@ public class AccountService(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await mailer.SendTemplatedEmailAsync<VerificationEmail, VerificationEmailModel>(
|
await mailer.SendTemplatedEmailAsync<DysonNetwork.Sphere.Pages.Emails.VerificationEmail, VerificationEmailModel>(
|
||||||
account.Nick,
|
account.Nick,
|
||||||
contact.Content,
|
contact.Content,
|
||||||
localizer["VerificationEmail"],
|
localizer["VerificationEmail"],
|
||||||
|
@ -5,8 +5,10 @@ using NodaTime;
|
|||||||
|
|
||||||
namespace DysonNetwork.Sphere.Auth;
|
namespace DysonNetwork.Sphere.Auth;
|
||||||
|
|
||||||
public class AuthService(AppDatabase db, IConfiguration config, IHttpClientFactory httpClientFactory)
|
public class AuthService(AppDatabase db, IConfiguration config, IHttpClientFactory httpClientFactory, IHttpContextAccessor httpContextAccessor)
|
||||||
{
|
{
|
||||||
|
private HttpContext HttpContext => httpContextAccessor.HttpContext!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Detect the risk of the current request to login
|
/// Detect the risk of the current request to login
|
||||||
/// and returns the required steps to login.
|
/// and returns the required steps to login.
|
||||||
@ -63,6 +65,33 @@ public class AuthService(AppDatabase db, IConfiguration config, IHttpClientFacto
|
|||||||
return totalRequiredSteps;
|
return totalRequiredSteps;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<Session> CreateSessionAsync(Account.Account account, Instant time)
|
||||||
|
{
|
||||||
|
var challenge = new Challenge
|
||||||
|
{
|
||||||
|
AccountId = account.Id,
|
||||||
|
IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||||
|
UserAgent = HttpContext.Request.Headers.UserAgent,
|
||||||
|
StepRemain = 1,
|
||||||
|
StepTotal = 1,
|
||||||
|
Type = ChallengeType.Oidc
|
||||||
|
};
|
||||||
|
|
||||||
|
var session = new Session
|
||||||
|
{
|
||||||
|
AccountId = account.Id,
|
||||||
|
CreatedAt = time,
|
||||||
|
LastGrantedAt = time,
|
||||||
|
Challenge = challenge
|
||||||
|
};
|
||||||
|
|
||||||
|
db.AuthChallenges.Add(challenge);
|
||||||
|
db.AuthSessions.Add(session);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<bool> ValidateCaptcha(string token)
|
public async Task<bool> ValidateCaptcha(string token)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(token)) return false;
|
if (string.IsNullOrWhiteSpace(token)) return false;
|
||||||
|
@ -10,4 +10,6 @@ public class AppleMobileSignInRequest
|
|||||||
public required string IdentityToken { get; set; }
|
public required string IdentityToken { get; set; }
|
||||||
[Required]
|
[Required]
|
||||||
public required string AuthorizationCode { get; set; }
|
public required string AuthorizationCode { get; set; }
|
||||||
|
[Required]
|
||||||
|
public required string DeviceId { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -140,8 +140,14 @@ public class AppleOidcService(
|
|||||||
var keyId = _configuration["Oidc:Apple:KeyId"];
|
var keyId = _configuration["Oidc:Apple:KeyId"];
|
||||||
var privateKeyPath = _configuration["Oidc:Apple:PrivateKeyPath"];
|
var privateKeyPath = _configuration["Oidc:Apple:PrivateKeyPath"];
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(teamId) || string.IsNullOrEmpty(clientId) || string.IsNullOrEmpty(keyId) ||
|
||||||
|
string.IsNullOrEmpty(privateKeyPath))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Apple OIDC configuration is missing required values (TeamId, ClientId, KeyId, PrivateKeyPath).");
|
||||||
|
}
|
||||||
|
|
||||||
// Read the private key
|
// Read the private key
|
||||||
var privateKey = File.ReadAllText(privateKeyPath!);
|
var privateKey = File.ReadAllText(privateKeyPath);
|
||||||
|
|
||||||
// Create the JWT header
|
// Create the JWT header
|
||||||
var header = new Dictionary<string, object>
|
var header = new Dictionary<string, object>
|
||||||
|
@ -10,136 +10,6 @@ namespace DysonNetwork.Sphere.Auth.OpenId;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("/auth/callback")]
|
[Route("/auth/callback")]
|
||||||
public class AuthCallbackController(
|
public class AuthCallbackController : ControllerBase
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -2,13 +2,20 @@ using DysonNetwork.Sphere.Account;
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using NodaTime;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Auth.OpenId;
|
namespace DysonNetwork.Sphere.Auth.OpenId;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("/api/connections")]
|
[Route("/api/accounts/me/connections")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
public class ConnectionController(AppDatabase db) : ControllerBase
|
public class ConnectionController(
|
||||||
|
AppDatabase db,
|
||||||
|
IEnumerable<OidcService> oidcServices,
|
||||||
|
AccountService accountService,
|
||||||
|
AuthService authService,
|
||||||
|
IClock clock
|
||||||
|
) : ControllerBase
|
||||||
{
|
{
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<ActionResult<List<AccountConnection>>> GetConnections()
|
public async Task<ActionResult<List<AccountConnection>>> GetConnections()
|
||||||
@ -40,4 +47,212 @@ public class ConnectionController(AppDatabase db) : ControllerBase
|
|||||||
|
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class ConnectProviderRequest
|
||||||
|
{
|
||||||
|
public string Provider { get; set; } = null!;
|
||||||
|
public string? ReturnUrl { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initiates manual connection to an OAuth provider for the current user
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("connect")]
|
||||||
|
public async Task<ActionResult<object>> InitiateConnection([FromBody] ConnectProviderRequest request)
|
||||||
|
{
|
||||||
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser)
|
||||||
|
return Unauthorized();
|
||||||
|
|
||||||
|
var oidcService = oidcServices.FirstOrDefault(s => s.ProviderName.Equals(request.Provider, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (oidcService == null)
|
||||||
|
return BadRequest($"Provider '{request.Provider}' is not supported");
|
||||||
|
|
||||||
|
var existingConnection = await db.AccountConnections
|
||||||
|
.AnyAsync(c => c.AccountId == currentUser.Id && c.Provider == oidcService.ProviderName);
|
||||||
|
|
||||||
|
if (existingConnection)
|
||||||
|
return BadRequest($"You already have a {request.Provider} connection");
|
||||||
|
|
||||||
|
var state = Guid.NewGuid().ToString("N");
|
||||||
|
var nonce = Guid.NewGuid().ToString("N");
|
||||||
|
HttpContext.Session.SetString($"oidc_state_{state}", $"{currentUser.Id}|{request.Provider}|{nonce}");
|
||||||
|
|
||||||
|
var finalReturnUrl = !string.IsNullOrEmpty(request.ReturnUrl) ? request.ReturnUrl : "/settings/connections";
|
||||||
|
HttpContext.Session.SetString($"oidc_return_url_{state}", finalReturnUrl);
|
||||||
|
|
||||||
|
var authUrl = oidcService.GetAuthorizationUrl(state, nonce);
|
||||||
|
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
authUrl,
|
||||||
|
message = $"Redirect to this URL to connect your {request.Provider} account"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[AllowAnonymous]
|
||||||
|
[Route("/auth/callback/{provider}")]
|
||||||
|
[HttpGet, HttpPost]
|
||||||
|
public async Task<IActionResult> HandleCallback([FromRoute] string provider)
|
||||||
|
{
|
||||||
|
var oidcService = oidcServices.FirstOrDefault(s => s.ProviderName.Equals(provider, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (oidcService == null)
|
||||||
|
return BadRequest($"Provider '{provider}' is not supported.");
|
||||||
|
|
||||||
|
var callbackData = await ExtractCallbackData(Request);
|
||||||
|
if (callbackData.State == null)
|
||||||
|
return BadRequest("State parameter is missing.");
|
||||||
|
|
||||||
|
var sessionState = HttpContext.Session.GetString($"oidc_state_{callbackData.State!}");
|
||||||
|
HttpContext.Session.Remove($"oidc_state_{callbackData.State}");
|
||||||
|
|
||||||
|
// If sessionState is present, it's a manual connection flow for an existing user.
|
||||||
|
if (sessionState != null)
|
||||||
|
{
|
||||||
|
var stateParts = sessionState.Split('|');
|
||||||
|
if (stateParts.Length != 3 || !stateParts[1].Equals(provider, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return BadRequest("State mismatch.");
|
||||||
|
|
||||||
|
var accountId = Guid.Parse(stateParts[0]);
|
||||||
|
return await HandleManualConnection(provider, oidcService, callbackData, accountId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, it's a login or registration flow.
|
||||||
|
return await HandleLoginOrRegistration(provider, oidcService, callbackData);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IActionResult> HandleManualConnection(string provider, OidcService oidcService, OidcCallbackData callbackData, Guid accountId)
|
||||||
|
{
|
||||||
|
OidcUserInfo userInfo;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
userInfo = await oidcService.ProcessCallbackAsync(callbackData);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return BadRequest($"Error processing callback: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var existingConnection = await db.AccountConnections
|
||||||
|
.FirstOrDefaultAsync(c => c.Provider.Equals(provider, StringComparison.OrdinalIgnoreCase) && c.ProvidedIdentifier == userInfo.UserId);
|
||||||
|
|
||||||
|
if (existingConnection != null && existingConnection.AccountId != accountId)
|
||||||
|
{
|
||||||
|
return BadRequest($"This {provider} account is already linked to another user.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var userConnection = await db.AccountConnections
|
||||||
|
.FirstOrDefaultAsync(c => c.AccountId == accountId && c.Provider.Equals(provider, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (userConnection != null)
|
||||||
|
{
|
||||||
|
userConnection.AccessToken = userInfo.AccessToken;
|
||||||
|
userConnection.RefreshToken = userInfo.RefreshToken;
|
||||||
|
userConnection.LastUsedAt = clock.GetCurrentInstant();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
db.AccountConnections.Add(new AccountConnection
|
||||||
|
{
|
||||||
|
AccountId = accountId,
|
||||||
|
Provider = provider,
|
||||||
|
ProvidedIdentifier = userInfo.UserId!,
|
||||||
|
AccessToken = userInfo.AccessToken,
|
||||||
|
RefreshToken = userInfo.RefreshToken,
|
||||||
|
LastUsedAt = clock.GetCurrentInstant()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
var returnUrl = HttpContext.Session.GetString($"oidc_return_url_{callbackData.State}");
|
||||||
|
HttpContext.Session.Remove($"oidc_return_url_{callbackData.State}");
|
||||||
|
|
||||||
|
return Redirect(string.IsNullOrEmpty(returnUrl) ? "/" : returnUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IActionResult> HandleLoginOrRegistration(string provider, OidcService oidcService, OidcCallbackData callbackData)
|
||||||
|
{
|
||||||
|
OidcUserInfo userInfo;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
userInfo = await oidcService.ProcessCallbackAsync(callbackData);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return BadRequest($"Error processing callback: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(userInfo.Email) || string.IsNullOrEmpty(userInfo.UserId))
|
||||||
|
{
|
||||||
|
return BadRequest($"Email or user ID is missing from {provider}'s response");
|
||||||
|
}
|
||||||
|
|
||||||
|
var connection = await db.AccountConnections
|
||||||
|
.Include(c => c.Account)
|
||||||
|
.FirstOrDefaultAsync(c => c.Provider == provider && c.ProvidedIdentifier == userInfo.UserId);
|
||||||
|
|
||||||
|
if (connection != null)
|
||||||
|
{
|
||||||
|
// Login existing user
|
||||||
|
var session = await authService.CreateSessionAsync(connection.Account, clock.GetCurrentInstant());
|
||||||
|
var token = authService.CreateToken(session);
|
||||||
|
return Redirect($"/?token={token}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var account = await accountService.LookupAccount(userInfo.Email);
|
||||||
|
if (account == null)
|
||||||
|
{
|
||||||
|
// Register new user
|
||||||
|
account = await accountService.CreateAccount(userInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (account == null)
|
||||||
|
{
|
||||||
|
return BadRequest("Unable to create or link account.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create connection for new or existing user
|
||||||
|
var newConnection = new AccountConnection
|
||||||
|
{
|
||||||
|
Account = account,
|
||||||
|
Provider = provider,
|
||||||
|
ProvidedIdentifier = userInfo.UserId!,
|
||||||
|
AccessToken = userInfo.AccessToken,
|
||||||
|
RefreshToken = userInfo.RefreshToken,
|
||||||
|
LastUsedAt = clock.GetCurrentInstant()
|
||||||
|
};
|
||||||
|
db.AccountConnections.Add(newConnection);
|
||||||
|
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
var loginSession = await authService.CreateSessionAsync(account, clock.GetCurrentInstant());
|
||||||
|
var loginToken = authService.CreateToken(loginSession);
|
||||||
|
return Redirect($"/?token={loginToken}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<OidcCallbackData> ExtractCallbackData(HttpRequest request)
|
||||||
|
{
|
||||||
|
var data = new OidcCallbackData();
|
||||||
|
if (request.Method == "GET")
|
||||||
|
{
|
||||||
|
data.Code = request.Query["code"].FirstOrDefault() ?? "";
|
||||||
|
data.IdToken = request.Query["id_token"].FirstOrDefault() ?? "";
|
||||||
|
data.State = request.Query["state"].FirstOrDefault();
|
||||||
|
}
|
||||||
|
else if (request.Method == "POST" && request.HasFormContentType)
|
||||||
|
{
|
||||||
|
var form = await request.ReadFormAsync();
|
||||||
|
data.Code = form["code"].FirstOrDefault() ?? "";
|
||||||
|
data.IdToken = form["id_token"].FirstOrDefault() ?? "";
|
||||||
|
data.State = form["state"].FirstOrDefault();
|
||||||
|
if (form.ContainsKey("user"))
|
||||||
|
{
|
||||||
|
data.RawData = form["user"].FirstOrDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
95
DysonNetwork.Sphere/Auth/OpenId/DiscordOidcService.cs
Normal file
95
DysonNetwork.Sphere/Auth/OpenId/DiscordOidcService.cs
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Sphere.Auth.OpenId;
|
||||||
|
|
||||||
|
public class DiscordOidcService : OidcService
|
||||||
|
{
|
||||||
|
public DiscordOidcService(IConfiguration configuration, IHttpClientFactory httpClientFactory, AppDatabase db)
|
||||||
|
: base(configuration, httpClientFactory, db)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ProviderName => "Discord";
|
||||||
|
protected override string DiscoveryEndpoint => ""; // Discord doesn't have a standard OIDC discovery endpoint
|
||||||
|
protected override string ConfigSectionName => "Discord";
|
||||||
|
|
||||||
|
public override string GetAuthorizationUrl(string state, string nonce)
|
||||||
|
{
|
||||||
|
var config = GetProviderConfig();
|
||||||
|
var queryParams = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "client_id", config.ClientId },
|
||||||
|
{ "redirect_uri", config.RedirectUri },
|
||||||
|
{ "response_type", "code" },
|
||||||
|
{ "scope", "identify email" },
|
||||||
|
{ "state", state },
|
||||||
|
};
|
||||||
|
|
||||||
|
var queryString = string.Join("&", queryParams.Select(p => $"{p.Key}={Uri.EscapeDataString(p.Value)}"));
|
||||||
|
return $"https://discord.com/api/oauth2/authorize?{queryString}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<OidcUserInfo> ProcessCallbackAsync(OidcCallbackData callbackData)
|
||||||
|
{
|
||||||
|
var tokenResponse = await ExchangeCodeForTokensAsync(callbackData.Code);
|
||||||
|
if (tokenResponse?.AccessToken == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Failed to obtain access token from Discord");
|
||||||
|
}
|
||||||
|
|
||||||
|
var userInfo = await GetUserInfoAsync(tokenResponse.AccessToken);
|
||||||
|
|
||||||
|
userInfo.AccessToken = tokenResponse.AccessToken;
|
||||||
|
userInfo.RefreshToken = tokenResponse.RefreshToken;
|
||||||
|
|
||||||
|
return userInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task<OidcTokenResponse?> ExchangeCodeForTokensAsync(string code, string? codeVerifier = null)
|
||||||
|
{
|
||||||
|
var config = GetProviderConfig();
|
||||||
|
var client = _httpClientFactory.CreateClient();
|
||||||
|
|
||||||
|
var content = new FormUrlEncodedContent(new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "client_id", config.ClientId },
|
||||||
|
{ "client_secret", config.ClientSecret },
|
||||||
|
{ "grant_type", "authorization_code" },
|
||||||
|
{ "code", code },
|
||||||
|
{ "redirect_uri", config.RedirectUri },
|
||||||
|
});
|
||||||
|
|
||||||
|
var response = await client.PostAsync("https://discord.com/api/oauth2/token", content);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
return await response.Content.ReadFromJsonAsync<OidcTokenResponse>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<OidcUserInfo> GetUserInfoAsync(string accessToken)
|
||||||
|
{
|
||||||
|
var client = _httpClientFactory.CreateClient();
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Get, "https://discord.com/api/users/@me");
|
||||||
|
request.Headers.Add("Authorization", $"Bearer {accessToken}");
|
||||||
|
|
||||||
|
var response = await client.SendAsync(request);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var json = await response.Content.ReadAsStringAsync();
|
||||||
|
var discordUser = JsonDocument.Parse(json).RootElement;
|
||||||
|
|
||||||
|
var userId = discordUser.GetProperty("id").GetString() ?? "";
|
||||||
|
var avatar = discordUser.TryGetProperty("avatar", out var avatarElement) ? avatarElement.GetString() : null;
|
||||||
|
|
||||||
|
return new OidcUserInfo
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
Email = (discordUser.TryGetProperty("email", out var emailElement) ? emailElement.GetString() : null) ?? "",
|
||||||
|
EmailVerified = discordUser.TryGetProperty("verified", out var verifiedElement) && verifiedElement.GetBoolean(),
|
||||||
|
DisplayName = (discordUser.TryGetProperty("global_name", out var globalNameElement) ? globalNameElement.GetString() : null) ?? "",
|
||||||
|
PreferredUsername = discordUser.GetProperty("username").GetString() ?? "",
|
||||||
|
ProfilePictureUrl = !string.IsNullOrEmpty(avatar) ? $"https://cdn.discordapp.com/avatars/{userId}/{avatar}.png" : "",
|
||||||
|
Provider = ProviderName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
121
DysonNetwork.Sphere/Auth/OpenId/GitHubOidcService.cs
Normal file
121
DysonNetwork.Sphere/Auth/OpenId/GitHubOidcService.cs
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Sphere.Auth.OpenId;
|
||||||
|
|
||||||
|
public class GitHubOidcService : OidcService
|
||||||
|
{
|
||||||
|
public GitHubOidcService(IConfiguration configuration, IHttpClientFactory httpClientFactory, AppDatabase db)
|
||||||
|
: base(configuration, httpClientFactory, db)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ProviderName => "GitHub";
|
||||||
|
protected override string DiscoveryEndpoint => ""; // GitHub doesn't have a standard OIDC discovery endpoint
|
||||||
|
protected override string ConfigSectionName => "GitHub";
|
||||||
|
|
||||||
|
public override string GetAuthorizationUrl(string state, string nonce)
|
||||||
|
{
|
||||||
|
var config = GetProviderConfig();
|
||||||
|
var queryParams = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "client_id", config.ClientId },
|
||||||
|
{ "redirect_uri", config.RedirectUri },
|
||||||
|
{ "scope", "user:email" },
|
||||||
|
{ "state", state },
|
||||||
|
};
|
||||||
|
|
||||||
|
var queryString = string.Join("&", queryParams.Select(p => $"{p.Key}={Uri.EscapeDataString(p.Value)}"));
|
||||||
|
return $"https://github.com/login/oauth/authorize?{queryString}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<OidcUserInfo> ProcessCallbackAsync(OidcCallbackData callbackData)
|
||||||
|
{
|
||||||
|
var tokenResponse = await ExchangeCodeForTokensAsync(callbackData.Code);
|
||||||
|
if (tokenResponse?.AccessToken == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Failed to obtain access token from GitHub");
|
||||||
|
}
|
||||||
|
|
||||||
|
var userInfo = await GetUserInfoAsync(tokenResponse.AccessToken);
|
||||||
|
|
||||||
|
userInfo.AccessToken = tokenResponse.AccessToken;
|
||||||
|
userInfo.RefreshToken = tokenResponse.RefreshToken;
|
||||||
|
|
||||||
|
return userInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task<OidcTokenResponse?> ExchangeCodeForTokensAsync(string code, string? codeVerifier = null)
|
||||||
|
{
|
||||||
|
var config = GetProviderConfig();
|
||||||
|
var client = _httpClientFactory.CreateClient();
|
||||||
|
|
||||||
|
var tokenRequest = new HttpRequestMessage(HttpMethod.Post, "https://github.com/login/oauth/access_token")
|
||||||
|
{
|
||||||
|
Content = new FormUrlEncodedContent(new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "client_id", config.ClientId },
|
||||||
|
{ "client_secret", config.ClientSecret },
|
||||||
|
{ "code", code },
|
||||||
|
{ "redirect_uri", config.RedirectUri },
|
||||||
|
})
|
||||||
|
};
|
||||||
|
tokenRequest.Headers.Add("Accept", "application/json");
|
||||||
|
|
||||||
|
var response = await client.SendAsync(tokenRequest);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
return await response.Content.ReadFromJsonAsync<OidcTokenResponse>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<OidcUserInfo> GetUserInfoAsync(string accessToken)
|
||||||
|
{
|
||||||
|
var client = _httpClientFactory.CreateClient();
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Get, "https://api.github.com/user");
|
||||||
|
request.Headers.Add("Authorization", $"Bearer {accessToken}");
|
||||||
|
request.Headers.Add("User-Agent", "DysonNetwork.Sphere");
|
||||||
|
|
||||||
|
var response = await client.SendAsync(request);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var json = await response.Content.ReadAsStringAsync();
|
||||||
|
var githubUser = JsonDocument.Parse(json).RootElement;
|
||||||
|
|
||||||
|
var email = githubUser.TryGetProperty("email", out var emailElement) ? emailElement.GetString() : null;
|
||||||
|
if (string.IsNullOrEmpty(email))
|
||||||
|
{
|
||||||
|
email = await GetPrimaryEmailAsync(accessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new OidcUserInfo
|
||||||
|
{
|
||||||
|
UserId = githubUser.GetProperty("id").GetInt64().ToString(),
|
||||||
|
Email = email,
|
||||||
|
DisplayName = githubUser.TryGetProperty("name", out var nameElement) ? nameElement.GetString() ?? "" : "",
|
||||||
|
PreferredUsername = githubUser.GetProperty("login").GetString() ?? "",
|
||||||
|
ProfilePictureUrl = githubUser.TryGetProperty("avatar_url", out var avatarElement) ? avatarElement.GetString() ?? "" : "",
|
||||||
|
Provider = ProviderName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string?> GetPrimaryEmailAsync(string accessToken)
|
||||||
|
{
|
||||||
|
var client = _httpClientFactory.CreateClient();
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Get, "https://api.github.com/user/emails");
|
||||||
|
request.Headers.Add("Authorization", $"Bearer {accessToken}");
|
||||||
|
request.Headers.Add("User-Agent", "DysonNetwork.Sphere");
|
||||||
|
|
||||||
|
var response = await client.SendAsync(request);
|
||||||
|
if (!response.IsSuccessStatusCode) return null;
|
||||||
|
|
||||||
|
var emails = await response.Content.ReadFromJsonAsync<List<GitHubEmail>>();
|
||||||
|
return emails?.FirstOrDefault(e => e.Primary)?.Email;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class GitHubEmail
|
||||||
|
{
|
||||||
|
public string Email { get; set; } = "";
|
||||||
|
public bool Primary { get; set; }
|
||||||
|
public bool Verified { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -29,7 +29,7 @@ public class OidcController(
|
|||||||
var nonce = Guid.NewGuid().ToString();
|
var nonce = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
// Get the authorization URL and redirect the user
|
// Get the authorization URL and redirect the user
|
||||||
var authUrl = oidcService.GetAuthorizationUrl(state, nonce);
|
var authUrl = oidcService.GetAuthorizationUrl(state ?? "/", nonce);
|
||||||
return Redirect(authUrl);
|
return Redirect(authUrl);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@ -43,7 +43,8 @@ public class OidcController(
|
|||||||
/// Handles Apple authentication directly from mobile apps
|
/// Handles Apple authentication directly from mobile apps
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpPost("apple/mobile")]
|
[HttpPost("apple/mobile")]
|
||||||
public async Task<ActionResult<AuthController.TokenExchangeResponse>> AppleMobileSignIn([FromBody] AppleMobileSignInRequest request)
|
public async Task<ActionResult<AuthController.TokenExchangeResponse>> AppleMobileSignIn(
|
||||||
|
[FromBody] AppleMobileSignInRequest request)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -65,7 +66,12 @@ public class OidcController(
|
|||||||
var account = await FindOrCreateAccount(userInfo, "apple");
|
var account = await FindOrCreateAccount(userInfo, "apple");
|
||||||
|
|
||||||
// Create session using the OIDC service
|
// Create session using the OIDC service
|
||||||
var session = await appleService.CreateSessionForUserAsync(userInfo, account);
|
var session = await appleService.CreateSessionForUserAsync(
|
||||||
|
userInfo,
|
||||||
|
account,
|
||||||
|
HttpContext,
|
||||||
|
request.DeviceId
|
||||||
|
);
|
||||||
|
|
||||||
// Generate token using existing auth service
|
// Generate token using existing auth service
|
||||||
var token = authService.CreateToken(session);
|
var token = authService.CreateToken(session);
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
using System.IdentityModel.Tokens.Jwt;
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Security.Claims;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using DysonNetwork.Sphere.Account;
|
using DysonNetwork.Sphere.Account;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@ -15,6 +13,8 @@ namespace DysonNetwork.Sphere.Auth.OpenId;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class OidcService(IConfiguration configuration, IHttpClientFactory httpClientFactory, AppDatabase db)
|
public abstract class OidcService(IConfiguration configuration, IHttpClientFactory httpClientFactory, AppDatabase db)
|
||||||
{
|
{
|
||||||
|
protected readonly IHttpClientFactory _httpClientFactory = httpClientFactory;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the unique identifier for this provider
|
/// Gets the unique identifier for this provider
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -67,7 +67,8 @@ public abstract class OidcService(IConfiguration configuration, IHttpClientFacto
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Exchange the authorization code for tokens
|
/// Exchange the authorization code for tokens
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected async Task<OidcTokenResponse?> ExchangeCodeForTokensAsync(string code, string? codeVerifier = null)
|
protected virtual async Task<OidcTokenResponse?> ExchangeCodeForTokensAsync(string code,
|
||||||
|
string? codeVerifier = null)
|
||||||
{
|
{
|
||||||
var config = GetProviderConfig();
|
var config = GetProviderConfig();
|
||||||
var discoveryDocument = await GetDiscoveryDocumentAsync();
|
var discoveryDocument = await GetDiscoveryDocumentAsync();
|
||||||
@ -160,7 +161,12 @@ public abstract class OidcService(IConfiguration configuration, IHttpClientFacto
|
|||||||
/// Creates a challenge and session for an authenticated user
|
/// Creates a challenge and session for an authenticated user
|
||||||
/// Also creates or updates the account connection
|
/// Also creates or updates the account connection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<Session> CreateSessionForUserAsync(OidcUserInfo userInfo, Account.Account account)
|
public async Task<Session> CreateSessionForUserAsync(
|
||||||
|
OidcUserInfo userInfo,
|
||||||
|
Account.Account account,
|
||||||
|
HttpContext request,
|
||||||
|
string deviceId
|
||||||
|
)
|
||||||
{
|
{
|
||||||
// Create or update the account connection
|
// Create or update the account connection
|
||||||
var connection = await db.AccountConnections
|
var connection = await db.AccountConnections
|
||||||
@ -194,7 +200,10 @@ public abstract class OidcService(IConfiguration configuration, IHttpClientFacto
|
|||||||
Platform = ChallengePlatform.Unidentified,
|
Platform = ChallengePlatform.Unidentified,
|
||||||
Audiences = [ProviderName],
|
Audiences = [ProviderName],
|
||||||
Scopes = ["*"],
|
Scopes = ["*"],
|
||||||
AccountId = account.Id
|
AccountId = account.Id,
|
||||||
|
DeviceId = deviceId,
|
||||||
|
IpAddress = request.Connection.RemoteIpAddress?.ToString() ?? null,
|
||||||
|
UserAgent = request.Request.Headers.UserAgent,
|
||||||
};
|
};
|
||||||
|
|
||||||
await db.AuthChallenges.AddAsync(challenge);
|
await db.AuthChallenges.AddAsync(challenge);
|
||||||
@ -202,9 +211,10 @@ public abstract class OidcService(IConfiguration configuration, IHttpClientFacto
|
|||||||
// Create a session
|
// Create a session
|
||||||
var session = new Session
|
var session = new Session
|
||||||
{
|
{
|
||||||
LastGrantedAt = now,
|
|
||||||
AccountId = account.Id,
|
AccountId = account.Id,
|
||||||
ChallengeId = challenge.Id,
|
CreatedAt = now,
|
||||||
|
LastGrantedAt = now,
|
||||||
|
Challenge = challenge
|
||||||
};
|
};
|
||||||
|
|
||||||
await db.AuthSessions.AddAsync(session);
|
await db.AuthSessions.AddAsync(session);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Net;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading.RateLimiting;
|
using System.Threading.RateLimiting;
|
||||||
using DysonNetwork.Sphere;
|
using DysonNetwork.Sphere;
|
||||||
@ -83,9 +84,17 @@ builder.Services.AddSingleton<IConnectionMultiplexer>(_ =>
|
|||||||
var connection = builder.Configuration.GetConnectionString("FastRetrieve")!;
|
var connection = builder.Configuration.GetConnectionString("FastRetrieve")!;
|
||||||
return ConnectionMultiplexer.Connect(connection);
|
return ConnectionMultiplexer.Connect(connection);
|
||||||
});
|
});
|
||||||
|
builder.Services.AddSingleton<IClock>(SystemClock.Instance);
|
||||||
|
builder.Services.AddHttpContextAccessor();
|
||||||
builder.Services.AddSingleton<ICacheService, CacheServiceRedis>();
|
builder.Services.AddSingleton<ICacheService, CacheServiceRedis>();
|
||||||
|
|
||||||
builder.Services.AddHttpClient();
|
builder.Services.AddHttpClient();
|
||||||
|
|
||||||
|
// Register OIDC services
|
||||||
|
builder.Services.AddScoped<OidcService, GoogleOidcService>();
|
||||||
|
builder.Services.AddScoped<OidcService, AppleOidcService>();
|
||||||
|
builder.Services.AddScoped<OidcService, GitHubOidcService>();
|
||||||
|
builder.Services.AddScoped<OidcService, DiscordOidcService>();
|
||||||
builder.Services.AddControllers().AddJsonOptions(options =>
|
builder.Services.AddControllers().AddJsonOptions(options =>
|
||||||
{
|
{
|
||||||
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
|
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
|
||||||
@ -113,6 +122,12 @@ builder.Services.Configure<RequestLocalizationOptions>(options =>
|
|||||||
|
|
||||||
// Other pipelines
|
// Other pipelines
|
||||||
|
|
||||||
|
builder.Services.AddSession(options =>
|
||||||
|
{
|
||||||
|
options.IdleTimeout = TimeSpan.FromMinutes(30);
|
||||||
|
options.Cookie.HttpOnly = !builder.Configuration["BaseUrl"]!.StartsWith("https");
|
||||||
|
options.Cookie.IsEssential = true;
|
||||||
|
});
|
||||||
builder.Services.AddRateLimiter(o => o.AddFixedWindowLimiter(policyName: "fixed", opts =>
|
builder.Services.AddRateLimiter(o => o.AddFixedWindowLimiter(policyName: "fixed", opts =>
|
||||||
{
|
{
|
||||||
opts.Window = TimeSpan.FromMinutes(1);
|
opts.Window = TimeSpan.FromMinutes(1);
|
||||||
@ -257,7 +272,7 @@ builder.Services.AddQuartz(q =>
|
|||||||
.WithIntervalInSeconds(60)
|
.WithIntervalInSeconds(60)
|
||||||
.RepeatForever())
|
.RepeatForever())
|
||||||
);
|
);
|
||||||
|
|
||||||
var lastActiveFlushJob = new JobKey("LastActiveFlush");
|
var lastActiveFlushJob = new JobKey("LastActiveFlush");
|
||||||
q.AddJob<LastActiveFlushJob>(opts => opts.WithIdentity(lastActiveFlushJob));
|
q.AddJob<LastActiveFlushJob>(opts => opts.WithIdentity(lastActiveFlushJob));
|
||||||
q.AddTrigger(opts => opts
|
q.AddTrigger(opts => opts
|
||||||
@ -267,7 +282,6 @@ builder.Services.AddQuartz(q =>
|
|||||||
.WithIntervalInMinutes(5)
|
.WithIntervalInMinutes(5)
|
||||||
.RepeatForever())
|
.RepeatForever())
|
||||||
);
|
);
|
||||||
|
|
||||||
});
|
});
|
||||||
builder.Services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);
|
builder.Services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);
|
||||||
|
|
||||||
@ -288,11 +302,29 @@ app.UseSwaggerUI();
|
|||||||
|
|
||||||
app.UseRequestLocalization();
|
app.UseRequestLocalization();
|
||||||
|
|
||||||
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
// Configure forwarded headers with known proxies from configuration
|
||||||
{
|
{
|
||||||
ForwardedHeaders = ForwardedHeaders.All
|
var knownProxiesSection = builder.Configuration.GetSection("KnownProxies");
|
||||||
});
|
var forwardedHeadersOptions = new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.All };
|
||||||
|
|
||||||
|
if (knownProxiesSection.Exists())
|
||||||
|
{
|
||||||
|
var proxyAddresses = knownProxiesSection.Get<string[]>();
|
||||||
|
if (proxyAddresses != null)
|
||||||
|
foreach (var proxy in proxyAddresses)
|
||||||
|
if (IPAddress.TryParse(proxy, out var ipAddress))
|
||||||
|
forwardedHeadersOptions.KnownProxies.Add(ipAddress);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
forwardedHeadersOptions.KnownProxies.Add(IPAddress.Any);
|
||||||
|
forwardedHeadersOptions.KnownProxies.Add(IPAddress.IPv6Any);
|
||||||
|
}
|
||||||
|
|
||||||
|
app.UseForwardedHeaders(forwardedHeadersOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
app.UseSession();
|
||||||
app.UseCors(opts =>
|
app.UseCors(opts =>
|
||||||
opts.SetIsOriginAllowed(_ => true)
|
opts.SetIsOriginAllowed(_ => true)
|
||||||
.WithExposedHeaders("*")
|
.WithExposedHeaders("*")
|
||||||
|
@ -95,5 +95,9 @@
|
|||||||
"KeyId": "B668YP4KBG",
|
"KeyId": "B668YP4KBG",
|
||||||
"PrivateKeyPath": "./Keys/Solarpass.p8"
|
"PrivateKeyPath": "./Keys/Solarpass.p8"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"KnownProxies": [
|
||||||
|
"127.0.0.1",
|
||||||
|
"::1"
|
||||||
|
]
|
||||||
}
|
}
|
@ -41,6 +41,7 @@
|
|||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIndexAttribute_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fe38f14ac86274ebb9b366729231d1c1a8838_003F8b_003F2890293d_003FIndexAttribute_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIndexAttribute_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fe38f14ac86274ebb9b366729231d1c1a8838_003F8b_003F2890293d_003FIndexAttribute_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIntentType_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8bb08a178b5b43c5bac20a5a54159a5b2a800_003Fbf_003Ffcb84131_003FIntentType_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIntentType_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8bb08a178b5b43c5bac20a5a54159a5b2a800_003Fbf_003Ffcb84131_003FIntentType_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AInternalServerError_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F01d30b32e2ff422cb80129ca2a441c4242600_003Fb5_003Fc55acdd2_003FInternalServerError_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AInternalServerError_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F01d30b32e2ff422cb80129ca2a441c4242600_003Fb5_003Fc55acdd2_003FInternalServerError_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIPAddress_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F9019acc02f6742e3b19ac2eab79854df3be00_003F58_003F61b957a0_003FIPAddress_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIServiceCollectionQuartzConfigurator_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F1edbd6e24d7b430fabce72177269baa19200_003F67_003Faee36f5b_003FIServiceCollectionQuartzConfigurator_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIServiceCollectionQuartzConfigurator_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F1edbd6e24d7b430fabce72177269baa19200_003F67_003Faee36f5b_003FIServiceCollectionQuartzConfigurator_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIStringLocalizerFactory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F6aa8ac544afb487082402c1fa422910f2e00_003F7f_003F8e728ed6_003FIStringLocalizerFactory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIStringLocalizerFactory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F6aa8ac544afb487082402c1fa422910f2e00_003F7f_003F8e728ed6_003FIStringLocalizerFactory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AITusStore_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8bb08a178b5b43c5bac20a5a54159a5b2a800_003Fb1_003F7e861de5_003FITusStore_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AITusStore_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8bb08a178b5b43c5bac20a5a54159a5b2a800_003Fb1_003F7e861de5_003FITusStore_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user