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!";
}
}
2
Answers
What follows is how to test with https, if you want an answer for http see dreamboatDevs answer.
OidcClient does use HttpClient and hence it is possible to use the approach suggested in the Microsoft docs.
If you inspect the code for
OidcClientOptions
there is an HttpClientFactory property that looks like thistherefore you can change your code for registering the OidcClient to this
Note the #if DEBUG because this code is only needed in development. When httpClientFactory is null the OidcClient will just new up a normal HttpClient.
The code for
HttpsClientHandlerService
comes straight from the Microsoft docs and is thisAs you can see when development is done on localhost in debug mode the certificate is automatically trusted as required.
I would create an additional wrapper in the form of new classes that will configure your service inside. The certificate problem(http or https) is solved using the Policy configuration:
Client mobile example in detail:
Configure services
Using the service
I also recommend using secrets.json to store settings(URI and etc). There is a video on YouTube on how to connect them to the Maui project. The video is called:
".Net MAUI & Xamarin Forms getting settings from secrets.json or appsettings.json"
And most importantly, it will be easier for you to implement try-catch blocks in a wrapper
If you will inject the service directly into the page constructor, do not forget to specify dependencies for it too
settings.json
Add to manifest(Android) if use http protocol