Skip to content

Commit

Permalink
Add Headway.App.Blazor.Maui #69
Browse files Browse the repository at this point in the history
Add Headway.App.Blazor.Maui #69
  • Loading branch information
grantcolley committed Nov 5, 2022
1 parent 5dbced9 commit 753c9bc
Show file tree
Hide file tree
Showing 60 changed files with 2,153 additions and 0 deletions.
26 changes: 26 additions & 0 deletions src/Headway.App.Blazor.Maui/App.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Headway.App.Blazor.Maui"
x:Class="Headway.App.Blazor.Maui.App">
<Application.Resources>
<ResourceDictionary>

<Color x:Key="PageBackgroundColor">#512bdf</Color>
<Color x:Key="PrimaryTextColor">White</Color>

<Style TargetType="Label">
<Setter Property="TextColor" Value="{DynamicResource PrimaryTextColor}" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
</Style>

<Style TargetType="Button">
<Setter Property="TextColor" Value="{DynamicResource PrimaryTextColor}" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="BackgroundColor" Value="#2b0b98" />
<Setter Property="Padding" Value="14,10" />
</Style>

</ResourceDictionary>
</Application.Resources>
</Application>
14 changes: 14 additions & 0 deletions src/Headway.App.Blazor.Maui/App.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Microsoft.Maui.Controls;

namespace Headway.App.Blazor.Maui
{
public partial class App : Application
{
public App()
{
InitializeComponent();

MainPage = new MainPage();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
using Headway.Core.Model;
using IdentityModel.Client;
using IdentityModel.OidcClient;
using IdentityModel.OidcClient.Browser;
using Microsoft.AspNetCore.Components.Authorization;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;

namespace Headway.App.Blazor.Maui.Authentication
{
public class CustomAuthenticationStateProvider : AuthenticationStateProvider
{
private readonly OidcClient oidcClient;
private readonly TokenProvider tokenProvider;
private readonly CustomAuthenticationStateProviderOptions options;

private ClaimsPrincipal currentUser = new ClaimsPrincipal(new ClaimsIdentity());

public CustomAuthenticationStateProvider(CustomAuthenticationStateProviderOptions options, TokenProvider tokenProvider)
{
oidcClient = new OidcClient(new OidcClientOptions
{
Authority = $"https://{options.Authority}",
ClientId = options.ClientId,
Scope = options.Scope,
RedirectUri = options.RedirectUri,
PostLogoutRedirectUri = options.PostLogoutRedirectUris,
Browser = options.Browser
});

this.options = options;
this.tokenProvider = tokenProvider;
}

public override Task<AuthenticationState> GetAuthenticationStateAsync() =>
Task.FromResult(new AuthenticationState(currentUser));

public IdentityModel.OidcClient.Browser.IBrowser Browser
{
get
{
return oidcClient.Options.Browser;
}
set
{
oidcClient.Options.Browser = value;
}
}

public async Task LogInAsync()
{
var loginRequest = new LoginRequest { FrontChannelExtraParameters = new Parameters(options.AdditionalProviderParameters) };
var loginResult = await oidcClient.LoginAsync(loginRequest);
tokenProvider.RefreshToken = loginResult.RefreshToken;
tokenProvider.AccessToken = loginResult.AccessToken;
tokenProvider.IdToken = loginResult.IdentityToken;
currentUser = loginResult.User;

if (currentUser.Identity.IsAuthenticated)
{
var identity = (ClaimsIdentity)currentUser.Identity;

if (identity.RoleClaimType != options.RoleClaim)
{
var roleClaims = identity.FindAll(options.RoleClaim).ToArray();

if (roleClaims != null && roleClaims.Any())
{
foreach (var roleClaim in roleClaims)
{
identity.RemoveClaim(roleClaim);
}

foreach (var roleClaim in roleClaims)
{
identity.AddClaim(new Claim(identity.RoleClaimType, roleClaim.Value));
}
}
}
}

NotifyAuthenticationStateChanged(
Task.FromResult(new AuthenticationState(currentUser)));
}

public async Task LogoutAsync()
{
var logoutParameters = new Dictionary<string, string>
{
{"client_id", oidcClient.Options.ClientId },
{"returnTo", oidcClient.Options.RedirectUri }
};

var logoutRequest = new LogoutRequest();
var endSessionUrl = new RequestUrl($"{oidcClient.Options.Authority}/v2/logout")
.Create(new Parameters(logoutParameters));
var browserOptions = new BrowserOptions(endSessionUrl, oidcClient.Options.RedirectUri)
{
Timeout = TimeSpan.FromSeconds(logoutRequest.BrowserTimeout),
DisplayMode = logoutRequest.BrowserDisplayMode
};

await oidcClient.Options.Browser.InvokeAsync(browserOptions);

currentUser = new ClaimsPrincipal(new ClaimsIdentity());

NotifyAuthenticationStateChanged(
Task.FromResult(new AuthenticationState(currentUser)));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System.Collections.Generic;

namespace Headway.App.Blazor.Maui.Authentication
{
public class CustomAuthenticationStateProviderOptions
{
public CustomAuthenticationStateProviderOptions()
{
Browser = new WebBrowserAuthenticator();
AdditionalProviderParameters = new Dictionary<string, string>();
}

public string Authority { get; set; }

public string ClientId { get; set; }

public string RedirectUri { get; set; }

public string PostLogoutRedirectUris { get; set; }

public string Scope { get; set; }

public string RoleClaim { get; set; }

public Dictionary<string, string> AdditionalProviderParameters { get; set; }

public IdentityModel.OidcClient.Browser.IBrowser Browser { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using IdentityModel.Client;
using IdentityModel.OidcClient.Browser;
using Microsoft.Maui.Authentication;
using System;
using System.Threading;
using System.Threading.Tasks;

namespace Headway.App.Blazor.Maui.Authentication
{
public class WebBrowserAuthenticator : IdentityModel.OidcClient.Browser.IBrowser
{
public async Task<BrowserResult> InvokeAsync(BrowserOptions options, CancellationToken cancellationToken = default)
{
try
{
WebAuthenticatorResult result = await WebAuthenticator.Default.AuthenticateAsync(
new Uri(options.StartUrl),
new Uri(options.EndUrl));

var url = new RequestUrl(options.EndUrl)
.Create(new Parameters(result.Properties));

return new BrowserResult
{
Response = url,
ResultType = BrowserResultType.Success
};
}
catch (TaskCanceledException)
{
return new BrowserResult
{
ResultType = BrowserResultType.UserCancel,
ErrorDescription = "Login canceled by the user."
};
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Net.Http;
using System.Net.Security;

namespace Headway.App.Blazor.Maui.DevHttp
{
public static class DevHttpClientHelperExtensions
{
/// <summary>
/// Adds the <see cref="IHttpClientFactory"/> and related services to the <see cref="IServiceCollection"/> and configures
/// a named <see cref="HttpClient"/> to use localhost or 10.0.2.2 and bypass certificate checking on Android.
/// </summary>
/// <param name="name">name</param>
/// <param name="sslPort">Development server port</param>
/// <returns>The IServiceCollection</returns>
/// <remarks>
/// <para>
/// https://github.com/dotnet/maui/discussions/8131
/// </para>
/// <para>
/// https://gist.github.com/Eilon/49e3c5216abfa3eba81e453d45cba2d4
/// by https://gist.github.com/Eilon
/// </para>
/// <para>
/// https://gist.github.com/EdCharbeneau/ed3d44d8298319c201f276de7a0580f1
/// by https://gist.github.com/EdCharbeneau
/// </para>
/// </remarks>
public static IServiceCollection AddDevHttpClient(this IServiceCollection services, string name, int sslPort)
{
var devServerRootUrl = new UriBuilder("https", DevServerName, sslPort).Uri.ToString();

#if WINDOWS
services.AddHttpClient(name, client =>
{
client.BaseAddress = new UriBuilder("https", DevServerName, sslPort).Uri;
});

return services;
#endif

#if ANDROID
services.AddHttpClient(name, client =>
{
client.BaseAddress = new UriBuilder("https", DevServerName, sslPort).Uri;
})
.ConfigurePrimaryHttpMessageHandler(() =>
{
var handler = new CustomAndroidMessageHandler();
handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) =>
{
if (cert != null && cert.Issuer.Equals("CN=localhost"))
return true;
return errors == SslPolicyErrors.None;
};
return handler;
});

return services;

#else
throw new PlatformNotSupportedException("Only Windows and Android currently supported.");
#endif
}

public static string DevServerName =>
#if WINDOWS
"localhost";
#elif ANDROID
"10.0.2.2";
#else
throw new PlatformNotSupportedException("Only Windows and Android currently supported.");
#endif

#if ANDROID
internal sealed class CustomAndroidMessageHandler : Xamarin.Android.Net.AndroidMessageHandler
{
protected override Javax.Net.Ssl.IHostnameVerifier GetSSLHostnameVerifier(Javax.Net.Ssl.HttpsURLConnection connection)
=> new CustomHostnameVerifier();

private sealed class CustomHostnameVerifier : Java.Lang.Object, Javax.Net.Ssl.IHostnameVerifier
{
public bool Verify(string hostname, Javax.Net.Ssl.ISSLSession session)
{
return
Javax.Net.Ssl.HttpsURLConnection.DefaultHostnameVerifier.Verify(hostname, session)
|| hostname == "10.0.2.2" && session.PeerPrincipal?.Name == "CN=localhost";
}
}
}
#endif
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Microsoft.Extensions.DependencyInjection;
using System.Collections.Generic;
using System.Reflection;

namespace Headway.App.Blazor.Maui.Extensions
{
public static class ServiceCollectionExtensions
{
/// <summary>
/// A collection of additional assemblies that should be eager loaded at startup so they can be
/// searched for classes with Headway attributes such as [DynamicModel] etc.
/// </summary>
/// <param name="services">The services collection.</param>
/// <param name="assemblies">A collection of assemblies to be eager loaded at startup.</param>
/// <returns>The services collection.</returns>
public static IServiceCollection UseAdditionalAssemblies(this IServiceCollection services, IEnumerable<Assembly> assemblies)
{
// Intentionally returns services without actually doing anything.
return services;
}
}
}
Loading

0 comments on commit 753c9bc

Please sign in to comment.