-
Notifications
You must be signed in to change notification settings - Fork 0
Passkeys + Major Rewrite #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
9b36f5d
Start passkeys
CoPokBl 2fecef1
More passkey stuff like sql, maybe ill finish if i get a fricken second
CoPokBl f19ef70
Tristan controller and more passkeys incl login
CoPokBl bd68ba7
Finish passkey logic, still needs organisation
CoPokBl 530e57e
Fix wrong config ref
CoPokBl d813b30
- New db schema
CoPokBl 61a6069
fix emailconfirm endpoint mixing up redirects
CoPokBl File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,34 +1,23 @@ | ||
| using System.Net; | ||
| using GeneralPurposeLib; | ||
| using Microsoft.AspNetCore.Mvc; | ||
| using Microsoft.AspNetCore.Mvc.Filters; | ||
| using Microsoft.Extensions.Primitives; | ||
|
|
||
| namespace SerbleAPI.API; | ||
|
|
||
| public class ControllerManager : Controller { | ||
| private ILogger Logger => HttpContext.RequestServices | ||
| .GetRequiredService<ILoggerFactory>() | ||
| .CreateLogger(GetType()); | ||
|
|
||
| public override void OnActionExecuting(ActionExecutingContext context) { | ||
|
|
||
| // Add CORS headers to all responses | ||
| context.HttpContext.Response.Headers.Add("Access-Control-Allow-Origin", "*"); | ||
| context.HttpContext.Response.Headers.Add("Access-Control-Allow-Headers", "*"); | ||
| context.HttpContext.Response.Headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH"); | ||
| context.HttpContext.Response.Headers.Add("Access-Control-Allow-Credentials", "true"); | ||
|
|
||
| // get ip address | ||
| IPAddress? ip = Request.HttpContext.Connection.RemoteIpAddress; | ||
|
|
||
| // Somehow it can be null | ||
| string ipStr = ip == null ? "Unknown IP" : ip.ToString(); | ||
|
|
||
| base.OnActionExecuting(context); | ||
|
|
||
| // Log the users information for debugging purposes | ||
| Logger.Debug(context.HttpContext.Request.Headers.TryGetValue("User-Agent", out StringValues header) | ||
| Logger.LogDebug(context.HttpContext.Request.Headers.TryGetValue("User-Agent", out StringValues header) | ||
| ? $"New request from: {ipStr} ({header})" | ||
| : $"New request from: {ipStr} (Unknown user agent)"); | ||
|
|
||
| } | ||
|
|
||
| } | ||
| } |
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| using Microsoft.Extensions.Options; | ||
| using SerbleAPI.Config; | ||
|
|
||
| namespace SerbleAPI.API; | ||
|
|
||
| /// <summary> | ||
| /// Simple redirection middleware that just uses the config | ||
| /// to redirect certain paths to their specified URLs. | ||
| /// </summary> | ||
| /// <param name="next"></param> | ||
| /// <param name="settings"></param> | ||
| public class RedirectsMiddleware(RequestDelegate next, IOptions<ApiSettings> settings) { | ||
|
|
||
| public async Task InvokeAsync(HttpContext context) { | ||
| string path = context.Request.Path; | ||
|
|
||
| if (settings.Value.Redirects.TryGetValue(path.Trim('/'), out string? redirect)) { | ||
| context.Response.Redirect(redirect); | ||
| return; | ||
| } | ||
|
|
||
| await next(context); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| using Microsoft.AspNetCore.Mvc.ActionConstraints; | ||
| using Microsoft.AspNetCore.Mvc.Controllers; | ||
| using Microsoft.AspNetCore.Mvc.Infrastructure; | ||
| using Microsoft.Extensions.Options; | ||
| using SerbleAPI.Config; | ||
|
|
||
| namespace SerbleAPI.API; | ||
|
|
||
| /// <summary> | ||
| /// Single middleware that handles both CORS and automatic OPTIONS responses. | ||
| /// | ||
| /// CORS policy: | ||
| /// • Passkey routes (<c>api/v1/auth/passkey/**</c>): reflects the request | ||
| /// <c>Origin</c> back only when it is in <see cref="PasskeySettings.AllowedOrigins"/>. | ||
| /// Allowed headers: serbleauth, Content-Type, authorization. | ||
| /// No origin reflected (= browser-blocked) when the origin is not in the list. | ||
| /// • All other routes: <c>Access-Control-Allow-Origin: *</c> (open). | ||
| /// All headers and methods allowed. | ||
| /// | ||
| /// OPTIONS handling: | ||
| /// Every OPTIONS request is short-circuited with 200 OK and an <c>Allow</c> header | ||
| /// built by inspecting the live MVC action descriptor registry — no explicit | ||
| /// <c>[HttpOptions]</c> endpoints needed anywhere in the codebase. | ||
| /// </summary> | ||
| public class SerbleCorsMiddleware( | ||
| RequestDelegate next, | ||
| IOptions<PasskeySettings> passkeySettings, | ||
| IActionDescriptorCollectionProvider descriptors) { | ||
|
|
||
| private const string PasskeyPathPrefix = "/api/v1/auth/passkey"; | ||
| private const string AllowedHeaders = "serbleauth, Content-Type, authorization"; | ||
| private const string AllowedMethods = "GET, POST, PUT, DELETE, OPTIONS, PATCH"; | ||
|
|
||
| public Task InvokeAsync(HttpContext context) { | ||
| string path = context.Request.Path.Value ?? "/"; | ||
| string? origin = context.Request.Headers.Origin; | ||
|
|
||
| bool isPasskeyPath = path.StartsWith(PasskeyPathPrefix, StringComparison.OrdinalIgnoreCase); | ||
|
|
||
| ApplyCorsHeaders(context.Response, origin, isPasskeyPath); | ||
|
|
||
| // Short-circuit OPTIONS — browser CORS preflight and plain method-discovery alike. | ||
| if (HttpMethods.IsOptions(context.Request.Method)) { | ||
| return HandleOptions(context, path); | ||
| } | ||
|
|
||
| return next(context); | ||
| } | ||
|
|
||
| // CORS header logic | ||
| private void ApplyCorsHeaders(HttpResponse response, string? origin, bool isPasskeyPath) { | ||
| if (isPasskeyPath) { | ||
| // Reflect the origin only when it is in the passkey allow-list. | ||
| if (origin != null && | ||
| passkeySettings.Value.AllowedOrigins.Contains(origin, StringComparer.OrdinalIgnoreCase)) { | ||
| response.Headers.AccessControlAllowOrigin = origin; | ||
| response.Headers.AccessControlAllowHeaders = AllowedHeaders; | ||
| response.Headers.AccessControlAllowMethods = AllowedMethods; | ||
| response.Headers.AccessControlAllowCredentials = "true"; | ||
| response.Headers.Vary = "Origin"; | ||
| } | ||
| // No ACAO header → browser enforces block for non-whitelisted origins. | ||
| } | ||
| else { | ||
| // Open policy for all non-passkey routes. | ||
| response.Headers.AccessControlAllowOrigin = "*"; | ||
| response.Headers.AccessControlAllowHeaders = "*"; | ||
| response.Headers.AccessControlAllowMethods = AllowedMethods; | ||
| } | ||
| } | ||
|
|
||
| // OPTIONS short-circuit | ||
| private Task HandleOptions(HttpContext context, string path) { | ||
| HashSet<string> methods = MethodsForPath(path); | ||
|
|
||
| // No registered route matched → fall through so the 404 pipeline runs. | ||
| if (methods.Count == 0) { | ||
| return next(context); | ||
| } | ||
|
|
||
| methods.Add("OPTIONS"); | ||
| context.Response.Headers.Allow = string.Join(", ", methods.OrderBy(m => m)); | ||
| context.Response.StatusCode = StatusCodes.Status200OK; | ||
| return Task.CompletedTask; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Walks the MVC route registry and returns every HTTP method registered | ||
| /// for any action whose route template matches <paramref name="requestPath"/>. | ||
| /// Route parameter segments (<c>{id}</c>, <c>{id:int}</c>, etc.) are treated | ||
| /// as wildcards so <c>api/v1/app/{appid}</c> matches <c>/api/v1/app/abc</c>. | ||
| /// </summary> | ||
| private HashSet<string> MethodsForPath(string requestPath) { | ||
| HashSet<string> methods = new(StringComparer.OrdinalIgnoreCase); | ||
|
|
||
| foreach (ControllerActionDescriptor d in | ||
| descriptors.ActionDescriptors.Items.OfType<ControllerActionDescriptor>()) { | ||
| string? template = d.AttributeRouteInfo?.Template; | ||
| if (template == null || !TemplateMatches(template, requestPath)) { | ||
| continue; | ||
| } | ||
|
|
||
| IEnumerable<string> actionMethods = | ||
| d.ActionConstraints?.OfType<HttpMethodActionConstraint>() | ||
| .SelectMany(c => c.HttpMethods) | ||
| ?? []; | ||
|
|
||
| foreach (string m in actionMethods) { | ||
| methods.Add(m.ToUpperInvariant()); | ||
| } | ||
| } | ||
|
|
||
| return methods; | ||
| } | ||
|
|
||
| private static bool TemplateMatches(string template, string requestPath) { | ||
| ReadOnlySpan<char> tSpan = template.TrimStart('/'); | ||
| ReadOnlySpan<char> pSpan = requestPath.TrimStart('/'); | ||
|
|
||
| // Drop query string | ||
| int q = pSpan.IndexOf('?'); | ||
| if (q >= 0) pSpan = pSpan[..q]; | ||
|
|
||
| string[] tParts = tSpan.Length == 0 ? [] : tSpan.ToString().Split('/'); | ||
| string[] pParts = pSpan.Length == 0 ? [] : pSpan.ToString().Split('/'); | ||
|
|
||
| if (tParts.Length != pParts.Length) { | ||
| return false; | ||
| } | ||
|
|
||
| for (int i = 0; i < tParts.Length; i++) { | ||
| string t = tParts[i]; | ||
| // Route parameters: {id}, {id?}, {id:guid}, etc. | ||
| if (t.StartsWith('{') && t.EndsWith('}')) continue; | ||
| if (!t.Equals(pParts[i], StringComparison.OrdinalIgnoreCase)) return false; | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.