Thanks. That is a very manual way of doing it, but perhaps the best way. We have since decided to pivot on our approach to one that we prefer that does not require OTP so I haven't confirmed whether this worked for us.
Morning. Any suggestions on this please?
After further investigation, immediately after the loginUsingGrant
, this.authService.isAuthenticated
returns true and the access_token
that was returned is added to the local storage, alongside access_token_stored_at
and expires_at
.
I can navigate to the user's dashboard (which has an auth guard on it), and in there it also finds this.authService.isAuthenticated
returns true. But if I make a query to the public backend, the backend throws Unauthorized
, and if I manually refresh the page, this.authService.isAuthenticated
is false and the access_token
is suddenly removed from local storage.
I can see the Authorization
header is set to Bearer XXX
from the access_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;
}
}
I've had some success. I've been able to write a PasswordlessExtensionGrant : ITokenExtensionGrant
that takes email
and token
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 the access_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 new access_token
s.
Thanks. Let me try and get back to you. Please leave this thread open til at least next weekend.
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:
This is what I've done so far, based on the article mentioned above:
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);
}
}
OK thanks. Do you know which version this will be fixed in?
On three occasions I've experienced cases where on submission of a new ABP support question I get an error on the ABP support website. I'm sorry that I don't have a print screen or anything. I think the behaviour occurs once I raise a question, then immediately after submission click the 'Add question' or something button and raise another question rather than starting from scratch and using the top level question button. If you're unable to replicate let me know and please allow me to create a few dummy questions to see if I can diagnose the problem as it's incredibly frustrating since you lose the question that you composed! Thanks.
As a developer
I want to be able to use the record
field in PropPredicate
and PropCallback
properties on Create forms
So that I can dynamically set, for example, the disabled property based on the value of other properties that are currently set in the create form
The syntax would remain identical to how it is now, only that it would actually work. It should be such that the documentation can be changed to say that the record
field (and others?) are available on create forms (e.g. create user).
https://docs.abp.io/en/abp/latest/UI/Angular/Dynamic-Form-Extensions
For example, a new extra property that I add to the form can be disabled or enabled based on whether the user has checked IsActive or not.
https://github.com/abpframework/abp/issues/17374
Please let us know when this will be fixed + refund the question. Cheers.