.net Core Authenticate with only WS-Fed

2k views Asked by At

We are adding a new .net core web application into an existing environment, the center of which is an Identity Server 2 for authentication. When creating the new application I am having trouble getting it to use the existing authentication.

The goal is for every page in the new app to require a logged-in user. The new application has some user tables, but should not require the default Identity database (AspNetUsers, etc.) nor should it need its own login dialogs (nor password recovery, etc.).

To do this, I have added the following to our Startup.cs file in ConfigureServices():

01:services.AddTransient<IUserStore<ApplicationUser>, E360UserStore<ApplicationUser>>();
02:services.AddTransient<IRoleStore<ApplicationRole<string>>, E360RoleStore<ApplicationRole<string>>>();
03:
04:var authenticationConfigSection = Configuration.GetSection("Authentication");
05:
06:services.AddAuthentication(/*"WsFederation"*/)
07:        .AddWsFederation(authenticationScheme: "WsFederation",
08:                         displayName: "Single Signin",
09:                         configureOptions: options =>
10:                         {
11:                             // MetadataAddress represents the Identity Server instance used to authenticate users.
12:                             options.MetadataAddress = authenticationConfigSection.GetValue<string>("MetadataAddress");
13:
14:                             // Wtrealm is the app's relying party identifier in the IS instance.
15:                             options.Wtrealm = authenticationConfigSection.GetValue<string>("Wtrealm");
16:
17:                             //options.SignInScheme = "WsFederation";
18:                         });
19:
20:services.AddMvc(config =>
21:                {
22:                    var policy = new AuthorizationPolicyBuilder(/*"WsFederation"*/)
23:                                 .RequireAuthenticatedUser()
24:                                 .Build();
25:
26:                    config.Filters.Add(new AuthorizeFilter(policy));
28:                })
29:        .AddJsonOptions(o => o.SerializerSettings.ContractResolver = new DefaultContractResolver())
30:        .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

This produces the error, "No authenticationScheme was specified, and there was no DefaultChallengeScheme found." So, I try uncommenting the three "WsFederation" strings, one at a time.

I try uncommenting the last one first:

22:var policy = new AuthorizationPolicyBuilder("WsFederation")
23:             .RequireAuthenticatedUser()
24:             .Build();

This produces the similar but different error, "No authenticationScheme was specified, and there was no DefaultAuthenticateScheme found."

If I instead uncomment either of the other lines (or both):

06:services.AddAuthentication("WsFederation")    
17:options.SignInScheme = "WsFederation";

This produces the error, "The SignInScheme for a remote authentication handler cannot be set to itself. If it was not explicitly set, the AuthenticationOptions.DefaultSignInScheme or DefaultScheme is used."

If I configure the application for "normal" authentication, like this:

services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(
        Configuration.GetConnectionString("DefaultConnection")));

services.AddDefaultIdentity<ApplicationUser>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

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

I then get a login dialog with a button on the right marked, "Single Signin" that works just fine. The claims from the WsFederation login maps to users manually created in a generated Identity database.

My intent with the E360UserStore and E360RoleStore in the new model are to map to IdentityUser in FindByIdAsync() (I haven't been able to test this):

public class ApplicationUser : IdentityUser
{
    public User User { get; set; }

    public ApplicationUser(User user)
    {
        base.Id = User.IdentityGuid.ToString("D");
        base.Email = user.Person.EmailAddress;
        base.NormalizedEmail = user.Person.EmailAddress.ToLowerInvariant();
        base.UserName = user.Username;
        base.NormalizedUserName = user.Username.ToLowerInvariant();
        base.EmailConfirmed = true;
        User = user;
    }
}

I can't seem to find the secret sauce that removes the "default" login and instead automatically redirects to the Identity Server login.

Thanks for reading!

Update:

@Rytmis suggested that what I was missing was defining the cookies that would indicate that the application is "logged in" which is a great observation, but I'm still missing a piece since I don't understand how I go from WS-Fed to the cookie auth (to identify the current user). I changed my ConfigureServices code as shown below, which does cause the program to immediately redirect to the identity Server, which is perfect, and after login it redirects back to the ws-fed endpoint, which then redirects back to the initial page. All that is good news, but the initial page still doesn't see the user as logged-in. The cookie is created, but I don't see how, where, or if the login gets converted from ws-fed to the cookie? I defined custom IUserStore and IRoleStore classes, but those aren't being used or even instantiated.

services.AddAuthentication(sharedOptions =>
                           {
                               sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                               sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                               sharedOptions.DefaultChallengeScheme = WsFederationDefaults.AuthenticationScheme;
                           })
        .AddWsFederation(authenticationScheme: "WsFederation",
                         displayName: "Single Signin",
                         configureOptions: options =>
                                           {
                                               // MetadataAddress represents the Identity Server instance used to authenticate users.
                                               options.MetadataAddress = authenticationConfigSection.GetValue<string>("MetadataAddress");

                                               // Wtrealm is the app's relying party identifier in the IS instance.
                                               options.Wtrealm = authenticationConfigSection.GetValue<string>("Wtrealm");

                                               //options.SignInScheme = "WsFederation";
                                           })
        .AddCookie(options =>
                   {
                       options.Cookie.SameSite = SameSiteMode.None;
                       options.Cookie.Name = "AuthCookie";
                       options.AccessDeniedPath = "/error/accessdenied";
                   });

services.AddScoped<UserManager<ApplicationUser>, UserManager<ApplicationUser>>();
services.AddTransient<IUserStore<ApplicationUser>, MyUserStore<ApplicationUser>>();
services.AddTransient<IRoleStore<ApplicationRole<string>>, MyRoleStore<ApplicationRole<string>>>();
1

There are 1 answers

2
Rytmis On

The thing with remote authentication is that you will have to store something locally unless you want to perform a remote auth redirect on every request -- which nobody does.

In addition to the remote scheme, you'll want to specify a local authentication scheme that will be used to store the results of the remote authentication. This is typically done by adding the Cookie authentication handler and using it as a SignInScheme.