How to authenticate a MAUI app using oauth2? #6595
Replies: 3 comments 8 replies
-
I'm using Keycloak too, someone can help us ? |
Beta Was this translation helpful? Give feedback.
-
Keycloak is just Auth-As-A-Service. If you're just dealing with 3rd Party OAuth providers, and especially if you ultimately need to have your own secured APIs you probably want to look at something that's built right into your API. How you deal with OAuth varies a bit between solutions. For instance the MSAL library for Azure AD B2C would have you expose the Client Id configuration in your app. Better solutions IMO move this requirement to the API where all of your Client Ids/Secrets are maintained on the server. You might want to look at the Mobile Auth library I published recently to help this scenario. The NuGet package is specific to the API side, but there is a sample API & Mobile app in the repo so you can deep dive & see how they work together. The flow looks something like this...
|
Beta Was this translation helpful? Give feedback.
-
Since this issue still gets replies and views after all this time I would like to share the progress I made after asking this question. However I would like to disclaim that after encountering all the problems with adopting MAUI my team and I gave up and we moved to flutter for mobile development and have been very happy there. The code below was targeting .NET 6 MAUI. Okay so based on the limited information I could find at the time I created a webAuthenticatorBrowser that would handle the authorization code flow with in our case Keycloak. using System.Diagnostics;
using IdentityModel.OidcClient.Browser;
namespace App.Auth;
internal class WebAuthenticatorBrowser : IdentityModel.OidcClient.Browser.IBrowser
{
public async Task<BrowserResult> InvokeAsync(BrowserOptions options, CancellationToken cancellationToken = default)
{
try
{
var authResult =
await WebAuthenticator.AuthenticateAsync(new Uri(options.StartUrl), new Uri(options.EndUrl));
var authorizeResponse = ToRawIdentityUrl(options.EndUrl, authResult);
return new BrowserResult
{
Response = authorizeResponse
};
}
catch (Exception ex)
{
Debug.WriteLine(ex);
return new BrowserResult()
{
ResultType = BrowserResultType.UnknownError,
Error = ex.ToString()
};
}
}
public string ToRawIdentityUrl(string redirectUrl, WebAuthenticatorResult result)
{
var parameters = result.Properties.Select(pair => $"{pair.Key}={pair.Value}");
var values = string.Join("&", parameters);
return $"{redirectUrl}#{values}";
}
} I then created a IServiceCollection extension method to configure authentication in our app. using App.Auth;
using IdentityModel.OidcClient;
namespace App.Configuration;
public static class AuthConfiguration
{
public static MauiAppBuilder ConfigureAuth(this MauiAppBuilder builder)
{
builder.Services.AddTransient<WebAuthenticatorBrowser>();
builder.Services.AddTransient(sp =>
new OidcClient(new OidcClientOptions
{
Authority = "https://keycloak.mydomain.com/auth/realms/myrealm",
ClientId = "my-app",
RedirectUri = "myapp://oauth/redirect",
Scope = "openid profile email offline_access",
ClientSecret = "18e444d2-7d85-4862-bd96-4f43df09bc6f", // This is not really neccesarry with client apps because they are unsecure anyway.
Browser = sp.GetRequiredService<WebAuthenticatorBrowser>(),
})
);
builder.Services.AddSingleton<AccessTokenHttpMessageHandler>();
builder.Services.AddTransient(sp =>
new HttpClient(sp.GetRequiredService<AccessTokenHttpMessageHandler>())
{
BaseAddress = new Uri("https://https://mydomain.com/api")
});
return builder;
}
} I also needed an AccessTokenHttpMessageHandler for the HTTP Client using IdentityModel.Client;
using IdentityModel.OidcClient;
namespace App.Auth;
public static class AuthConsts
{
internal const string AccessTokenKeyName = "__access_token";
internal const string RefreshTokenKeyName = "__refresh_token";
}
public class AccessTokenHttpMessageHandler : DelegatingHandler
{
protected OidcClient OidcClient { get; }
public AccessTokenHttpMessageHandler(OidcClient oidcClient) : base(new HttpClientHandler())
{
OidcClient = oidcClient;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (Preferences.Default.ContainsKey(AuthConsts.AccessTokenKeyName))
{
var currentTokenValue = Preferences.Default.Get(AuthConsts.AccessTokenKeyName, "");
request.SetBearerToken(currentTokenValue);
request.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
}
var response = await base.SendAsync(request, cancellationToken);
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
if (Preferences.Default.ContainsKey(AuthConsts.RefreshTokenKeyName))
{
var refreshTokenValue = Preferences.Default.Get(AuthConsts.RefreshTokenKeyName, "");
var refreshResult = await OidcClient.RefreshTokenAsync(refreshTokenValue?.ToString());
Preferences.Default.Set(AuthConsts.AccessTokenKeyName, refreshResult.AccessToken);
Preferences.Default.Set(AuthConsts.RefreshTokenKeyName, refreshResult.RefreshToken);
request.SetBearerToken(refreshResult.AccessToken);
return await base.SendAsync(request, cancellationToken);
}
else
{
var result = await OidcClient.LoginAsync(new LoginRequest());
request.SetBearerToken(result.AccessToken);
Preferences.Default.Set(AuthConsts.AccessTokenKeyName, result.AccessToken);
Preferences.Default.Set(AuthConsts.RefreshTokenKeyName, result.RefreshToken);
request.SetBearerToken(result.AccessToken);
return await base.SendAsync(request, cancellationToken);
}
}
return response;
}
} Next I needed to handle the platform integration. I only ever got Android working since I was unfamiliar with IOS at the time. In the Android project Directory I added a using Android.App;
using Android.Content.PM;
namespace App.Platforms.Android
{
[Activity(NoHistory = true, LaunchMode = LaunchMode.SingleTop, Exported = true)]
[IntentFilter(new[] { "android.intent.action.VIEW" }, Categories = new[] { "android.intent.category.DEFAULT", "android.intent.category.BROWSABLE" }, DataScheme = SCHEME)]
public class WebAuthenticationCallbackActivity : WebAuthenticatorCallbackActivity
{
public const string SCHEME = "myapp";
}
} Here are the nuget packages we used <ItemGroup>
<PackageReference Include="CommunityToolkit.Maui" Version="1.2.0" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.0.0" />
<PackageReference Include="IdentityModel" Version="6.0.0" />
<PackageReference Include="IdentityModel.OidcClient" Version="5.0.0" />
</ItemGroup> This allowed us to authenticate to keycloak using an authorization code flow. Other functionality like logging out or even persisting the auth token where never implemented because we moved away from MAUI, but hopefully this gives someone a starting point. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
I need to make an app that authenticates using oauth2. The IDP we use is keycloak.
I tried finding resources on the subject but i can't find anything. There is a Xamarin expert day video describing using the identity libraries. But this requires detailed config in both the IOS and Android projects. and i don't know if this is still relevant.
My question what is the best practice to setup oauth login using code or password flow? Ideally it only needs to be configured in the single project.
Beta Was this translation helpful? Give feedback.
All reactions