- ABP Framework version: v7.3.3
- UI Type: Angular
- Database System: EF Core (SQL Server)
- Tiered (for MVC) or Auth Server Separated (for Angular): Auth Server Separated
I am trying to implement a system where the user is sent a OTP to their email address that they can use to login without needing their password. This article doesn't quite follow our use case. Our solution has:
- Main ABP Backend (not relevant)
- Main ABP Front end (not relevant)
- Custom public site back end (not relevant)
- Custom public site front end
- Standard ABP Separated Auth Server (this serves both the main ABP front end and the custom public site front end
This is what I've done so far, based on the article mentioned above:
- Followed steps 1-3
- Implemented my own endpoint that sends a OTP to the user's email address as an alternative login flow, taking inspiration from step 4, and manually creating an angular proxy to call this endpoint.
- Added a page which the email has a link to that calls another endpoint that's from step 7, passing in the token and email address to attempt login.
It all works up until my stage 3, including validating the OTP token and updating the user's security stamp, but the SignInManager.SignInAsync(user, isPersistent: false)
call doesn't log the user into our public site (where this endpoint is being called from) according to angular AuthService.IsAuthenticated
. I've also tried using other authenticationMethod
s, such as OidcConstants.AuthenticationMethods.OneTimePassword
, but without success.
What the SignInAsync method does do is provide a Set-Cookie
for .AspNetCore.Identity.Application
.
Any tips on how to progress? Cheers.
My AppService (which is wrapped in a Controller with HttpPost
and Route("login")
attributes):
using System;
using System.Threading.Tasks;
using IdentityModel;
using MyCompany.MyProject.Email;
using Microsoft.AspNetCore.Authorization;
using OpenIddict.Abstractions;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Entities;
using Volo.Abp.Identity;
using Volo.Abp.Identity.AspNetCore;
namespace MyCompany.MyProject.PasswordlessLogin;
public class PasswordlessLoginAppService : ApplicationService, IPasswordlessLoginAppService
{
private readonly IMyProjectAuthServerEmailManager _emailManager;
private readonly IdentityUserManager _userManager;
private readonly AbpSignInManager _signInManager;
public PasswordlessLoginAppService(IMyProjectAuthServerEmailManager emailManager,
IdentityUserManager userManager, AbpSignInManager signInManager)
{
_emailManager = emailManager;
_userManager = userManager;
_signInManager = signInManager;
}
// [AllowAnonymous]
public async Task SendOtpEmail(SendOtpEmailInputDto input)
{
var user = await _userManager.FindByEmailAsync(input.Email);
if (user is null)
{
throw new EntityNotFoundException(typeof(IdentityUser));
}
var token = await _userManager.GenerateUserTokenAsync(user, tokenProvider: "PasswordlessLoginProvider",
purpose: "passwordless-auth");
await _emailManager.SendOtpEmailAsync(new SendOtpEmailInput()
{
Email = user.Email,
Token = token,
});
}
public async Task Login(PasswordlessLoginInputDto input)
{
var user = await _userManager.FindByEmailAsync(input.Email);
if (user is null)
{
throw new EntityNotFoundException(typeof(IdentityUser));
}
var isValid = await _userManager.VerifyUserTokenAsync(user, "PasswordlessLoginProvider", "passwordless-auth", input.Token);
if (!isValid)
{
throw new UnauthorizedAccessException("The token " + input.Token + " is not valid for the user " + input.Email);
}
await _userManager.UpdateSecurityStampAsync(user);
await _signInManager.SignInAsync(user, isPersistent: false, authenticationMethod: OidcConstants.AuthenticationMethods.OneTimePassword);
}
}
5 Answer(s)
-
0
Hello
Can you please check this https://abp.io/support/questions/6240/Angular-Passwordless it will helps you.
Thank you.
-
0
Thanks. Let me try and get back to you. Please leave this thread open til at least next weekend.
-
0
Sure
-
0
I've had some success. I've been able to write a
PasswordlessExtensionGrant : ITokenExtensionGrant
that takesemail
andtoken
request params, verifies the token against the user and calls:var principal = await _signInManager.CreateUserPrincipalAsync(user); return new SignInResult(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, principal);
This actually returns an access token when I call from the front end:
let result = await this.authService.loginUsingGrant('passwordless-auth2', {token: token, email: email},);
{ "access_token": "...", "token_type": "Bearer", "expires_in": 299 }
The problem I'm left with is how I do use the
access_token
in the response to actually login the user in the angular app? When using a password login, it saves theaccess_token
in the local storage (perhaps amongst other things). I haven't been able to see where in abp code it does that, to be able to mimic something similar.My second concern is that I don't have a
refresh_token
, so it won't be able to periodically gain newaccess_token
s. -
0
After further investigation, immediately after the
loginUsingGrant
,this.authService.isAuthenticated
returns true and theaccess_token
that was returned is added to the local storage, alongsideaccess_token_stored_at
andexpires_at
. I can navigate to the user's dashboard (which has an auth guard on it), and in there it also findsthis.authService.isAuthenticated
returns true. But if I make a query to the public backend, the backend throwsUnauthorized
, and if I manually refresh the page,this.authService.isAuthenticated
is false and theaccess_token
is suddenly removed from local storage.I can see the
Authorization
header is set toBearer XXX
from theaccess_token
.Any tips?
using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using OpenIddict.Abstractions; using OpenIddict.Server.AspNetCore; using Volo.Abp.Domain.Entities; using Volo.Abp.Identity; using Volo.Abp.Identity.AspNetCore; using Volo.Abp.OpenIddict.ExtensionGrantTypes; using IdentityUser = Volo.Abp.Identity.IdentityUser; using SignInResult = Microsoft.AspNetCore.Mvc.SignInResult; namespace MyCompany.MyProject.PasswordlessLogin; /// <summary> /// Inspired by https://abp.io/community/articles/how-to-add-a-custom-grant-type-in-openiddict.-6v0df94z /// </summary> public class PasswordlessExtensionGrant : ITokenExtensionGrant { public const string ExtensionGrantName = "PasswordlessAuth"; private readonly IdentityUserManager _userManager; private readonly AbpSignInManager _signInManager; public PasswordlessExtensionGrant(IdentityUserManager userManager, AbpSignInManager signInManager) { _userManager = userManager; _signInManager = signInManager; } public async Task<IActionResult> HandleAsync(ExtensionGrantContext context) { var email = context.Request.GetParameter("email")?.ToString(); var token = context.Request.GetParameter("token")?.ToString(); var user = await _userManager.FindByEmailAsync(email); if (user is null) { throw new EntityNotFoundException(typeof(IdentityUser)); } var isValid = await _userManager.VerifyUserTokenAsync(user, tokenProvider: "PasswordlessLoginProvider", purpose: "passwordless-auth", token); if (!isValid) { throw new UnauthorizedAccessException("The token " + token + " is not valid for the user " + email); } await _userManager.UpdateSecurityStampAsync(user); var principal = await _signInManager.CreateUserPrincipalAsync(user); principal.SetScopes(principal.GetScopes()); principal.SetResources(await GetResourcesAsync(context, principal.GetScopes())); return new SignInResult(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, principal, new OpenIdConnectChallengeProperties() { AllowRefresh = true, // doesn't seem to return a refresh token IsPersistent = true, }); } public string Name => ExtensionGrantName; private static 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; } }