- ABP Framework version: v8.2.1
- UI Type: Angular / MVC / Blazor Server
- Database System: EF Core (MySQL)
- Tiered (for MVC) or Auth Server Separated (for Angular): Auth server separated angular
- Exception message and full stack trace: NA
- Steps to reproduce the issue: NA
We have use case where we want to configure authentication scheme based on the user type. if a user is not a backend user/company user but a consumer than we want to configure mobile OTP based authentication and for company/tenant backend user , we want to use user name password authentication. Please suggest how to achieve this.
39 Answer(s)
-
0
Hello ,
Can you please check the similar issue https://abp.io/support/questions/8010/OTP-based-login-Implementation-Passwordless-login-implementation-guider
Thank you.
-
0
your ticket is reopened
-
0
Thanks for re-opening the ticket. We are trying to implement OTP based authentication for mobile app. Our users may not necessary have email ids. In the AbpUsers table emailid is not null by design. We would like to make the Emailid column nullable. In any system design there should be flexibility for authentication and by making the emailid not null we are loosing this flexibility. please advise on the same
-
0
Any update on this?
-
0
Hello ,
Can you please check the similar issue https://abp.io/support/questions/8010/OTP-based-login-Implementation-Passwordless-login-implementation-guider
Thank you.
We want to implement this for react native abp mobile app. After implementing this, how do we redirect user back to mobile?. SHall we return the token generated or claims or both?
-
0
hi
We would like to make the Emailid column nullable.
This may break many functions of the system. You can set the email field to a regular value, eg
username@yourdomain.com
-
0
hi
. if a user is not a backend user/company user but a consumer than we want to configure mobile OTP based authentication and for company/tenant backend user , we want to use user name password authentication. Please suggest how to achieve this.
You can add a new page to let the user enter the username, then check the user type and redirect it to your OTP or password page.
You can also do this function on one page. This is based on yours.
-
0
hi
. if a user is not a backend user/company user but a consumer than we want to configure mobile OTP based authentication and for company/tenant backend user , we want to use user name password authentication. Please suggest how to achieve this.
You can add a new page to let the user enter the username, then check the user type and redirect it to your OTP or password page.
You can also do this function on one page. This is based on yours.
We have created new page in mobile react native app to get the mobile number as user name and verify the OTP. Now we have implemented the password-less-login using https://abp.io/support/questions/8010/OTP-based-login-Implementation-Passwordless-login-implementation-guider if OTP is verified successfully. The confusion here is how to handle or set the permissions from this post request at the mobile side. Do we need to return the token from the post request and use it every time or we need to simply call application-configuration method with userid/mobilenumber from the mobile app?
-
0
hi
The confusion here is how to handle or set the permissions from this post request at the mobile side. Do we need to return the token from the post request and use it every time or we need to simply call application-configuration method with userid/mobilenumber from the mobile app?
This is based on your authentication scheme.
Cookies or JWT token?
You should attach the cookie and jwt token in HTTP request headers.
-
0
hi
The confusion here is how to handle or set the permissions from this post request at the mobile side. Do we need to return the token from the post request and use it every time or we need to simply call application-configuration method with userid/mobilenumber from the mobile app?
This is based on your authentication scheme.
Cookies or JWT token?
You should attach the cookie and jwt token in HTTP request headers.
https://abp.io/community/articles/implementing-passwordless-authentication-with-asp.net-core-identity-c25l8koj This article creates token based on user object with below code : var token = await UserManager.GenerateUserTokenAsync(adminUser, "PasswordlessLoginProvider", "passwordless-auth"); and it does not look like a JWT token. We are getting the reponse headers with cookie like this :
shall we use application configuration call with this cookie to get permission? or please share us the documentation to proceed further? Can you review the article https://abp.io/community/articles/implementing-passwordless-authentication-with-asp.net-core-identity-c25l8koj and confirm if we can use it for OTP authentication as is.
-
0
hi
You are using the
Cookies
authentication now.So you have to carry the
Cookies
when you call the API.The backend will get user info and permission grants info from cookies.
-
0
hi
You are using the
Cookies
authentication now.So you have to carry the
Cookies
when you call the API.The backend will get user info and permission grants info from cookies.
In the same flow can you help us to change the password-less authentication to use JWT bearer token. How to generate the JWT token in the above flow as its more secure. Also as we are implementing this in abp react mobile app cross site cookie issue may come and not work.
-
0
-
0
hi
For token authentication, you can use OpenIddict as oauth2 server.
The built-in template projects have integrated the OpenIddict.
https://abp.io/docs/latest/modules/openiddict
Example: use username and password to get a access_token from openiddict
The whole idea behind the ticket is to implement passwordless authentication. We got some idea behind this from the article https://abp.io/community/articles/implementing-passwordless-authentication-with-asp.net-core-identity-c25l8koj but as its using cookies authentication the existing flow of react native is breaking. in order to continue it we would like to use JWT token authentication with passwordless. so please suggest how to integrate with current middleware.
-
0
hi
You can create a custom grant type to get a token.
Pass the
passwordless token
to theconnect/token
endpoint to get the token.https://abp.io/community/articles/how-to-add-a-custom-grant-type-in-openiddict.-6v0df94z
-
0
Hi, I went through the implementation you have provided. Here is what I have done.
I have two APIs, the first one is a send OTP API, which sends the OTP to the mobile number and the other API validates that OTP and on successful validation of the API it let's the user signs in. We are creating a new user using the Mobile Number as the username of the user and mobile_number@example.com as the Email of the user. If the user is already registered, then we just fetch the user details, otherwise we create the user and let it log in into the application.
Here's the controller :
[HttpPost] [AllowAnonymous] [Route("passwordless-authentication")] public async Task<VerifyOTPAndSignInResponseDto> VerifyOTPAndSignIn(VerifyOTPAndSignInInput input) { using (CurrentTenant.Change(input.TenantId)) { using (var unitOfWork = UnitOfWorkManager.Begin()) { try { VerifyOTPResponseDto otpVerification = await VerifyOTP( new VerifyOTPInput() { CountryCode = input.CountryCode, MobileNumber = input.MobileNumber, OTP = input.OTP, }); if (otpVerification.type == "success") { var existingUser = await UserManager.FindByNameAsync(input.CountryCode + input.MobileNumber); var user = new Volo.Abp.Identity.IdentityUser(GuidGenerator.Create(), input.CountryCode + input.MobileNumber, input.CountryCode + input.MobileNumber + "@example.com", null); user.SetPhoneNumber(input.CountryCode + "-" + input.MobileNumber, true); if (existingUser == null) { (await UserManager.CreateAsync(user)).CheckErrors(); using (CurrentTenant.Change(null)) { (await UserManager.AddToRoleAsync(user, "Patient")).CheckErrors(); } await UsersAppService.PasswordlessUserRegistration(new PasswordlessUserRegistration(input.CountryCode, input.MobileNumber, user.Id)); } else { user = existingUser; } var token = await UserManager.GenerateUserTokenAsync(user, "PasswordlessLoginProvider", "passwordless-auth"); var isValid = await UserManager.VerifyUserTokenAsync(user, "PasswordlessLoginProvider", "passwordless-auth", token); if (!isValid) { throw new UnauthorizedAccessException("The token " + token + " is not valid for the user " + user.Id); } await UserManager.UpdateSecurityStampAsync(user); await unitOfWork.CompleteAsync(); await SignInManager.SignInAsync(user, isPersistent: true); return new VerifyOTPAndSignInResponseDto() { message = otpVerification.message, type = otpVerification.type, token = token }; } else { return new VerifyOTPAndSignInResponseDto() { message = otpVerification.message, type = otpVerification.type }; } } catch (Exception e) { await unitOfWork.RollbackAsync(); unitOfWork.Dispose(); throw; } } } }
Here the UserManager is our UserManager, that we have overridden.
Also, this i the file I have added as well,
using Microsoft.AspNetCore.Identity; using System.Threading.Tasks;
namespace G1.health.AuthServer.PasswordlessAuthentication;
public class PasswordlessLoginProvider<TUser> : TotpSecurityStampBasedTokenProvider<TUser> where TUser : class { public override Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<TUser> manager, TUser user) { return Task.FromResult(false); }
public async override Task<string> GetUserModifierAsync(string purpose, UserManager<TUser> manager, TUser user) { var userId = await manager.GetUserIdAsync(user); return "PasswordlessLogin:" + purpose + ":" + userId; }
}
And this one as well,
using Microsoft.AspNetCore.Identity;
namespace G1.health.AuthServer.PasswordlessAuthentication;
public static class IdentityBuilderExtensions { public static IdentityBuilder AddPasswordlessLoginProvider(this IdentityBuilder builder) { var userType = builder.UserType; var totpProvider = typeof(PasswordlessLoginProvider<>).MakeGenericType(userType); return builder.AddTokenProvider("PasswordlessLoginProvider", totpProvider); } }
As mentioned in this article.
https://abp.io/community/articles/implementing-passwordless-authentication-with-asp.net-core-identity-c25l8koj
Now, after configuring all these things, I added whatever you asked in this implementation : https://abp.io/community/articles/how-to-add-a-custom-grant-type-in-openiddict.-6v0df94z
public const string ExtensionGrantName = "PasswordlessAuthentication";
This is the ExtensionGrantName name that I have given and the same Grant Name I have added in the OpenIddictApplications table as well for the Angular client.
After that, using postman, I am trying to validate the user and let it log in, but it says, invalid token.
The token that I am passing that I am deriving from here in my Validating OTP controller :
var token = await UserManager.GenerateUserTokenAsync(user, "PasswordlessLoginProvider", "passwordless-auth"); var isValid = await UserManager.VerifyUserTokenAsync(user, "PasswordlessLoginProvider", "passwordless-auth", token);
Can you tell me if anything I need to configure or if I am missing something.
-
0
Also, the Token that I am getting is not like a JWT Token, it's like a 6 digit number, so I am not sure how it's working in the background.
-
0
-
0
Sure, here it is.
namespace G1.health.AuthServer.PasswordlessAuthentication; public class EmpowermTokenExtensionGrant : ITokenExtensionGrant { public const string ExtensionGrantName = "PasswordlessLoginProvider"; public string Name => ExtensionGrantName; public async Task<IActionResult> HandleAsync(ExtensionGrantContext context) { var userToken = context.Request.GetParameter("token").ToString(); if (string.IsNullOrEmpty(userToken)) { return new ForbidResult( new[] { OpenIddictServerAspNetCoreDefaults.AuthenticationScheme }, properties: new AuthenticationProperties(new Dictionary<string, string> { [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidRequest }!)); } var transaction = await context.HttpContext.RequestServices.GetRequiredService<IOpenIddictServerFactory>().CreateTransactionAsync(); transaction.EndpointType = OpenIddictServerEndpointType.Introspection; transaction.Request = new OpenIddictRequest { ClientId = context.Request.ClientId, ClientSecret = context.Request.ClientSecret, Token = userToken }; var notification = new OpenIddictServerEvents.ProcessAuthenticationContext(transaction); var dispatcher = context.HttpContext.RequestServices.GetRequiredService<IOpenIddictServerDispatcher>(); await dispatcher.DispatchAsync(notification); if (notification.IsRejected) { return new ForbidResult( new[] { OpenIddictServerAspNetCoreDefaults.AuthenticationScheme }, properties: new AuthenticationProperties(new Dictionary<string, string> { [OpenIddictServerAspNetCoreConstants.Properties.Error] = notification.Error ?? OpenIddictConstants.Errors.InvalidRequest, [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = notification.ErrorDescription, [OpenIddictServerAspNetCoreConstants.Properties.ErrorUri] = notification.ErrorUri })); } var principal = notification.GenericTokenPrincipal; if (principal == null) { return new ForbidResult( new[] { OpenIddictServerAspNetCoreDefaults.AuthenticationScheme }, properties: new AuthenticationProperties(new Dictionary<string, string> { [OpenIddictServerAspNetCoreConstants.Properties.Error] = notification.Error ?? OpenIddictConstants.Errors.InvalidRequest, [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = notification.ErrorDescription, [OpenIddictServerAspNetCoreConstants.Properties.ErrorUri] = notification.ErrorUri })); } var userId = principal.FindUserId(); var userManager = context.HttpContext.RequestServices.GetRequiredService<EmpowermIdentityUserManager>(); var user = await userManager.GetByIdAsync(userId.Value); var userClaimsPrincipalFactory = context.HttpContext.RequestServices.GetRequiredService<IUserClaimsPrincipalFactory<Volo.Abp.Identity.IdentityUser>>(); var claimsPrincipal = await userClaimsPrincipalFactory.CreateAsync(user); claimsPrincipal.SetScopes(principal.GetScopes()); claimsPrincipal.SetResources(await GetResourcesAsync(context, principal.GetScopes())); await context.HttpContext.RequestServices.GetRequiredService<AbpOpenIddictClaimsPrincipalManager>().HandleAsync(context.Request, claimsPrincipal); return new Microsoft.AspNetCore.Mvc.SignInResult(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, claimsPrincipal); } private async Task<IEnumerable<string>> GetResourcesAsync(ExtensionGrantContext context, ImmutableArray<string> scopes) { var resources = new List<string>(); if (!scopes.Any()) { return resources; } await foreach (var resource in context.HttpContext.RequestServices.GetRequiredService<IOpenIddictScopeManager>().ListResourcesAsync(scopes)) { resources.Add(resource); } return resources; } }
I have updated the ExtensionGrantName value to "PasswordlessLoginProvider", since I am generating and validating the token with the same token provider name (I am not sure if it's necessary), but to avoid any confusion I have done that.
-
0
hi
These code are not compatible with your case.
You should use
UserManager.VerifyUserTokenAsync(user, "PasswordlessLoginProvider", "passwordless-auth", token)
to check the code.If the code is correct, you can generate the access_token for the user.
And there is no user in your token request. You can consider passing a
userid
in the request.var transaction = await context.HttpContext.RequestServices.GetRequiredService<IOpenIddictServerFactory>().CreateTransactionAsync(); transaction.EndpointType = OpenIddictServerEndpointType.Introspection; transaction.Request = new OpenIddictRequest { ClientId = context.Request.ClientId, ClientSecret = context.Request.ClientSecret, Token = userToken }; var notification = new OpenIddictServerEvents.ProcessAuthenticationContext(transaction); var dispatcher = context.HttpContext.RequestServices.GetRequiredService<IOpenIddictServerDispatcher>(); await dispatcher.DispatchAsync(notification); if (notification.IsRejected) { return new ForbidResult( new[] { OpenIddictServerAspNetCoreDefaults.AuthenticationScheme }, properties: new AuthenticationProperties(new Dictionary<string, string> { [OpenIddictServerAspNetCoreConstants.Properties.Error] = notification.Error ?? OpenIddictConstants.Errors.InvalidRequest, [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = notification.ErrorDescription, [OpenIddictServerAspNetCoreConstants.Properties.ErrorUri] = notification.ErrorUri })); } var principal = notification.GenericTokenPrincipal; if (principal == null) { return new ForbidResult( new[] { OpenIddictServerAspNetCoreDefaults.AuthenticationScheme }, properties: new AuthenticationProperties(new Dictionary<string, string> { [OpenIddictServerAspNetCoreConstants.Properties.Error] = notification.Error ?? OpenIddictConstants.Errors.InvalidRequest, [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = notification.ErrorDescription, [OpenIddictServerAspNetCoreConstants.Properties.ErrorUri] = notification.ErrorUri })); }
-
0
How do I generate the access_token if the code is valid?
-
0
hi
Something like this:
return new Microsoft.AspNetCore.Mvc.SignInResult(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, claimsPrincipal);
will generate a access token.namespace G1.health.AuthServer.PasswordlessAuthentication; public class EmpowermTokenExtensionGrant : ITokenExtensionGrant { public const string ExtensionGrantName = "PasswordlessLoginProvider"; public string Name => ExtensionGrantName; public async Task<IActionResult> HandleAsync(ExtensionGrantContext context) { var userToken = context.Request.GetParameter("token").ToString(); if (string.IsNullOrEmpty(userToken)) { return new ForbidResult( new[] { OpenIddictServerAspNetCoreDefaults.AuthenticationScheme }, properties: new AuthenticationProperties(new Dictionary<string, string> { [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidRequest }!)); } var userId = context.Request.GetParameter("userid").ToString(); if (string.IsNullOrEmpty(userId)) { return new ForbidResult( new[] { OpenIddictServerAspNetCoreDefaults.AuthenticationScheme }, properties: new AuthenticationProperties(new Dictionary<string, string> { [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidRequest }!)); } var userManager = context.HttpContext.RequestServices.GetRequiredService<EmpowermIdentityUserManager>(); var user = await userManager.GetByIdAsync(userId); if(!await UserManager.VerifyUserTokenAsync(user, "PasswordlessLoginProvider", "passwordless-auth", token)) { return new ForbidResult( new[] { OpenIddictServerAspNetCoreDefaults.AuthenticationScheme }, properties: new AuthenticationProperties(new Dictionary<string, string> { [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidRequest }!)); } var userClaimsPrincipalFactory = context.HttpContext.RequestServices.GetRequiredService<IUserClaimsPrincipalFactory<Volo.Abp.Identity.IdentityUser>>(); var claimsPrincipal = await userClaimsPrincipalFactory.CreateAsync(user); claimsPrincipal.SetScopes(principal.GetScopes()); claimsPrincipal.SetResources(await GetResourcesAsync(context, principal.GetScopes())); await context.HttpContext.RequestServices.GetRequiredService<AbpOpenIddictClaimsPrincipalManager>().HandleAsync(context.Request, claimsPrincipal); return new Microsoft.AspNetCore.Mvc.SignInResult(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, claimsPrincipal); } private async Task<IEnumerable<string>> GetResourcesAsync(ExtensionGrantContext context, ImmutableArray<string> scopes) { var resources = new List<string>(); if (!scopes.Any()) { return resources; } await foreach (var resource in context.HttpContext.RequestServices.GetRequiredService<IOpenIddictScopeManager>().ListResourcesAsync(scopes)) { resources.Add(resource); } return resources; } }
-
0
As you mentioned, I removed the validation of user from my controller where token is getting generated, and now I am validating the token in my grant extension handler method, but the problem is, in the controller, the token gets validated, but the grant extension method it doesn't validate the token.
-
0
-
0