OnTokenValidated event not triggered - asp.net-core

What I'm trying to do is add a claim after authentication. The following example of triggering an OnTokenValidation event not actually working.
I'm using Microsoft.Identity.Web to authenticate on Azure AD. That part works! How can I register events using AddMicrosoftIdentityWebAppAuthentication to add custom claims
services.AddMicrosoftIdentityWebApiAuthentication(_configuration);
services.Configure<MicrosoftIdentityOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.Events = new OpenIdConnectEvents
{
OnTokenValidated = ctx =>
{
var claims = new List<Claim> {
new Claim(ClaimTypes.OtherPhone, "somevalue")
};
ctx.Principal.AddIdentity(new ClaimsIdentity(claims));
return Task.CompletedTask;
},
};
});

You are using AddMicrosoftIdentityWebApiAuthentication, so the events that will be triggered are JwtBearerEvents.
You can set them up as below (.NET 6 API):
// Add services to the container.
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(builder.Configuration);
// Enable PII for logging
IdentityModelEventSource.ShowPII = true;
// Configure middleware events
builder.Services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.Events = new JwtBearerEvents
{
OnTokenValidated = ctx =>
{
var accessToken = ctx.SecurityToken;
Debug.WriteLine("[OnTokenVaidated]: I can do stuff here! ");
return Task.CompletedTask;
},
OnMessageReceived = ctx =>
{
Debug.WriteLine("[OnMessageReceived]: I can do stuff here! ");
return Task.CompletedTask;
},
OnAuthenticationFailed = ctx =>
{
Debug.WriteLine("[OnAuthenticationFailed]: Authentication failed with the following error: ");
Debug.WriteLine(ctx.Exception);
return Task.CompletedTask;
},
OnChallenge = ctx =>
{
Debug.WriteLine("[OnChallenge]: I can do stuff here! ");
return Task.CompletedTask;
}
};
});

Related

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

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.

OpenIdConnectEvents.OnTokenValidated not being reached

using asp.net core 2.2, I have the following in my startup below
i reach OnRedirectToIdentityProvider breakpoint , and then I reach relative path in appsettings "CallbackPath": " . But I don't i reach OnTokenValidated breakpoint . the Auth is triggered by [Authorize] decoration of a controller.
What am i missing ?
services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
.AddAzureAD(options => Configuration.Bind("AzureAd", options))
.AddCookie();
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
options.Authority = options.Authority + "/v2.0/"; // Microsoft identity platform
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = async n =>
{
//save url to state
n.ProtocolMessage.State = n.HttpContext.Request.Path.Value.ToString();
},
OnTokenValidated = ctx =>
{
var url = ctx.ProtocolMessage.GetParameter("state");
var claims = new List<Claim>
{
new Claim("myurl", url)
};
var appIdentity = new ClaimsIdentity(claims);
//add url to claims
ctx.Principal.AddIdentity(appIdentity);
return Task.CompletedTask;
},
OnTicketReceived = ctx =>
{
var url = ctx.Principal.FindFirst("myurl").Value;
ctx.ReturnUri = url;
return Task.CompletedTask;
}
};
options.TokenValidationParameters.ValidateIssuer = false; // accept several tenants (here simplified)
});
You can change of ResponseMode to FormPost and add async to OnTokenValidated then it will be fixed.
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
options.Authority = options.Authority + "/v2.0/"; // Microsoft identity platform
options.ResponseMode = OpenIdConnectResponseMode.FormPost;
options.CallbackPath = "/";
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = async n =>
{
...
},
OnTokenValidated = async ctx =>
{
...
},

AddJwtBearer OnAuthenticationFailed return custom error

I am using Openidict.
I am trying to return custom message with custom status code, but I am unable to do it. My configuration in startup.cs:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(o =>
{
o.Authority = this.Configuration["Authentication:OpenIddict:Authority"];
o.Audience = "MyApp"; //Also in Auhorization.cs controller.
o.RequireHttpsMetadata = !this.Environment.IsDevelopment();
o.Events = new JwtBearerEvents()
{
OnAuthenticationFailed = context =>
{
context.Response.StatusCode = HttpStatusCodes.AuthenticationFailed;
context.Response.ContentType = "application/json";
var err = this.Environment.IsDevelopment() ? context.Exception.ToString() : "An error occurred processing your authentication.";
var result = JsonConvert.SerializeObject(new {err});
return context.Response.WriteAsync(result);
}
};
});
But the problem is no content is returned. Chrome developer tools report
(failed)
for Status and
Failed to load response data
for response.
I also tried:
context.Response.WriteAsync(result).Wait();
return Task.CompletedTask;
but the result is the same.
Desired behaviour:
I would like to return custom status code with message what went wrong.
It's important to note that both the aspnet-contrib OAuth2 validation and the MSFT JWT handler automatically return a WWW-Authenticate response header containing an error code/description when a 401 response is returned:
If you think the standard behavior is not convenient enough, you can use the events model to manually handle the challenge. E.g:
services.AddAuthentication()
.AddJwtBearer(options =>
{
options.Authority = "http://localhost:54540/";
options.Audience = "resource_server";
options.RequireHttpsMetadata = false;
options.Events = new JwtBearerEvents();
options.Events.OnChallenge = context =>
{
// Skip the default logic.
context.HandleResponse();
var payload = new JObject
{
["error"] = context.Error,
["error_description"] = context.ErrorDescription,
["error_uri"] = context.ErrorUri
};
context.Response.ContentType = "application/json";
context.Response.StatusCode = 401;
return context.Response.WriteAsync(payload.ToString());
};
});
Was facing same issue, tried the solution provided by Pinpoint but it didnt work for me on ASP.NET core 2.0. But based on Pinpoint's solution and some trial and error, the following code works for me.
var builder = services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(o =>
{
o.Authority = "http://192.168.0.110/auth/realms/demo";
o.Audience = "demo-app";
o.RequireHttpsMetadata = false;
o.Events = new JwtBearerEvents()
{
OnAuthenticationFailed = c =>
{
c.NoResult();
c.Response.StatusCode = 500;
c.Response.ContentType = "text/plain";
c.Response.WriteAsync(c.Exception.ToString()).Wait();
return Task.CompletedTask;
},
OnChallenge = c =>
{
c.HandleResponse();
return Task.CompletedTask;
}
};
});
This is what worked for me after finding issues related to this exception that seemed to appear after updating packages.
System.InvalidOperationException: StatusCode cannot be set because the response has already started.
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ThrowResponseAlreadyStartedException(String value)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.set_StatusCode(Int32 value)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.Microsoft.AspNetCore.Http.Features.IHttpResponseFeature.set_StatusCode(Int32 value)
at Microsoft.AspNetCore.Http.DefaultHttpResponse.set_StatusCode(Int32 value)
The implementation is below,
OnAuthenticationFailed = context =>
{
context.NoResult();
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
context.Response.ContentType = "application/json";
string response =
JsonConvert.SerializeObject("The access token provided is not valid.");
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
{
context.Response.Headers.Add("Token-Expired", "true");
response =
JsonConvert.SerializeObject("The access token provided has expired.");
}
context.Response.WriteAsync(response);
return Task.CompletedTask;
},
OnChallenge = context =>
{
context.HandleResponse();
return Task.CompletedTask;
}
please check with the bellow code for .net core 2.1
OnAuthenticationFailed =context =>
{
context.Response.OnStarting(async () =>
{
context.NoResult();
context.Response.Headers.Add("Token-Expired", "true");
context.Response.ContentType = "text/plain";
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
await context.Response.WriteAsync("Un-Authorized");
});
return Task.CompletedTask;
},
Below code work with .Net 6(minimal API
var app = builder.Build();
app.Use(async (context, next) =>
{
await next();
if (context.Response.StatusCode == (int)HttpStatusCode.Unauthorized) // 401
{
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(JsonConvert.SerializeObject(new Error()
{
Message = "Token is not valid"
}));
}
});