OpenIddict Authorization Code Flow & Refresh Tokens - asp.net-core

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
});
.......

Related

net core api and vue js SPA keycloak authentification

I am using net core for my back-end rest api and vuejs for the front-end.
I made an authentication via a cookie
I want to authenticate via keycloak. I want authentication to go through keycloak's authentication page and not my app's (vueJs) authentication page.
I haven't been able to find any sample code to do this.
If I do the authentication via VueJS, how then do I do so that the authentication is done on the net core API (sending the token to the API or other)?
Here is the solution.
I added this in the Startup.cs file:
private static bool ServerCertificateCustomValidation(HttpRequestMessage requestMessage, X509Certificate2 certificate, X509Chain chain, SslPolicyErrors sslErrors)
{
//It is possible inpect the certificate provided by server
Log($"Requested URI: {requestMessage.RequestUri}");
Log($"Effective date: {certificate.GetEffectiveDateString()}");
Log($"Exp date: {certificate.GetExpirationDateString()}");
Log($"Issuer: {certificate.Issuer}");
Log($"Subject: {certificate.Subject}");
//Based on the custom logic it is possible to decide whether the client considers certificate valid or not
Log($"Errors: {sslErrors}");
return true;
//return sslErrors == SslPolicyErrors.None;
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(options => {
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(o =>
{
o.Authority = Configuration["Jwt:Authority"];
o.Audience = Configuration["Jwt:Audience"];
o.RequireHttpsMetadata = true;
o.SaveToken = true;
HttpClientHandler handler = new HttpClientHandler()
{
CheckCertificateRevocationList = false,
UseDefaultCredentials = false,
ClientCertificateOptions = ClientCertificateOption.Manual,
SslProtocols = System.Security.Authentication.SslProtocols.Tls12 | System.Security.Authentication.SslProtocols.Tls11,
ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => ServerCertificateCustomValidation(message, cert, chain, errors),
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
//ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator,
//ServerCertificateCustomValidationCallback = delegate { return true; },
CookieContainer = new CookieContainer()
};
//HttpClientHandler handler = new HttpClientHandler();
//handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
handler.SslProtocols = System.Security.Authentication.SslProtocols.Tls12;
o.BackchannelHttpHandler = handler;
o.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "preferred_username"
};
o.Events = new JwtBearerEvents()
{
OnTokenValidated = c =>
{
var test = c;
/*ClaimsIdentity identity = c.Principal.Identity as ClaimsIdentity;
identity.Se*/
JwtSecurityToken accessToken = c.SecurityToken as JwtSecurityToken;
if (accessToken != null)
{
ClaimsIdentity identity = c.Principal.Identity as ClaimsIdentity;
if (identity != null)
{
identity.AddClaim(new Claim("access_token", accessToken.RawData));
}
}
//c.NoResult();
return Task.CompletedTask;
},
OnAuthenticationFailed = c =>
{
c.NoResult();
Log($"test");
c.Response.StatusCode = 500;
c.Response.ContentType = "text/plain";
//if (Environment.IsDevelopment())
//{
//return c.Response.WriteAsync(c.Exception.ToString());
//}
return c.Response.WriteAsync("An error occured processing your authentication." + c.Exception.InnerException.Message);
}
};
});
...
services.Configure<SecurityStampValidatorOptions>(options =>
{
options.ValidationInterval = TimeSpan.Zero;
});
services.AddControllers()
.AddNewtonsoftJson(options =>
{
options.SerializerSettings.DateFormatString = "yyyy-MM-ddTHH:mm:ss";
});
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp";
});
}
And in appsettings.json :
"Jwt": {
"Authority": "https://auth.myauthority/auth/realms/myauthority",
"https": null,
"Audience": "MyAudience"
},

Get access_token working calling API on same IS4 webapp

I'm trying to get the this access_token stuff working to do a call to a API which is declared with an Autohorize attrib from a BaseController. I think there is something wrong with my configuration.
Can anybody tell me what I'm doing wrong?
I have attached my Startup.cs for reference.
I'm trying to get a access token to send with the API called in the code below:
var httpClient = _httpClientFactory.CreateClient(BaseController.AUTHORIZATION_SERVICE_CLIENT_NAME);
httpClient.DefaultRequestHeaders.Clear();
httpClient.DefaultRequestHeaders.Add(HeaderNames.Accept, "application/json");
//AccessToken is always null
var accessToken = _httpContextAccessor.HttpContext.GetTokenAsync("access_token").Result;
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var request = new HttpRequestMessage(HttpMethod.Get, "/api/auth/user/?id=" + id);
var response = httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).Result;
if (response.IsSuccessStatusCode)
return response.Content.ReadAsStringAsync().Result as string;
return "-";
My IS4 startup:
Confi
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddMvc().AddDataAnnotationsLocalization();
services.AddRazorPages()
.AddRazorPagesOptions(options =>
{
options.Conventions.AuthorizeAreaFolder("Identity", "/Account/Manage");
});
//START IS4
var builder = services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
options.UserInteraction.LoginUrl = "/Identity/Account/Login";
options.UserInteraction.LogoutUrl = "/Identity/Account/Logout";
options.Authentication = new AuthenticationOptions()
{
CookieLifetime = TimeSpan.FromHours(10), // ID server cookie timeout set to 10 hours
CookieSlidingExpiration = true
};
})
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = b => b.UseSqlServer(connectionString, sql => sql.MigrationsAssembly(migrationsAssembly));
})
.AddOperationalStore(options =>
{
options.ConfigureDbContext = b => b.UseSqlServer(connectionString, sql => sql.MigrationsAssembly(migrationsAssembly));
options.EnableTokenCleanup = true;
})
.AddAspNetIdentity<User>();
if (env.IsDevelopment())
builder.AddDeveloperSigningCredential();
else
builder.AddSigningCredential(LoadCertificateFromStore());
//END IS4
//START IDENTITY
services.AddIdentity<User, Role>(options =>
{
options.SignIn.RequireConfirmedEmail = true;
})
.AddEntityFrameworkStores<IdentityContext>()
.AddUserStore<CustomUserStore>()
.AddRoleStore<CustomRoleStore>()
.AddDefaultTokenProviders()
.AddClaimsPrincipalFactory<CustomUserClaimsPrincipalFactory>();
services.ConfigureApplicationCookie(options => {
options.LoginPath = Startup.LoginPath;
options.LogoutPath = Startup.LogoutPath;
options.AccessDeniedPath = Startup.AccessDeniedPath;
});
services.AddAuthentication(o =>{})
.AddGoogle("Google", "Google", options =>
{
options.SignInScheme = IdentityConstants.ExternalScheme;
options.ClientId = configuration.GetValue<string>("Google:ClientId");
options.ClientSecret = configuration.GetValue<string>("Google:ClientSecret");
})
.AddOpenIdConnect("azuread", "Azure AD", options => configuration.Bind("AzureAd", options));
services.Configure<OpenIdConnectOptions>("azuread", options =>
{
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Events = new OpenIdConnectEvents()
{
OnRedirectToIdentityProviderForSignOut = context =>
{
context.HandleResponse();
context.Response.Redirect("/Identity/Account/Logout");
return Task.FromResult(0);
}
};
});
services.AddTransient<IClaimsTransformation, ClaimsTransformer>();
//END IDENTITY
//Set named HttpClient settings for API to get roles of user
services.AddHttpContextAccessor();
services.AddTransient<BearerTokenHandler>();
services.AddHttpClient(BaseController.AUTHORIZATION_SERVICE_CLIENT_NAME, client =>
{
client.BaseAddress = new Uri("https://localhost:44318/");
}).AddHttpMessageHandler<BearerTokenHandler>();
}
public void Configure(IApplicationBuilder app)
{
if (Environment.IsDevelopment())
{
Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true;
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler(ErrorPath);
}
app.UseStaticFiles();
app.UseRouting();
app.UseIdentityServer();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(name: "defaultArea",
pattern: "{area=Identity}/{controller=Account}/{action=Login}/{id?}");
endpoints.MapControllerRoute(name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
endpoints.MapRazorPages();
});
}
Bearer token handler, when I add 'AddHttpMessageHandler<BearerTokenHandler' to the client it gives null at the 'expiresAt' variable;
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var accessToken = await GetAccessTokenAsync();
if (!string.IsNullOrWhiteSpace(accessToken))
request.SetBearerToken(accessToken);
return await base.SendAsync(request, cancellationToken);
}
public async Task<string> GetAccessTokenAsync()
{
// get the expires_at value & parse it
var expiresAt = await _httpContextAccessor.HttpContext.GetTokenAsync("expires_at");
var expiresAtAsDateTimeOffset = DateTimeOffset.Parse(expiresAt, CultureInfo.InvariantCulture);
if ((expiresAtAsDateTimeOffset.AddSeconds(-60)).ToUniversalTime() > DateTime.UtcNow)
return await _httpContextAccessor.HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken); // no need to refresh, return the access token
var idpClient = _httpClientFactory.CreateClient("IDPClient");
// get the discovery document
var discoveryReponse = await idpClient.GetDiscoveryDocumentAsync();
// refresh the tokens
var refreshToken = await _httpContextAccessor.HttpContext.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken);
var refreshResponse = await idpClient.RequestRefreshTokenAsync(new RefreshTokenRequest {
Address = discoveryReponse.TokenEndpoint,
ClientId = "mvc",
ClientSecret = "secret",
RefreshToken = refreshToken
});
// store the tokens
var updatedTokens = new List<AuthenticationToken>();
updatedTokens.Add(new AuthenticationToken {
Name = OpenIdConnectParameterNames.IdToken,
Value = refreshResponse.IdentityToken
});
updatedTokens.Add(new AuthenticationToken {
Name = OpenIdConnectParameterNames.AccessToken,
Value = refreshResponse.AccessToken
});
updatedTokens.Add(new AuthenticationToken {
Name = OpenIdConnectParameterNames.RefreshToken,
Value = refreshResponse.RefreshToken
});
updatedTokens.Add(new AuthenticationToken {
Name = "expires_at",
Value = (DateTime.UtcNow + TimeSpan.FromSeconds(refreshResponse.ExpiresIn)).
ToString("o", CultureInfo.InvariantCulture)
});
// get authenticate result, containing the current principal & properties
var currentAuthenticateResult = await _httpContextAccessor.HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
// store the updated tokens
currentAuthenticateResult.Properties.StoreTokens(updatedTokens);
// sign in
await _httpContextAccessor.HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, currentAuthenticateResult.Principal, currentAuthenticateResult.Properties);
return refreshResponse.AccessToken;
}
One idea could be to try to set the SaveTokens=true in your configuration.
See this article or ideas.

Identity server identity token is always null

I'm using identity server with .net core application.
Current scenario: Getting token from Facebook, authenticating the user using Facebook API. And then creating a user in our database.
Everything is working fine, except that the returned token always have null identity_token.
I've tried to add id_token token to the scopes but didn't work also.
Login API:
[HttpPost("SocialLogin")]
public async Task<IActionResult> SocialMediaLogin()
{
try
{
var protocol = _httpContextAccessor.HttpContext.Request.Scheme;
var host = _httpContextAccessor.HttpContext.Request.Host.Value;
var baseUrl = $"{protocol}://{host}/";
#region Validating Headers and Parameters
StringValues jwtToken = Request.Headers["token"];
if (!jwtToken.Any())
{
return BadRequest("Each user should have a token");
}
var secretKey = _configuration["SecretKey"].ToString();
var (status, paylod) = GeneralUtilities.CheckSocialMediaTokenHeaderAndValues(jwtToken, secretKey);
if (status != ResponseStatus.Success)
{
return BadRequest("User Id or token are wrong");
}
#endregion
var clientId = _configuration["Clients:Mobile:Id"].ToString();
var secret = _configuration["Clients:Mobile:Secret"].ToString();
var consumerKey = _configuration["ConsumerKey"].ToString();
var consumerKeySecret = _configuration["ConsumerKeySecret"].ToString();
var disco = await DiscoveryClient.GetAsync(baseUrl);
if (disco.IsError) throw new Exception(disco.Error);
var tokenClient = new TokenClient(disco.TokenEndpoint, clientId, secret);
var payload = new
{
provider = paylod.Provider,
external_token = paylod.Token,
email = paylod.Email,
profilePicture = paylod.ProfilePictureUrl,
twitter_secret = paylod.TwitterSecret,
consumerKeySecret,
consumerKey,
displayName = paylod.DisplayName
};
//If user exist, we should update the profile Url of the user
var user = await _userManager.FindByEmailAsync(payload.email);
if (user != null)
{
user.ProfilePictureUrl = payload.profilePicture;
await _userManager.UpdateAsync(user);
}
var result = await tokenClient.RequestCustomGrantAsync(grantType: "external", scope: "my-api offline_access", extra: payload);
if (result.IsError) return new JsonResult(result.Json);
if (!string.IsNullOrWhiteSpace(result.AccessToken))
{
return Ok(result);
}
return new JsonResult(null);
}
catch (Exception)
{
return BadRequest();
}
}
As for the Startup.cs here's what I'm doing for Identity:
services.AddIdentity<AppUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddIdentityServer()
.AddOperationalStore(options =>
options.ConfigureDbContext = builder =>
builder.UseSqlServer(connectionString, sqloptions => sqloptions.MigrationsAssembly(migrationsAssembly)))
.AddConfigurationStore(options =>
options.ConfigureDbContext = builder =>
builder.UseSqlServer(connectionString, sqloptions => sqloptions.MigrationsAssembly(migrationsAssembly)))
.AddAspNetIdentity<AppUser>()
.AddDeveloperSigningCredential();
// Configure Identity
services.Configure<IdentityOptions>(options =>
{
// Password settings
options.Password.RequireDigit = true;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Password.RequireLowercase = false;
});
Where does the identity_token or access_token are built?

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

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