Self-contained oidc receiver page

This commit is contained in:
LittleSheep 2025-06-16 00:12:19 +08:00
parent 70aeb5e0cb
commit c806c5d139
4 changed files with 84 additions and 14 deletions

View File

@ -205,7 +205,7 @@ public class ConnectionController(
// Login existing user // Login existing user
var session = await auth.CreateSessionAsync(connection.Account, clock.GetCurrentInstant()); var session = await auth.CreateSessionAsync(connection.Account, clock.GetCurrentInstant());
var token = auth.CreateToken(session); var token = auth.CreateToken(session);
return Redirect($"/?token={token}"); return Redirect($"/auth/token?token={token}");
} }
// Register new user // Register new user
@ -228,7 +228,7 @@ public class ConnectionController(
var loginSession = await auth.CreateSessionAsync(account, clock.GetCurrentInstant()); var loginSession = await auth.CreateSessionAsync(account, clock.GetCurrentInstant());
var loginToken = auth.CreateToken(loginSession); var loginToken = auth.CreateToken(loginSession);
return Redirect($"/?token={loginToken}"); return Redirect($"/auth/token?token={loginToken}");
} }
private static async Task<OidcCallbackData> ExtractCallbackData(HttpRequest request) private static async Task<OidcCallbackData> ExtractCallbackData(HttpRequest request)

View File

@ -11,9 +11,18 @@ namespace DysonNetwork.Sphere.Auth.OpenId;
/// <summary> /// <summary>
/// Base service for OpenID Connect authentication providers /// Base service for OpenID Connect authentication providers
/// </summary> /// </summary>
public abstract class OidcService(IConfiguration configuration, IHttpClientFactory httpClientFactory, AppDatabase db) public abstract class OidcService
{ {
protected readonly IHttpClientFactory _httpClientFactory = httpClientFactory; protected readonly IConfiguration _configuration;
protected readonly IHttpClientFactory _httpClientFactory;
protected readonly AppDatabase _db;
protected OidcService(IConfiguration configuration, IHttpClientFactory httpClientFactory, AppDatabase db)
{
_configuration = configuration;
_httpClientFactory = httpClientFactory;
_db = db;
}
/// <summary> /// <summary>
/// Gets the unique identifier for this provider /// Gets the unique identifier for this provider
@ -47,9 +56,9 @@ public abstract class OidcService(IConfiguration configuration, IHttpClientFacto
{ {
return new ProviderConfiguration return new ProviderConfiguration
{ {
ClientId = configuration[$"Oidc:{ConfigSectionName}:ClientId"] ?? "", ClientId = _configuration[$"Oidc:{ConfigSectionName}:ClientId"] ?? "",
ClientSecret = configuration[$"Oidc:{ConfigSectionName}:ClientSecret"] ?? "", ClientSecret = _configuration[$"Oidc:{ConfigSectionName}:ClientSecret"] ?? "",
RedirectUri = configuration["BaseUrl"] + "/auth/callback/" + ProviderName RedirectUri = _configuration["BaseUrl"] + "/auth/callback/" + ProviderName
}; };
} }
@ -58,7 +67,7 @@ public abstract class OidcService(IConfiguration configuration, IHttpClientFacto
/// </summary> /// </summary>
protected async Task<OidcDiscoveryDocument?> GetDiscoveryDocumentAsync() protected async Task<OidcDiscoveryDocument?> GetDiscoveryDocumentAsync()
{ {
var client = httpClientFactory.CreateClient(); var client = _httpClientFactory.CreateClient();
var response = await client.GetAsync(DiscoveryEndpoint); var response = await client.GetAsync(DiscoveryEndpoint);
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<OidcDiscoveryDocument>(); return await response.Content.ReadFromJsonAsync<OidcDiscoveryDocument>();
@ -78,7 +87,7 @@ public abstract class OidcService(IConfiguration configuration, IHttpClientFacto
throw new InvalidOperationException("Token endpoint not found in discovery document"); throw new InvalidOperationException("Token endpoint not found in discovery document");
} }
var client = httpClientFactory.CreateClient(); var client = _httpClientFactory.CreateClient();
var content = new FormUrlEncodedContent(BuildTokenRequestParameters(code, config, codeVerifier)); var content = new FormUrlEncodedContent(BuildTokenRequestParameters(code, config, codeVerifier));
var response = await client.PostAsync(discoveryDocument.TokenEndpoint, content); var response = await client.PostAsync(discoveryDocument.TokenEndpoint, content);
@ -169,7 +178,7 @@ public abstract class OidcService(IConfiguration configuration, IHttpClientFacto
) )
{ {
// Create or update the account connection // Create or update the account connection
var connection = await db.AccountConnections var connection = await _db.AccountConnections
.FirstOrDefaultAsync(c => c.Provider == ProviderName && .FirstOrDefaultAsync(c => c.Provider == ProviderName &&
c.ProvidedIdentifier == userInfo.UserId && c.ProvidedIdentifier == userInfo.UserId &&
c.AccountId == account.Id c.AccountId == account.Id
@ -186,7 +195,7 @@ public abstract class OidcService(IConfiguration configuration, IHttpClientFacto
LastUsedAt = SystemClock.Instance.GetCurrentInstant(), LastUsedAt = SystemClock.Instance.GetCurrentInstant(),
AccountId = account.Id AccountId = account.Id
}; };
await db.AccountConnections.AddAsync(connection); await _db.AccountConnections.AddAsync(connection);
} }
// Create a challenge that's already completed // Create a challenge that's already completed
@ -206,7 +215,7 @@ public abstract class OidcService(IConfiguration configuration, IHttpClientFacto
UserAgent = request.Request.Headers.UserAgent, UserAgent = request.Request.Headers.UserAgent,
}; };
await db.AuthChallenges.AddAsync(challenge); await _db.AuthChallenges.AddAsync(challenge);
// Create a session // Create a session
var session = new Session var session = new Session
@ -217,8 +226,8 @@ public abstract class OidcService(IConfiguration configuration, IHttpClientFacto
Challenge = challenge Challenge = challenge
}; };
await db.AuthSessions.AddAsync(session); await _db.AuthSessions.AddAsync(session);
await db.SaveChangesAsync(); await _db.SaveChangesAsync();
return session; return session;
} }

View File

@ -0,0 +1,50 @@
@page "/auth/token"
@model DysonNetwork.Sphere.Pages.Auth.TokenModel
@{
ViewData["Title"] = "Authentication Successful";
Layout = "_Layout";
}
<div class="h-full flex items-center justify-center">
<div class="max-w-lg w-full mx-auto p-6 text-center">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Authentication Successful</h1>
<p class="mb-6 text-gray-900 dark:text-white">You can now close this window and return to the application.</p>
</div>
</div>
@section Scripts {
<script>
(function() {
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token');
if (token) {
console.log("Authentication token received.");
// For WebView2/UWP apps that can handle window.external.notify
if (window.external && typeof window.external.notify === 'function') {
try {
window.external.notify(token);
console.log("Token sent via window.external.notify.");
return; // Exit after successful notification
} catch (e) {
console.error("Failed to send token via window.external.notify:", e);
}
}
// For mobile apps that use custom URI schemes
try {
const customSchemeUrl = `dyson://auth?token=${encodeURIComponent(token)}`;
window.location.href = customSchemeUrl;
console.log("Attempting to redirect to custom scheme:", customSchemeUrl);
} catch (e) {
console.error("Failed to redirect to custom scheme:", e);
}
} else {
console.error("Authentication token not found in URL.");
document.querySelector('p').innerText = "Authentication failed: No token was provided.";
}
})();
</script>
}

View File

@ -0,0 +1,11 @@
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace DysonNetwork.Sphere.Pages.Auth
{
public class TokenModel : PageModel
{
public void OnGet()
{
}
}
}