I'm working on a mobile app that consumes an API, which I'll refer to as the "Main API." I'm securing the Main API using Identity Server 4. Additionally, I need authentication within the mobile app, which includes registration, signing in, and signing in via external providers like Google and Apple, along with password changes that involve OTP confirmation.
To manage this authentication logic separately from the Main API, I've decided to create a dedicated API, which I'm calling "Identity.Admin.Api." In this Identity.Admin.Api, I've added an AuthController
with the following endpoints:
SigninRequest: This endpoint takes user credentials, validates them, and generates an access token from Identity Server using the Password grant. It also stores this access token and a confirmation token (a GUID) in memory cache for validation. Additionally, it sends an OTP (to email or mobile).
SigninConfirm: This endpoint validates the confirmation token and OTP. If successful, it returns the access token.
RegisterRequest: This endpoint takes registration data and checks its validity (e.g., if the username or email is already taken). If the data is valid, it generates a confirmation token (a GUID) and stores it in the cache. It then sends an OTP.
RegisterConfirm: This endpoint takes the confirmation token and OTP. If they are correct, it registers the user and generates an access token using the Password grant.
All of this works as intended. However, I'm faced with a challenge when it comes to supporting external login providers (Google and Apple) in the mobile app. While Identity Server supports external providers, it's not suitable for my use case as the mobile app relies on the native controls of Google and Apple for authentication. Therefore, I've opted to use Identity.Admin.Api for external login provider authentication as well.
In this scenario, the mobile app obtains a token directly from Google or Apple. It then sends this token to Identity.Admin.Api, which registers the user without a password using userManagerInstance.CreateAsync(UserRegisteredWithGoogleOrApple)
. However, I still need to generate an access token for the mobile app to use with the Main API.
Here lies my challenge: I don't have a password to use the Password flow. While I could generate a random GUID as a password in the backend, this doesn't seem like the right approach. Using the client credentials grant won't work either because the access token needs to contain the username.
It appears that my only option is to use an extension grant. My ExternalLoginGrantValidator
looks like this:
public class ExternalLoginGrantValidator : IExtensionGrantValidator
{
private readonly IUserService _userService;
public ExternalLoginGrantValidator(IUserService userService)
{
_userService = userService;
}
public async Task ValidateAsync(ExtensionGrantValidationContext context)
{
var username = context.Request.Raw.Get("username");
var socialMediaType = (SocialMediaType)(Convert.ToInt32(context.Request.Raw.Get("socialmediatype")));
var socialUserId = context.Request.Raw.Get("socialuserid");
// Check if the user registered via this social media provider before and if the socialUserId is correct.
var valid = await _userService.CheckIfSocialSignInValid(username, socialUserId, socialMediaType);
if (valid)
{
var claims = GetClaimsFromUserManager(username);
context.Result = new GrantValidationResult(context.Subject.Identity, "external_login", claims);
}
else
{
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Invalid external credentials");
}
}
public string GrantType => "external_login";
}
I also have an IProfileService
:
public class MyProfileService : IProfileService
{
private readonly UserManager<UserIdentity> _userManager;
public MyProfileService(UserManager<UserIdentity> userManager)
{
_userManager = userManager;
}
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var userId = context.Subject.GetSubjectId();
var user = await _userManager.FindByIdAsync(userId);
var claims = GetAllClaimsOfUserByUserManager(user);
context.IssuedClaims = claims;
}
public async Task IsActiveAsync(IsActiveContext context)
{
var userId = context.Subject.GetSubjectId();
var user = await _userManager.FindByIdAsync(userId);
context.IsActive = (user != null);
}
}
I've registered these components as follows:
services.AddIdentityServer<.....
.AddProfileService<MyProfileService>()
.AddExtensionGrantValidator<ExternalLoginGrantValidator>();
Am I on the right track with this approach? My issue is that I can't seem to generate a token with the username. If I return new GrantValidationResult(new Dictionary<object, string>())
, the token is generated without any claims, but I need the username. However, if I set context.Result
as shown above, I get an "invalid_grant" error. I have also ensured that "external_login" is added to my client.
I understand that this is a complex issue, but any help or insights would be greatly appreciated.