CORS-ошибка при обновлении токена доступа из keycloak - PullRequest
0 голосов
/ 29 апреля 2020

мы в настоящее время разрабатываем веб-приложение, состоящее из ASP. NET Core Frontend, Java JaxRS.Jersey API и Keycloak в качестве сервера аутентификации OpenID. В разработке все работает с http. Для нашего OpenID мы используем поток кода. Таким образом, webapi не возвращает перенаправлений в случае отсутствия или старых токенов. У нас есть контроль над каждым компонентом.

Мы столкнулись с проблемой, когда пользователь был неактивен в течение времени, превышающего время жизни токена доступа: Console output

Мы подозреваем , что это проблема конфигурации, и мы неправильно настроили заголовок CORS для одного компонента. Нужно ли настраивать заголовок CORS на нашем Keycloak? Если да, то как мы можем добавить отсутствующую конфигурацию?

Это наш текущий код в форме ConfigureServices-Method Startup.cs в. NET Core Frontend:

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

Для для полноты, вот код нашего промежуточного программного обеспечения:

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

Редактировать 1 [2020-04-30 10:45]

Это наш текущая конфигурация Чтобы устранить проблемы, связанные с локальным хостом, мы ввели DNS-имя нашей тестовой машины в качестве веб-источника. Keycloak Configuration

Ответы [ 2 ]

0 голосов
/ 30 апреля 2020

Это на самом деле ошибка в ядре do tnet.

Попытайтесь добавить политику Cors прямо в методе "Configure".

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    app.UseRouting();
    app.UseCors(option =>
        option.AllowAnyOrigin()
            .AllowAnyMethod()
            .AllowAnyHeader()
            );
}
0 голосов
/ 29 апреля 2020

Полагаю, вы не настроили Web Origins (это не то же самое, что Redirect URIs) в конфигурации клиента OID C в Keycloak. Вы можете использовать '*', потому что вы используете протокол http.

Надеюсь, вы знаете, что протокол https является обязательным для потоков OID C, а также что * * не является допустимым значением Web Origin для протокола https. Так что неплохо бы настроить Web Origins явно, а не подстановочный знак для будущего.

Лучше всего использовать уже в dev:

  • https, потому что он может работать в dev на http, но затем вы перейдете к https в prod, и он будет поврежден
  • доменом (вы можете «подделать» его с локальным файлом hosts) вместо localhost, потому что некоторые браузеры могут иметь проблемы с localhost/127.0.0.1/...
...