diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json index 20e4793e56..d1890e1786 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json @@ -306,10 +306,10 @@ "PaymentTimeCannotBeFutureTime": "Payment time cannot be a future time!", "SaveAndDownload": "Save and Download", "BillingInfo": "Billing Info", - "DeleteInvoice" : "Delete Invoice", - "PaymentStateSetTo" : "Payment state set to {0}", + "DeleteInvoice": "Delete Invoice", + "PaymentStateSetTo": "Payment state set to {0}", "ChangeState": "Change State", - "Permission:TrialLicense" : "Trial License", + "Permission:TrialLicense": "Trial License", "Menu:TrialLicenses": "Trial Licenses", "TrialLicenses": "Trial Licenses", "UserNameFilter": "Username", @@ -359,18 +359,38 @@ "PurchaseDate": "Purchase date", "IsAbpBookDownloaded": "ABP book downloaded", "IsMasteringAbpBookDownloadEnabled": "ABP Book download enabled", - "Permission:Accounting:CustomPaymentLinkGenerator": "Generate Custom Payment Link", + "Permission:Accounting:CustomPaymentLinkGenerator": "Custom Payment Link", "CustomPaymentLink": "Custom Payment Link", "Menu:CustomPaymentLink": "Custom Payment Link", "Amount": "Amount", "GenerateCustomPaymentLink": "Generate Custom Payment Link", "GeneratedPaymentLink": "Generated Payment Link", "CopyText": "Copy Text", + "Permission:CommunityEvents": "Events", + "Menu:Events": "Events", + "Events": "Events", + "EventType": "Event Type", + "Number": "Number", + "RegistrationURL": "Registration URL", + "URL": "URL", + "EventDeletionConfirmationMessage": "Are you sure you want to delete this event?", + "Enum:EventType:0": "Community Talks", + "CreateAnEvent": "Create an event", + "Permission:CommunitySpeakers": "Speakers", + "CreateASpeaker": "Create a speaker", + "Speakers": "Speakers", + "Image": "Image", + "GithubURL": "Github URL", + "SpeakerDeletionConfirmationMessage": "Are you sure you want to delete this speaker?", + "Menu:Speakers": "Speakers", + "ChooseSpeakerImage": "Choose a speaker image...", + "SpeakerImage": "Speaker image", "ShowPurchaseItemsOfOrganizations": "Purchase Items", "Enum:OrganizationPurchaseState:0": "Not delivered", "Enum:OrganizationPurchaseState:1": "Delivered", "PurchaseItems": "Purchase Items", "SuccessfullyUpdated": "Successfully updated", - "SuccessfullyAdded": "Successfully added" + "SuccessfullyAdded": "Successfully added", + "PurchaseState": "Purchase State" } -} +} \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json index 8b84c3512e..3bb6320874 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json @@ -98,6 +98,11 @@ "EULA": "EULA", "ABPCommercialIntroductionMessage": "Pre-built application modules, advanced startup templates, rapid application development tooling, professional UI themes and premium support.", "MasteringAbpFrameworkEBook": "Mastering ABP Framework", - "MasteringTheABPFrameworkExplanation": "Written by the creator of the ABP Framework, this book will help you gain a complete understanding of the framework and modern web application development techniques." + "MasteringTheABPFrameworkExplanation": "Written by the creator of the ABP Framework, this book will help you gain a complete understanding of the framework and modern web application development techniques.", + "Speakers": "Speakers", + "PreviousEvents": "Previous Events", + "WatchTheEvent": "Watch the Event", + "RegisterNow": "Register Now", + "ThereIsNoEvent": "There is no event." } } diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json index 1919f23d0a..e653a51483 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json @@ -162,6 +162,9 @@ "Previous": "Previous", "Next": "Next", "Share": "Share", - "SortBy": "Sort by" + "SortBy": "Sort by", + "NoPublishedEventsYet": "No published events yet.", + "SubscribeYoutubeChannel": "Subscribe to the Youtube Channel", + "Enum:EventType:0": "Talks" } } 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..0d86c98a1d --- /dev/null +++ b/docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/README.md @@ -0,0 +1,601 @@ +# 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 + + + + + +