Search code examples
c#asp.net-coreasp.net-core-2.0single-page-applicationidentityserver4

How to validate if user exist inside IdentityServer4 after being authenticated from External Provider?


I'm trying to find a proper way where I can inject a service to validate if user exists or registered in my application after being successfully authenticated from an external identity provider like Azure Active Directory. What I want to do is to redirect user to a custom error page or display an Unauthorized message if his account is not yet registered in my application.

I tried utilizing the IProfileService interface but it seems not the right way to go.

Here is my Startup.cs setup:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    services
        .AddIdentityServer()
        .AddDeveloperSigningCredential()
        .AddTestUsers(Config.GetUsers())
        .AddInMemoryIdentityResources(Config.GetIdentityResources())
        .AddInMemoryApiResources(Config.GetApiResources())
        .AddInMemoryClients(Config.GetClients()) // Client was configured with RequireConsent = false, EnableLocalLogin = false,
        .AddProfileService<ProfileService>()
        .Services.AddTransient<IUserRepository,UserRepository>();

    services.AddAuthentication()
        .AddOpenIdConnect("AAD", "Azure Active Directory", options =>
        {
            options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
            options.SignOutScheme = IdentityServerConstants.SignoutScheme;
            options.Authority = "https://login.microsoftonline.com/MyTenant";
            options.ClientId = "MyClientId";
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuer = false
            };

            options.GetClaimsFromUserInfoEndpoint = true;                    
        });
}


public class ProfileService : IProfileService
{
    private readonly IUserRepository _userRepository;

    public ProfileService(IUserRepository userRepository)
    {
        _userRepository = userRepository 
    }

    public Task GetProfileDataAsync(ProfileDataRequestContext context)
    {
        var user = _userRepository.FindByUser(context.Subject.Identity.Name);

        // This will display HTTP 500 instead of 401 
        if(user == null) throw new UnauthorizedAccessException("You're not registered");

        // I add custom claims here

        return Task.FromResult(0);
    }

    public Task IsActiveAsync(IsActiveContext context) => Task.FromResult(0);
}

Is there any available service or interface I can use where I can inject my user validation as well as allowing me to inject my user repository in that service? Is it possible to inject this kind of process inside IdentityServer4? Can someone point me in the right direction to accomplish my goal using IdentityServer4?

Note: Lets assume I have SPA web app and I have my own separate registration mechanism. I don't want to redirect back to my SPA if user doesn't exist and handle it inside IdentityServer4 instead. Btw, some of the code above are not included for brevity.


Solution

  • The IdentityServer4 QuickStart UI is configured to auto-provision local user accounts when signing-in through an external provider. That's all handled in ExternalController.Callback:

    // lookup our user and external provider info
    var (user, provider, providerUserId, claims) = FindUserFromExternalProvider(result);
    if (user == null)
    {
        // this might be where you might initiate a custom workflow for user registration
        // in this sample we don't show how that would be done, as our sample implementation
        // simply auto-provisions new external user
        user = AutoProvisionUser(provider, providerUserId, claims);
    }
    

    In your situation, you can perform whatever logic you need to perform instead of calling AutoProvisionUser. As this is a regular MVC action that's being executed, you have the ability to inject your own classes into ExternalController's constructor or into Callback itself (using [FromServices]). Here's a rough idea of the changes you might want to make:

    public async Task<IActionResult> Callback([FromServices] IUserRepository userRepository)
    {
        ...
    
        // lookup our user and external provider info
        var (user, provider, providerUserId, claims) = FindUserFromExternalProvider(result);
        if (user == null)
        {
            // We don't have a local user.
            return RedirectToAction("SomeAction", "SomeController");
        }
    
        ...
    }