How to redirect already authorized user back to the Previous page in identity server 4 .NET CORE? - asp.net-core

Below is the code added in Auth Server and Client machine
If any more info needed on this please comment.
StartUp.cs -- Auth Server
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
var builder = services.AddIdentityServer(options =>
{
options.EmitStaticAudienceClaim = true;
})
.AddInMemoryIdentityResources(Config.IdentityResources)
.AddInMemoryApiScopes(Config.ApiScopes)
.AddInMemoryClients(Config.Clients)
.AddResourceOwnerValidator<ResourceOwnerPasswordValidator>()
/*.AddProfileService<ProfileService>()*/;
builder.AddDeveloperSigningCredential();
services.AddTransient<IResourceOwnerPasswordValidator, ResourceOwnerPasswordValidator>();
//services.AddTransient<IProfileService, ProfileService>();
}
StartUp.cs -- Client App
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Authority = "https://localhost:44356/";
options.ClientId = "TestIdpApp";
options.ResponseType = "code";
//options.UsePkce = false;
//options.CallbackPath = new PathString("...")
options.Scope.Add("openid");
options.Scope.Add("profile");
options.SaveTokens = true;
options.ClientSecret = "secret";
});
}
ResourceOwnerPasswordValidator.cs -- Auth Server
public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
{
public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
{
try
{
UserInfo user = await GetUserDetails(context.UserName, context.Password).ConfigureAwait(false);
if (user != null && string.IsNullOrEmpty(user.ErrorMessage))
{
//set the result
context.Result = new GrantValidationResult(
subject: user.UserId.ToString(),
authenticationMethod: "custom",
claims: GetUserClaims(user));
return;
}
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, user.ErrorMessage);
return;
}
catch (Exception ex)
{
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Invalid username or password");
}
}
//build claims array from user data
public static Claim[] GetUserClaims(UserInfo user)
{
return new Claim[]
{
new Claim("user_id", user.UserId.ToString() ?? ""),
new Claim(JwtClaimTypes.Name, (!string.IsNullOrEmpty(user.FirstName) && !string.IsNullOrEmpty(user.Surname)) ? (user.FirstName + " " + user.Surname) : ""),
new Claim(JwtClaimTypes.GivenName, user.FirstName ?? ""),
new Claim(JwtClaimTypes.FamilyName, user.Surname ?? ""),
new Claim(JwtClaimTypes.Email, user.Email ?? "")
};
}
}
Login Logic ---
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginInputModel model, string button)
{
// check if we are in the context of an authorization request
var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl);
if (button == "reset")
{
return Redirect("ResetPasswordWithoutCode");
}
else
{
// the user clicked the "cancel" button
if (button == "cancel")
{
if (context != null)
{
// if the user cancels, send a result back into IdentityServer as if they
// denied the consent (even if this client does not require consent).
// this will send back an access denied OIDC error response to the client.
await _interaction.DenyAuthorizationAsync(context, AuthorizationError.AccessDenied);
// we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
if (context.IsNativeClient())
{
// The client is native, so this change in how to
// return the response is for better UX for the end user.
return this.LoadingPage("Redirect", model.ReturnUrl);
}
return Redirect(model.ReturnUrl);
}
else
{
// since we don't have a valid context, then we just go back to the home page
return Redirect("~/");
}
}
if (ModelState.IsValid)
{
ResourceOwnerPasswordValidationContext context1 = new ResourceOwnerPasswordValidationContext();
context1.UserName = model.Username;
context1.Password = model.Password;
// validate username/password against in-memory store
await _resourceOwner.ValidateAsync(context1);
if (context1.Result.Subject!=null && context1.Result.Subject.Identity.IsAuthenticated)
{
var user = await ResourceOwnerPasswordValidator.GetUserDetails(model.Username,model.Password).ConfigureAwait(false);
await _events.RaiseAsync(new UserLoginSuccessEvent(user.Username, user.UserId, user.Username, clientId: context?.Client.ClientId));
// only set explicit expiration here if user chooses "remember me".
// otherwise we rely upon expiration configured in cookie middleware.
AuthenticationProperties props = null;
if (AccountOptions.AllowRememberLogin && model.RememberLogin)
{
props = new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration)
};
};
// issue authentication cookie with subject ID and username
var isuser = new IdentityServerUser(user.UserId)
{
DisplayName = user.Username
};
await HttpContext.SignInAsync(isuser, props);
if (context != null)
{
if (context.IsNativeClient())
{
// The client is native, so this change in how to
// return the response is for better UX for the end user.
return this.LoadingPage("Redirect", model.ReturnUrl);
}
// we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
return Redirect(model.ReturnUrl);
}
// request for a local page
if (Url.IsLocalUrl(model.ReturnUrl))
{
return Redirect(model.ReturnUrl);
}
else if (string.IsNullOrEmpty(model.ReturnUrl))
{
return Redirect("~/");
}
else
{
// user might have clicked on a malicious link - should be logged
throw new Exception("invalid return URL");
}
}
await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials", clientId: context?.Client.ClientId));
ModelState.AddModelError(string.Empty, AccountOptions.InvalidCredentialsErrorMessage);
}
// something went wrong, show form with error
var vm = await BuildLoginViewModelAsync(model);
return View(vm);
}
}
I'm calling a webapi to check the credentials and return info about user like First name last name.
RedirectUri - https://localhost:44356/Account/Login?ReturnUrl=%2Fconnect%2Fauthorize%2Fcallback%3Fclient_id%3DTestIdpApp%26redirect_uri%3Dhttps%253A%252F%252Flocalhost%253A44335%252Fsignin-oidc%26response_type%3Dcode%26scope%3Dopenid%2520profile%26code_challenge%3DLEZaLWC8ShzJz6LGUsdeUPr974clsUSYVPXWDNmbwOE%26code_challenge_method%3DS256%26response_mode%3Dform_post%26nonce%3D637360236231259886.MmIxNjlhODMtZTJhYy00YzUzLTliYjMtZWJmNzM3ZjRiM2VlZmYwYzI2MDAtNWRjYS00NThlLWI4MjAtY2ViYjgxY2RlYmZi%26state%3DCfDJ8LFJDL-5o71Ao2KksnBDgPVrH1DIIiM9LZSGUG43HRwLS6OjGUiGPwZ_xxT1RVryTZh7z3zwezVbdiy1L94mFlWausuYQrDNTWtzxrpTf2CrKjHRjcUIyNt5tX_g-yZYkWvxzCiyrpxnp7cctbNGoCmj_kqidhxCWsZee_26c3eVqfJfH7XEDfKUMj2BHeKQe_Ar9f2SkZJ0SBuy6MBe6zpU7-DDOYotDn-oO5zrtaHL8GCZfSqckqalL5yaGeolZ1ZDcubY01InyrBh1NwlVQRdGZRRWIZ-WnqqFKrTboQyw4rQswR-7BaLTtL8QitRkUmwS17LBLUvXKRBs8C0NUsX9HyREnmCVG2qW6s2AVpnE4iSt4XVSRcY-crXml2FjA%26x-client-SKU%3DID_NETSTANDARD2_0%26x-client-ver%3D5.5.0.0

You could have something like this in your GET Login action:
if (this.User.Identity.IsAuthenticated)
{
return Redirect(returnUrl);
}

Related

Trying to Authenticate using CookieAuthenticationDefaults.AuthenticationScheme with OpenIddict through angular client application

Trying to Authenticate using CookieAuthenticationDefaults.AuthenticationScheme with OpenIddict through angular client application but cookie not authenticated. does anyone know whats the issue
[HttpPost("login")]
[AllowAnonymous]
public async Task<IActionResult> Login(LoginRequestDTO model)
{
var user = await _context.Users.Where(x => x.Username == model.Username).FirstOrDefaultAsync();
if(user == null)
{
return Ok(GenericResponse<bool>.Failure("Invalid Username or Password!", Models.Enums.ApiStatusCode.RecordNotFound));
}
else
{
if(user.Password != model.Password)
{
return Ok(GenericResponse<bool>.Failure("Invalid Username or Password!", Models.Enums.ApiStatusCode.RecordNotFound));
}
else
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, model.Username)
};
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignInAsync(new ClaimsPrincipal(claimsIdentity));
return Redirect(model.ReturnUrl);
}
}
}
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
//options.LoginPath = "/login";
options.Events = new CookieAuthenticationEvents()
{
OnRedirectToLogin = (context) =>
{
context.HttpContext.Response.Redirect("http://localhost:4200/login" + context.RedirectUri.Split("Login")[1]);
return Task.CompletedTask;
}
};
});

OpenIddict Authorization Code Flow & Refresh Tokens

I've been experimenting with the OpenIddict sample projects, more specifically Zirku to better understand Authorization Code Flow and Introspection.
Based on a fair bit of research I've been able to develop a Client MVC Web App, an Auth Server, and a separate Resource Server (API), all of which were influenced by the samples linked above. In testing I've been able to login and access an endpoint from my API that is prefixed with the [Authorize] attribute successfully, by passing the access token in the request header. After waiting for a minute any attempt to access the API again, will result in a 401 Unauthorized as expected since the access token has now expired based on the Auth Server configuration. The only way to call the endpoint successfully after this, is to complete a logout and login thus generating a new access token and a grace period of a minute before it expires.
I've therefore implemented Refresh Tokens, through adding the RefreshTokenFlow and required offline_access scope to the relevant projects as seen below. Whilst I have the ability to obtain the access and refresh tokens in my Client application I am unsure on how to handle the process of using the refresh token to obtain a new access token.
In essence, how do I use the refresh token to obtain a new access token, once the original is nearing its expiry, and how can use the new token throughout my client application until it needs refreshing, or until the user has singed out? Presumably I need to call the connect/token endpoint with a grant_type of refresh_token, but will this update the HttpContext in my client app with the new tokens?
Client MVC:
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.LoginPath = "/login";
})
.AddOpenIdConnect(options =>
{
options.ClientId = "ExampleClientId";
options.ClientSecret = "ExampleClientSecret";
options.RequireHttpsMetadata = false;
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
options.ResponseType = OpenIdConnectResponseType.Code;
options.AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet;
options.Authority = "https://localhost:5001/";
options.Scope.Add("email");
options.Scope.Add("roles");
options.Scope.Add("offline_access");
options.Scope.Add("example_api");
options.MapInboundClaims = false;
options.TokenValidationParameters.NameClaimType = "name";
options.TokenValidationParameters.RoleClaimType = "role";
});
...
app.UseAuthentication();
app.UseAuthorization();
Auth Server:
builder.Services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"));
options.UseOpenIddict();
});
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders()
.AddDefaultUI();
builder.Services.Configure<IdentityOptions>(options =>
{
options.ClaimsIdentity.UserNameClaimType = Claims.Name;
options.ClaimsIdentity.UserIdClaimType = Claims.Subject;
options.ClaimsIdentity.RoleClaimType = Claims.Role;
options.ClaimsIdentity.EmailClaimType = Claims.Email;
options.SignIn.RequireConfirmedAccount = false;
});
builder.Services.AddQuartz(options =>
{
options.UseMicrosoftDependencyInjectionJobFactory();
options.UseSimpleTypeLoader();
options.UseInMemoryStore();
});
builder.Services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true);
builder.Services.AddOpenIddict()
.AddCore(options =>
{
options.UseEntityFrameworkCore()
.UseDbContext<ApplicationDbContext>();
options.UseQuartz();
})
.AddServer(options =>
{
options.SetAuthorizationEndpointUris("/connect/authorize")
.SetLogoutEndpointUris("/connect/logout")
.SetTokenEndpointUris("/connect/token")
.SetUserinfoEndpointUris("/connect/userinfo")
.SetIntrospectionEndpointUris("/connect/introspect");
options.RegisterScopes(Scopes.Email, Scopes.Profile, Scopes.Roles, Scopes.OfflineAccess);
options.AllowAuthorizationCodeFlow()
.AllowRefreshTokenFlow()
.SetAccessTokenLifetime(TimeSpan.FromMinutes(1))
.SetRefreshTokenLifetime(TimeSpan.FromDays(1));
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
options.UseAspNetCore()
.EnableAuthorizationEndpointPassthrough()
.EnableLogoutEndpointPassthrough()
.EnableTokenEndpointPassthrough()
.EnableUserinfoEndpointPassthrough()
.EnableStatusCodePagesIntegration();
})
.AddValidation(options =>
{
options.UseLocalServer();
options.UseAspNetCore();
});
// Register the worker responsible for seeding the database.
// Note: in a real world application, this step should be part of a setup script.
builder.Services.AddHostedService<Worker>();
builder.Services.AddAuthorization();
...
app.UseAuthentication();
app.UseAuthorization();
Woker.cs:
public class Worker : IHostedService
{
private readonly IServiceProvider _serviceProvider;
public Worker(IServiceProvider serviceProvider)
=> _serviceProvider = serviceProvider;
public async Task StartAsync(CancellationToken cancellationToken)
{
await using var scope = _serviceProvider.CreateAsyncScope();
var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
await context.Database.EnsureCreatedAsync();
var manager = scope.ServiceProvider.GetRequiredService<IOpenIddictApplicationManager>();
if (await manager.FindByClientIdAsync("SampleClientMVC") == null)
{
await manager.CreateAsync(new OpenIddictApplicationDescriptor
{
ClientId = "ExampleClientId",
ClientSecret = "ExampleClientSecret",
ConsentType = ConsentTypes.Explicit,
DisplayName = "MVC Client Application",
PostLogoutRedirectUris =
{
new Uri("https://localhost:7001/signout-callback-oidc")
},
RedirectUris =
{
new Uri("https://localhost:7001/signin-oidc")
},
Permissions =
{
Permissions.Endpoints.Authorization,
Permissions.Endpoints.Logout,
Permissions.Endpoints.Token,
Permissions.GrantTypes.AuthorizationCode,
Permissions.GrantTypes.RefreshToken,
Permissions.ResponseTypes.Code,
Permissions.Scopes.Email,
Permissions.Scopes.Profile,
Permissions.Scopes.Roles,
Permissions.Prefixes.Scope + "example_api"
},
Requirements =
{
Requirements.Features.ProofKeyForCodeExchange
}
});
}
if (await manager.FindByClientIdAsync("sample_resource_server") is null)
{
await manager.CreateAsync(new OpenIddictApplicationDescriptor
{
ClientId = "example_resource_server",
ClientSecret = "ExampleResourceServerSecret",
Permissions =
{
Permissions.Endpoints.Introspection
}
});
}
var scopeManager = scope.ServiceProvider.GetRequiredService<IOpenIddictScopeManager>();
if (await scopeManager.FindByNameAsync("example_api") is null)
{
await scopeManager.CreateAsync(new OpenIddictScopeDescriptor
{
Name = "example_api",
Resources =
{
"example_resource_server"
}
});
}
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
Token Endpoint:
[HttpPost("~/connect/token"), Produces("application/json")]
public async Task<IActionResult> Exchange()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
if (request.IsAuthorizationCodeGrantType())
{
var principal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal;
var user = await _userManager.GetUserAsync(principal);
if (user == null)
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The token is no longer valid."
}));
}
if (!await _signInManager.CanSignInAsync(user))
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in."
}));
}
foreach (var claim in principal.Claims)
{
claim.SetDestinations(GetDestinations(claim, principal));
}
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
else if (request.IsRefreshTokenGrantType())
{
var info = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
var user = await _userManager.GetUserAsync(info.Principal);
if (user == null)
{
var properties = new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The refresh token is no longer valid."
});
return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
if (!await _signInManager.CanSignInAsync(user))
{
var properties = new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in."
});
return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
var principal = await _signInManager.CreateUserPrincipalAsync(user);
foreach (var claim in principal.Claims)
{
claim.SetDestinations(GetDestinations(claim, principal));
}
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
throw new InvalidOperationException("The specified grant type is not supported.");
}
Resource Server - API:
builder.Services.AddOpenIddict()
.AddValidation(options =>
{
options.SetIssuer("https://localhost:7235/");
options.AddAudiences("example_resource_server");
options.UseIntrospection()
.SetClientId("example_resource_server")
.SetClientSecret("ExampleResourceServerSecret");
options.UseSystemNetHttp();
options.UseAspNetCore();
});
builder.Services.AddAuthentication(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme);
builder.Services.AddAuthorization();
...
app.UseAuthentication();
app.UseAuthorization();
if you have saved the token in cookie,I think you could try as below to check the remaining time of the token ,and you could try to get a new token with httpclient
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.LoginPath = "/login";
options.Events = new CookieAuthenticationEvents()
{
OnValidatePrincipal = async CookieValiCoText =>
{
var now = DateTimeOffset.UtcNow;
var expiresAt = CookieValiCoText.Properties.GetTokenValue("expires_in");
.......some logical codes
//to get the accesstoken with refresh token if the token expires soon
if ( about toexpires )
{
var refreshToken = CookieValiCoText.Properties.GetTokenValue("refresh_token");
var response = await new HttpClient().RequestRefreshTokenAsync(new RefreshTokenRequest
{
Address = "your exchange end point",
ClientId = "ExampleClientId",
ClientSecret = "ExampleClientSecret",
RefreshToken = refreshToken
});
.......

Use external claims form OpenId server

I'm trying to use Keycloak as an OpenId server for my ASP.Net core app.
It's almost ok. The user gets authorized (passes through [Authorize] attribute), but his claims are empty.
My configuration:
services.AddAuthentication(options => { options.DefaultScheme = "cookie"; })
.AddCookie("cookie")
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "http://localhost:8080/auth/realms/master/";
options.RequireHttpsMetadata = false;
options.ClientId = "test-client";
options.ClientSecret = "ee117d6d-25c9-4317-83a0-54c2f252aa89";
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
options.RemoteSignOutPath = "/SignOut";
options.SignedOutRedirectUri = "Redirect-here";
options.ResponseType = "code";
});
A controller action which works fine (gets authorized):
[Authorize]
public IActionResult About()
{
ViewData["Message"] = "Your application description page.";
return View();
}
A controller action which is unauthorized (as it requires a role):
[Authorize(Roles = "Administrators")]
public IActionResult About()
{
ViewData["Message"] = "Your application description page.";
return View();
}
AccountController.ExternalLoginCallback:
public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
{
if (remoteError != null)
{
ErrorMessage = $"Error from external provider: {remoteError}";
return RedirectToAction(nameof(Login));
}
var info = await _signInManager.GetExternalLoginInfoAsync(); // it has claims
if (info == null)
{
return RedirectToAction(nameof(Login));
}
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true);
if (result.Succeeded)
{
_logger.LogInformation("User logged in with {Name} provider.", info.LoginProvider);
return RedirectToLocal(returnUrl);
}
if (result.IsLockedOut)
{
return RedirectToAction(nameof(Lockout));
}
else
{
ViewData["ReturnUrl"] = returnUrl;
ViewData["LoginProvider"] = info.LoginProvider;
var email = info.Principal.FindFirstValue(ClaimTypes.Email);
return View("ExternalLogin", new ExternalLoginViewModel { Email = email });
}
}
info objct from the snippet above has the claims, especially roles claims:
But when I'm logged in and try to see the roles and clams, the are emtpy:
var user = await _signInManager.UserManager.GetUserAsync(HttpContext.User); // user is found
var roles = await _signInManager.UserManager.GetRolesAsync(user); // empty
var claims = await _signInManager.UserManager.GetClaimsAsync(user); //empty
So it looks like Keycloak is doing everything fine, but my app has problems consuming the claims.
What am I missing?
You need to configure the authentication provider so it knows which claim type to use for roles.
You can configure it as follows:
services.AddOpenIdConnect("oidc", options =>
{
...
options.TokenValidationParameters = new TokenValidationParameters
{
RoleClaimType = "user_roles"; // or "user_realm_roles"?
};
});

Cortana - OAuth 2 connected service

We have created an OAuth2 provider as an app service as given in below example.
https://learn.microsoft.com/en-us/aspnet/aspnet/overview/owin-and-katana/owin-oauth-20-authorization-server
Step 2:
We need to configure this endpoints to Cortana connected service OAuth2 end points to get the access token from Cortana app
Cortana Auth Config
Sample Code in the Auth provider authorize function is below:
public ActionResult Login()
{
var authentication = HttpContext.GetOwinContext().Authentication;
if (Request.HttpMethod == "POST")
{
var isPersistent = !string.IsNullOrEmpty(Request.Form.Get("isPersistent"));
if (!string.IsNullOrEmpty(Request.Form.Get("submit.Signin")))
{
authentication.SignIn(
new AuthenticationProperties { IsPersistent = isPersistent },
new ClaimsIdentity(new[] { new Claim(ClaimsIdentity.DefaultNameClaimType, Request.Form["username"]) }, "Application"));
}
}
return View();
// Response.End();
}
Authorize
public ActionResult Authorize()
{
if (Response.StatusCode != 200)
{
return View("AuthorizeError");
}
var authentication = HttpContext.GetOwinContext().Authentication;
var ticket = authentication.AuthenticateAsync("Application").Result;
var identity = ticket != null ? ticket.Identity : null;
if (identity == null)
{
authentication.Challenge("Application");
return new HttpUnauthorizedResult();
}
var scopes = (Request.QueryString.Get("scope") ?? "").Split(' ');
if (Request.HttpMethod == "POST")
{
if (!string.IsNullOrEmpty(Request.Form.Get("submit.Grant")))
{
identity = new ClaimsIdentity(identity.Claims, "Bearer", identity.NameClaimType, identity.RoleClaimType);
foreach (var scope in scopes)
{
identity.AddClaim(new Claim("urn:oauth:scope", scope));
}
authentication.SignIn(identity);
}
//if (!string.IsNullOrEmpty(Request.Form.Get("submit.Login")))
//{
// authentication.SignOut("Application");
// authentication.Challenge("Application");
// return new HttpUnauthorizedResult();
//}
}
return View();
}
I want to change this code in a way that it could be able to send back data to Cortana channel. Please let know your suggestions.
Thanks

Prevent users to have multiple sessions with JWT Tokens

I am building an application which uses JWT bearer authentication in ASP.NET Core. I need to prevent users to have multiple sessions open at the same time. I am wondering if there is way using Microsoft.AspNetCore.Authentication.JwtBearer middleware to list out all the tokens of an user and then verify if there are other tokens issued for that user in order to invalidate the incoming authentication request.
If the claims are able to be validated on the server, I guess that in order to do that, the server has a record of those claims and the user who owns them. Right?
Any ideas How Can I achieve this?
I have achieved my goal, saving in the db the timestamp when the user login, adding that timestamp in the payload of the token and then adding an additional layer of security to validate the JWT against the db, returning 401 if the timestamp doesn't match. This is the code implemented with .net Core 2.0 if someone need it.
Controller:
[HttpPost]
[Route("authenticate")]
public async Task<IActionResult> AuthenticateAsync([FromBody] UserModel user)
{
try
{
.......
if (userSecurityKey != null)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_appSettings.Secret);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
// This claim allows us to store information and use it without accessing the db
new Claim("userSecurityKey", userDeserialized.SecurityKey.ToString()),
new Claim("timeStamp",timeStamp),
new Claim("verificationKey",userDeserialized.VerificationKey.ToString()),
new Claim("userName",userDeserialized.UserName)
}),
Expires = DateTime.UtcNow.AddDays(7),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key),
SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
var tokenString = tokenHandler.WriteToken(token);
// Updates timestamp for the user if there is one
VerificationPortalTimeStamps userTimeStamp = await _context.VerificationPortalTimeStamps.AsNoTracking().FirstOrDefaultAsync(e => e.UserName == userDeserialized.UserName);
if (userTimeStamp != null)
{
userTimeStamp.TimeStamp = timeStamp;
_context.Entry(userTimeStamp).State = EntityState.Modified;
await _context.SaveChangesAsync();
}
else
{
_context.VerificationPortalTimeStamps.Add(new VerificationPortalTimeStamps { TimeStamp = timeStamp, UserName = userDeserialized.UserName });
await _context.SaveChangesAsync();
}
// return basic user info (without password) and token to store client side
return Json(new
{
userName = userDeserialized.UserName,
userSecurityKey = userDeserialized.SecurityKey,
token = tokenString
});
}
return Unauthorized();
}
catch (Exception)
{
return Unauthorized();
}
}
Then, to configure the JWT Bearer Authentication with .Net Core 2.0
Startup.cs:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IServiceProvider provider)
{
.................
app.UseAuthentication();
app.UseMvc();
...........
}
To configure the JWT Bearer authentication:
public IServiceProvider ConfigureServices(IServiceCollection services)
{
............
var key = Configuration["AppSettings:Secret"];
byte[] keyAsBytes = Encoding.ASCII.GetBytes(key);
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(o =>
{
o.RequireHttpsMetadata = false;
o.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(keyAsBytes),
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true
};
o.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
if (Configuration["AppSettings:IsGodMode"] != "true")
context.Response.StatusCode = 401;
return Task.FromResult<object>(0);
}
};
o.SecurityTokenValidators.Clear();
o.SecurityTokenValidators.Add(new MyTokenHandler());
});
services.AddMvc()
.AddJsonOptions(opt =>
{
opt.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
});
var provider = services.BuildServiceProvider();
return provider;
}
Then, we implement the custom validation in the controller as follows:
[Authorize]
[HttpGet]
[Route("getcandidate")]
public async Task<IActionResult> GetCandidateAsync()
{
try
{
.....
//Get user from the claim
string userName = User.FindFirst("UserName").Value;
//Get timestamp from the db for the user
var currentUserTimeStamp = _context.VerificationPortalTimeStamps.AsNoTracking().FirstOrDefault(e => e.UserName == userName).TimeStamp;
// Compare timestamp from the claim against timestamp from the db
if (User.FindFirst("timeStamp").Value != currentUserTimeStamp)
{
return NotFound();
}
...........
}
catch (Exception)
{
return NotFound();
}
}