3

I have integrated the Azure Active Directory in Identity Server 4 as an external provider.

I want to authenticate Azure Active Directory users from Identity Server 4 by using the APIs.

Here is the code:

var disco = await client.GetDiscoveryDocumentAsync("https://localhost:5001");

if (disco.IsError)
{
    Console.WriteLine(disco.Error);
    return;
}

// request token
var tokenResponse = await client.RequestPasswordTokenAsync(new PasswordTokenRequest
        {
            Address = disco.TokenEndpoint,
            ClientId = "client",
            ClientSecret = "secret",

            Scope = "api1",
            UserName = "ad user name",
            Password = "add user password"
        });
        

When I execute this piece of code, I got an invalid username or password error.

Note: the provided credentials are valid.

Here is my startup.cs file

using IdentityServer4;
using IdentityServer4.EntityFramework.DbContexts;
using IdentityServer4.EntityFramework.Mappers;
using MorpheusIdentityServer.Quickstart.UI;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
using IdentityServer4.Validation;

namespace MorpheusIdentityServer
{
    public class Startup
    {
        public Startup(IConfiguration configuration, IWebHostEnvironment env)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

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

            var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
            string connectionString = Configuration.GetSection("ConnectionString").Value;

            var builder = services.AddIdentityServer()
                .AddConfigurationStore(options =>
                {
                    options.ConfigureDbContext = b => b.UseSqlServer(connectionString,
                        sql => sql.MigrationsAssembly(migrationsAssembly));
                })
                .AddOperationalStore(options =>
                {
                    options.ConfigureDbContext = b => b.UseSqlServer(connectionString,
                        sql => sql.MigrationsAssembly(migrationsAssembly));
                })
                .AddProfileService<ProfileService>();

                 

            services.AddAuthentication()
                 .AddOpenIdConnect("aad", "Azure AD", options =>
                 {

                     options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
                     options.SignOutScheme = IdentityServerConstants.SignoutScheme;

                     options.Authority = Configuration.GetSection("ActiveDirectoryAuthority").Value;
                     options.ClientId = Configuration.GetSection("ActiveDirectoryClientId").Value;
                     options.ResponseType = OpenIdConnectResponseType.IdToken;
                     options.CallbackPath = "/signin-aad";
                     options.SignedOutCallbackPath = "/signout-callback-aad";
                     options.RemoteSignOutPath = "/signout-aad";
                     options.TokenValidationParameters = new TokenValidationParameters
                     {
                         NameClaimType = "name",
                         RoleClaimType = "role",
                         ValidateIssuer = false
                     };

                 });
            services.AddSingleton<IResourceOwnerPasswordValidator, ValidateExternalUser>();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            InitializeDatabase(app);
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseStaticFiles();
            app.UseRouting();

            app.UseIdentityServer();
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapDefaultControllerRoute();
            });
        }

        private void InitializeDatabase(IApplicationBuilder app)
        {
            using (var serviceScope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope())
            {
                serviceScope.ServiceProvider.GetRequiredService<PersistedGrantDbContext>().Database.Migrate();

                var context = serviceScope.ServiceProvider.GetRequiredService<ConfigurationDbContext>();
                context.Database.Migrate();
                if (!context.Clients.Any())
                {
                    foreach (var client in Config.Clients)
                    {
                        context.Clients.Add(client.ToEntity());
                    }
                    context.SaveChanges();
                }

                if (!context.IdentityResources.Any())
                {
                    foreach (var resource in Config.IdentityResources)
                    {
                        context.IdentityResources.Add(resource.ToEntity());
                    }
                    context.SaveChanges();
                }

                if (!context.ApiScopes.Any())
                {
                    foreach (var resource in Config.ApiScopes)
                    {
                        context.ApiScopes.Add(resource.ToEntity());
                    }
                    context.SaveChanges();
                }
            }
        }
    }
}
Rafaqat Ali
  • 676
  • 7
  • 26
  • Are you using ROPC flow with password grant? – Niladri Jan 18 '21 at 11:54
  • yes, I'm using ROPC flow with password grant – Rafaqat Ali Jan 18 '21 at 12:03
  • you check this link , your code looks fine to me , https://stackoverflow.com/questions/65199330/invalid-grant-aadsts50126-error-validating-credentials-due-to-invalid-username – Niladri Jan 18 '21 at 12:04
  • What is the value of `disco.TokenEndpoint,` ? and are you passing `openid` in one of your scope `Scope = "api1",` – Niladri Jan 18 '21 at 12:05
  • The value for disco.TokenEndpoint will be https://localhost:5001/connect/token – Rafaqat Ali Jan 18 '21 at 12:07
  • The client configured client: new Client { ClientId = "client", EnableLocalLogin=false, ClientName = "Workflow", AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, AccessTokenType = AccessTokenType.Jwt, AlwaysSendClientClaims = true, ClientSecrets = { new Secret("secret".Sha256()) }, AllowedScopes = { "api1" } }, – Rafaqat Ali Jan 18 '21 at 12:08
  • https://stackoverflow.com/questions/65199330/invalid-grant-aadsts50126-error-validating-credentials-due-to-invalid-username I have checked this one. It's using azure add token end point. my requirement is to use identity server end point and in identity, server use an azure active directory as an external provider – Rafaqat Ali Jan 18 '21 at 12:11
  • Do you have any ADFS setup with AAD? – Niladri Jan 18 '21 at 12:20
  • No, I'm using cloud azure active directory accounts only. – Rafaqat Ali Jan 18 '21 at 12:21
  • There are some limitation with ROPC like `The Microsoft identity platform endpoint only supports ROPC for Azure AD tenants, not personal accounts. This means that you must use a tenant-specific endpoint (https://login.microsoftonline.com/{TenantId_or_Name}) or the organizations endpoint.` – Niladri Jan 18 '21 at 12:28
  • Where do I need to provide a tenant-specific endpoint? from my code mentioned above. – Rafaqat Ali Jan 18 '21 at 12:32
  • Can you post your startup.cs code it's difficult to comment without that. Also make sure your passing `IdentityServerConstants.StandardScopes.OpenId,` along with your `Scope = "api1"` – Niladri Jan 18 '21 at 12:33
  • Added my startup.cs class in question detail. kindly check – Rafaqat Ali Jan 18 '21 at 12:55
  • Why are you saying "When I execute this piece of code, I got an invalid username or password error"? AAD should not pass through this part of code. `https://localhost:5001/connect/token` is the token endpoint for Identity Provider users rather than AAD users. I see you have implement `services.AddAuthentication().AddOpenIdConnect("aad"...)`, which is used to sign in with AAD user. Are you using the "Azure AD button" to login in your application? – Allen Wu Jan 19 '21 at 09:32
  • @AllenWu No, from the azure aad button, I'm able to log in. But my requirement is how I may be logged using API with a password of azure aad accounts – Rafaqat Ali Jan 19 '21 at 10:23
  • @AllenWu I know this way AAd will not be able to authenticate. let me know how I need to change at the identity server side to work this piece of code. – Rafaqat Ali Jan 19 '21 at 10:24

2 Answers2

0

By default Resource Owner Password workflow uses the connect/token endpoint which validates the users who are present in the AspNetUser table only (it will not validate external users), so that's the reason you are getting the Invalid User name or password error message because your user is present in Microsoft AD and not in ID4 database.

Log in using the Resource Owner Password password isn't the recommended approach but if you still need it anyway, you can override the endpoint implementation and write your own custom logic to authenticate the user with a Microsoft AD data source.

Mahesh More
  • 821
  • 1
  • 8
  • 23
  • How do I write custom logic and at which place to authenticate a user from an external provider? – Rafaqat Ali Jan 19 '21 at 12:01
  • You can create/override endpoint by implementing the `IEndpointHandler` interface. Please take a look at my [blog post](https://dev.to/maheshmore2691/identity-server-4-with-net-core-app-4ih0) where I have added detailed steps for custom handlers. – Mahesh More Jan 19 '21 at 15:34
0

I have searched and found some possible ways to customize usernames and passwords from external providers. Here are the steps

1- Implement a class like below

public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
{
    public Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
    {
       var username= context.UserName;
       var password= context.Password;
      // write the code which will validate the username and password from external provider.    
    }
}

2- Then register this interface in a startup.cs file like below:

services.AddTransient<IResourceOwnerPasswordValidator, ResourceOwnerPasswordValidator>();

So, when you try to call the endpoint /connect/token this class will be triggered.

Rafaqat Ali
  • 676
  • 7
  • 26