My system consists of two authorization steps (but not the standard way).
My client application first connects with server passing login and password (it's just something like ApiSecret and ApiKey).
Next, after authentication, server returns bearer token with basic info (username, roles etc). But notice that this user is like ApiClient not a living person :)
Next, application shows login form. And this is time for a living person to login. So he passes his credentials to API which checks whether this user can be logged in.
And this is the place that I have problem with. Until now I thought it would be work like that:
If the user can be logged to the application, I create new ClaimsIdentity and ADD it to ClaimsPrincipal Identities.
The idea is great but it doesn't work :/ It turns out that next requests don't send this second identity information. I even know why. Becase ClaimsPrincipal is created based on received bearer token. But this knowledge doesn't solve my problem.
What should I do to add new ClaimsIdentity to existing ClaimsPrincipal and store this value between requests? (until user logges out of the application)
After a lot of digging and research I was able to create a solution (.Net Core 2).
You have to add Cookie Authentication in configure services and all of this should look like that:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(cfg =>
{
//standard settings
})
.AddCookie(AuthTypes.CLIENT_AUTHENTICATION_TYPE, cfg =>
{
//cookie settings; the most important is following event:
cfg.Events.OnValidatePrincipal = (CookieValidatePrincipalContext ctx) =>
{
ClaimsPrincipal mainUser = ctx.HttpContext.User; //get ClaimsPrincipal from JwtBearer
ClaimsPrincipal cookieUser = ctx.Principal; //get ClaimsPrincipal read from Cookie
Debug.Assert(mainUser.Identities.Count() == 1);
//now we have to add ClaimsIdentity to main ClaimsPrincipal (from JwtBearer). We add only those absent in main ClaimsPrincipal (here is simplified solution)
var claimsToAdd = cookieUser.Identities.Where(id => id.AuthenticationType != mainUser.Identities.ElementAt(0).AuthenticationType);
mainUser.AddIdentities(claimsToAdd);
return Task.CompletedTask;
};
}
);
AuthTypes.CLIENT_AUTHENTICATION_TYPE - it's just string with your authentication type name.
Next we have to configure default policy filter later on in ConfigureServices (basic configuration):
services.AddMvc(config =>
{
var defaultPolicy = new AuthorizationPolicyBuilder(new[] { JwtBearerDefaults.AuthenticationScheme, AuthTypes.CLIENT_AUTHENTICATION_TYPE })
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(defaultPolicy));
});
What's important here is to pass this array in AuthorizationPolicyBuilder.
Now Authorization will take under consideration JwtBearer, but cookie will also be read.
And now how to set the cookie at all. This can be additional login process (you do it at the controller level):
var authProps = new AuthenticationProperties
{
IsPersistent = true,
IssuedUtc = DateTimeOffset.Now
};
await HttpContext.SignInAsync(AuthTypes.CLIENT_AUTHENTICATION_TYPE, User, authProps);
User here is just ClaimsPrincipal with additional ClaimsIdentities.
And that's all folks :)
Related
I have a React project with API (net core). My website menus/fields will be shown/hidden based on the Role of the user. The user will login to my website via external oidc.
However, the access_token and id_token coming from the oidc doesn't have the Role information, it will only have their email, which I will use to check against my Database to determine which Role is the logged in user. Currently I have an API to get Role based on their access_token, so it's something like
public string getRoles(string access_token)
{
//check Database
return role;
}
This function will be called in almost every page so I was wondering is there any more efficient way to do this?
You need to add the claim to the HttpContext.User when the signin is confirmed with the role from the DB. When you define this connection in your startup, be sure to handle the OnTokenValidated event.
.AddOpenIdConnect("oidc", options =>
{
options.Events = new OpenIdConnectEvents
{
OnTokenValidated = async ctx =>
{
var claim = new Claim("Role", "TheirRole");
var identity = new ClaimsIdentity(new[] { claim });
ctx.Principal.AddIdentity(identity);
await Task.CompletedTask;
}
};
}
Then you can access this within the controller (or anywhere with HttpContext) like so
var claim = HttpContext.User.Claims.First(c => c.Role == "TheirRole");
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 JWTs that are included in all the calls to my service. The first one (called UserJwt) is the one that I want to be used for [Authorize] for most service operations. The second one (called ApplicationJwt) has useful information that I would also like to be in the User.Identities list of ClaimsIdentity objects.
I setup my code so that the UserJwt is always used and the ApplicationJwt is ignored:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
// Setup the JWT stuff here for the UserJwt
})
This works fine, but but I want both JWTs parsed and put into the User.Identities list.
I tried setting up my code to follow this answer, (to the question: Use multiple JWT Bearer Authentication) but this allows either one (UserJwt or ApplicationJwt).
At worst I need it to require both. However, I prefer it to only require UserJwt but include ApplicationJwt as a ClaimsIdentity if found.
services.AddAuthentication()
.AddJwtBearer("UserJwt", options =>
{
// Setup the JWT stuff here for the UserJwt
})
.AddJwtBearer("ApplicationJwt", options =>
{
// Setup the JWT stuff here for the ApplicationJwt
});
services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes("UserJwt", "ApplicationJwt")
.Build();
});
How can I get both JWTs to be in the User.Identities list, have the UserJwt be the one that is used for [Authorize], but also allow [Authorize(Policy = "ApplicationJwt")] on some service operations?
One option here is adding custom requirements to default policy.
Assume UserJwt contains specific claims:
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes("UserJwt", "ApplicationJwt")
.RequireAuthenticatedUser()
.AddRequirements(
// Assume UserJwt contains a AuthenticationMethod claim where
// value is equal to User
new ClaimsAuthorizationRequirement(
ClaimTypes.AuthenticationMethod, new[] { "User" }
)
)
.Build();
Also see other built-in requirement types.
Note that when requirement is not met, user is still authenticated but denied access. To handle forbidden scenario, use JwtBearerEvents.OnForbidden event:
OnForbidden = async context =>
{
context.Response.StatusCode = 401;
}
I have a problem enabling Facebook auth in my ASP.NET Core web app. I'm using ASP.NET Core Authentication but not Identity. The auth is configured in Startup like this:
services
.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
.AddFacebook(options =>
{
options.ClientId = "clientid";
options.ClientSecret = "secret";
options.CallbackPath = "/signinfacebookcallback";
});
As shown in the code, I want to use cookie auth, but also allow people to sign in with Facebook. Once they have been successfully signed in, I want to set the auth cookie.
To show the challenge, I have the following action:
[HttpGet]
[Route("signinfacebook")]
public ActionResult SignInFacebook()
{
return Challenge(FacebookDefaults.AuthenticationScheme);
}
This redirects the user to the Facebook login screen. Once they sign in, the user is redirected to the URL specified in config:
[HttpGet]
[Route("signinfacebookcallback")]
public async Task<ActionResult> SignInFacebookCallback()
{
var result = await HttpContext.AuthenticateAsync();
if (!result.Succeeded) return Redirect("/login/");
...
}
When I debug the code, result.Succeeded returns false and the AuthenticationResult object doesn't contain more information on why Succeeded is false.
I verified that the app id and secret are correct.
What could be the issue here?
The CallbackPath in the OpenID Connect middleware is internal path that are used for the authentication flow of the OpenID Connect protocol. After Identity provider redirect user to that url in your application , middeware will handle token valiation ,token decode,exchange token and finally fill the user principle , and that process is fired before your controller gets involved .
Since CallbackPath is internal and will handled by OpenID Connect middleware automatically , you don't need to care about , make sure the callback is registered in facebook's allowed redirect url and let middleware handle the callback .If you want to redirect user to specific route/page after authentication , put the url to AuthenticationProperties :
if (!User.Identity.IsAuthenticated)
{
return Challenge(new AuthenticationProperties() { RedirectUri = "/home/Privacy" } ,FacebookDefaults.AuthenticationScheme);
}
And you should remove the callback path route (signinfacebookcallback) in your application .
UPDATE
If you want to access database and manage local user , you can use built-in events in middleware, for AddFacebook middleware , you can use OnTicketReceived to add access database , manage users and add claims to user's princple :
.AddFacebook(options =>
{
options.ClientId = "xxxxxxxxxxxxx";
options.ClientSecret = "xxxxxxxxxxxxxxxxxxxx";
options.CallbackPath = "/signinfacebookcallback";
options.Events = new OAuthEvents
{
OnTicketReceived = ctx =>
{
//query the database to get the role
var db = ctx.HttpContext.RequestServices.GetRequiredService<YourDbContext>();
// add claims
var claims = new List<Claim>
{
new Claim(ClaimTypes.Role, "Admin")
};
var appIdentity = new ClaimsIdentity(claims);
ctx.Principal.AddIdentity(appIdentity);
return Task.CompletedTask;
},
};
});
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.