Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

IdentityServer4 - sub claim is missing

I've got an IdentityServer4 instance that I'm trying to get running in a Docker container behind an nginx proxy. I've based it on the AspNet identity sample from the Git repo, but after a user successfully registers a new account I get "An error occurred" from IdentityServer and the logs show

[07:46:39 ERR] An unhandled exception has occurred: sub claim is missing System.InvalidOperationException: sub claim is missing at IdentityServer4.IdentityServerPrincipal.AssertRequiredClaims(ClaimsPrincipal principal at IdentityServer4.Hosting.IdentityServerAuthenticationService.AugmentPrincipal(ClaimsPrincipal principal at IdentityServer4.Hosting.IdentityServerAuthenticationService.<SignInAsync>d__7.MoveNext

My Startup.cs looks like this

var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().
var connectionString = Configuration.GetConnectionString("DefaultConnection");
var issuerUri = Configuration.GetSection("IssuerUri").Value;

services.AddDbContext<ApplicationDbContext>(options => 
    options.UseSqlServer(connectionString));

services.AddIdentity<ApplicationUser, IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders();

services.AddTransient<IEmailSender, EmailSender>();

services.AddMvc();

services.AddCors(o => o.AddPolicy("CorsPolicy", b =>
{
    b.AllowAnyOrigin()
        .AllowAnyMethod()
        .AllowAnyHeader();
}));

services.AddIdentityServer(options =>
{
    options.IssuerUri = issuerUri;
    options.PublicOrigin = issuerUri;
})
.AddDeveloperSigningCredential()

// this adds the config data from DB (clients, resources)
.AddConfigurationStore(options =>
{
    options.ConfigureDbContext = builder =>
        builder.UseSqlServer(connectionString,
            sql => sql.MigrationsAssembly(migrationsAssembly));
})

// this adds the operational data from DB (codes, tokens, consents)
.AddOperationalStore(options =>
{
    options.ConfigureDbContext = builder =>
        builder.UseSqlServer(connectionString,
            sql => sql.MigrationsAssembly(migrationsAssembly));

    // this enables automatic token cleanup. this is optional.
    //options.EnableTokenCleanup = true;
    //options.TokenCleanupInterval = 30;
});

I must have missed some obvious config but I can't see where. Any ideas?

UPDATE I've made some progress with this and seemed to have got past the initial error. The user is now authenticated but the signin-oidc page throws the error

[11:33:21 INF] Request starting HTTP/1.1 POST http://mvcportal.co.uk/signin-oidc application/x-www-form-urlencoded 1565
[11:33:21 INF] AuthenticationScheme: Cookies signed in.
[11:33:21 INF] Request finished in 684.8425ms 302
[11:33:27 INF] Request starting HTTP/1.1 POST http://mvcportal.co.uk/signin-oidc application/x-www-form-urlencoded 1565
[11:33:27 ERR] Message contains error: 'invalid_grant', error_description: 'error_description is null', error_uri: 'error_uri is null', status code '400'.

I've got a valid JWT but I notice the idp is not equal to the issuer. Is that correct?

{
  "nbf": 1508758474,
  "exp": 1508758774,
  "iss": "http://myproxiedlogonsitebehindnginx.co.uk",
  "aud": "mvc.portal",
  "nonce": "636443552746808541.MGVjMzk2NTEtYmYwNS00NmQwLTllOTQtZDVjNjdlYTA2YWVlYTQ3Zjg1NjgtZDA1Yi00NDE0LWJiYmYtMjM4YzI1NjZlYTcx",
  "iat": 1508758474,
  "c_hash": "kG7wG8vSgRe5zdriHQ6iMA",
  "sid": "c9410ee8f27b69c32e43d5ac3d407f37",
  "sub": "e80fb854-cab2-4381-8057-19de0fea73f4",
  "auth_time": 1508757008,
  "idp": "local",
  "amr": [
    "pwd"
  ]
}

UPDATE 2 This is the client configuration on idsrv if that helps

new Client
{
    ClientId = "mvc.portal",
    ClientName = "Customer Portal",
    ClientUri = customerPortalBaseUri,

    ClientSecrets =
    {
        new Secret("21f51463-f436-4a84-92ce-1b520dd63a81".Sha256())
    },

    AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
    AllowAccessTokensViaBrowser = false,

    RedirectUris = { $"{customerPortalBaseUri}/signin-oidc"},
    FrontChannelLogoutUri = $"{customerPortalBaseUri}/signout-oidc",
    PostLogoutRedirectUris = { $"{customerPortalBaseUri}/signout-callback-oidc" },

    AllowOfflineAccess = true,

    RequireConsent = false,

    AllowedScopes =
    {
        IdentityServerConstants.StandardScopes.OpenId,
        IdentityServerConstants.StandardScopes.Profile,
        IdentityServerConstants.StandardScopes.Email
    }
}

And this is the client/portal config

services.AddAuthentication(options =>
    {
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = "oidc";
    })
    .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddOpenIdConnect("oidc", options =>
    {
        options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.Authority = "http://myproxiedlogonsitebehindnginx.co.uk";
        options.RequireHttpsMetadata = false;
        options.ClientId = "mvc.portal";
        options.ClientSecret = "21f51463-f436-4a84-92ce-1b520dd63a81";
        options.ResponseType = "code id_token";
        options.SaveTokens = true;
        options.GetClaimsFromUserInfoEndpoint = true;
    });

UPDATE 3 So now I'm convinced it has something to do with the deployment, because if I run the mvc app on my local machine but use the deployed idsvr in the container (behind nginx) I can authenticate without a problem, however if I try the version of the containerised portal I still get an unhandled 500 without it being logged and then if I retry the action I get this logged:

[11:22:51 INF] Request starting HTTP/1.1 POST http://mvcportal.co.uk/signin-oidc application/x-www-form-urlencoded 1559
[11:22:51 ERR] Message contains error: 'invalid_grant', error_description: 'error_description is null', error_uri: 'error_uri is null', status code '400'.
[11:22:51 ERR] Exception occurred while processing message.
Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectProtocolException: Message contains error: 'invalid_grant'], error_description: 'error_description is null', error_uri: 'error_uri is null'.
like image 808
Dan Kennedy Avatar asked Oct 23 '17 08:10

Dan Kennedy


Video Answer


3 Answers

I was having the same exact issue and resolved it by adding:

.AddAspNetIdentity<ApplicationUser>();

to

services.AddIdentityServer()

in Startup.cs

like image 53
Amirhossein Yari Avatar answered Sep 22 '22 17:09

Amirhossein Yari


So I finally got to the bottom of it. It seems the default header limits in nginx don't play nice and I found this in the logs upstream sent too big header while reading response header from upstream

Updating the nginx config to include the lines

proxy_buffer_size          128k;
proxy_buffers              4 256k;
proxy_busy_buffers_size    256k;

prevented the 502 error and it all authenticates fine now.

like image 40
Dan Kennedy Avatar answered Sep 23 '22 17:09

Dan Kennedy


        services.AddIdentityServer()
              .AddAspNetIdentity<ApplicationUser>()
              .AddConfigurationStore(options =>
              {
                  options.ConfigureDbContext = builder => builder.UseSqlServer(connectionString,
                      sqlServerOptionsAction: sqlOptions =>
                      {
                          sqlOptions.MigrationsAssembly(migrationsAssembly);
                      });
              })
             .AddOperationalStore(options =>
             {
                 options.ConfigureDbContext = builder => builder.UseSqlServer(connectionString,
                     sqlServerOptionsAction: sqlOptions =>
                     {
                         sqlOptions.MigrationsAssembly(migrationsAssembly);
                     });
             }).Services.AddTransient<IProfileService, ProfileService>();

And

ProfileService.cs

public class ProfileService : IProfileService
 {
     private readonly UserManager<ApplicationUser> _userManager;

     public ProfileService(UserManager<ApplicationUser> userManager)
     {
         _userManager = userManager;
     }

     async public Task GetProfileDataAsync(ProfileDataRequestContext context)
     {
         var subject = context.Subject ?? throw new ArgumentNullException(nameof(context.Subject));

         var subjectId = subject.Claims.Where(x => x.Type == "sub").FirstOrDefault().Value;

         var user = await _userManager.FindByIdAsync(subjectId);
         if (user == null)
             throw new ArgumentException("Invalid subject identifier");

         var claims = GetClaimsFromUser(user);
         context.IssuedClaims = claims.ToList();
     }

     async public Task IsActiveAsync(IsActiveContext context)
     {
         var subject = context.Subject ?? throw new ArgumentNullException(nameof(context.Subject));

         var subjectId = subject.Claims.Where(x => x.Type == "sub").FirstOrDefault().Value;
         var user = await _userManager.FindByIdAsync(subjectId);

         context.IsActive = false;

         if (user != null)
         {
             if (_userManager.SupportsUserSecurityStamp)
             {
                 var security_stamp = subject.Claims.Where(c => c.Type == "security_stamp").Select(c => c.Value).SingleOrDefault();
                 if (security_stamp != null)
                 {
                     var db_security_stamp = await _userManager.GetSecurityStampAsync(user);
                     if (db_security_stamp != security_stamp)
                         return;
                 }
             }

             context.IsActive =
                 !user.LockoutEnabled ||
                 !user.LockoutEnd.HasValue ||
                 user.LockoutEnd <= DateTime.Now;
         }
     }

     private IEnumerable<Claim> GetClaimsFromUser(ApplicationUser user)
     {
         var claims = new List<Claim>
         {
             new Claim(JwtClaimTypes.Subject, user.Id),
             new Claim(JwtClaimTypes.PreferredUserName, user.UserName),
             new Claim(JwtRegisteredClaimNames.UniqueName, user.UserName)
         };

         if (!string.IsNullOrWhiteSpace(user.Name))
             claims.Add(new Claim("name", user.Name));

         if (!string.IsNullOrWhiteSpace(user.LastName))
             claims.Add(new Claim("last_name", user.LastName));


         if (_userManager.SupportsUserEmail)
         {
             claims.AddRange(new[]
             {
                 new Claim(JwtClaimTypes.Email, user.Email),
                 new Claim(JwtClaimTypes.EmailVerified, user.EmailConfirmed ? "true" : "false", ClaimValueTypes.Boolean)
             });
         }

         if (_userManager.SupportsUserPhoneNumber && !string.IsNullOrWhiteSpace(user.PhoneNumber))
         {
             claims.AddRange(new[]
             {
                 new Claim(JwtClaimTypes.PhoneNumber, user.PhoneNumber),
                 new Claim(JwtClaimTypes.PhoneNumberVerified, user.PhoneNumberConfirmed ? "true" : "false", ClaimValueTypes.Boolean)
             });
         }

         return claims;
     }
 }
like image 42
user2959823 Avatar answered Sep 20 '22 17:09

user2959823