From ac3453a13f44257eb3ca377e364ac19554bfb624 Mon Sep 17 00:00:00 2001 From: enisn Date: Tue, 22 Feb 2022 08:26:27 +0300 Subject: [PATCH] Add "Integrating MAUI Client via using OpenID Connect" Community Article --- .../README.md | 600 ++++++++++++++++++ .../art/android-login-demo.gif | Bin 0 -> 1311083 bytes .../art/compiler-select.png | Bin 0 -> 39492 bytes .../art/identity-users-request-result.png | Bin 0 -> 115598 bytes .../art/ios-login-demo.gif | Bin 0 -> 520566 bytes .../art/macos-login-demo.gif | Bin 0 -> 661738 bytes .../art/openid-configuration.png | Bin 0 -> 41790 bytes 7 files changed, 600 insertions(+) create mode 100644 docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/README.md create mode 100644 docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/android-login-demo.gif create mode 100644 docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/compiler-select.png create mode 100644 docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/identity-users-request-result.png create mode 100644 docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/ios-login-demo.gif create mode 100644 docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/macos-login-demo.gif create mode 100644 docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/openid-configuration.png diff --git a/docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/README.md b/docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/README.md new file mode 100644 index 0000000000..c702820bb2 --- /dev/null +++ b/docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/README.md @@ -0,0 +1,600 @@ +# Integrating MAUI Client via using OpenID Connect + This is a demonstration for connecting ABP backend from MAUI app via using openid connect. + + In this flow, a web browser will be opened when user tries to log in and user will perform login operation in the browser. Then IdentityServer will redirect user to application with login credentials (state, token etc.) will be handled by application. + +> This is by intent. The code flow does not allow the user to log in using a native view in the app. The reason being that this flow ensures that the username and password are never seen by the client (except the browser, which is part of the OS system - aka we trust it). You could enable using a native login view with the Resource Owner Password Credentials (ROPC) flow. But this is also an attack vector. Suppose someone makes a fraud duplicate of your application and tricking users into entering their credentials. The fraudulent app could store those credentials in-between. You just got to enjoy those tin-foil-hat moments when doing security. In other words, using the code flow does not give an attacker that opportunity and therefore is the recommended option for mobile clients. +> +> - [@Mark Allibone](https://mallibone.com/post/xamarin-oidc) + +By the way, my motivation for building this sample is presenting just another way for authentication. **Resource Owner Password Credentials** authentication is already provided and it's more common way to do. This is yet another way to authenticate users. + +## Source Code +You can also find source code on GitHub in ABP-Samples. +- [abpframework/abp-samples/MAUI-OpenId](https://github.com/abpframework/abp-samples/tree/master/MAUI-OpenId) + +## Creating projects +- Create an ABP project without UI + +```bash +abp new Acme.BookStore -t app --no-ui -d mongodb --no-random-ports +``` + +- Create a maui application + +```bash +mkdir maui +cd maui +dotnet new maui -n Acme.BookStore.MauiClient +``` + +There is a long way for configuring scopes and callback urls for both server and clients. We'll use [WebAuthenticator](https://docs.microsoft.com/en-us/xamarin/essentials/web-authenticator?tabs=android) to perform this operation. + +## Configuring IdentityServer + +- Go to DbMigrator folder and MAUI client in **appsettings.json**. Add following client code in **IdentityServer:Clients** path: + +```json + "BookStore_Maui": { + "ClientId": "BookStore_Maui", + "ClientSecret": "1q2w3e*", + "RootUrl": "bookstore://" + } +``` + +- Go to **IdentityServerDataSeedContributor** in Domain project under IdentityServer folder. Append following code section into **CreateClientsAsync()** method. + +```csharp +// Maui Client +var mauiClientId = configurationSection["BookStore_Maui:ClientId"]; +if (!mauiClientId.IsNullOrWhiteSpace()) +{ + var mauiRootUrl = configurationSection["BookStore_Maui:RootUrl"]; + + await CreateClientAsync( + name: mauiClientId, + scopes: commonScopes, + grantTypes: new[] { "authorization_code" }, + secret: configurationSection["BookStore_Maui:ClientSecret"]?.Sha256(), + requireClientSecret: false, + redirectUri: $"{mauiRootUrl}" + ); +} +``` + +- Run DbMigrator + +- Then run HttpApi.Host + +### Configuring NGROK +Client will check configuration from `/.well-known/openid-configuration` path and it must be a secured connection between client & server. I prefer to use ngrok to open my backend app to entire web. + +- Go to [getting started](https://dashboard.ngrok.com/get-started/setup) page of ngrok _(login or register first)_ and download the ngrok tool. + +- Don't forget to login from tool: + + ```bash + ngrok authtoken XXX + ``` + + _A sample command is being displayed at dashboard where you download ngrok from_ + +- Open your HttpApi.Host with ngrok + + ```bash + .\ngrok.exe http https://localhost:44350 + ``` + +- You'll see a generated xxx.ngrok.io url. Navigate to `/.well-known/openid-configuration` to check if it's working right. + + You should see something like that: + ![](art/openid-configuration.png) + Issuer must be your URL, not localhost! If you see still localhost, try to disable host header rewrite. + + +- Also, ValidIssuers must be defined to validate tokens. + + - Add **ValidIssuers** section to your `appsettings.json` of HttpApi.Host + + ```js + "AuthServer": { + "Authority": "https://localhost:44350", + "RequireHttpsMetadata": "false", + "SwaggerClientId": "BookStore_Swagger", + "SwaggerClientSecret": "1q2w3e*", + "ValidIssuers": [ + "https://46fd-45-156-29-175.ngrok.io" + ] + }, + ``` + + - Then define it in **ConfigureAuthentication** method in Module class + + ```csharp + private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration) + { + context.Services.AddAuthentication() + .AddJwtBearer(options => + { + // ... + options.TokenValidationParameters.ValidIssuers = configuration.GetSection("AuthServer:ValidIssuers").Get(); + }); + } + ``` + +We're done with backend. Let's continue with MAUI app. + + +## Developing MAUI App + +Before we go, there is something to do like configuring dependency injection to get rid of unnecessary huge class coupling. + +### Configuring Dependency Injection + +- Go to **MauiApplication** class and add `MainPage` in services. + + ```csharp + public static MauiApp CreateMauiApp() + { + var builder = MauiApp.CreateBuilder(); + builder + .UseMauiApp() + .ConfigureFonts(fonts => + { + fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); + }); + + builder.Services.AddTransient(); + + return builder.Build(); + } + ``` + +- And inject MainPage from constructor in **App.xaml.cs** + + ```csharp + public App(MainPage mainPage) + { + InitializeComponent(); + + MainPage = mainPage; + } + ``` + +Now MainPage is ready for injecting dependencies to it. + + +### Configuring OIDC + +- Add `IdentityModel.OidcClient` package to project + + ```xml + + + + ``` + +- Create **WebAuthenticatorBrowser** + + ```csharp + internal class WebAuthenticatorBrowser : IBrowser + { + public async Task InvokeAsync(BrowserOptions options, CancellationToken cancellationToken = default) + { + try + { + WebAuthenticatorResult 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) + { + IEnumerable parameters = result.Properties.Select(pair => $"{pair.Key}={pair.Value}"); + var values = string.Join("&", parameters); + + return $"{redirectUrl}#{values}"; + } + } + ``` + +- Configure **OidcClient** in **MauiProgram** + + ```csharp + builder.Services.AddTransient(); + + builder.Services.AddTransient(sp => + new OidcClient(new OidcClientOptions + { + // Use your own ngrok url: + Authority = "https://46fd-45-156-29-175.ngrok.io", + ClientId = "BookStore_Maui", + RedirectUri = "bookstore://", + Scope = "openid email profile role BookStore", + ClientSecret = "1q2w3E*", + Browser = sp.GetRequiredService(), + }) + ); + ``` + +- Go to **MainPage.xaml**, remove everyting and add a button for login + + ```xml + + + + + +