- ABP Framework version: v7.3.2
- UI Type: Angular
- Database System: EF Core (SQL Server)
- Tiered (for MVC) or Auth Server Separated (for Angular): yes
- Exception message and full stack trace: -NA-
- Steps to reproduce the issue: -NA-
We need to accomplish the following customization while onboarding a new tenant.
Display existing & extra properties of Identity module in create/edit tenant page. We are not sure how to display the existing or extra properties of the Identity module while creating the tenant. Along with Tenant admin's email address, Tenant admin's password, we need to get the Phone Number (available in Identity module) and Country (new extra property added in Identity Module). How to achieve this?
Customize the tenant onboarding flow As per the requirement, the end user will enter the Tenant and Tenant admin details and click on payment button. For payment, the user has to be redirected to stripe payment gateway. Upon successful payment, the tenant has to be created in database. Any idea on how to add this customization as part of onboarding flow?
26 Answer(s)
-
0
Hi
is the tenant onboarding done without login to host user?
to add extraproperties please follow below links
https://docs.abp.io/en/abp/latest/Module-Entity-Extensions https://docs.abp.io/en/abp/latest/Customizing-Application-Modules-Extending-Entities https://docs.abp.io/en/abp/latest/Object-Extensions#addorupdateproperty
-
0
Hi
is the tenant onboarding done without login to host user?
Yes, it will be self-onboarding. How do I remove the Authentication for the APIs that are part of Tenant Management Module & Identity Module so that tenant can be onboarded without Host login.
I referred the link https://docs.abp.io/en/abp/latest/Authorization#changing-permission-definitions-of-a-depended-module . But when the permission is disabled, it prohibits the access to the API.
But in self-onboarding scenario, the API should be accessed anonymously.
-
0
Are you really sure that you want to allow anonymous access to that?
what is the exact flow?
depending on your specific needs, you may want to create your own registration flow. Just a suggestion based on what you wrote:
Upon registration, you may create a form where you ask for the information you need to build a tenant. Create your own appservice for that, where registration alone can be done without the requirement of being logged in. When the user registrates -> you create a tenant with the information you need, based on what the user specified. First create a tenant, then create a new user in that tenant.
After that, you'll end up with a fresh tenant with a user inside. The rest can be done with an authorized user. You would not need to expose too many things.
In addition, you may want to store information about the registration state to allow for clean up scenarios.
In addition to the link you referred to: I think you can only change the permission needed, not disabling it entirely. You could try to add the
[AllowAnonymous]
attribute - but even if it works it would not be best practice to do that. Application Services handle specific needs. Your needs seem to be different from what the default provides. Therefore it's best to create your own implementation of Tenant/ User creation. -
0
We are building a Saas product where an end user (as a tenant) can subscribe on their own. So, when they subscribe, we need to create a tenant and an admin user for them.
If it is as per the default flow, then the host user has to get the details in offline from the end user, login to the portal to create respective tenant on behalf of end user.
Any suggestions here?
Even if we write own appservice, still it has to be anonymous, and the flow will remain the same.
Also, we are not planning to expose all the Tenant APIs but only Create. Can you please share a code sample on how to override the existing permission of dependent module with [AllowAnonymous] attribute?
-
0
Hi
Yes instead of that you can create your own RegisterTenant Page in authserver and register a client with
client_credentials
flowStep 1 : In
OpenIddictDataSeedContributor
register a client wit client_credentials flow make sure you run migrator on a empty db or delete data from the openidApplication table// BookStore_Web_Account var bookStore_Web_AccountClientId = configurationSection["BookStore_Web_Account:ClientId"]; if (!bookStore_Web_AccountClientId.IsNullOrWhiteSpace()) { var bookStore_Web_AccountClientIdRootUrl = configurationSection["BookStore_Web_Account:RootUrl"]?.TrimEnd('/'); await CreateApplicationAsync( name: bookStore_Web_AccountClientId!, type: OpenIddictConstants.ClientTypes.Confidential, consentType: OpenIddictConstants.ConsentTypes.Implicit, displayName: "Swagger Application", secret: configurationSection["BookStore_Web_Account:ClientSecret"] ?? "1q2w3e*", grantTypes: new List<string> {OpenIddictConstants.GrantTypes.ClientCredentials }, scopes: new List<string> { "BookStore" }, clientUri: bookStore_Web_AccountClientIdRootUrl, logoUri: "/images/clients/swagger.svg", permissions: new List<string> { "Saas.Tenants", "Saas.Tenants.Create", "Saas.Tenants.Update", "Saas.Tenants.ManageConnectionStrings", "Saas.Tenants.SetPassword", "Saas.Editions", "Saas.Editions.Create", "Saas.Editions.Update" } ); }
add this in dbmigrator appsetting.json
Step 2 Create your own RegisterTenant.cshtml create UI as it is in Register.cshtml Page in the Volo.Account.Pro module you can download the source code from abp suite.
in the RegisterTenant.cshtml.cs you can use this code make sure to create your own PostInput class just as it is in Register.cshtml.cs with the the extra fields that you need then you can use that to send it to tenant api..
using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Net.Http.Headers; using System.Net.Http; using System.Text; using System.Threading.Tasks; using Volo.Abp; using Volo.Abp.Account; using Volo.Abp.Account.Public.Web.Pages.Account; using Volo.Abp.Account.Public.Web.Security.Recaptcha; using Volo.Abp.Identity.Settings; using Volo.Abp.Settings; using Volo.Abp.Uow; using Volo.Saas.Host; using IdentityUser = Volo.Abp.Identity.IdentityUser; using Microsoft.Extensions.Configuration; using System.Text.Json; namespace Acme.BookStore.Pages.Account { public class RegisterTenantModel : RegisterModel { private readonly IConfiguration _configuration; public RegisterTenantModel(IConfiguration configuration) { _configuration = configuration; } protected override async Task< IdentityUser > RegisterLocalUserAsync() { ValidateModel(); var captchaResponse = string.Empty; if (UseCaptcha) { captchaResponse = HttpContext.Request.Form[RecaptchaValidatorBase.RecaptchaResponseKey]; } string token = await GetClientCredentialsTokenAsync($"{_configuration["AuthServer:Authority"]?.EnsureEndsWith('/')}connect/token" ?? string.Empty, "BookStore_Web_Account", "1q2w3e*", new string[] { "BookStore" }); var tenant = new { name = "demo", adminEmailAddress = "demo@demo.com", adminPassword = "Welcome1$" }; var tenantId = await CreateTenant(token, JsonSerializer.Serialize(tenant)); using (CurrentTenant.Change(tenantId)) { var user = await UserManager.FindByEmailAsync("example@example.com"); if (user == null) { throw new UserFriendlyException("UserNotFound"); } user.SetPhoneNumber("9999999999", false); user.Name = "Demo"; user.Surname = "Demo"; await UserManager.UpdateAsync(user); return user; } } private async Task< Guid? > CreateTenant(string token,string body) { using (HttpClient client = new HttpClient()) { var request = new HttpRequestMessage(HttpMethod.Post, $"{_configuration["RemoteServices:BaseUrl"]?.EnsureEndsWith('/')}api/saas/tenants"); request.Headers.Add("Authorization", $"Bearer {token}"); request.Content = new StringContent(body, null, "application/json"); var response = await client.SendAsync(request); response.EnsureSuccessStatusCode(); var responseBody = await response.Content.ReadAsStringAsync(); var responseDeserilized = JsonSerializer.Deserialize<TenantResponse>(responseBody); return responseDeserilized?.id; } } private async Task< string > GetClientCredentialsTokenAsync(string tokenUrl, string clientId, string clientSecret, IEnumerable<string> scopes = null) { // Create a HttpClient to send the request to the token endpoint using (HttpClient client = new HttpClient()) { // Set the client credentials (client_id and client_secret) in the request headers client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes($"{clientId}:{clientSecret}"))); // Prepare the request content for the token endpoint List<KeyValuePair< string, string > > requestContent = new List<KeyValuePair<string, string>> { new KeyValuePair<string, string>("grant_type", "client_credentials"), }; // Add scopes, if provided if (scopes != null) { requestContent.Add(new KeyValuePair<string, string>("scope", string.Join(" ", scopes))); } // Send the request to the token endpoint HttpResponseMessage response = await client.PostAsync(tokenUrl, new FormUrlEncodedContent(requestContent)); // Check if the request was successful (status code 200) if (response.IsSuccessStatusCode) { // Read the response content (which should contain the access token) string responseContent = await response.Content.ReadAsStringAsync(); // Deserialize the response to extract the access token dynamic responseData = JObject.Parse(responseContent); string accessToken = responseData.access_token; return accessToken; } else { // If the request was not successful, handle the error here // You might want to log or throw an exception throw new HttpRequestException($"Error getting access token. Status code: {response.StatusCode}"); } } } } public class TenantResponse { public Guid id { get; set; } } }
in this way you avoid making anything public or anonymous and only will be doable on the registeration page
-
0
Are you really sure that you want to allow anonymous access to that?
what is the exact flow?
depending on your specific needs, you may want to create your own registration flow. Just a suggestion based on what you wrote:
Upon registration, you may create a form where you ask for the information you need to build a tenant. Create your own appservice for that, where registration alone can be done without the requirement of being logged in. When the user registrates -> you create a tenant with the information you need, based on what the user specified. First create a tenant, then create a new user in that tenant.
After that, you'll end up with a fresh tenant with a user inside. The rest can be done with an authorized user. You would not need to expose too many things.
In addition, you may want to store information about the registration state to allow for clean up scenarios.
In addition to the link you referred to: I think you can only change the permission needed, not disabling it entirely. You could try to add the
[AllowAnonymous]
attribute - but even if it works it would not be best practice to do that. Application Services handle specific needs. Your needs seem to be different from what the default provides. Therefore it's best to create your own implementation of Tenant/ User creation.jfistelmann i will look into this. thanks
-
0
Yes, we will have Captcha as well.
We are using Angular as front end. Do you have any code samples in Angular?
-
0
Hi
to enable captcha
you follow this
https://support.abp.io/QA/Questions/489/How-to-enable-reCaptcha-in-ABP?_ga=2.177433699.75017784.1696827709-327044615.1686624402
i am confused why are you asking for angular code snippets, can you please explain?
read more about account module
https://docs.abp.io/en/commercial/latest/modules/account
All these you can override in AuthServer Project which is in mvc the previous registeration code that i share you have to put that in MVC authserver project.
read more about https://docs.abp.io/en/commercial/latest/modules/payment
if you want to do the registration in angular. you just have to do the convert the code inside
RegisterLocalUserAsync()
to angular which are basically httpcalls, please refer to angular documentation about how to make http requests https://angular.io/api/common/http/HttpClient -
0
Hi Anjali,
Getting bad request when trying to get the token in GetClientCredentialsTokenAsync with scope. Here my request content
Without scope, token created successfully but since it doesn't have necessary scope, it fails with 401 when trying to create tenant. But we need scope and also CreateApplicationAsync checks for scope and throws error if not found.
-
0
-
0
Yes, it got seeded successfully in database as well.
Here is the code
Seed Code
//Auth Server Client var authClientId = configurationSection["Application_Auth:ClientId"]; if (!authClientId.IsNullOrWhiteSpace()) { var authClientIdRootUrl = configurationSection["Application_Auth:RootUrl"]?.TrimEnd('/'); await CreateApplicationAsync( name: authClientId!, type: OpenIddictConstants.ClientTypes.Confidential, consentType: OpenIddictConstants.ConsentTypes.Implicit, displayName: "Auth Application", secret: configurationSection["Application_Auth:ClientSecret"] ?? "1q2w3e*", grantTypes: new List<string> { OpenIddictConstants.GrantTypes.ClientCredentials }, scopes: new List<string> { "BookStore" }, clientUri: authClientIdRootUrl, logoUri: "/images/clients/swagger.svg", permissions: new List<string> { "Saas.Tenants", "Saas.Tenants.Create", "Saas.Tenants.Update", "Saas.Tenants.ManageConnectionStrings", "Saas.Tenants.SetPassword", "Saas.Editions", "Saas.Editions.Create", "Saas.Editions.Update" } ); }
appsettings.json
"Application_Auth": { "ClientId": "Application_Auth", "ClientSecret": "1q2w3e*", "RootUrl": "https://localhost:44370" },
RegisterTeanantModel.cs
public class TempTenant { public Guid id { get; set; } } public class RegisterTenantModel : RegisterModel { private readonly IConfiguration _configuration; public RegisterTenantModel(IConfiguration configuration) { _configuration = configuration; } protected override async Task<IdentityUser> RegisterLocalUserAsync() { //ValidateModel(); //var captchaResponse = string.Empty; //if (UseCaptcha) //{ // captchaResponse = HttpContext.Request.Form[RecaptchaValidatorBase.RecaptchaResponseKey]; //} string token = await GetClientCredentialsTokenAsync($"{_configuration["AuthServer:Authority"]?.EnsureEndsWith('/')}connect/token" ?? string.Empty, "HealthySmiles_Auth", "1q2w3e*", new string[] { "BookStore" }); var tenant = new { name = "demo", adminEmailAddress = "demo@demo.com", adminPassword = "Welcome1$" }; var tenantId = await CreateTenant(token, JsonSerializer.Serialize(tenant)); using (CurrentTenant.Change(tenantId)) { var user = await UserManager.FindByEmailAsync("example@example.com"); if (user == null) { throw new UserFriendlyException("UserNotFound"); } user.SetPhoneNumber("9999999999", false); user.Name = "Demo"; user.Surname = "Demo"; await UserManager.UpdateAsync(user); return await UserManager.GetByIdAsync(user.Id); } } private async Task<Guid?> CreateTenant(string token, string body) { using (HttpClient client = new HttpClient()) { var apiUrl = ""+"/api/saas/tenants"; //var request = new HttpRequestMessage(HttpMethod.Post, $"{_configuration["RemoteServices:BaseUrl"]?.EnsureEndsWith('/')}api/saas/tenants"); var request = new HttpRequestMessage(HttpMethod.Post, "https://localhost:44396/api/saas/tenants"); request.Headers.Add("Authorization", $"Bearer {token}"); request.Content = new StringContent(body, null, "application/json"); var response = await client.SendAsync(request); response.EnsureSuccessStatusCode(); var responseBody = await response.Content.ReadAsStringAsync(); var responseDeserilized = JsonSerializer.Deserialize<TempTenant>(responseBody); return responseDeserilized?.id; } } private async Task<string> GetClientCredentialsTokenAsync(string tokenUrl, string clientId, string clientSecret, IEnumerable scopes = null) { // Create a HttpClient to send the request to the token endpoint using (HttpClient client = new HttpClient()) { // Set the client credentials (client_id and client_secret) in the request headers client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes($"{clientId}:{clientSecret}"))); // Prepare the request content for the token endpoint var requestContent = new List<KeyValuePair<string, string>>() { new KeyValuePair<string,string>("grant_type", "client_credentials") }; // Add scopes, if provided if (scopes != null) { requestContent.Add(new KeyValuePair<string, string>("scope", string.Join(" ", scopes))); } // Send the request to the token endpoint HttpResponseMessage response = await client.PostAsync(tokenUrl, new FormUrlEncodedContent(requestContent)); // Check if the request was successful (status code 200) if (response.IsSuccessStatusCode) { // Read the response content (which should contain the access token) string responseContent = await response.Content.ReadAsStringAsync(); // Deserialize the response to extract the access token dynamic responseData = JObject.Parse(responseContent); string accessToken = responseData.access_token; return accessToken; } else { // If the request was not successful, handle the error here // You might want to log or throw an exception throw new HttpRequestException($"Error getting access token. Status code: {response.StatusCode}"); } } } } public class TenantResponse { public Guid id { get; set; } }
-
0
-
0
This is the scope I see in my table, and I have provided the same in the code as well.
Though I am able to create token, it fails with 401 error code when trying to create tenant using the API api/saas/tenants
I tried both "HealthySmiles" as well as commonScopes yet 401 unauthorized error.
Is there anything to add or remove in the permission list?
This is turning out to be a showstopper for the release :(
-
0
-
0
As mentioned in the earlier post, I mentioned the scope as HealthySmiles and passing the scope as HealthySmiles too
string token = await GetClientCredentialsTokenAsync($"{_configuration["AuthServer:Authority"]?.EnsureEndsWith('/')}connect/token" ?? string.Empty, "HealthySmiles_Auth", "1q2w3e*", new List<string> { "HealthySmiles" });
DB records
-
0
Hi
can you decode your access_token on https://jwt.io/ ? or please share the acess_token
-
0
Here is the token
eyJhbGciOiJSUzI1NiIsImtpZCI6IjkwM0E5OUVFRjI2NTJERDMyM0EwOUM2Q0NEMjM1ODU2MzVGOEUzQzAiLCJ4NXQiOiJrRHFaN3ZKbExkTWpvSnhzelNOWVZqWDQ0OEEiLCJ0eXAiOiJhdCtqd3QifQ.eyJvaV9wcnN0IjoiSGVhbHRoeVNtaWxlc19BdXRoIiwiY2xpZW50X2lkIjoiSGVhbHRoeVNtaWxlc19BdXRoIiwib2lfdGtuX2lkIjoiODc2MzlkYWEtYWQ1Zi0yNGUxLTliYTItM2EwZTM1OWYyNWJlIiwianRpIjoiYTkwNThkMDctMzFmMS00NzIwLWI1M2ItODcyNDEyODE0NzJmIiwiZXhwIjoxNjk3MTEwMzc1LCJpc3MiOiJodHRwczovL2xvY2FsaG9zdDo0NDM3MC8iLCJpYXQiOjE2OTcxMDY3NzV9.R6Fb44yErOlKj-FE_7CX-GrsmmDtv3BZgXCkWjeq02Mkyr3MEkB33eHydA6iEYblDPeqvHxmjQD3dgzJisTS9YTUe52qF8GwzE-PYIeUas37ejEdvA8JIs5VwMtxe4q_FOa2X9gffQihYtWdXd8I2doZVO-iYhp3l7VUHcTpQh_zcqP1bNonqv4ES5noHEizHb3ZPPTIFByAOoq-Eiu0fvXaZ_lOwNbiqHQNlShsPLk5ViEDVmkKGOrdbtwaLlyHh4H5LSKvUfq2oCfQilMoKeg0xg-_Ar-wDF_rH4topL-wgrJPGN2bVVXOffO66OM6lWT4vERHqgUEXBILUTaIpg
Upon further analysing the 401 issue I found inner exception as
-
0
-
0
-
0
-
0
-
0
Hi
Is it possible to share your solution on support@abp.io with the ticket id or share a google drive link? I'll have a look what is it missing
-
0
HealthySmilesAuthServerModule
https://localhost:44370/.well-known/openid-configuration { "issuer": "https://localhost:44370/", "authorization_endpoint": "https://localhost:44370/connect/authorize", "token_endpoint": "https://localhost:44370/connect/token", "introspection_endpoint": "https://localhost:44370/connect/introspect", "end_session_endpoint": "https://localhost:44370/connect/logout", "revocation_endpoint": "https://localhost:44370/connect/revocat", "userinfo_endpoint": "https://localhost:44370/connect/userinfo", "device_authorization_endpoint": "https://localhost:44370/device", "jwks_uri": "https://localhost:44370/.well-known/jwks", "grant_types_supported": [ "authorization_code", "implicit", "password", "client_credentials", "refresh_token", "urn:ietf:params:oauth:grant-type:device_code", "LinkLogin", "Impersonation" ], "response_types_supported": [ "code", "code id_token", "code id_token token", "code token", "id_token", "id_token token", "token", "none" ], "response_modes_supported": [ "form_post", "fragment", "query" ], "scopes_supported": [ "openid", "offline_access", "email", "profile", "phone", "roles", "address", "HealthySmiles" ], "claims_supported": [ "aud", "exp", "iat", "iss", "sub" ], "id_token_signing_alg_values_supported": [ "RS256" ], "code_challenge_methods_supported": [ "S256" ], "subject_types_supported": [ "public" ], "token_endpoint_auth_methods_supported": [ "client_secret_basic", "client_secret_post" ], "introspection_endpoint_auth_methods_supported": [ "client_secret_basic", "client_secret_post" ], "revocation_endpoint_auth_methods_supported": [ "client_secret_basic", "client_secret_post" ], "device_authorization_endpoint_auth_methods_supported": [ "client_secret_basic", "client_secret_post" ], "claims_parameter_supported": false, "request_parameter_supported": false, "request_uri_parameter_supported": false, "authorization_response_iss_parameter_supported": true }
-
0
Hi
Is it possible to share your solution on support@abp.io with the ticket id or share a google drive link? I'll have a look what is it missing
Hi can share your project I'll have a look.
-
0
Upon analysing further, by changing the below line in the method GetClientCredentialsTokenAsync
HttpResponseMessage response = await client.PostAsync(tokenUrl, new FormUrlEncodedContent(requestContent));
to
var response = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest { Address = tokenUrl, ClientId = clientId, ClientSecret = clientSecret, Scope = "HealthySmiles", }
has helped to resolve the Bad Request issue and was able to create the tenant successfully.
Will let you know if there are any issues during actual implementation