Skip to content

Commit 6efdd30

Browse files
committed
feat(AuthCallbackHandler): Add a AuthCallback Handler instance that can be already used build in
Made the Handler specific Methods that use magic numbers (status, messages...) virtual so someone who would like to have other behavior could simply override them and implement it with littlest effort Added ErrorResponse Default uri key names of the official standard document and implemented in the OAuthCallbackHandler
1 parent 99c96f3 commit 6efdd30

6 files changed

Lines changed: 147 additions & 0 deletions

File tree

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
namespace Yllibed.HttpServer.Defaults;
2+
/// <summary>
3+
/// OAuth Error Response standardized error codes. <see href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1">4.1.2.1 Error Response</see>
4+
/// </summary>
5+
public class OAuthErrorResponseDefaults
6+
{
7+
public const string ErrorKey = "error";
8+
public const string ErrorDescriptionKey = "error_description";
9+
public const string ErrorUriKey = "error_uri";
10+
11+
public const string AccessDenied = "access_denied";
12+
public const string InvalidRequest = "invalid_request";
13+
public const string UnauthorizedClient = "unauthorized_client";
14+
public const string InvalidClient = "invalid_client";
15+
public const string InvalidGrant = "invalid_grant";
16+
public const string UnsupportedGrantType = "unsupported_grant_type";
17+
public const string InvalidScope = "invalid_scope";
18+
public const string TemporarilyUnavailable = "temporarily_unavailable";
19+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
4+
namespace Yllibed.HttpServer.Extensions;
5+
6+
public static class UriExtensions
7+
{
8+
private static readonly char[] _uriSplitChars = new char[2] { '?', '&' };
9+
public static IDictionary<string, string> GetParameters(this Uri uri)
10+
{
11+
12+
return (from p in uri.OriginalString.Split(_uriSplitChars, StringSplitOptions.RemoveEmptyEntries)
13+
select p.Split('=') into parts
14+
where parts.Length > 1
15+
select parts).ToDictionary((parts) => parts[0], (parts) => string.Join("=", parts.Skip(1)), StringComparer.Ordinal);
16+
}
17+
}

Yllibed.HttpServer/GlobalUsings.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@
55
global using System.Threading;
66
global using System.Threading.Tasks;
77
global using Yllibed.HttpServer.Extensions;
8+
global using Yllibed.HttpServer.Handlers;
9+
global using Yllibed.HttpServer.Defaults;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System.ComponentModel.DataAnnotations;
2+
3+
namespace Yllibed.HttpServer.Handlers;
4+
public record AuthCallbackHandlerOptions
5+
{
6+
public const string SectionName = "AuthCallback";
7+
/// <summary>
8+
/// Configures the expected URI for authentication Callbacks.
9+
/// </summary>
10+
[Required, Url]
11+
public Uri? CallbackUri { get; init; }
12+
13+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace Yllibed.HttpServer.Handlers;
2+
3+
public interface IAuthCallbackHandler : IHttpHandler
4+
{
5+
public Uri CallbackUri { get; }
6+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using Microsoft.Extensions.Options;
4+
5+
namespace Yllibed.HttpServer.Handlers;
6+
public record OAuthCallbackHandler : IAuthCallbackHandler
7+
{
8+
private readonly TaskCompletionSource<WebAuthenticationResult> _tcs = new();
9+
public Uri CallbackUri { get; init; }
10+
public OAuthCallbackHandler(Uri callbackUri)
11+
{
12+
if (callbackUri is null || callbackUri.Scheme != Uri.UriSchemeHttp && callbackUri.Scheme != Uri.UriSchemeHttps)
13+
{
14+
throw new ArgumentException("The CallbackUri must be an absolute URI with HTTP or HTTPS scheme.", nameof(callbackUri));
15+
}
16+
CallbackUri = callbackUri;
17+
}
18+
[Microsoft.Extensions.DependencyInjection.ActivatorUtilitiesConstructor]
19+
public OAuthCallbackHandler(
20+
IOptions<AuthCallbackHandlerOptions> options)
21+
{
22+
if (options?.Value?.CallbackUri is not Uri uri
23+
|| uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps)
24+
{
25+
throw new ArgumentException("The CallbackUri must be an absolute URI with HTTP or HTTPS scheme.", nameof(options));
26+
}
27+
CallbackUri = uri;
28+
}
29+
30+
public Task HandleRequest(CancellationToken ct, IHttpServerRequest request, string relativePath)
31+
{
32+
if (request.Url.AbsolutePath.StartsWith(CallbackUri.AbsolutePath, StringComparison.OrdinalIgnoreCase))
33+
{
34+
35+
var parameters = request.Url.GetParameters();
36+
37+
var statusCode = GetStatusCode(parameters);
38+
39+
var result = GetWebAuthenticationResult(statusCode, request.Url.OriginalString);
40+
41+
_tcs.TrySetResult(result);
42+
request.SetResponse(
43+
System.Net.Mime.MediaTypeNames.Text.Plain,
44+
GetWebAuthenticationResponseMessage(result));
45+
}
46+
47+
return Task.CompletedTask;
48+
}
49+
protected virtual WebAuthenticationResult GetWebAuthenticationResult(uint statusCode, string requestUriString)
50+
{
51+
return statusCode switch
52+
{
53+
200 => new WebAuthenticationResult(requestUriString, statusCode, WebAuthenticationStatus.Success),
54+
403 => new WebAuthenticationResult(requestUriString, statusCode, WebAuthenticationStatus.UserCancel),
55+
_ => new WebAuthenticationResult(requestUriString, statusCode, WebAuthenticationStatus.ErrorHttp),
56+
};
57+
}
58+
59+
protected virtual uint GetStatusCode(IDictionary<string, string> parameters)
60+
{
61+
if (parameters.TryGetValue(OAuthErrorResponseDefaults.ErrorKey, out var error))
62+
{
63+
return error switch
64+
{
65+
OAuthErrorResponseDefaults.AccessDenied => 403,
66+
OAuthErrorResponseDefaults.InvalidClient or OAuthErrorResponseDefaults.UnauthorizedClient or OAuthErrorResponseDefaults.InvalidScope => 401,
67+
OAuthErrorResponseDefaults.TemporarilyUnavailable => 503,
68+
OAuthErrorResponseDefaults.UnsupportedGrantType => 500,
69+
_ => 400 // For all others: Bad Request
70+
};
71+
72+
}
73+
74+
return 200;
75+
}
76+
protected virtual string GetWebAuthenticationResponseMessage(WebAuthenticationResult result)
77+
{
78+
return result.ResponseStatus switch
79+
{
80+
WebAuthenticationStatus.Success => "Authentication completed successfully - you can close this browser now.",
81+
WebAuthenticationStatus.UserCancel => "Authentication was cancelled by the user.",
82+
WebAuthenticationStatus.ErrorHttp => "Authentication failed due to an error.",
83+
_ => "Authentication completed - you can close this browser now."
84+
};
85+
}
86+
public Task<WebAuthenticationResult> WaitForCallbackAsync()
87+
{
88+
return _tcs.Task;
89+
}
90+
}

0 commit comments

Comments
 (0)