C# 从KeyClope刷新访问令牌时出现CORS错误
我们目前正在开发一个web应用程序,该应用程序由一个ASP.NET核心前端、一个Java JaxRS.Jersey API和一个作为OpenID身份验证服务器的KeyClope组成。在开发中,一切都使用http运行。对于我们的OpenID,我们使用代码流。因此,webapi在丢失或旧令牌的情况下不返回重定向。我们可以控制每一个组件 当用户处于非活动状态的时间超过访问令牌生存期时,我们面临一个问题: 我们怀疑这是一个配置问题,我们没有在一个组件上正确配置CORS头。我们是否也需要在KeyClope上配置CORS标头?如果是,我们如何添加缺少的配置 这是我们在.NET核心前端的ConfigureServices方法表单Startup.cs中的当前代码:C# 从KeyClope刷新访问令牌时出现CORS错误,c#,asp.net-core,cors,keycloak,openid-connect,C#,Asp.net Core,Cors,Keycloak,Openid Connect,我们目前正在开发一个web应用程序,该应用程序由一个ASP.NET核心前端、一个Java JaxRS.Jersey API和一个作为OpenID身份验证服务器的KeyClope组成。在开发中,一切都使用http运行。对于我们的OpenID,我们使用代码流。因此,webapi在丢失或旧令牌的情况下不返回重定向。我们可以控制每一个组件 当用户处于非活动状态的时间超过访问令牌生存期时,我们面临一个问题: 我们怀疑这是一个配置问题,我们没有在一个组件上正确配置CORS头。我们是否也需要在KeyClop
using DefectsWebApp.Middleware;
using IdentityModel;
using IdentityModel.Client;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System;
using System.Diagnostics;
using System.IdentityModel.Tokens.Jwt;
using System.Net.Http;
using System.Security.Claims;
using System.Threading.Tasks;
namespace DefectsWebApp
{
public class Startup
{
private bool isTokenRefreshRunning = false;
private readonly object lockObj = new object();
readonly string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
private bool IsTokenRefreshRunning
{
get
{
lock(lockObj)
{
return isTokenRefreshRunning;
}
}
set
{
lock (lockObj)
{
isTokenRefreshRunning = value;
}
}
}
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
JsonConvert.DefaultSettings = () => new JsonSerializerSettings
{
Formatting = Newtonsoft.Json.Formatting.Indented,
ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore,
};
services.AddCors(options =>
{
options.AddPolicy(name: MyAllowSpecificOrigins,
builder =>
{
builder.WithOrigins("http://keycloak:8080", "https://keycloak")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
// get URL from Config
services.Configure<QRoDServiceSettings>(Configuration.GetSection("QRodService"));
services.AddSession();
services.AddAuthorization(options =>
{
options.AddPolicy("Users", policy =>
policy.RequireRole("Users"));
});
// source: https://stackoverflow.com/a/43875291
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
// source: https://stackoverflow.com/questions/40032851/how-to-handle-expired-access-token-in-asp-net-core-using-refresh-token-with-open
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.Events = new CookieAuthenticationEvents
{
// this event is fired everytime the cookie has been validated by the cookie middleware,
// so basically during every authenticated request
// the decryption of the cookie has already happened so we have access to the user claims
// and cookie properties - expiration, etc..
OnValidatePrincipal = async x =>
{
// since our cookie lifetime is based on the access token one,
// check if we're more than halfway of the cookie lifetime
var identity = (ClaimsIdentity)x.Principal.Identity;
var accessTokenClaim = identity.FindFirst("access_token");
var refreshTokenClaim = identity.FindFirst("refresh_token");
var accessToken = new JwtSecurityToken(accessTokenClaim.Value);
var now = DateTime.UtcNow.AddMinutes(2);
var timeRemaining = accessToken.ValidTo.Subtract(now);
var refreshtoken = new JwtSecurityToken(refreshTokenClaim.Value);
var timeRemainingRT = refreshtoken.ValidTo.Subtract(now);
timeRemaining = timeRemaining.TotalSeconds > 0 ? timeRemaining : new TimeSpan(0);
timeRemainingRT = timeRemainingRT.TotalSeconds > 0 ? timeRemainingRT : new TimeSpan(0);
Debug.WriteLine("Access-Token: {0} | timeleft: {1}", accessToken.Id, timeRemaining.ToString(@"hh\:mm\:ss"));
Debug.WriteLine("Refresh-Token: {0} | timeleft: {1}", refreshtoken.Id, timeRemainingRT.ToString(@"hh\:mm\:ss"));
if (timeRemaining.TotalMinutes <= 0 && !IsTokenRefreshRunning)
{
IsTokenRefreshRunning = true;
// if we have to refresh, grab the refresh token from the claims, and request
// new access token and refresh token
var refreshToken = refreshTokenClaim.Value;
var refreshTokenRequest = new RefreshTokenRequest
{
Address = Configuration["Authentication:oidc:OIDCRoot"] + Configuration["Authentication:oidc:Token"],
ClientId = Configuration["Authentication:oidc:ClientId"],
ClientSecret = Configuration["Authentication:oidc:ClientSecret"],
RefreshToken = refreshToken,
};
if (!refreshTokenRequest.Headers.Contains(Constants.ORIGIN_HEADER))
{
refreshTokenRequest.Headers.Add(Constants.ORIGIN_HEADER, Configuration["Authentication:oidc:OIDCRoot"] + "/*, *");
}
if (!refreshTokenRequest.Headers.Contains(Constants.CONTENT_HEADER))
{
refreshTokenRequest.Headers.Add(Constants.CONTENT_HEADER, "Origin, X-Requested-With, Content-Type, Accept");
}
var response = await new HttpClient().RequestRefreshTokenAsync(refreshTokenRequest);
Debug.WriteLine("Cookie.OnValidatePrincipal - Trying to refresh Token");
if (!response.IsError)
{
Debug.WriteLine("Cookie.OnValidatePrincipal - Response received");
// everything went right, remove old tokens and add new ones
identity.RemoveClaim(accessTokenClaim);
identity.RemoveClaim(refreshTokenClaim);
// indicate to the cookie middleware to renew the session cookie
// the new lifetime will be the same as the old one, so the alignment
// between cookie and access token is preserved
identity.AddClaims(new[]
{
new Claim("access_token", response.AccessToken),
new Claim("refresh_token", response.RefreshToken)
});
x.ShouldRenew = true;
x.HttpContext.Session.Set<string>(Constants.ACCESS_TOKEN_SESSION_ID, response.AccessToken);
Debug.WriteLine("Cookie.OnValidatePrincipal - Token refreshed");
IsTokenRefreshRunning = false;
}
else
{
Debug.WriteLine(string.Format("Cookie.OnValidatePrincipal - {0}", response.Error));
IsTokenRefreshRunning = false;
}
}
}
};
})
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
//options.AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet;
options.Authority = Configuration["Authentication:oidc:OIDCRoot"];
options.ClientId = Configuration["Authentication:oidc:ClientId"];
options.ClientSecret = Configuration["Authentication:oidc:ClientSecret"];
options.MetadataAddress = Configuration["Authentication:oidc:OIDCRoot"] + Configuration["Authentication:oidc:MetadataAddress"];
options.CallbackPath = new PathString("/Home");
options.RequireHttpsMetadata = false;
// openid is already present by default: https://github.com/aspnet/Security/blob/e98a0d243a7a5d8076ab85c3438739118cdd53ff/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectOptions.cs#L44-L45
// adding offline_access to get a refresh token
options.Scope.Add("offline_access");
// we want IdSrv to post the data back to us
//options.ResponseMode = OidcConstants.ResponseModes.FormPost;
// we use the authorisation code flow, so only asking for a code
options.ResponseType = OidcConstants.ResponseTypes.Code;
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
// when the identity has been created from the data we receive,
// persist it with this authentication scheme, hence in a cookie
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
// using this property would align the expiration of the cookie
// with the expiration of the identity token
options.UseTokenLifetime = true;
options.Events = new OpenIdConnectEvents
{
// that event is called after the OIDC middleware received the auhorisation code,
// redeemed it for an access token and a refresh token,
// and validated the identity token
OnTokenValidated = x =>
{
// store both access and refresh token in the claims - hence in the cookie
var identity = (ClaimsIdentity)x.Principal.Identity;
identity.AddClaims(new[]
{
new Claim("access_token", x.TokenEndpointResponse.AccessToken),
new Claim("refresh_token", x.TokenEndpointResponse.RefreshToken)
});
// so that we don't issue a session cookie but one with a fixed expiration
x.Properties.IsPersistent = true;
// align expiration of the cookie with expiration of the
// access token
var accessToken = new JwtSecurityToken(x.TokenEndpointResponse.AccessToken);
x.Properties.ExpiresUtc = accessToken.ValidTo;
x.Properties.IssuedUtc = DateTime.UtcNow;
x.Properties.AllowRefresh = true;
Debug.WriteLine("OIDC.OnTokenValidated - Token validated, Issued UTC: {0}, Expires UTC: {1}", x.Properties.IssuedUtc, x.Properties.ExpiresUtc);
x.HttpContext.Session.Set<string>(Constants.ACCESS_TOKEN_SESSION_ID, x.TokenEndpointResponse.AccessToken);
return Task.CompletedTask;
}
};
});
services.AddAntiforgery(options => options.HeaderName = "X-CSRF-TOKEN");
services.AddControllersWithViews();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
loggerFactory.AddLog4Net();
app.UseSession();
//Register Syncfusion license
Syncfusion.Licensing.SyncfusionLicenseProvider.RegisterLicense("License");
app.UseAuthentication();
app.UseCors();
app.UseCorsHeaderMiddleware();
app.UseExceptionHandlingMiddleware();
if (!env.IsDevelopment())
{
app.UseHttpsRedirection();
}
app.UseStaticFiles();
app.UseRouting();
app.UseCors(MyAllowSpecificOrigins);
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
使用DefectsWebApp.中间件;
使用IdentityModel;
使用IdentityModel.Client;
使用Microsoft.AspNetCore.Authentication.Cookies;
使用Microsoft.AspNetCore.Authentication.OpenIdConnect;
使用Microsoft.AspNetCore.Builder;
使用Microsoft.AspNetCore.Hosting;
使用Microsoft.AspNetCore.Http;
使用Microsoft.Extensions.Configuration;
使用Microsoft.Extensions.DependencyInjection;
使用Microsoft.Extensions.Hosting;
使用Microsoft.Extensions.Logging;
使用Newtonsoft.Json;
使用制度;
使用系统诊断;
使用System.IdentityModel.Tokens.Jwt;
使用System.Net.Http;
使用System.Security.Claims;
使用System.Threading.Tasks;
名称空间缺陷BAPP
{
公营创业
{
private bool isTokenRefreshRunning=false;
私有只读对象lockObj=新对象();
只读字符串MyAllowSpecificCorigins=“\u MyAllowSpecificCorigins”;
私家车正在运行
{
得到
{
锁(lockObj)
{
返回并重新运行;
}
}
设置
{
锁(lockObj)
{
isTokenRefreshRunning=值;
}
}
}
公共启动(IConfiguration配置)
{
配置=配置;
}
公共IConfiguration配置{get;}
//此方法由运行时调用。请使用此方法将服务添加到容器中。
public void配置服务(IServiceCollection服务)
{
JsonConvert.DefaultSettings=()=>新的JsonSerializerSettings
{
格式化=Newtonsoft.Json.Formatting.Indented,
ReferenceLoopHandling=Newtonsoft.Json.ReferenceLoopHandling.Ignore,
};
services.AddCors(选项=>
{
options.AddPolicy(名称:MyAllowSpecificCorigins,
生成器=>
{
建筑商。来源(“http://keycloak:8080", "https://keycloak")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
//从配置中获取URL
services.Configure(Configuration.GetSection(“QRodService”);
services.AddSession();
services.AddAuthorization(选项=>
{
options.AddPolicy(“用户”,策略=>
policy.RequireRole(“用户”);
});
//资料来源:https://stackoverflow.com/a/43875291
services.AddAuthentication(选项=>
{
options.DefaultAuthenticateScheme=CookieAuthenticationDefaults.AuthenticationScheme;
options.defaultsignnscheme=CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme=OpenIdConnectDefaults.AuthenticationScheme;
})
//资料来源:https://stackoverflow.com/questions/40032851/how-to-handle-expired-access-token-in-asp-net-core-using-refresh-token-with-open
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme,选项=>
{
options.Events=新建CookieAuthenticationEvents
{
//每次cookie中间件验证cookie时都会触发此事件,
//所以基本上在每个经过身份验证的请求中
//cookie的解密已经发生,因此我们可以访问用户声明
//和cookie属性-过期等。。
OnValidatePrincipal=异步x=>
{
//因为我们的cookie生存期是基于访问令牌1的,
//检查我们是否超过了饼干寿命的一半
var identity=(ClaimsIdentity)x.Principal.identity;
var accessTokenClaim=identity.FindFirst(“访问令牌”);
var refreshttokenclaim=identity.FindFirst(“刷新令牌”);
var accessToken=新的JwtSecurityToken(accessTokenClaim.Value);
var now=DateTime.UtcNow.AddMinutes(2);
var timeRemaining=accessToken.ValidTo.Subtract(现在);
var refreshttoken=newjwtsecuritytoken(refreshttokenclaim.Value);
var timeremaingrt=refreshtoken.ValidTo.Subtract(现在);
剩余时间=剩余时间。总秒数>0?剩余时间:新的时间跨度(0);
timeRemainingRT=timeRemainingRT.TotalSeconds>0?timeRemainingRT:新的时间跨度(0);
Debug.WriteLine(“访问令牌:{0}| timeleft:{1}”、accessToken.Id、timeleveling.ToString(@“hh\:mm\:ss”);
WriteLine(“刷新令牌:{0}| timeleft:{1}”、refreshtoken.Id、timeRemainingRT.ToString(@“hh\:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using System.Threading.Tasks;
namespace DefectsWebApp.Middleware
{
public class CorsHeaderMiddleware
{
private readonly RequestDelegate _next;
private IConfiguration _configuration;
private string _origin;
/// <summary>
/// Ctor
/// </summary>
/// <param name="next">Reference to following request</param>
public CorsHeaderMiddleware(RequestDelegate next, IConfiguration configuration)
{
_next = next;
_configuration = configuration;
_origin = _configuration["Authentication:oidc:OIDCRoot"] + "/*, /*";
}
/// <summary>
/// Fügt dem Request IMMER den Header "Access-Control-Allow-Origin" hinzu
/// </summary>
public async Task Invoke(HttpContext httpContext)
{
var request = httpContext.Request;
if (!request.Headers.ContainsKey(Constants.ORIGIN_HEADER))
{
request.Headers.Add(Constants.ORIGIN_HEADER, _origin);
}
if (!request.Headers.ContainsKey(Constants.CONTENT_HEADER))
{
request.Headers.Add(Constants.CONTENT_HEADER, "Origin, X-Requested-With, Content-Type, Accept");
}
await _next(httpContext);
}
}
public static class CorsHeaderMiddlewareExtensions
{
public static IApplicationBuilder UseCorsHeaderMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<CorsHeaderMiddleware>();
}
}
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseCors(option =>
option.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
);
}