Основным приложением является веб-приложение ASP.NET Core 2.2, использующее Razor Pages.
- Пользователь получает доступ к URL-адресу приложения и перенаправляется на экран входа IdentityServer4
- Пользователь успешно входит в систему
- Пользователь выходит из системы и перенаправляется на «Вы вышли из системы» IdentityServer4 после успешного пропуска запроса на подтверждение выхода из системы.
- Этот шаг не выполняется: пользователь НЕ перенаправляется автоматически на страницу входа в системуIdentityServer4.Вместо этого он перенаправляется на страницу, где ему сообщают, что он вышел из системы, здесь его также спрашивают, хочет ли он перейти на страницу входа, на которую я хочу, чтобы он автоматически перенаправлялся без каких-либо подсказок.
То, что я пробовал до сих пор:
- В IDP >> Быстрый старт >> Аккаунт >> AccountOptions >>
public static bool AutomaticRedirectAfterSignOut = true;
(это должно рассматриваться как значение true для всего сообщения) - В IDP >> Запуск >> ConfigureServices >> .AddOpenIdConnect >>
options.SignedOutCallbackPath = "/signout-callback-oidc";
Добавление этой строки кода не помогло. - Я попытался запустить с использованием Chrome, Firefox, Edge -проблема сохраняется
код из IDP Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// adds MVC framework services
services.AddMvc();
/*// adds services Razor Pages and ASP.NET MVC require
.AddRazorPagesOptions(options => {options.Conventions.AddPageRoute("/index", "home"); });*/
// dependency injection of services
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); // registers an IHttpContextAccessor so we can access the current HttpContext in services by injecting it
services.AddScoped<IThisHttpClient, ThisHttpClient>(); // registers an "ThisHttpClient"
services.AddScoped<IOrderService, OrderService>();
services.AddScoped<IAccountService, AccountService>();
services.AddLogging();
// adding open id connect authentication
services
.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies") // configures the cookie handler and enables the application to use cookie based authentication for the default scheme
// The below handler creates authorization requests, token and other requests, and handles the identity token validation
// "oidc" ensure when part of application requires authentication, "OpenIdConnect" will be triggered by default
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies"; // matches the default scheme for authentication, ensures that succesful authentication result will be stored in a our applications "cookie"
options.Authority = "https://localhost:44370/"; // the authority is set to be the identity provider (the authority responsible for the IDP part of the OIDC flow), the middleware will use this value to read the metadata on the discovery end point, so it knows where to find different endpoints ad other information
options.ClientId = "3TL"; // must match client ID at IDP level
options.ResponseType = "code id_token"; // one of response type of the Hybrid ground to be used
//options.CallbackPath = new PathString("..."); // allows the change of the redirect Uri from inside the IdentityServerConfig (hand made class)
//options.SignedOutCallbackPath = new PathString("...") //
options.Scope.Add("openid"); // required scope which requires in sub value being included
options.Scope.Add("profile"); // ensures profile related claims are included
options.Scope.Add("roles"); // requesting a CUSTOM MADE scope (check IDP scopes/roles for details)
options.SaveTokens = true; // allows the middleware to save the tokens it receives from the identity provider so we can easelly use them afterwards
options.ClientSecret = "test_secret"; // must match secret at IDP level
options.GetClaimsFromUserInfoEndpoint = true; // enables GETing claims from user info endpoint regarding the current authenticate user
//options.ClaimActions.Remove("amr"); // allows us to remove CLAIM FILTERS (AKA this ensures the AMR(Authentication Method Reference) claim is dispalyed and not filtered out)
options.ClaimActions.DeleteClaim("sid"); // removing unnecessary claims from the initial cookie (session ID at level of IDP)
options.ClaimActions.DeleteClaim("idp"); // removing unnecessary claims from the initial cookie (the Identity Provider)
options.ClaimActions.DeleteClaim("name"); // removing unnecessary claims from the initial cookie (removing this type of data reduces the cookie size)
options.ClaimActions.DeleteClaim("given_name"); // removing unnecessary claims from the initial cookie (removing this type of data reduces the cookie size)
options.ClaimActions.DeleteClaim("family_name"); // removing unnecessary claims from the initial cookie (removing this type of data reduces the cookie size)
options.ClaimActions.MapUniqueJsonKey("role", "role"); // adding the CUSTOM MADE claim to the claims map
//options.SignedOutCallbackPath = "/signout-callback-oidc"; // NOTE THIS DOES NOT WORK
});
}
код из IDP Config.cs
public static IEnumerable<Client> GetClients()
{
return new List<Client>
{
/// IMPORTANT: The client details declared in here must be matched with their exact copy on the client
new Client
{
ClientId = "3TL",
ClientName = "3 Tier Logistics",
//using hybrid flow to authenticate users
AllowedGrantTypes = GrantTypes.Hybrid,
// limits the URIs the user can be redirected to after getting authenticated or logging out
RedirectUris = {"https://localhost:44321/signin-oidc" },
PostLogoutRedirectUris = { "https://localhost:44321/signout-callback-oidc" },
AllowedScopes = // configures the allowed scopes for this particular client (aka what user info to share from all the available)
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"roles"
},
ClientSecrets =
{
new Secret("test_secret".Sha256())
}
}
};
}
код со страницы выхода из сервера приложений
public class LoginModel : PageModel
{
private OrderService orderService;
private string targetUrlRegisterClient;
[BindProperty]
public Client Client { get; set; }
public void OnGet()
{
Task.Run(() => HttpContext.SignOutAsync("Cookies"));
Task.Run(() => HttpContext.SignOutAsync("oidc"));
}
public void OnPost()
{
targetUrlRegisterClient = "http://localhost:8080/server_war_exploded/root/api/registerclient";
orderService = new OrderService();
Task<string> response = orderService.PostRegisterClientAsync(Client, targetUrlRegisterClient);
}
}
Отладочный вывод при выходе из системы: хит
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/1.1 GET http://localhost:44321/Login
Microsoft.AspNetCore.Mvc.RazorPages.Internal.PageActionInvoker:Information: Route matched with {page = "/Login"}. Executing page /Login
Microsoft.AspNetCore.Mvc.RazorPages.Internal.PageActionInvoker:Information: Executing handler method Client_Customer.Pages.LoginModel.OnGet with arguments ((null)) - ModelState is Valid
Microsoft.AspNetCore.Mvc.RazorPages.Internal.PageActionInvoker:Information: Executed handler method OnGet, returned result .
Microsoft.AspNetCore.Mvc.RazorPages.Internal.PageActionInvoker:Information: Executing an implicit handler method - ModelState is Valid
Microsoft.AspNetCore.Mvc.RazorPages.Internal.PageActionInvoker:Information: Executed an implicit handler method, returned result Microsoft.AspNetCore.Mvc.RazorPages.PageResult.
Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler:Information: AuthenticationScheme: oidc signed out.
Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler:Information: AuthenticationScheme: Cookies signed out.
Microsoft.AspNetCore.Mvc.RazorPages.Internal.PageActionInvoker:Information: Executed page /Login in 69.7362ms
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/2.0 GET https://localhost:44370/connect/endsession?post_logout_redirect_uri=https%3A%2F%2Flocalhost%3A44321%2Fsignout-callback-oidc&id_token_hint=eyJhbGciOiJSUzI1NiIsImtpZCI6IjA5MmI0Yjk0MjQzNjJiZmQ3ZWM3Y2MyMDU1NGFiMTZlIiwidHlwIjoiSldUIn0.eyJuYmYiOjE1NTc5MDkzNzAsImV4cCI6MTU1NzkwOTY3MCwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NDQzNzAiLCJhdWQiOiIzVEwiLCJub25jZSI6IjYzNjkzNTA2MTYwMzY3NjQyMC5ZMkl6WTJReVlqRXRZMlF5TXkwME5UTmlMVGhpWkdFdFlqQTBNRGsxTVRWak1qazRNak5rTVdaaU1XRXRNV1EwTXkwME0yVTBMVGsxTlRNdFlUTmpZalEwWlRVd1pHSm0iLCJpYXQiOjE1NTc5MDkzNzAsImF0X2hhc2giOiJWWHVYRHJZcTZNWnF3X2N2T0h3eDNnIiwic2lkIjoiNGViNzQ5ZjA5ZGQ4MjNkNzI5NmQzMjU1NWU5MGJiMDYiLCJzdWIiOiIyMjIyIiwiYXV0aF90aW1lIjoxNTU3OTA5MzY3LCJpZHAiOiJsb2NhbCIsImFtciI6WyJwd2QiXX0.NzqA4kILvZgjlTd6dhku6827dG-_9MkJpAH11inQ0-biR0GXP7fkrklIRy8DgxDh8zEriNMUSM8gd9E_p7Zn4hn-HRZ5MJf1hOHfyo3Pdih0sgZ6eNzOvAManiLgNb85n6hcNx04H7PRLHjlZOR01dYkjZrnRCNTWLnVlrsu3xmnonagOtvtF5a_QuZqVJvUedqxby95RH-U5AuqW2pdPTQfzQVZBvUXrAdJGj6wOXwHCn9TSpRJcH4OPtWOMvP8Z84Iiz8vH_lK_qtBUkcSmjs_kOt_qFeGYgDE_xv71HMa0HhcbJlQ-GPwTJu2cA0teGUby33Sj-td92A7y1v5mQ&state=CfDJ8IMSTeB9liZHhYIais0HVw5svLoMCzrej-fgkjkCV_TaQjqMXAXfoVdkgkWNdpnCfCNjv9hXQ_qcU3uSC7KVbJaFghyxVZD1b3eL8Yeb_G8gnDDGoJYODAljLU_pki5M9aZbR_UbjmpgodcofaWnccPgRlLOf3nSTH1eiS2zoe8n&x-client-SKU=ID_NETSTANDARD2_0&x-client-ver=5.3.0.0
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 94.0857ms 302 text/html; charset=utf-8
IdentityServer4.Hosting.EndpointRouter:Debug: Request path /connect/endsession matched to endpoint type Endsession
IdentityServer4.Hosting.EndpointRouter:Debug: Endpoint enabled: Endsession, successfully created handler: IdentityServer4.Endpoints.EndSessionEndpoint
IdentityServer4.Hosting.IdentityServerMiddleware:Information: Invoking IdentityServer endpoint: IdentityServer4.Endpoints.EndSessionEndpoint for /connect/endsession
IdentityServer4.Endpoints.EndSessionEndpoint:Debug: Processing signout request for 2222
IdentityServer4.Validation.EndSessionRequestValidator:Debug: Start end session request validation
IdentityServer4.Validation.TokenValidator:Debug: Start identity token validation
IdentityServer4.Stores.ValidatingClientStore:Debug: client configuration validation for client 3TL succeeded.
IdentityServer4.Validation.TokenValidator:Debug: Client found: 3TL / 3 Tier Logistics
IdentityServer4.Test.TestUserProfileService:Debug: IsActive called from: IdentityTokenValidation
IdentityServer4.Validation.TokenValidator:Debug: Calling into custom token validator: IdentityServer4.Validation.DefaultCustomTokenValidator
IdentityServer4.Validation.TokenValidator:Debug: Token validation success
{
"ClientId": "3TL",
"ClientName": "3 Tier Logistics",
"ValidateLifetime": false,
"Claims": {
"nbf": 1557909370,
"exp": 1557909670,
"iss": "https://localhost:44370",
"aud": "3TL",
"nonce": "636935061603676420.Y2IzY2QyYjEtY2QyMy00NTNiLThiZGEtYjA0MDk1MTVjMjk4MjNkMWZiMWEtMWQ0My00M2U0LTk1NTMtYTNjYjQ0ZTUwZGJm",
"iat": 1557909370,
"at_hash": "VXuXDrYq6MZqw_cvOHwx3g",
"sid": "4eb749f09dd823d7296d32555e90bb06",
"sub": "2222",
"auth_time": 1557909367,
"idp": "local",
"amr": "pwd"
}
}
IdentityServer4.Validation.EndSessionRequestValidator:Information: End session request validation success
{
"ClientId": "3TL",
"ClientName": "3 Tier Logistics",
"SubjectId": "2222",
"PostLogOutUri": "https://localhost:44321/signout-callback-oidc",
"State": "CfDJ8IMSTeB9liZHhYIais0HVw5svLoMCzrej-fgkjkCV_TaQjqMXAXfoVdkgkWNdpnCfCNjv9hXQ_qcU3uSC7KVbJaFghyxVZD1b3eL8Yeb_G8gnDDGoJYODAljLU_pki5M9aZbR_UbjmpgodcofaWnccPgRlLOf3nSTH1eiS2zoe8n",
"Raw": {
"post_logout_redirect_uri": "https://localhost:44321/signout-callback-oidc",
"id_token_hint": "***REDACTED***",
"state": "CfDJ8IMSTeB9liZHhYIais0HVw5svLoMCzrej-fgkjkCV_TaQjqMXAXfoVdkgkWNdpnCfCNjv9hXQ_qcU3uSC7KVbJaFghyxVZD1b3eL8Yeb_G8gnDDGoJYODAljLU_pki5M9aZbR_UbjmpgodcofaWnccPgRlLOf3nSTH1eiS2zoe8n",
"x-client-SKU": "ID_NETSTANDARD2_0",
"x-client-ver": "5.3.0.0"
}
}
IdentityServer4.Endpoints.EndSessionEndpoint:Debug: Success validating end session request from 3TL
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 60.2054ms 302
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/2.0 GET https://localhost:44370/Account/Logout?logoutId=CfDJ8IMSTeB9liZHhYIais0HVw7kc9GyTnHxoDNbRDCp7qA3cjwUvTmwOc8iOz2QeBz21IjWognFICseoXkQwH-eQJtXY9bWxa1vP3GLUz98cWKI1VMcnArspUbxRT9bcUF1ZrmloS7t_Us-uV9Ipa-VkgSUmLZfXAWa2f-WXPIv3VRmGwLNV7dqQa_pQOTkyDoiW_ddGElcwit15bJ3BLS6f52dYIU5fzjlSBtzGqT516-usiS-wmfacbACtJQn1VaEahKiBnW7X1gI4PRhQhCZF-IFzeXjESuMigwFyUay7K79DOZCqJ7ReU-RZ7GR1TsFsnqS8212Dr10hkznljRMnDeB6CZbB87LorJxZvf_eH33NBhzJzCZ1bwvPoz_vJeQoHO50P1IfNUGZjO8Y7pYimUC52SCe0jCKUUF8a5t_HZHMNVvtoCgC8b42zHE9rM5ms25BWLTFsgQH6wJFG09fmI5Eu_ICWCTm7XbQxMsBLK8cXdyIb_g1ccqaoz1gohMtpfciokB5_xInN1EcResbtkRUNeLO5DN_c5aFX5QZrC-HJVqxLAdKzZ4coL-x06s8Emvu9w3S1ZjlYLZCPMKHfK1LKgAFnqq1rUEV9PDtwmWDe_gz9ga45MHyMYrrFTswq2ut2gylVFbnb9nEt-g4OWzC6Mqi1mq6Y9CmWefYDwmbsyO-hM9r-p3bZVgYCJkw77zP0UDzFndpM_82gwMau8PH85qxEk1Hz3kJJxilbvAOX1lowfBBymXZH1M0qaiIVS0V2MPL19ySkhIiBQZs7rYcsXMU-wILzhm_729Xt9TxUwZxRFgwngE2fFizgyG_g
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Information: Route matched with {action = "Logout", controller = "Account"}. Executing action IdentityServer4.Quickstart.UI.AccountController.Logout (IdentityServer)
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Information: Executing action method IdentityServer4.Quickstart.UI.AccountController.Logout (IdentityServer) with arguments (CfDJ8IMSTeB9liZHhYIais0HVw7kc9GyTnHxoDNbRDCp7qA3cjwUvTmwOc8iOz2QeBz21IjWognFICseoXkQwH-eQJtXY9bWxa1vP3GLUz98cWKI1VMcnArspUbxRT9bcUF1ZrmloS7t_Us-uV9Ipa-VkgSUmLZfXAWa2f-WXPIv3VRmGwLNV7dqQa_pQOTkyDoiW_ddGElcwit15bJ3BLS6f52dYIU5fzjlSBtzGqT516-usiS-wmfacbACtJQn1VaEahKiBnW7X1gI4PRhQhCZF-IFzeXjESuMigwFyUay7K79DOZCqJ7ReU-RZ7GR1TsFsnqS8212Dr10hkznljRMnDeB6CZbB87LorJxZvf_eH33NBhzJzCZ1bwvPoz_vJeQoHO50P1IfNUGZjO8Y7pYimUC52SCe0jCKUUF8a5t_HZHMNVvtoCgC8b42zHE9rM5ms25BWLTFsgQH6wJFG09fmI5Eu_ICWCTm7XbQxMsBLK8cXdyIb_g1ccqaoz1gohMtpfciokB5_xInN1EcResbtkRUNeLO5DN_c5aFX5QZrC-HJVqxLAdKzZ4coL-x06s8Emvu9w3S1ZjlYLZCPMKHfK1LKgAFnqq1rUEV9PDtwmWDe_gz9ga45MHyMYrrFTswq2ut2gylVFbnb9nEt-g4OWzC6Mqi1mq6Y9CmWefYDwmbsyO-hM9r-p3bZVgYCJkw77zP0UDzFndpM_82gwMau8PH85qxEk1Hz3kJJxilbvAOX1lowfBBymXZH1M0qaiIVS0V2MPL19ySkhIiBQZs7rYcsXMU-wILzhm_729Xt9TxUwZxRFgwngE2fFizgyG_g) - Validation state: Valid
Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler:Information: AuthenticationScheme: idsrv signed out.
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Information: Executed action method IdentityServer4.Quickstart.UI.AccountController.Logout (IdentityServer), returned result Microsoft.AspNetCore.Mvc.ViewResult in 24.8267ms.
Microsoft.AspNetCore.Mvc.ViewFeatures.ViewResultExecutor:Information: Executing ViewResult, running view LoggedOut.
Microsoft.AspNetCore.Mvc.ViewFeatures.ViewResultExecutor:Information: Executed ViewResult - view LoggedOut executed in 19.1822ms.
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Information: Executed action IdentityServer4.Quickstart.UI.AccountController.Logout (IdentityServer) in 48.1906ms
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 59.083ms 200 text/html; charset=utf-8
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/2.0 GET https://localhost:44370/lib/bootstrap/css/bootstrap.css
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/2.0 GET https://localhost:44370/icon.png
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/2.0 GET https://localhost:44370/lib/jquery/jquery.js
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 17.4182ms 404
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 16.464ms 404
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/2.0 GET https://localhost:44370/css/site.css
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 20.1358ms 404
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 8.4111ms 404
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/2.0 GET https://localhost:44370/lib/bootstrap/js/bootstrap.js
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/2.0 GET https://localhost:44370/js/signout-redirect.js
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/2.0 GET https://localhost:44370/connect/endsession/callback?endSessionId=CfDJ8IMSTeB9liZHhYIais0HVw4UP5MHpGg4UIrZ1rPYhDNKZ0T8aLNc6sQ00tDxQN7898mdUQGymNjElfE09nHu53Jcmj2OlrmLZdqwrS33_ea8BVUC1KpYuh1NtSAGTbqHF-Z4GVqWLIM3--4-kv1Jwggs2PBPjytq65cjCge00Zg2lNQEsKjgNxupv-gNwSWvdklOEQ9gRuGAd8dTXhUJqHomK7a87OWqQvuE1hQieeDesgtCSaVhC9-CcaEJycYkOkmyrxFrOWG4Npw6smPd-XU
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 7.3577ms 404
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 7.7551ms 404
IdentityServer4.Hosting.EndpointRouter:Debug: Request path /connect/endsession/callback matched to endpoint type Endsession
'iisexpress.exe' (CoreCLR: clrhost): Loaded 'C:\Program Files\dotnet\shared\Microsoft.NETCore.App\2.2.4\System.Net.Security.dll'. Symbols loaded.
IdentityServer4.Hosting.EndpointRouter:Debug: Endpoint enabled: Endsession, successfully created handler: IdentityServer4.Endpoints.EndSessionCallbackEndpoint
IdentityServer4.Hosting.IdentityServerMiddleware:Information: Invoking IdentityServer endpoint: IdentityServer4.Endpoints.EndSessionCallbackEndpoint for /connect/endsession/callback
IdentityServer4.Endpoints.EndSessionCallbackEndpoint:Debug: Processing signout callback request
IdentityServer4.Stores.ValidatingClientStore:Debug: client configuration validation for client 3TL succeeded.
IdentityServer4.Validation.EndSessionRequestValidator:Debug: No client front-channel logout URLs
IdentityServer4.Validation.EndSessionRequestValidator:Debug: No client back-channel logout URLs
IdentityServer4.Endpoints.EndSessionCallbackEndpoint:Information: Successful signout callback.
IdentityServer4.Endpoints.EndSessionCallbackEndpoint:Debug: No client front-channel iframe urls
IdentityServer4.Endpoints.EndSessionCallbackEndpoint:Debug: No client back-channel iframe urls
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 103.2685ms 200 text/html; charset=UTF-8
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/2.0 GET https://localhost:44370/favicon.ico
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 1.9017ms 404
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/2.0 GET https://localhost:44370/favicon.ico
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 2.2642ms 404
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/2.0 GET https://localhost:44370/favicon.ico
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 2.4325ms 404
Основная проблема:
Я хочу быть перенаправлен на страницу входа в систему IDP, чего я не могу в настоящее время достичь, иМне нужна помощь, чтобы понять, что я делаю не так.
PS Яя 3-й семестр в IT University, и я впервые использую IndetityServer4, я надеюсь использовать его для всех своих будущих приложений, спасибо за ваше терпение.