Skip to content

Commit

Permalink
Unify platform callbacks handling using a new OpenIddictClientSystemI…
Browse files Browse the repository at this point in the history
…ntegrationPlatformCallback type
  • Loading branch information
kevinchalet committed Aug 19, 2024
1 parent 290e415 commit 8410484
Show file tree
Hide file tree
Showing 11 changed files with 375 additions and 447 deletions.
5 changes: 4 additions & 1 deletion src/OpenIddict.Abstractions/OpenIddictResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1480,7 +1480,7 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId
<value>The web authentication broker is only supported on UWP and requires running Windows 10 version 1709 (Fall Creators) or higher.</value>
</data>
<data name="ID0393" xml:space="preserve">
<value>The web authentication result cannot be resolved or contains invalid data.</value>
<value>The platform callback cannot be resolved or contains invalid data.</value>
</data>
<data name="ID0394" xml:space="preserve">
<value>The issuer attached to the static configuration must be the same as the one configured in the validation options.</value>
Expand Down Expand Up @@ -1692,6 +1692,9 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId
<data name="ID0452" xml:space="preserve">
<value>Custom tabs intents are only supported on Android.</value>
</data>
<data name="ID0453" xml:space="preserve">
<value>The specified intent doesn't contain a valid data URI.</value>
</data>
<data name="ID2000" xml:space="preserve">
<value>The security token is missing.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,17 +103,18 @@ public static OpenIddictClientSystemIntegrationBuilder UseSystemIntegration(this

// Register the built-in filters used by the default OpenIddict client system integration event handlers.
builder.Services.TryAddSingleton<RequireASWebAuthenticationSession>();
builder.Services.TryAddSingleton<RequireASWebAuthenticationCallbackUrl>();
builder.Services.TryAddSingleton<RequireAuthenticationNonce>();
builder.Services.TryAddSingleton<RequireCustomTabsIntent>();
builder.Services.TryAddSingleton<RequireCustomTabsIntentData>();
builder.Services.TryAddSingleton<RequireEmbeddedWebServerEnabled>();
builder.Services.TryAddSingleton<RequireHttpListenerContext>();
builder.Services.TryAddSingleton<RequireInteractiveSession>();
builder.Services.TryAddSingleton<RequirePlatformCallback>();
builder.Services.TryAddSingleton<RequireProtocolActivation>();
builder.Services.TryAddSingleton<RequireSystemBrowser>();
builder.Services.TryAddSingleton<RequireWebAuthenticationBroker>();
#pragma warning disable CS0618
builder.Services.TryAddSingleton<RequireWebAuthenticationResult>();
#pragma warning restore CS0618

// Register the built-in event handlers used by the OpenIddict client system integration components.
// Note: the order used here is not important, as the actual order is set in the options.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,34 +17,6 @@ namespace OpenIddict.Client.SystemIntegration;
[EditorBrowsable(EditorBrowsableState.Advanced)]
public static class OpenIddictClientSystemIntegrationHandlerFilters
{
/// <summary>
/// Represents a filter that excludes the associated handlers if no AS web
/// authentication callback URL can be found in the transaction properties.
/// </summary>
public sealed class RequireASWebAuthenticationCallbackUrl : IOpenIddictClientHandlerFilter<BaseContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(BaseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}

#if SUPPORTS_AUTHENTICATION_SERVICES && SUPPORTS_FOUNDATION
if (IsASWebAuthenticationSessionSupported())
{
return new(ContainsASWebAuthenticationSessionResult(context.Transaction));
}

[MethodImpl(MethodImplOptions.NoInlining)]
static bool ContainsASWebAuthenticationSessionResult(OpenIddictClientTransaction transaction)
=> transaction.GetASWebAuthenticationCallbackUrl() is not null;
#endif
return new(false);
}
}

/// <summary>
/// Represents a filter that excludes the associated handlers if
/// the AS web authentication session integration was not enabled.
Expand Down Expand Up @@ -133,33 +105,6 @@ public ValueTask<bool> IsActiveAsync(BaseContext context)
return new(false);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no
/// custom tabs intent data can be found in the transaction properties.
/// </summary>
public sealed class RequireCustomTabsIntentData : IOpenIddictClientHandlerFilter<BaseContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(BaseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}

#if SUPPORTS_ANDROID && SUPPORTS_ANDROIDX_BROWSER
if (IsCustomTabsIntentSupported())
{
return new(ContainsCustomTabsIntentData(context.Transaction));
}

[MethodImpl(MethodImplOptions.NoInlining)]
static bool ContainsCustomTabsIntentData(OpenIddictClientTransaction transaction)
=> transaction.GetCustomTabsIntentData() is not null;
#endif
return new(false);
}
}

/// <summary>
/// Represents a filter that excludes the associated handlers if the embedded web server was not enabled.
Expand Down Expand Up @@ -217,6 +162,24 @@ public ValueTask<bool> IsActiveAsync(BaseContext context)
}
}

/// <summary>
/// Represents a filter that excludes the associated handlers if no
/// platform callback can be found in the transaction properties.
/// </summary>
public sealed class RequirePlatformCallback : IOpenIddictClientHandlerFilter<BaseContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(BaseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}

return new(context.Transaction.GetPlatformCallback() is not null);
}
}

/// <summary>
/// Represents a filter that excludes the associated handlers if no protocol activation was found.
/// </summary>
Expand Down Expand Up @@ -304,6 +267,7 @@ public ValueTask<bool> IsActiveAsync(BaseContext context)
/// Represents a filter that excludes the associated handlers if no
/// web authentication operation was triggered during the transaction.
/// </summary>
[Obsolete("This filter is obsolete and will be removed in a future version.")]
public sealed class RequireWebAuthenticationResult : IOpenIddictClientHandlerFilter<BaseContext>
{
/// <inheritdoc/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,7 @@ public static class Authentication
*/
ExtractGetOrPostHttpListenerRequest<ExtractRedirectionRequestContext>.Descriptor,
ExtractProtocolActivationParameters<ExtractRedirectionRequestContext>.Descriptor,
ExtractASWebAuthenticationCallbackUrlData<ExtractRedirectionRequestContext>.Descriptor,
ExtractCustomTabsIntentData<ExtractRedirectionRequestContext>.Descriptor,
ExtractWebAuthenticationResultData<ExtractRedirectionRequestContext>.Descriptor,
ExtractPlatformCallbackParameters<ExtractRedirectionRequestContext>.Descriptor,

/*
* Redirection response handling:
Expand All @@ -68,9 +66,7 @@ public static class Authentication
AttachCacheControlHeader<ApplyRedirectionResponseContext>.Descriptor,
ProcessEmptyHttpResponse.Descriptor,
ProcessProtocolActivationResponse<ApplyRedirectionResponseContext>.Descriptor,
ProcessASWebAuthenticationSessionResponse<ApplyRedirectionResponseContext>.Descriptor,
ProcessCustomTabsIntentResponse<ApplyRedirectionResponseContext>.Descriptor,
ProcessWebAuthenticationResultResponse<ApplyRedirectionResponseContext>.Descriptor
ProcessPlatformCallbackResponse<ApplyRedirectionResponseContext>.Descriptor
]);

/// <summary>
Expand Down Expand Up @@ -123,7 +119,8 @@ public async ValueTask HandleAsync(ApplyAuthorizationRequestContext context)
throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0446));
}

var source = new TaskCompletionSource<NSUrl>(TaskCreationOptions.RunContinuationsAsynchronously);
var source = new TaskCompletionSource<OpenIddictClientSystemIntegrationPlatformCallback>(
TaskCreationOptions.RunContinuationsAsynchronously);

// OpenIddict represents the complete interactive authentication dance as a two-phase process:
// - The challenge, during which the user is redirected to the authorization server, either
Expand Down Expand Up @@ -161,11 +158,11 @@ public async ValueTask HandleAsync(ApplyAuthorizationRequestContext context)
throw new InvalidOperationException(SR.GetResourceString(SR.ID0448));
}

NSUrl url;
OpenIddictClientSystemIntegrationPlatformCallback callback;

try
{
url = await source.Task.WaitAsync(context.CancellationToken);
callback = await source.Task.WaitAsync(context.CancellationToken);
}

// Since the result of this operation is known by the time the task signaled by ASWebAuthenticationSession
Expand Down Expand Up @@ -195,7 +192,7 @@ public async ValueTask HandleAsync(ApplyAuthorizationRequestContext context)
return;
}

await _service.HandleASWebAuthenticationCallbackUrlAsync(url, context.CancellationToken);
await _service.HandlePlatformCallbackAsync(callback, context.CancellationToken);
context.HandleRequest();
return;

Expand Down Expand Up @@ -250,7 +247,43 @@ void HandleCallback(NSUrl? url, NSError? error)
{
if (url is not null)
{
source.SetResult(url);
var parameters = new Dictionary<string, OpenIddictParameter>(StringComparer.Ordinal);

if (!string.IsNullOrEmpty(url.Query))
{
foreach (var parameter in OpenIddictHelpers.ParseQuery(url.Query))
{
parameters[parameter.Key] = parameter.Value.Count switch
{
0 => default,
1 => parameter.Value[0],
_ => parameter.Value.ToArray()
};
}
}

// Note: the fragment is always processed after the query string to ensure that
// parameters extracted from the fragment are preferred to parameters extracted
// from the query string when they are present in both parts.

if (!string.IsNullOrEmpty(url.Fragment))
{
foreach (var parameter in OpenIddictHelpers.ParseFragment(url.Fragment))
{
parameters[parameter.Key] = parameter.Value.Count switch
{
0 => default,
1 => parameter.Value[0],
_ => parameter.Value.ToArray()
};
}
}

source.SetResult(new OpenIddictClientSystemIntegrationPlatformCallback(url!, parameters)
{
// Attach the raw URL to the callback properties.
Properties = { [typeof(NSUrl).FullName!] = url }
});
}

else if (error is not null)
Expand Down Expand Up @@ -426,8 +459,47 @@ public async ValueTask HandleAsync(ApplyAuthorizationRequestContext context)
parameter => new StringValues((string?[]?) parameter.Value))),
callbackUri: new Uri(context.RedirectUri, UriKind.Absolute)))
{
case { ResponseStatus: WebAuthenticationStatus.Success } result:
await _service.HandleWebAuthenticationResultAsync(result, context.CancellationToken);
case { ResponseStatus: WebAuthenticationStatus.Success } result
when Uri.TryCreate(result.ResponseData, UriKind.Absolute, out Uri? uri):
var parameters = new Dictionary<string, OpenIddictParameter>(StringComparer.Ordinal);

if (!string.IsNullOrEmpty(uri.Query))
{
foreach (var parameter in OpenIddictHelpers.ParseQuery(uri.Query))
{
parameters[parameter.Key] = parameter.Value.Count switch
{
0 => default,
1 => parameter.Value[0],
_ => parameter.Value.ToArray()
};
}
}

// Note: the fragment is always processed after the query string to ensure that
// parameters extracted from the fragment are preferred to parameters extracted
// from the query string when they are present in both parts.

if (!string.IsNullOrEmpty(uri.Fragment))
{
foreach (var parameter in OpenIddictHelpers.ParseFragment(uri.Fragment))
{
parameters[parameter.Key] = parameter.Value.Count switch
{
0 => default,
1 => parameter.Value[0],
_ => parameter.Value.ToArray()
};
}
}

var callback = new OpenIddictClientSystemIntegrationPlatformCallback(uri, parameters)
{
// Attach the authentication result to the properties.
Properties = { [typeof(WebAuthenticationResult).FullName!] = result }
};

await _service.HandlePlatformCallbackAsync(callback, context.CancellationToken);
context.HandleRequest();
return;

Expand Down
Loading

0 comments on commit 8410484

Please sign in to comment.