AspNetCore OpenIdConnect Error Handling - asp.net-core-mvc

In my MVC Core project, after authenticating against Azure AD, I check to see if the User exists in my application database. If User does not exist I want to throw an exception and redirect to my Home/Error page.
Instead, the redirect code in OnAuthenticationFailed results in a redirect loop and finally quits with the error:
Bad Request - Request Too Long HTTP Error 400. The size of the request
headers is too long.
app.UseExceptionHandler("/Home/Error");
app.UseCookieAuthentication();
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
...
Events = new OpenIdConnectEvents()
{
OnAuthorizationCodeReceived = async (context) =>
{
...
upn = identity.FindFirst(ClaimTypes.Upn).Value;
MyDbContext db =
new MyDbContext(Configuration.GetConnectionString("DefaultConnection"));
if (db.Users.FirstOrDefault(b => (b.UPN == upn)) == null)
{
throw new System.IdentityModel.Tokens.SecurityTokenValidationException("You are not registered to use this application.");
}
},
OnAuthenticationFailed = (context) =>
{
context.Response.Redirect("/Home/Error");
context.HandleResponse();
return Task.FromResult(0);
}
}
});
UPDATE:
This has been resolved. By default the HomeController has [Authroize] attribute set on the entire class so the redirect could not reach the Error action without being authenticated.

Related

AJAX call to an Action with Authorize attribute in .NET Core 3.1

On my pet project (a lyrics website), I wish to add "like" functionality, like this:
Code is open source (here's my current branch). A click on the heart icon should add a like to the databse for the logged in user, and if the user isn't logged in, it should redirect to the login page (IdentityServer 4, separate project and domain).
Controller Action:
[Authorize]
[Route("lyrics/like/{lyricId}")]
public async Task<IActionResult> Like(
int lyricId)
{
try
{
string userId = User.GetUserId().ToString();
await _lyricsService.LikeLyricAsync(userId, lyricId);
return RedirectToAction("Index", "Home");
}
catch
{
return RedirectToAction("Index", "Home");
}
}
JavaScript on the View:
<script>
docReady(function () {
let likeBtn = document.getElementById('like-btn');
let likeLyric = (event) => {
event.preventDefault();
console.log('attemping to like a lyric!');
// 1. create a new XMLHttpRequest object
let request = new XMLHttpRequest();
// 2. configure the request
request.open('GET', 'https://localhost:5001/lyrics/like/#Model.Id');
// 3. send the request over the network
request.send();
// 4. this will be called after the response is received
request.onload = function () {
if (request.status != 200) {
// analyse http status of the response
alert(`Error ${request.status}: ${request.statusText}`);
} else {
// show the result
alert(`Done, got ${request.response.length} bytes`); // response is the server response
}
};
request.onprogress = function (event) {
if (event.lengthComputable) {
alert(`Received ${event.loaded} of ${event.total} bytes`);
} else {
alert(`Received ${event.loaded} bytes`); // no Content-Length
}
};
request.onerror = function () {
alert("Request failed");
};
}
likeBtn.addEventListener('click', likeLyric);
});
</script>
I tried to expand on the request.onload function by adding an:
else if (request.status === 302) {
window.location = request.response;
}
But it doesn't seem to get to that, the .send() fails. What am I doing wrong here?
Here's a screen grab of what is happening:
The error is:
attemping to like a lyric!
govenda-sera:1 Access to XMLHttpRequest at 'https://localhost:5006/connect/authorize?client_id=bejebeje-mvc-local&redirect_uri=https%3A%2F%2Flocalhost%3A5001%2Fsignin-oidc&response_type=code&scope=openid&code_challenge=2mUDM3-gR1jhn7E2EY7T17FkPTHikE8v-KQOBMskazM&code_challenge_method=S256&response_mode=form_post&nonce=637437449511000684.OWQ3MTM4MjItOTJhOS00YjgzLTk1OTYtYWE2ZGUyMzRlYzUyOWE1MTkwNjgtNzI2YS00OWJjLTgzYjAtOTY1MDQ1ZDU3YzE1&state=CfDJ8DxKnFiqfK1HscY3j3s4hc-YvLoUa_X_46X1CclU7U-RahgrNQULQOLJu6943zTWCYa5Q5acO7g7vx03ddXSOOKkUtxZQAMHSgnQHFzBvhXnoC2i6yS0PpGxns7oA7tuvcgnp-jxub7RePZl5QAe5BwfXWkyHtMkFAmTkuultwz5w-Duenyb4KNrZRk1RLn6TLL93BS6YfIfoozorOnvKel4cFFjxIc7F_QXgVFKZm6ud5lN2nItw5WhkDfU6qMHhUUSQXQRJqWSit4CW_1hPpbHZhJmatXWxD8mLVFcSEKMNQz2UIU00RDxBCQW09Skuy3Uoz50Vwp4dEYPtNIcolIKrLn1pJguNsYRWBw391uWO7rMy9W5DPJV44fMVe8UR5xKNUarkelFX4CzHidF-rE&x-client-SKU=ID_NETSTANDARD2_0&x-client-ver=5.5.0.0' (redirected from 'https://localhost:5001/lyrics/like/938') from origin 'https://localhost:5001' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
govenda-sera:131 GET https://localhost:5006/connect/authorize?client_id=bejebeje-mvc-local&redirect_uri=https%3A%2F%2Flocalhost%3A5001%2Fsignin-oidc&response_type=code&scope=openid&code_challenge=2mUDM3-gR1jhn7E2EY7T17FkPTHikE8v-KQOBMskazM&code_challenge_method=S256&response_mode=form_post&nonce=637437449511000684.OWQ3MTM4MjItOTJhOS00YjgzLTk1OTYtYWE2ZGUyMzRlYzUyOWE1MTkwNjgtNzI2YS00OWJjLTgzYjAtOTY1MDQ1ZDU3YzE1&state=CfDJ8DxKnFiqfK1HscY3j3s4hc-YvLoUa_X_46X1CclU7U-RahgrNQULQOLJu6943zTWCYa5Q5acO7g7vx03ddXSOOKkUtxZQAMHSgnQHFzBvhXnoC2i6yS0PpGxns7oA7tuvcgnp-jxub7RePZl5QAe5BwfXWkyHtMkFAmTkuultwz5w-Duenyb4KNrZRk1RLn6TLL93BS6YfIfoozorOnvKel4cFFjxIc7F_QXgVFKZm6ud5lN2nItw5WhkDfU6qMHhUUSQXQRJqWSit4CW_1hPpbHZhJmatXWxD8mLVFcSEKMNQz2UIU00RDxBCQW09Skuy3Uoz50Vwp4dEYPtNIcolIKrLn1pJguNsYRWBw391uWO7rMy9W5DPJV44fMVe8UR5xKNUarkelFX4CzHidF-rE&x-client-SKU=ID_NETSTANDARD2_0&x-client-ver=5.5.0.0 net::ERR_FAILED
likeLyric
You can't do an AJAX call to this URL to login the user:
https://localhost:5006/connect/authorize?....
If you want the user to login/authenticate, then you need to redirect the browser to that page.
Or better, don't show the heart icon if the user is not logged in, better to have a login to like button? The user might otherwise be surprised why he needs to login.
It is caused by cors, you need to enable cors in backend.
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options =>
{
options.AddPolicy(name: "AllowOrigins",
builder =>
{
builder.WithOrigins("http://example.com",
"http://www.contoso.com");
});
});
services.AddControllers();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
//...
app.UseRouting();
app.UseCors("AllowOrigins");
//...
}
}
In addition, can you switch to another browser to access correctly?

How to handle an unauthorized ajax call

I am trying to figure out how to prevent a cors error from showing up in developer tools. The way I get the cors error is when I am using an application but in another tab/window I log out of that application but then go back to the other tab and try to do work. Below is my ajax call.
function RemoveScholarshipRequest(id, name) {
if (confirm("Are you sure you want to delete the scholarship request for " + name + "?")) {
var dataSource = $('#Pending').data('kendoGrid').dataSource;
$.ajax({
type: "POST",
url: '#Url.Action("RemoveRequest", "Admin")',
data: {id: id}
}).done(function (response, data, xhr) {
if (response.success) {
dataSource.read();
alert(response.responseText);
}
else if (!response.success) {
if (response.responseText === "Not Authenticated")
alert(response.responseText);
console.log("error", data.status);
//This shows status message eg. Forbidden
console.log("STATUS: "+JSON.stringify(xhr.status));
}
}).fail(function (response) {
console.log(response);
console.log(JSON.stringify(response));
//window.location.href = "/forms/ScholarshipDisbursement/Admin/PendingRequests";
});
}
}
The controller action that the above ajax method calls is below:
[AllowAnonymous]
[HttpPost]
public ActionResult RemoveRequest(string id)
{
if (!User.Identity.IsAuthenticated)
{
return Json(new { success = false, responseText = "Not Authenticated" }, JsonRequestBehavior.AllowGet);
}
if (User.IsInRole("Developer") || User.IsInRole("BannerAdmin"))
{
new ScholarshipRequestStore().DeleteScholarshipRequest(id);
return Json(new { success = true, responseText = "Successfully deleted" }, JsonRequestBehavior.AllowGet);
}
else
{
return Json(new { success = false, responseText = "You are not an authorized user" }, JsonRequestBehavior.AllowGet);
}
}
One way I get around the cors error is by putting AllowAnonymous on the method and then checking for authentication in the method itself but I don't really like that idea. Is there another way of resolving this issue?
Allow anonymous will not solve this, instead you need to send the allow origin header in your api. You can do this by enabling CORs in the startup class as follows
public void ConfigureServices(IServiceCollection services)
{
// Add Cors
services.AddCors(o => o.AddPolicy("MyPolicy", builder =>
{
builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
}));
// Add framework services.
services.AddMvc();
services.Configure<MvcOptions>(options =>
{
options.Filters.Add(new CorsAuthorizationFilterFactory("MyPolicy"));
});
...
...
...
}
// This method gets called by the runtime. Use this method to configure
//the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env,
ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
// Enable Cors
app.UseCors("MyPolicy");
//app.UseMvcWithDefaultRoute();
app.UseMvc();
...
...
...
}
and then using the "Enable cors" attribute on your controller
[EnableCors("MyPolicy")]
[AllowAnonymous]
[HttpPost]
public ActionResult RemoveRequest(string id)
read this for better idea https://learn.microsoft.com/en-us/aspnet/core/security/cors?view=aspnetcore-2.2
Note: I have allowed any origin to talk to the API, you can specify whatever origin you want like "https://example.com"
AllowAnonymous won't resolve a "cross-origin" request. The issue you are getting is due to tabbed browsing within your browser having a shared store of authenticated sessions. When you log out in tab 1, the session cookie is removed and then tab 2 is no longer authenticated. This is why AllowAnonymous "works" because without a current authenticated session, you're an anonymous user.
CORS, on the other hand, is when you allow calls to http://myservice.com to come from a different host like http://myclient.com. Anonymous access won't have any impact on that.

When using an API route, return Http Response 401 instead of redirect to login page when not authorised

I'm building an ASP.NET Core 2.0 website using MVC and WebAPI to provide access to a series of microservices. Where a WebAPI controller requires a user to be authenticated and authorised (using the Authorize attribute), any unauthorised or not-logged in user gets the response back as the entire HTML for the MVC login page.
When unauthorised users access the API, I would like to return the HTTP status code 401 and its associated error message in the response, instead of an entire HTML page.
I've looked at a few existing questions and noticed that they either refer to ASP.NET MVC (such as SuppressDefaultHostAuthentication in WebApi.Owin also suppressing authentication outside webapi) which is no good for ASP.NET Core 2.0. Or they are using a hackaround for Core 1.x, which just doesn't seem right (ASP.Net core MVC6 Redirect to Login when not authorised).
Has a proper solution been implemented in Core 2.0 that anyone is aware of? If not, any ideas how it could be implemented properly?
For reference, there's part of a controller as an example:
[Authorize]
[ApiVersion("1.0")]
[Produces("application/json")]
[Route("api/V{ver:apiVersion}/Organisation")]
public class OrganisationController : Controller
{
...
[HttpGet]
public async Task<IEnumerable<string>> Get()
{
return await _organisationService.GetAllSubdomains();
}
...
}
And the configurations within Statup.cs:
public void ConfigureServices(IServiceCollection services)
{
...
// Add API version control
services.AddApiVersioning(options =>
{
options.ReportApiVersions = true;
options.AssumeDefaultVersionWhenUnspecified = true;
options.DefaultApiVersion = new ApiVersion(1, 0);
options.ErrorResponses = new DefaultErrorResponseProvider();
});
// Add and configure MVC services.
services.AddMvc()
.AddJsonOptions(setupAction =>
{
// Configure the contract resolver that is used when serializing .NET objects to JSON and vice versa.
setupAction.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
});
...
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
...
app.UseStatusCodePagesWithRedirects("/error/index?errorCode={0}");
app.UseStaticFiles();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
...
}
There is an easy way to suppress redirect to Login page for unathorized requests. Just add following call of ConfigureApplicationCookie extension method in your ConfigureServices:
services.ConfigureApplicationCookie(options =>
{
options.Events.OnRedirectToLogin = context =>
{
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
return Task.CompletedTask;
};
});
Or if you need custom error message in response body:
services.ConfigureApplicationCookie(options =>
{
options.Events.OnRedirectToLogin = async context =>
{
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
await context.Response.WriteAsync("Some custom error message if required");
};
});
As far as you're using redirects to custom error pages for error codes (UseStatusCodePagesWithRedirects() call in Configure method), you should add filter for 401 error. To achieve this, remove call to UseStatusCodePagesWithRedirects and use UseStatusCodePages extension method with skip of redirect for Unauthorized code:
//app.UseStatusCodePagesWithRedirects("/error/index?errorCode={0}");
app.UseStatusCodePages(context =>
{
if (context.HttpContext.Response.StatusCode != (int)HttpStatusCode.Unauthorized)
{
var location = string.Format(CultureInfo.InvariantCulture, "/error/index?errorCode={0}", context.HttpContext.Response.StatusCode);
context.HttpContext.Response.Redirect(location);
}
return Task.CompletedTask;
});
If you're using JWT for authentication with an ASP.NET Core 2 API; you can configure the unauthorized response when you're configuring the services for Authentication & JWT:
services.AddAuthentication( JwtBearerDefaults.AuthenticationScheme )
.AddJwtBearer(options => options.Events = new JwtBearerEvents()
{
OnAuthenticationFailed = c =>
{
c.NoResult();
c.Response.StatusCode = 401;
c.Response.ContentType = "text/plain";
return c.Response.WriteAsync("There was an issue authorizing you.");
}
});

Openiddict multiple refresh tokens

How do you create custom provider for openiddict in Asp.net core to allow multiple refresh tokens? This way if the user logs in from their computer and then goes home and logs in on their phone, they don't have to login each time they get on to a different device. The app.UseOAuthValidation() runs in the background before the authorize controller ever gets called so there is no handle to verify if more than 1 refresh token matches. Another issue is that I am using this:
services.AddDbContext<ApplicationDbContext>(options => {
options.UseMySql(Configuration.GetConnectionString("DefaultConnection"))
.UseOpenIddict();
});
So I do not have access to the openiddict tables via DbContext to do this manually.
Startup.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.EntityFrameworkCore;
using DPInventoryPOAPI.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using OpenIddict.Core;
using OpenIddict.Models;
using System.Threading;
using System.Linq;
namespace DPInventoryPOAPI
{
public class Startup
{
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
}
public IConfigurationRoot Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy",
builder => builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials() );
});
services.AddMvc();
services.AddDbContext<ApplicationDbContext>(options => {
options.UseMySql(Configuration.GetConnectionString("DefaultConnection"))
.UseOpenIddict();
});
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddOpenIddict()
.AddEntityFrameworkCoreStores<ApplicationDbContext>()
.AddMvcBinders()
.EnableTokenEndpoint("/token")
.AllowPasswordFlow()
.AllowRefreshTokenFlow()
.DisableHttpsRequirement()
.AddEphemeralSigningKey();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime applicationLifetime, ILoggerFactory loggerFactory)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
//app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseCors("CorsPolicy");
app.UseIdentity();
app.UseOpenIddict();
app.UseOAuthValidation();
app.UseMvcWithDefaultRoute();
//SeedDatabase(app);
}
}
}
And authorize controller
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Extensions;
using AspNet.Security.OpenIdConnect.Primitives;
using AspNet.Security.OpenIdConnect.Server;
using AuthorizationServer.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using OpenIddict.Core;
using OpenIddict.Models;
// For more information on enabling MVC for empty projects, visit http://go.microsoft.com/fwlink/?LinkID=397860
namespace AuthorizationServer.Controllers {
public class AuthorizationController : Controller {
private readonly OpenIddictApplicationManager<OpenIddictApplication> _applicationManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly UserManager<ApplicationUser> _userManager;
public AuthorizationController(
OpenIddictApplicationManager<OpenIddictApplication> applicationManager,
SignInManager<ApplicationUser> signInManager,
UserManager<ApplicationUser> userManager) {
_applicationManager = applicationManager;
_signInManager = signInManager;
_userManager = userManager;
}
[HttpPost("~/connect/token"), Produces("application/json")]
public async Task<IActionResult> Exchange(OpenIdConnectRequest request) {
Debug.Assert(request.IsTokenRequest(),
"The OpenIddict binder for ASP.NET Core MVC is not registered. " +
"Make sure services.AddOpenIddict().AddMvcBinders() is correctly called.");
if (request.IsPasswordGrantType()) {
var user = await _userManager.FindByNameAsync(request.Username);
if (user == null) {
return BadRequest(new OpenIdConnectResponse {
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The username/password couple is invalid."
});
}
// Ensure the user is allowed to sign in.
if (!await _signInManager.CanSignInAsync(user)) {
return BadRequest(new OpenIdConnectResponse {
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The specified user is not allowed to sign in."
});
}
// Reject the token request if two-factor authentication has been enabled by the user.
if (_userManager.SupportsUserTwoFactor && await _userManager.GetTwoFactorEnabledAsync(user)) {
return BadRequest(new OpenIdConnectResponse {
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The specified user is not allowed to sign in."
});
}
// Ensure the user is not already locked out.
if (_userManager.SupportsUserLockout && await _userManager.IsLockedOutAsync(user)) {
return BadRequest(new OpenIdConnectResponse {
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The username/password couple is invalid."
});
}
// Ensure the password is valid.
if (!await _userManager.CheckPasswordAsync(user, request.Password)) {
if (_userManager.SupportsUserLockout) {
await _userManager.AccessFailedAsync(user);
}
return BadRequest(new OpenIdConnectResponse {
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The username/password couple is invalid."
});
}
if (_userManager.SupportsUserLockout) {
await _userManager.ResetAccessFailedCountAsync(user);
}
// Create a new authentication ticket.
var ticket = await CreateTicketAsync(request, user);
return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
}
else if (request.IsRefreshTokenGrantType()) {
// Retrieve the claims principal stored in the refresh token.
var info = await HttpContext.Authentication.GetAuthenticateInfoAsync(
OpenIdConnectServerDefaults.AuthenticationScheme);
// Retrieve the user profile corresponding to the refresh token.
var user = await _userManager.GetUserAsync(info.Principal);
if (user == null) {
return BadRequest(new OpenIdConnectResponse {
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The refresh token is no longer valid."
});
}
// Ensure the user is still allowed to sign in.
if (!await _signInManager.CanSignInAsync(user)) {
return BadRequest(new OpenIdConnectResponse {
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The user is no longer allowed to sign in."
});
}
// Create a new authentication ticket, but reuse the properties stored
// in the refresh token, including the scopes originally granted.
var ticket = await CreateTicketAsync(request, user, info.Properties);
return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
}
return BadRequest(new OpenIdConnectResponse {
Error = OpenIdConnectConstants.Errors.UnsupportedGrantType,
ErrorDescription = "The specified grant type is not supported."
});
}
private async Task<AuthenticationTicket> CreateTicketAsync(
OpenIdConnectRequest request, ApplicationUser user,
AuthenticationProperties properties = null) {
// Create a new ClaimsPrincipal containing the claims that
// will be used to create an id_token, a token or a code.
var principal = await _signInManager.CreateUserPrincipalAsync(user);
// Note: by default, claims are NOT automatically included in the access and identity tokens.
// To allow OpenIddict to serialize them, you must attach them a destination, that specifies
// whether they should be included in access tokens, in identity tokens or in both.
foreach (var claim in principal.Claims) {
// In this sample, every claim is serialized in both the access and the identity tokens.
// In a real world application, you'd probably want to exclude confidential claims
// or apply a claims policy based on the scopes requested by the client application.
claim.SetDestinations(OpenIdConnectConstants.Destinations.AccessToken,
OpenIdConnectConstants.Destinations.IdentityToken);
}
// Create a new authentication ticket holding the user identity.
var ticket = new AuthenticationTicket(principal, properties,
OpenIdConnectServerDefaults.AuthenticationScheme);
if (!request.IsRefreshTokenGrantType()) {
// Set the list of scopes granted to the client application.
// Note: the offline_access scope must be granted
// to allow OpenIddict to return a refresh token.
ticket.SetScopes(new[] {
OpenIdConnectConstants.Scopes.OpenId,
OpenIdConnectConstants.Scopes.Email,
OpenIdConnectConstants.Scopes.Profile,
OpenIdConnectConstants.Scopes.OfflineAccess,
OpenIddictConstants.Scopes.Roles
}.Intersect(request.GetScopes()));
}
return ticket;
}
}
}
How do you create custom provider for openiddict in Asp.net core to allow multiple refresh tokens? This way if the user logs in from their computer and then goes home and logs in on their phone, they don't have to login each time they get on to a different device.
OTB, OpenIddict allows you to retrieve multiple (independent) refresh tokens as long as they are requested using different grant_type=password requests. In your case, if the token retrieved by the mobile app is revoked (e.g manually or because it was already used), the refresh token used by the desktop app can still be used to retrieve new access/refresh tokens.
The app.UseOAuthValidation() runs in the background before the authorize controller ever gets called so there is no handle to verify if more than 1 refresh token matches.
The validation middleware never deals with refresh tokens, as it's only responsible of validating access tokens.
So I do not have access to the openiddict tables via DbContext to do this manually.
You can add a DbSet<OpenIddictToken> property in your DbContext or retrieve the DbSet<OpenIddictToken> via context.Set<OpenIddictToken>().

IdentityServer4 move login UI on the client

I am implementing IdentityServer4 with variety of clients, One of the clients is a Javascript application, I have implemented the implicit flow for authentication and everything is working fine.
On my Javascript application , I have a button to login, once I click on the button I am redirected to IdentityServer and after successful login I am redirected back to my application along with my access token.
Now what I want to do is, move the login to the client side so that each application can have its own login UI (with its own theme).
app.js
function log() {
document.getElementById('results').innerText = "";
Array.prototype.forEach.call(arguments, function (msg) {
if (msg instanceof Error) {
msg = "Error:" + msg.message;
}
else if (typeof msg !== 'string') {
msg = JSON.stringify(msg, null, 2);
}
document.getElementById('results').innerHTML += msg + "\r\n";
});
}
document.getElementById("login").addEventListener('click', login, false);
document.getElementById('api').addEventListener('click', api, false);
document.getElementById("logout").addEventListener("click", logout, false);
//configure client
var config = {
authority: "http://localhost:5000",
client_id: "js",
redirect_uri: "http://localhost:5004/callback.html",
response_type: "id_token token",
scope: "openid profile api1 role",
post_logout_redirect_uri: "http://localhost:5004/index.html"
};
//init user manager
var mgr = new Oidc.UserManager(config);
//check if user is logged in already
mgr.getUser().then(function (user) {
if (user) {
log("User logged in", user.profile);
} else {
log("User is not logged in.");
}
});
function login() {
mgr.signinRedirect();
}
function api() {
mgr.getUser().then(function (user) {
var url = "http://localhost:5001/identity/getfree";
var xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.onload = function () {
log(xhr.status, JSON.parse(xhr.responseText));
};
xhr.setRequestHeader("Authorization", "Bearer " + user.access_token);
xhr.send();
});
}
function logout() {
mgr.signoutRedirect();
}
IdentityServer StartUp.cs
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
var connectionString = "Server=localhost;port=3306;database=netcore;uid=root;Password=Liverpool1";
services.AddApplicationInsightsTelemetry(Configuration);
services.AddDbContext<ApplicationDbContext>(options => options.UseMySQL(connectionString));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddMvc();
// Add application services.
services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<ISmsSender, AuthMessageSender>();
services.AddIdentityServer()
.AddTemporarySigningCredential()
.AddInMemoryScopes(Config.GetScopes())
.AddInMemoryClients(Config.GetClients())
// .AddConfigurationStore(builder => builder.UseMySQL(connectionString))
//.AddOperationalStore(builder => builder.UseMySQL(connectionString))
.AddAspNetIdentity<ApplicationUser>();
}
This is not possible and breaks the whole point of implicit flow and all the other federated sign on flows. The whole point of implicit flow is that you do not pass user credentials through the client but rather it goes to the identity provider.
You have two options:
Finding out a way to serve up different logins per "tenant" in
ASP.NET Core.
Use Resource Owner flow and pass the user credentials
through the client.
Option 1 is probably the best but requires more work, option 2 is a cop out and using RO flow is an anti-pattern.

Resources