I need to build a .NET 7 MAUI app which authenticates on a .NET 7 ASP.NET Core app running Duende IdentityServer (version 6.2.3). I'm starting with a proof of concept app but I'm having trouble testing it when I run IdentityServer on localhost.
My code is based on an example app for doing this which is found here https://github.com/DuendeSoftware/Samples/tree/main/various/clients/Maui/MauiApp2. And the IdentityServer code is pretty much an out of the box IdentityServer with a standard ui done with ASP.NET Core razor pages code.
I've tried testing using an android emulator that calls the IDP using a url generated by ngrok but I get the following error:
System.InvalidOperationException: 'Error loading discovery document: Endpoint is on a different host than authority: https://localhost:5001/.well-known/openid-configuration/jwks'
I.e. my authority is something like https://4cec-81-134-5-170.ngrok.io but all the urls on the discovery document still use the localhost urls and so don't match.
I've tried testing on an android emulator and using the authority https://10.0.2.2 but this fails with the following:
System.InvalidOperationException: 'Error loading discovery document: Error connecting to https://10.0.2.2/.well-known/openid-configuration. java.security.cert.CertPathValidatorException: Trust anchor for certification path not found..'
Since I'm only testing in development here I set up the local IDP to work with http (not https) and tested with http://10.0.2.2 but this failed with the following:
System.InvalidOperationException: 'Error loading discovery document: Error connecting to http://10.0.2.2/.well-known/openid-configuration. HTTPS required.'
I'd like to know if there is a way I can get my code to work via testing through localhost (using an emulator for the mobile app or a device). When I say I work I mean that when _client.LoginAsync()
is called on the main page the 3 errors mentioned above don't happen and you see the success message. I think this can be achieved either through a solution to the ngrok problem or getting Android to trust the ASP.NET Core localhost certificate or something else. I found this https://learn.microsoft.com/en-us/dotnet/maui/data-cloud/local-web-services?view=net-maui-7.0#bypass-the-certificate-security-check. This explains how you can bypass the certificate security check when you are connecting to localhost by passing a custom HttpMessageHandler to the httpclient. Can something similar be done when using the OidcClient?
Source code for OidcClient found here
I also found the solutions here https://github.com/dotnet/maui/discussions/8131 but I can't make any of the 4 options work for me. Either they don't enable localhost testing or they don't work.
Below are the key parts of my code:
IDP code
I add identity server in my Program.cs code like this
builder.Services.AddIdentityServer(options =>
{
options.EmitStaticAudienceClaim = true;
})
.AddInMemoryIdentityResources(Config.IdentityResources)
.AddInMemoryApiScopes(Config.ApiScopes)
.AddInMemoryClients(Config.Clients)
.AddTestUsers(TestUsers.Users);
Here is the Config class that is being referenced
using Duende.IdentityServer;
using Duende.IdentityServer.Models;
namespace MyApp.IDP;
public static class Config
{
public static IEnumerable<IdentityResource> IdentityResources =>
new IdentityResource[]
{
new IdentityResources.OpenId(),
new IdentityResources.Profile()
};
public static IEnumerable<ApiScope> ApiScopes =>
new ApiScope[]
{ };
public static IEnumerable<Client> Clients =>
new Client[]
{
new Client()
{
ClientName = My App Mobile",
ClientId = "myappmobile.client",
AllowedGrantTypes = GrantTypes.Code,
RedirectUris = {
"myapp://callback"
},
PostLogoutRedirectUris = {
"myapp://callback"
},
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile
}
}
};
}
Client mobile code
I register my OidcClient like this
var options = new OidcClientOptions
{
Authority = "https://10.0.2.2",
ClientId = "myappmobile.client",
RedirectUri = "myapp://callback",
Browser = new MauiAuthenticationBrowser()
};
builder.Services.AddSingleton(new OidcClient(options));
The code for MauiAuthenticationBrowser is this
using IdentityModel.Client;
using IdentityModel.OidcClient.Browser;
namespace MyFirstAuth;
public class MauiAuthenticationBrowser : IdentityModel.OidcClient.Browser.IBrowser
{
public async Task<BrowserResult> InvokeAsync(BrowserOptions options, CancellationToken cancellationToken = default)
{
try
{
var result = await WebAuthenticator.Default.AuthenticateAsync(
new Uri(options.StartUrl),
new Uri(options.EndUrl));
var url = new RequestUrl("myapp://callback")
.Create(new Parameters(result.Properties));
return new BrowserResult
{
Response = url,
ResultType = BrowserResultType.Success
};
}
catch (TaskCanceledException)
{
return new BrowserResult
{
ResultType = BrowserResultType.UserCancel
};
}
}
}
The app is just a page with a login button on it. Here is the code behind for this page
using IdentityModel.OidcClient;
namespace MyFirstAuth;
public partial class MainPage
{
private readonly OidcClient _client;
public MainPage(OidcClient client)
{
InitializeComponent();
_client = client;
}
private async void OnLoginClicked(object sender, EventArgs e)
{
var result = await _client.LoginAsync();
if (result.IsError)
{
editor.Text = result.Error;
return;
}
editor.Text = "Success!";
}
}