It's a rather question related with conceptual approach.
So I have two projects - in one the Authentication is already implemented and working ok. It's a .net core SPA with OpenID + Cookie (without without ASP.NET Core Identity).
The second project is the REST API that I want to use that cookie to Authorize some of the endpoints so basically SSO.
The biggest challenge to me is I don't know how I should modify second project to "accept" the cookie from first one - I need at least some starting point.
This one is not helping at all: https://learn.microsoft.com/en-us/aspnet/core/security/cookie-sharing?view=aspnetcore-6.0
It's not clear from documentation what I need to do in both projects to make it work.
So what I'm thinking is to copy-paste the services.AddAuthentication(...) from first project to the second one so they can share same Authority - I believe there is no other way that second project knows that cookie is "ours" cookie?
Update:
Here I'm giving code samples - with setup below it keeps trying to redirect me to login as don't treat it's authenticated:
Project A (where user login and we want "generate" cookie for this and other projects):
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(options => {
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(options => {
options.Cookie.Name = "test";
options.Cookie.Domain = ".localhost";
options.Cookie.Path = "/";
})
.AddOpenIdConnect(options =>
{
options.ClientId = "xxx";
options.ClientSecret = "xxx";
options.MetadataAddress = $"https://login.microsoftonline.com/xxx/v2.0/.well-known/openid-configuration?appid=xxx";
options.Authority = $"https://login.microsoftonline.com/xxx/oauth2/v2.0";
options.ResponseType = "code";
options.GetClaimsFromUserInfoEndpoint = true;
options.TokenValidationParameters = new TokenValidationParameters {
NameClaimType = "name"
};
});
services.AddDataProtection()
.PersistKeysToFileSystem(new System.IO.DirectoryInfo("/cookies"))
.SetApplicationName("SharedCookieApp");
services.ConfigureApplicationCookie(options =>
{
options.Cookie.Name = "test";
options.Cookie.Domain = ".localhost";
options.Cookie.Path = "/";
});
}
Project B (REST API - here we wan't just to "consume" cookie generated in Project A):
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.Cookie.Name = "test";
options.Cookie.Domain = ".localhost";
options.Cookie.Path = "/";
options.Events.OnRedirectToLogin = context =>
{
context.HttpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
return Task.CompletedTask;
};
});
services.AddDataProtection()
.PersistKeysToFileSystem(new System.IO.DirectoryInfo("/cookies"))
.SetApplicationName("SharedCookieApp");
services.ConfigureApplicationCookie(options =>
{
options.Cookie.Name = "test";
options.Cookie.Domain = ".localhost";
options.Cookie.Path = "/";
options.Events.OnRedirectToLogin = context =>
{
context.HttpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
return Task.CompletedTask;
};
});
}
Error I'm getting:
The documentation you referenced actually has everything you need. But it requires some basic knowledge to understand that. To use the cookie set by one app in another you need to ensure two things:
The cookie should be sent to both app
1.1. If apps hosted on the same domain but on different paths, you need to set cookie Path to the common denominator. For example, here the common denominator is /:
https://my-domain.com/app1
https://my-domain.com/app2
Here is /api (but / is also valid)
https://my-domain.com/api/app1
https://my-domain.com/api/app2
1.2 If apps hosted on different domains, they must be subdomains of some common domain. Set cookie domain to the common domain value to share it between subdomains.
For example, here the common domain is .company.com:
https://sub-domain1.company.com
https://sub-domain2.company.com
This is also example of common domain .company.com
https://company.com
https://sub-domain2.company.com
But these 2 domains can't share cookies because they don't have common domain:
https://sub-domain1.company1.com
https://sub-domain2.company2.com
1.3 You can also mix domain and path configuration if your apps hosted on different domains with common sub-domain and different path. For example, here the domain should be .company.com and path /:
https://sub-domain1.company.com/api/app1
https://sub-domain2.company.com/app2
Both app can decrypt the cookie and understand its content.
2.1 If apps hosted on the same machine you can use file storage to persist data protection keys:
services.AddDataProtection()
.PersistKeysToFileSystem("{PATH TO COMMON KEY RING FOLDER}")
2.2 If the apps hosted on different machines you need to use another type of storage so that both app will be able to access it and read protection keys. For example, you can use Amazon KMS service with Amazon.AspNetCore.DataProtection.SSM nuget:
services.AddDataProtection()
.PersistKeysToAWSSystemsManager("/MyApplication/DataProtection");
Related
i've created an API and set up JWT auth from the same API (I chose not to use IdentityServer4).
I did this through services.AddAuthentication
And then I created tokens in the controller and it works.
However I now want to add registration etc. But i prefer not to write my own code for hashing passwords, handling registration emails etc.
So I came across ASP.NET Core Identity and it seems like what I need, except from that it adds some UI stuff that I dont need (because its just an API and the UI i want completely independent).
But on MSDN is written:
ASP.NET Core Identity adds user interface (UI) login functionality to
ASP.NET Core web apps. To secure web APIs and SPAs, use one of the
following:
Azure Active Directory
Azure Active Directory B2C (Azure AD B2C)
IdentityServer4
So is it really a bad idea to use Core Identity just for hashing and registration logic for an API? Cant I just ignore the UI functionality? It's very confusing because I'd rather not use IdentityServer4 or create my own user management logic.
Let me just get off my chest that the bundling Identity does with the UI, the cookies and the confusing various extension methods that add this or that, but don't add this or that, is pretty annoying, at least when you build modern web APIs that need no cookies nor UI.
In some projects I also use manual JWT token generation with Identity for the membership features and user/password management.
Basically the simplest thing to do is to check the source code.
AddDefaultIdentity() adds authentication, adds the Identity cookies, adds the UI, and calls AddIdentityCore(); but has no support for roles:
public static IdentityBuilder AddDefaultIdentity<TUser>(this IServiceCollection services, Action<IdentityOptions> configureOptions) where TUser : class
{
services.AddAuthentication(o =>
{
o.DefaultScheme = IdentityConstants.ApplicationScheme;
o.DefaultSignInScheme = IdentityConstants.ExternalScheme;
})
.AddIdentityCookies(o => { });
return services.AddIdentityCore<TUser>(o =>
{
o.Stores.MaxLengthForKeys = 128;
configureOptions?.Invoke(o);
})
.AddDefaultUI()
.AddDefaultTokenProviders();
}
AddIdentityCore() is a more stripped down version that only adds basic services, but it doesn't even add authentication, and also no support for roles (here you can already see what individual services are added, to change/override/remove them if you want):
public static IdentityBuilder AddIdentityCore<TUser>(this IServiceCollection services, Action<IdentityOptions> setupAction)
where TUser : class
{
// Services identity depends on
services.AddOptions().AddLogging();
// Services used by identity
services.TryAddScoped<IUserValidator<TUser>, UserValidator<TUser>>();
services.TryAddScoped<IPasswordValidator<TUser>, PasswordValidator<TUser>>();
services.TryAddScoped<IPasswordHasher<TUser>, PasswordHasher<TUser>>();
services.TryAddScoped<ILookupNormalizer, UpperInvariantLookupNormalizer>();
services.TryAddScoped<IUserConfirmation<TUser>, DefaultUserConfirmation<TUser>>();
// No interface for the error describer so we can add errors without rev'ing the interface
services.TryAddScoped<IdentityErrorDescriber>();
services.TryAddScoped<IUserClaimsPrincipalFactory<TUser>, UserClaimsPrincipalFactory<TUser>>();
services.TryAddScoped<UserManager<TUser>>();
if (setupAction != null)
{
services.Configure(setupAction);
}
return new IdentityBuilder(typeof(TUser), services);
}
Now that kind of makes sense so far, right?
But enter AddIdentity(), which appears to be the most bloated, the only one that supports roles directly, but confusingly enough it doesn't seem to add the UI:
public static IdentityBuilder AddIdentity<TUser, TRole>(
this IServiceCollection services,
Action<IdentityOptions> setupAction)
where TUser : class
where TRole : class
{
// Services used by identity
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
})
.AddCookie(IdentityConstants.ApplicationScheme, o =>
{
o.LoginPath = new PathString("/Account/Login");
o.Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = SecurityStampValidator.ValidatePrincipalAsync
};
})
.AddCookie(IdentityConstants.ExternalScheme, o =>
{
o.Cookie.Name = IdentityConstants.ExternalScheme;
o.ExpireTimeSpan = TimeSpan.FromMinutes(5);
})
.AddCookie(IdentityConstants.TwoFactorRememberMeScheme, o =>
{
o.Cookie.Name = IdentityConstants.TwoFactorRememberMeScheme;
o.Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = SecurityStampValidator.ValidateAsync<ITwoFactorSecurityStampValidator>
};
})
.AddCookie(IdentityConstants.TwoFactorUserIdScheme, o =>
{
o.Cookie.Name = IdentityConstants.TwoFactorUserIdScheme;
o.ExpireTimeSpan = TimeSpan.FromMinutes(5);
});
// Hosting doesn't add IHttpContextAccessor by default
services.AddHttpContextAccessor();
// Identity services
services.TryAddScoped<IUserValidator<TUser>, UserValidator<TUser>>();
services.TryAddScoped<IPasswordValidator<TUser>, PasswordValidator<TUser>>();
services.TryAddScoped<IPasswordHasher<TUser>, PasswordHasher<TUser>>();
services.TryAddScoped<ILookupNormalizer, UpperInvariantLookupNormalizer>();
services.TryAddScoped<IRoleValidator<TRole>, RoleValidator<TRole>>();
// No interface for the error describer so we can add errors without rev'ing the interface
services.TryAddScoped<IdentityErrorDescriber>();
services.TryAddScoped<ISecurityStampValidator, SecurityStampValidator<TUser>>();
services.TryAddScoped<ITwoFactorSecurityStampValidator, TwoFactorSecurityStampValidator<TUser>>();
services.TryAddScoped<IUserClaimsPrincipalFactory<TUser>, UserClaimsPrincipalFactory<TUser, TRole>>();
services.TryAddScoped<IUserConfirmation<TUser>, DefaultUserConfirmation<TUser>>();
services.TryAddScoped<UserManager<TUser>>();
services.TryAddScoped<SignInManager<TUser>>();
services.TryAddScoped<RoleManager<TRole>>();
if (setupAction != null)
{
services.Configure(setupAction);
}
return new IdentityBuilder(typeof(TUser), typeof(TRole), services);
}
All in all what you probably need is the AddIdentityCore(), plus you have to use AddAuthentication() on your own.
Also, if you use AddIdentity(), be sure to run your AddAuthentication() configuration after calling AddIdentity(), because you have to override the default authentication schemes (I ran into problems related to this, but can't remember the details).
(Another tidbit of information that might be interesting for people reading this is the distinction between SignInManager.PasswordSignInAsync(), SignInManager.CheckPasswordSignInAsync() and UserManager.CheckPasswordAsync(). These are all public methods you can find and call for authorization purposes. PasswordSignInAsync() implements two-factor signin (also sets cookies; probably only when using AddIdentity() or AddDefaultIdentity()) and calls CheckPasswordSignInAsync(), which implements user lockout handling and calls UserManager.CheckPasswordAsync(), which just checks the password. So to get a proper authentication it's better not to call UserManager.CheckPasswordAsync() directly, but to do it through CheckPasswordSignInAsync(). But, in a single-factor JWT token scenario, calling PasswordSignInAsync() is probably not needed (and you can run into redirect issues). If you have included UseAuthentication()/AddAuthentication() in the Startup with the proper JwtBearer token schemes set, then the next time the client sends a request with a valid token attached, the authentication middleware will kick in, and the client will be 'signed in'; i.e. any valid JWT token will allow client to access controller actions protected with [Authorize].)
And IdentityServer is thankfully completely separate from Identity. In fact the decent implementation of IdentityServer is to use it as a standalone literal identity server that issues tokens for your services. But since ASP.NET Core has no token generation capability built-in, a lot of people end up running this bloated server inside their apps just to be able to use JWT tokens, even though they have a single app and they have no real use for a central authority. I don't mean to hate on it, it's a really great solution with a lot of features, but it would be nice to have something simpler for the more common use cases.
You just configure Identity to use JWT bearer tokens. In my case I'm using encrypted token, so depending on your use-case you may want to adjust the configuration:
// In Startup.ConfigureServices...
services.AddDefaultIdentity<ApplicationUser>(
options =>
{
// Configure password options etc.
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// Configure authentication
services.AddAuthentication(
opt =>
{
opt.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
TokenDecryptionKey =
new SymmetricSecurityKey(Encoding.UTF8.GetBytes("my key")),
RequireSignedTokens = false, // False because I'm encrypting the token instead
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
});
// Down in Startup.Configure add authn+authz middlewares
app.UseAuthentication();
app.UseAuthorization();
Then generate a token when the user wants to sign in:
var encKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("my key"));
var encCreds = new EncryptingCredentials(encKey, SecurityAlgorithms.Aes256KW, SecurityAlgorithms.Aes256CbcHmacSha512);
var claimsIdentity = await _claimsIdentiyFactory.CreateAsync(user);
var desc = new SecurityTokenDescriptor
{
Subject = claimsIdentity,
Expires = DateTime.UtcNow.AddMinutes(_configuration.Identity.JwtExpirationMinutes),
Issuer = _configuration.Identity.JwtIssuer,
Audience = _configuration.Identity.JwtAudience,
EncryptingCredentials = encCreds
};
var token = new JwtSecurityTokenHandler().CreateEncodedJwt(desc);
// Return it to the user
You can then use the UserManager to handle creating new users and retrieving users, while SignInManager can be used to check for valid login/credentials before generating the token.
I have two .net core 2.2 projects; the first one is an MVC project which is like presentation layer that user can login and the other Web API that handles DB operations. I want to handle login request in Web API and return the login result to MVC project by using MVC Core Identity. My DbContext is in the Web API project. Is there anyway to create identity cookie according to result of Web API request?
In this situation the authentication should be handled with an access token rather than a cookie.
For your Web API, implement Resource Owner Password Credentials grant type of OAuth2 protocol with the help of IdentityServer 4 library. At the end you should be able to get an access token from the API in exchange of login credentials.
For your MVC project, create a table to store token-sessid pairs, so when the MVC application gets an access token from the API during a session, it will save them in the table. For the subsequent requests the MVC app will get the token from the table (by using the sessid) and use it to access the Web API.
First in the startup class of your MVC on the server side
add->
services.AddAuthentication(options => {
options.DefaultScheme = "Cookies";
}).AddCookie("Cookies", options => {
options.Cookie.Name = "auth_cookie";
options.Cookie.SameSite = SameSiteMode.None;
options.Events = new CookieAuthenticationEvents
{
OnRedirectToLogin = redirectContext =>
{
redirectContext.HttpContext.Response.StatusCode = 401;
return Task.CompletedTask;
}
};
});
Then in your Login Controller of MVC
[HttpPost]
public async Task<IActionResult> Login(string username, string password)
{
if (!IsValidUsernameAndPasswod(username, password))
return BadRequest();
var user = GetUserFromUsername(username);
var claimsIdentity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, user.Username),
//...
}, "Cookies");
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
await Request.HttpContext.SignInAsync("Cookies", claimsPrincipal);
return NoContent();
}
Notice that we are referencing the “Cookies” authentication scheme we’ve defined in Startup.cs.
Afterwards on your web api->
CookieContainer cookieContainer = new CookieContainer();
HttpClientHandler handler = new HttpClientHandler
{
CookieContainer = cookieContainer
};
handler.CookieContainer = cookieContainer;
var client = new HttpClient(handler);
var loginResponse = await client.PostAsync("http://yourdomain.com/api/account/login?username=theUsername&password=thePassword", null);
if (!loginResponse.IsSuccessStatusCode){
//handle unsuccessful login
}
var authCookie = cookieContainer.GetCookies(new Uri("http://yourdomain.com")).Cast<Cookie>().Single(cookie => cookie.Name == "auth_cookie");
//Save authCookie.ToString() somewhere
//authCookie.ToString() -> auth_cookie=CfDJ8J0_eoL4pK5Hq8bJZ8e1XIXFsDk7xDzvER3g70....
This should help you achieve your Task. Of course change values based on your requirements, but thus code would be a good reference point.
Also add the required code to set cookies from your web api application.
Hope it helps!
I am not sure I’m fully up to speed with your problem statement. I am assuming that you’re after some sort of mechanism that enables a user to pass their identity all the way from web client to your DbContext.
I also assume you then don’t really authenticate users on the MVC app and basically proxy their requests onto WebAPI.
If so, you might want to consider crafting a JWT (preferably, signed) token as your WebAPI response and then storing it on the client (I guess cookie is a good enough mechanism).
Then MVC project will naturally get the token by virtue of Session State and all you will have to do would be to pass it along with every WebAPI request you make.
1. Only two services
If your system has only two services (front and back) you could use Cookies for all your authentication schemes in your front and consume your api for user validation.
Implement the login page in your web app and verify the users from your login action method (post) calling a backend endpoint (your api) where you can validate the credentials against your database. Note that you do not need to publish this endpoint in internet.
ConfigureServices:
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.LoginPath = "/auth/login";
options.LogoutPath = "/auth/logout";
});
AuthController:
[HttpPost]
public IActionResult Login([FromBody] LoginViewModel loginViewModel)
{
User user = authenticationService.ValidateUserCredentials(loginViewModel.Username, loginViewModel.Password);
if (user != null)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, user.UserName),
new Claim(ClaimTypes.Role, user.Role),
new Claim(ClaimTypes.Email, user.Email)
};
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var principal = new ClaimsPrincipal(identity);
await HttpContext.SignInAsync(principal);
return Redirect(loginViewModel.ReturnUrl);
}
ModelState.AddModelError("LoginError", "Invalid credentials");
return View(loginViewModel);
}
2. OAuth2 or OpenId Connect dedicated server
But if you want to implement your own authorization service or idenitity provider which can be used by all your applications (both front and back) I would recommend creating your own server with a standard like OAuth2 or OpenId. This service should be dedicated exclusively for this purpose.
If your services are net core you could use IdentityServer. It is a middleware that is certified by OpenIdConnect and is very complete and extensible. You have extensive documentation and it's easy to implement for both OAuth2 and OpenId. You could add your dbContext to use your user model.
Your Web app ConfigureServices:
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = "https://myauthority.com";
options.ClientId = "client";
options.ClientSecret = "secret";
options.SaveTokens = true;
options.Scope.Clear();
options.Scope.Add("myapi");
// ...
}
Your Identity Provider ConfigureServices:
services.AddIdentityServer()
.AddInMemoryClients(Config.GetClients())
.AddInMemoryApiResources(Config.GetApis())
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = builder =>
builder.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
})
.AddDeveloperSigningCredential();
In this way your fronts would request access to the scopes necessary to access the apis. The front would receive an access token with this scope (if this client is allowed for the requested scopes). These apis in turn could validate the access token with a validation middleware like the following:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://myauthority.com";
options.Audience = "myapi";
});
3. Custom remote handler
If you still prefer to implement something remote by yourself you can implement your custom RemoteAuthenticationHandler. This abstract class helps you to redirect to a remote login service (your api) and handles results on callback redirections with the authorization result in your web app. This result is used to populate the user ClaimsPrincipal and if you configure your web app authentication services in this way you could maintain the user session in a Cookie:
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = "CustomScheme";
})
.AddCookie()
.AddRemoteScheme<CustomRemoteAuthenticationOptions, CustomRemoteAuthenticationHandler>("CustomScheme", "Custom", options =>
{
options.AuthorizationEndpoint = "https://myapi.com/authorize";
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.SaveTokens = true;
options.CallbackPath = "/mycallback";
});
You can see the remote handlers OAuthHandler or OpenIdConnectHandler as a guide to implement yours.
Implementing your own handler (and handler options) could be cumbersome and insecure, so you should to consider the first options.
I have configured the MVC client by adding the following lines.
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer();
The error message was, as (kind of) expected, 401 Unauthorized. So I added config for the bearer as suggested by Microsoft.
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(_ =>
{
_.Authority = "http://localhost:5000";
_.Audience = "http://localhost:5002";
});
In my solution, port 5000 hosts the IDS4 provider and port 5002 hosts the MVC application. At that point I got an error because I'm running strictly HTTP for the moment. The suggestion was to take the security down a notch by setting RequireHttpsMetadata to false, which I did as shown below.
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(_ =>
{
_.Authority = "http://localhost:5000";
_.Audience = "http://localhost:5002";
_.RequireHttpsMetadata = false;
})
To my disappointment, I'm back on getting 401 Unauthorized in my browser when requesting the page under action decorated by [Authorize].
I'm not sure how to diagnoze it further. I'm trying to compare my code to gazillion of examples but fail to see any significant difference. Also, many exmaples regard other version of Core, IDS or scheme. I need advise on where the smell might be coming from.
From IdentityServer4 samples you can see that they are using AddOpenIdConnect and not AddJwtBearer for the MVC Client sample. Your MVC client service registration should then look like below:
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.ClientId = "mvc";
options.SaveTokens = true;
});
Lastly, make sure you have a client which has allowed scope to access your api resource and an appropriate grant type:
// OpenID Connect implicit flow client (MVC)
new Client
{
ClientId = "mvc",
ClientName = "MVC Client",
AllowedGrantTypes = GrantTypes.Implicit,
RedirectUris = { "http://localhost:5002/signin-oidc" },
PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile
}
}
AddOpenIdConnect basically preconfigures you the handler endpoints for callbacks from IDS4 to sign the user in and out as well as creates the appropriate ClaimsPrincipal.
I am in the process of integrating a simplefied authentication process into a asp.net core 2.1 application, where users are logging in via the UI by default, but there is also the possibility to aquire a token and call some secured api endpoints to retrieve some data needed for reporting.
The issue I am facing is, that with the default configuration everything works, but adding the token config throws some weird errors.
If I do not add AddCookie("Identity.External"), call to the onGet method at /Identity/Account/Login throws the exception
InvalidOperationException: No sign-out authentication handler is registered for the scheme 'Identity.External'. The registered sign-out schemes are: Identity.Application. Did you forget to call AddAuthentication().AddCookies("Identity.External",...)?
If I do not specify options.DefaultScheme = "Identity.Application"; the user is not successfully signed in.
If I do not add .AddCookie("Identity.External") and .AddCookie("Identity.TwoFactorUserId") the logout process throws the same exception as above.
For the login process, this is simply rectified by removing the line await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);. If I do not use external schemes I do not need to sign out of them, right?
This brings me to my problem: How can I disable external logins and multi factor authentication in Identity Core, so I do not have to add those cookies in the first place? Furthermore, why do I have to specifiy a cookie named "Identity.Application", which is not the case in the default configuration? I'm pretty sure this is just another issue of me not thoroughly understanding the problem at hand, so I am grateful for any clarification on this.
This is my Identity config from the Startup.cs I have also scaffolded out the complete Identity UI with a custom IdentityUser class.
var jwtAppSettingOptions = Configuration.GetSection(nameof(JwtIssuerOptions));
services.Configure<JwtIssuerOptions>(options =>
{
options.Issuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)];
options.Audience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)];
options.SigningCredentials = new SigningCredentials(_signingKey, SecurityAlgorithms.HmacSha256);
});
var tokenValidationParameters = new TokenValidationParameters
{
/*...*/
};
services.AddAuthentication(options =>
{
options.DefaultScheme = "Identity.Application";
})
//.AddCookie("Identity.External")
//.AddCookie("Identity.TwoFactorUserId")
.AddCookie("Identity.Application", opt =>
{
opt.SlidingExpiration = true;
})
.AddJwtBearer(options =>
{
options.ClaimsIssuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)];
options.TokenValidationParameters = tokenValidationParameters;
options.SaveToken = true;
});
var builder = services.AddIdentityCore<AppUser>(o =>
{
//removed
});
builder = new IdentityBuilder(builder.UserType, typeof(IdentityRole), builder.Services);
builder.AddEntityFrameworkStores<ApplicationDbContext>().AddDefaultTokenProviders();
I have an IDP (Identity Server 4) hosted with multiple bindings: auth.company1.com and auth.company2.com
I also have an API protected from that IDP. So in order to access the API I need to get the access token from the IDP. This is configured at startup class at the API level like this:
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = "https://auth.company1.com/";
options.RequireHttpsMetadata = true;
options.ApiName = "atb_api";
});
How can I configure options.Authority dynamically so it allows authority from multiple domains https://auth.company1.com/ and https://auth.company2.com/ ?
I solved this.
At the protecting API level at the startup class I have this configuration:
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = "https://shared-domain-for-every-tenant/";
options.RequireHttpsMetadata = true;
options.ApiName = "atb_api";
});
The magic happens at the IDP level (IdentityServer4), while configuring the IdentityServer I add the option IssuerUri like this:
services.AddIdentityServer(options => {
options.IssuerUri = "https://shared-domain-for-every-tenant/";
})..AddDeveloperSigningCredential() ...other configurations ...
When I navigate to https://auth.company1.com/.well-known/openid-configuration
the returned document is like this:
{
"issuer": "https://shared-domain-for-every-tenant/",
"jwks_uri": "https://auth.company1.com/.well-known/openid-configuration/jwks",
"authorization_endpoint": "https://auth.company1.com/connect/authorize",
"token_endpoint": "https://auth.company1.com/connect/token",
"userinfo_endpoint": "https://auth.company1.com/connect/userinfo",
...
}
Notice the issure is a static url while all the other endpoints are specific to the tenant that made the request. This allows the API to validate the access token and also have different endpoints for each tenant (I need this to show a different login screen for each of them).
Hope it helps someone out there :)