How to Get token from Duende Identity Server (IdentityServer4) - c#

There are a few days I am reading the Duende Identity server (IdentityServer4) so I know about the different concepts and their usages such as Scopes, Resources, Client ...
The area I am confused about it is the clients. So I integrated the AspIdentity as an ApplicationUser in the IdentityServer (you can find the configs below in the code sections) but when I want to call the /connect/token which is a pre-defined endpoint from Duende, it needs to add ClientId and Secret but I want to use Username and the password of my registered user.
So the idea that comes to my mind is to Create a custom endpoint: after validating the user's credentials using SignInManager then I will find the Users client and then sign in to the Duende IdentityServer however I tried to do that but it is a bit inconvenience way to have an HTTP-call again to the same service to get the token of the User.
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlite(connectionString));
builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
builder.Services.AddSwaggerGen();
builder.Services
.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
options.EmitStaticAudienceClaim = true;
})
.AddAspNetIdentity<ApplicationUser>()
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = b =>
b.UseSqlite(connectionString, dbOpts => dbOpts.MigrationsAssembly(typeof(Program).Assembly.FullName));
})
.AddOperationalStore(options =>
{
options.ConfigureDbContext = b =>
b.UseSqlite(connectionString, dbOpts => dbOpts.MigrationsAssembly(typeof(Program).Assembly.FullName));
options.EnableTokenCleanup = true;
options.RemoveConsumedTokens = true;
});
builder.Services.AddAuthentication();
if I can solve this issue in a convenient way so the other steps are very obvious and straightforward.

The clientID and secrets are meant to identify the application that wants to connect to IdentityServer and not the user; why can you not use clientID/secret for what it is intended for?
Also, the main purpose of OpenID connect is to not let the client application ever touch or see the user's username/password. That is why we delegate the authentication to IdentityServer.

Related

C# How to share authentication/authorization cookie between projects?

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");

ASP.NET Core MVC AzureAD run code after successful sign-in

I'm using I have authentication with AzureAD set up using OpenID Connect with an ASP.NET Core 2.2 MVC app. I want to be able to log info about the user right after a successful sign-in into the application.
In the code below, I use options.Events.OnTicketReceived event to add role claims from an external database to the Identity after they have successfully authenticated with AzureAD. However, my research tells me that the OnTicketReceived event is called right before the user is signed-in, that is, right before the local cookie is created and the user is authenticated with the app (Source: post1, post2). This would mean that the user hasn't been authenticated with the app yet, and I don't know if after the OnTicketReceived event the user is guaranteed to be authenticated.
All of this makes me wonder is it correct to log my "user signed in" message on the OnTicketReceived event or is there another way of doing it? I am also unsure if it is in Startup.cs that I should be logging this info.
Startup.cs > ConfigureServices method
public void ConfigureServices(IServiceCollection Services)
{
//https://weblog.west-wind.com/posts/2017/dec/12/easy-configuration-binding-in-aspnet-core-revisited
IAppSettings config = new AppSettings();
Configuration.Bind("AppSettings", config);
Services.AddMvc();
Services.AddSingleton(config);
//bunch of service dependencies here...
Services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
Services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
.AddAzureAD(options =>
{
options.Instance = config.AzureAD.Instance;
options.Domain = config.AzureAD.Domain;
options.TenantId = config.AzureAD.TenantId; //TenantID is set to "organizations"
options.ClientId = config.AzureAD.ClientId; //the ID of the registered app in AzureAD
options.CallbackPath = config.AzureAD.CallbackPath;
});
Services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
options.UseTokenLifetime = false;
options.Authority = options.Authority + "/v2.0/"; //Microsoft identity platform
options.TokenValidationParameters.ValidateIssuer = true;
options.TokenValidationParameters.ValidIssuers = config.AzureAD.Organizations; //List<string> of allowed organizations
// https://stackoverflow.com/questions/49469979/azure-ad-b2c-user-identity-name-is-null-but-user-identity-m-instance-claims9
// https://stackoverflow.com/questions/54444747/user-identity-name-is-null-after-federated-azure-ad-login-with-aspnetcore-2-2
options.TokenValidationParameters.NameClaimType = "http://schemas.microsoft.com/identity/claims/objectidentifier";
//Code below is to add claims during login (we add roles from db): https://stackoverflow.com/questions/51965665/adding-custom-claims-to-claimsprincipal-when-using-addazureadb2c-in-mvc-core-app
//some discussion about this here: https://stackoverflow.com/questions/59564952/net-core-add-claim-after-azuerad-authentication
//and here: https://stackoverflow.com/questions/52727146/net-core-2-openid-connect-authentication-and-multiple-identities
options.Events.OnTicketReceived = context =>
{
string userEmail = context.Principal.FindFirstValue(ClaimTypes.Email);
//I get an instance of UserAccess service so I can check termination status and get the roles by email.
IUserAccess userAccess = context.HttpContext.RequestServices.GetService<IUserAccess>();
bool terminated = userAccess.GetUserTerminationStatusByUserEmail(userEmail, config.ConnectionString);
if (terminated == false)
{
ClaimsIdentity claimsIdentity = (ClaimsIdentity)context.Principal.Identity;
List<string> roles = userAccess.GetUserRolesByUserEmail(userEmail, config.ConnectionString);
foreach (var role in roles)
{
claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, role));
}
}
return Task.CompletedTask;
};
});
Services.Configure<CookieAuthenticationOptions>(AzureADDefaults.CookieScheme, options =>
{
options.AccessDeniedPath = "/UserAccess/NotAuthorized";
options.LogoutPath = "/UserAccess/SignOut";
options.ExpireTimeSpan = TimeSpan.FromMinutes(config.AzureAD.TimeoutInMinutes);
options.SlidingExpiration = true;
});
}
To sign-in and sign-out, I use the existing functions from Microsoft.AspNetCore.Authentication.AzureAD.UI NuGet package, and call them in my views like this:
<!-- to sign out -->
<a asp-area="AzureAD" asp-controller="Account" asp-action="SignOut">Sign out</a>
<!-- to sign in -->
<a asp-area="AzureAD" asp-controller="Account" asp-action="SignIn">Sign in with Microsoft</a>
As you said, OnTicketReceived is called when clicking on the sign-in button, but what you want are the successful sign-ins.
You will see the sign-ins and Audit logs from Azure AD in the portal.
Sign-ins – Information about the usage of managed applications and user sign-in activities.
Audit logs - Audit logs provide system activity information about users and group management, managed applications, and directory activities.
So you don't need to add logs using NLog, the sign-in logs are automatically stored.
For more details about listing sign-ins, see here.
Code Sample:
// Read application settings from appsettings.json (tenant ID, app ID, client secret, etc.)
AppSettings config = AppSettingsFile.ReadFromJsonFile();
// Initialize the client credential auth provider
IConfidentialClientApplication confidentialClientApplication = ConfidentialClientApplicationBuilder
.Create(config.AppId)
.WithTenantId(config.TenantId)
.WithClientSecret(config.ClientSecret)
.Build();
ClientCredentialProvider authProvider = new ClientCredentialProvider(confidentialClientApplication);
// Set up the Microsoft Graph service client with client credentials
GraphServiceClient graphClient = new GraphServiceClient(authProvider);
// status/errorCode eq '0' means sign-in success
var signIns = await graphClient.AuditLogs.SignIns
.Request()
.Filter("status/errorCode eq '0'")
.GetAsync();

authenticate MVC website user from Web API by using MVC Core Identity

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.

SSO with identityserver4

I have to implement SSO with identitiserver4 (OAuth server).
I'm quite about authentication standards so before start working on it I make some research over the internet and I discovered that SSO is basically a social login where the provider is not a social network.
I read that identityserver4 implements oidc with which use the SSO.
In the Startup.cs file I added this:
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
})
.AddOpenIdConnect("oidc", options =>
{
//options.SignInScheme = "Cookies";
options.Authority = "<url-sso-server>";
options.ClientId = "<client-id>";
options.SaveTokens = true;
});
Is this settings enough? Whenever I will have to add a new SSO provider I just have to ask them for their url and clientId ? Of course, they will have to whitelist my redirect url /signin-oidc
Besides this I'm having trouble intercepting the redirect callback, I can't read any provider info:
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null)
{
return RedirectToAction(nameof(Login));
}
This always provide a null info.
Any advice?

How to disable External Logins in Identity Core?

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();

Categories

Resources