Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

public sealed class UAuthConflictException : UAuthRuntimeException
{
public override int StatusCode => 409;

public UAuthConflictException(string code) : base(code, "A resource conflict occurred.")
{
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

public sealed class UAuthForbiddenException : UAuthRuntimeException
{
public override int StatusCode => 403;

public UAuthForbiddenException(string code = "forbidden") : base(code, "Forbidden.")
{
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

public class UAuthNotFoundException : UAuthRuntimeException
{
public override int StatusCode => 400;
public override int StatusCode => 404;

public override string Title => "The resource is not found.";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

public sealed class UAuthUnauthorizedException : UAuthRuntimeException
{
public override int StatusCode => 401;

public UAuthUnauthorizedException(string code = "unauthorized") : base(code, "Unauthorized.")
{
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public async Task ExecuteAsync(AccessContext context, IAccessCommand command, Ca
var decision = _authority.Decide(context, policies);

if (decision.RequiresReauthentication)
throw new InvalidOperationException("Requires reauthentication.");
throw new UAuthChallengeRequiredException();

if (decision.IsDenied)
throw new UAuthAuthorizationException(decision.DenyReason ?? "authorization_denied");
Expand All @@ -53,7 +53,7 @@ public async Task<TResult> ExecuteAsync<TResult>(AccessContext context, IAccessC
var decision = _authority.Decide(context, policies);

if (decision.RequiresReauthentication)
throw new InvalidOperationException("Requires reauthentication.");
throw new UAuthChallengeRequiredException();

if (decision.IsDenied)
throw new UAuthAuthorizationException(decision.DenyReason ?? "authorization_denied");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Text.Json;
using CodeBeam.UltimateAuth.Core.Errors;
using Microsoft.AspNetCore.Http;

namespace CodeBeam.UltimateAuth.Server.Extensions;
Expand All @@ -15,10 +16,10 @@ public static async Task<T> ReadJsonAsync<T>(this HttpContext ctx, CancellationT
var request = ctx.Request;

if (!request.HasJsonContentType())
throw new InvalidOperationException("Request content type must be application/json.");
throw new UAuthValidationException("request_content_type_invalid");

if (request.Body == null || request.ContentLength == 0)
throw new InvalidOperationException("Request body is empty.");
throw new UAuthValidationException("request_body_required");

request.EnableBuffering();

Expand All @@ -28,16 +29,24 @@ public static async Task<T> ReadJsonAsync<T>(this HttpContext ctx, CancellationT
{
var result = await JsonSerializer.DeserializeAsync<T>(request.Body, JsonOptions, ct);

request.Body.Position = 0;

if (result == null)
throw new InvalidOperationException("Request body could not be deserialized.");
throw new UAuthValidationException("request_body_invalid");

return result;
}
catch (JsonException)
{
throw new InvalidOperationException("Invalid JSON");
throw new UAuthValidationException("request_json_invalid");
}
// TODO: Remove this catch block in the future, as it is a workaround.
catch (ArgumentException ex) when (ex.ParamName == "value" && ex.Message.Contains("permission_required", StringComparison.Ordinal))
{
throw new UAuthValidationException("permission_required");
}
finally
{
if (request.Body.CanSeek)
request.Body.Position = 0;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ public static IApplicationBuilder UseUAuthExceptionHandling(this IApplicationBui
});
}

// TODO: Standardize runtime error codes as stable machine-readable identifiers.
// Keep human-readable details separate from codes and ProblemDetails.Type.
private static Task WriteProblemDetails(HttpContext context, UAuthRuntimeException ex)
{
var status = MapStatusCode(ex);
var status = ex.StatusCode;

var problem = new ProblemDetails
{
Expand All @@ -45,20 +47,20 @@ private static Task WriteProblemDetails(HttpContext context, UAuthRuntimeExcepti
return context.Response.WriteAsJsonAsync(problem, options: null, contentType: "application/problem+json");
}

private static int MapStatusCode(UAuthRuntimeException ex) =>
ex switch
{
UAuthAuthenticationException => StatusCodes.Status401Unauthorized,
UAuthAuthorizationException => StatusCodes.Status403Forbidden,
UAuthConflictException => StatusCodes.Status409Conflict,
UAuthValidationException => StatusCodes.Status400BadRequest,
UAuthUnauthorizedException => StatusCodes.Status401Unauthorized,
UAuthForbiddenException => StatusCodes.Status403Forbidden,
UAuthNotFoundException => StatusCodes.Status404NotFound,
UAuthChallengeRequiredException => StatusCodes.Status401Unauthorized,
UAuthIdentifierNotFoundException => StatusCodes.Status404NotFound,
UAuthIdentifierConflictException => StatusCodes.Status409Conflict,
UAuthIdentifierValidationException => StatusCodes.Status400BadRequest,
_ => StatusCodes.Status400BadRequest
};
//private static int MapStatusCode(UAuthRuntimeException ex) =>
// ex switch
// {
// UAuthAuthenticationException => StatusCodes.Status401Unauthorized,
// UAuthAuthorizationException => StatusCodes.Status403Forbidden,
// UAuthConflictException => StatusCodes.Status409Conflict,
// UAuthValidationException => StatusCodes.Status400BadRequest,
// UAuthUnauthorizedException => StatusCodes.Status401Unauthorized,
// UAuthForbiddenException => StatusCodes.Status403Forbidden,
// UAuthNotFoundException => StatusCodes.Status404NotFound,
// UAuthChallengeRequiredException => StatusCodes.Status401Unauthorized,
// UAuthIdentifierNotFoundException => StatusCodes.Status404NotFound,
// UAuthIdentifierConflictException => StatusCodes.Status409Conflict,
// UAuthIdentifierValidationException => StatusCodes.Status400BadRequest,
// _ => StatusCodes.Status400BadRequest
// };
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace CodeBeam.UltimateAuth.Server.Infrastructure;
using CodeBeam.UltimateAuth.Core.Errors;

namespace CodeBeam.UltimateAuth.Server.Infrastructure;

internal static class ReturnUrlParser
{
Expand All @@ -22,8 +24,8 @@ public static ReturnUrlInfo Parse(string? returnUrl)
}

if (returnUrl.StartsWith("//", StringComparison.Ordinal))
throw new InvalidOperationException("Invalid returnUrl.");
throw new UAuthValidationException("return_url_invalid");

throw new InvalidOperationException("Invalid returnUrl.");
throw new UAuthValidationException("return_url_invalid");
}
}
5 changes: 3 additions & 2 deletions src/CodeBeam.UltimateAuth.Server/Services/PkceService.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using CodeBeam.UltimateAuth.Core.Abstractions;
using CodeBeam.UltimateAuth.Core.Contracts;
using CodeBeam.UltimateAuth.Core.Domain;
using CodeBeam.UltimateAuth.Core.Errors;
using CodeBeam.UltimateAuth.Server.Auth;
using CodeBeam.UltimateAuth.Server.Flows;
using CodeBeam.UltimateAuth.Server.Options;
Expand Down Expand Up @@ -31,10 +32,10 @@ public PkceService(IAuthStore authStore, IPkceAuthorizationValidator validator,
public async Task<PkceAuthorizeResponse> AuthorizeAsync(PkceAuthorizeCommand command, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(command.CodeChallenge))
throw new InvalidOperationException("code_challenge is required.");
throw new UAuthValidationException("code_challenge is required.");

if (!string.Equals(command.ChallengeMethod, "S256", StringComparison.Ordinal))
throw new InvalidOperationException("Only S256 supported.");
throw new UAuthValidationException("Only S256 supported.");

var authorizationCode = AuthArtifactKey.New();

Expand Down
54 changes: 42 additions & 12 deletions src/client/CodeBeam.UltimateAuth.Client.Blazor/TScripts/uauth.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,12 +203,27 @@ window.uauth.post = async function (options) {
headers["Content-Type"] = "application/x-www-form-urlencoded";
}

const response = await fetch(url, {
method: "POST",
credentials: "include",
headers: headers,
body: body
});
let response;

try {
response = await fetch(url, {
method: "POST",
credentials: "include",
headers: headers,
body: body
});
} catch (error) {
if (error instanceof TypeError) {
return {
ok: false,
status: 0,
refreshOutcome: null,
body: null
};
}

throw error;
}

let responseBody = null;
try {
Expand Down Expand Up @@ -243,12 +258,27 @@ window.uauth.postJson = async function (options) {
"X-Requested-With": "UAuth"
};

const response = await fetch(url, {
method: "POST",
credentials: "include",
headers: headers,
body: payload ? JSON.stringify(payload) : null
});
let response;

try {
response = await fetch(url, {
method: "POST",
credentials: "include",
headers: headers,
body: payload ? JSON.stringify(payload) : null
});
} catch (error) {
if (error instanceof TypeError) {
return {
ok: false,
status: 0,
refreshOutcome: null,
body: null
};
}

throw error;
}

let responseBody = null;
try {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ private static void EnsureTransport(UAuthTransportResult raw)
};
}

// TODO: Preserve flattened ProblemDetails extensions during deserialization.
// Ensure custom extensions and traceId survive the server-to-SDK round-trip.
private static UAuthProblem? TryDeserializeProblem(UAuthTransportResult raw)
{
if (raw.Body is null || raw.Body.Value.ValueKind != JsonValueKind.Object)
Expand Down
Loading
Loading