diff --git a/Yuna.Website/Yuna.Website.Server/API/OpenAuthEndpoints.cs b/Yuna.Website/Yuna.Website.Server/API/OpenAuthEndpoints.cs new file mode 100644 index 0000000..a83950d --- /dev/null +++ b/Yuna.Website/Yuna.Website.Server/API/OpenAuthEndpoints.cs @@ -0,0 +1,159 @@ +using Microsoft.AspNetCore.Mvc; +using System.Text.Json.Serialization; +using Yuna.Website.Server.Infrastructure; +using Yuna.Website.Server.Services.OpenAuthService; + +namespace Yuna.Website.Server.API +{ + public class OpenAuthEndpoints + { + public void Define(WebApplication app) + { + app.MapGet("~/.well-known/openid-configuration", GetConfiguration); + + app.MapMethods("/v1.0", ["HEAD"], Ping); + + app.MapGet("api/oauth/login", LoginViaOauth); + } + + public class DiscoveryResponse + { + [JsonPropertyName("issuer")] + public string? Issuer { get; set; } + + [JsonPropertyName("authorization_endpoint")] + public string? AuthorizationEndpoint { get; set; } + + [JsonPropertyName("token_endpoint")] + public string? TokenEndpoint { get; set; } + + [JsonPropertyName("token_endpoint_auth_methods_supported")] + public IList? TokenEndpointAuthMethodsSupported { get; set; } + + [JsonPropertyName("token_endpoint_auth_signing_alg_values_supported")] + public IList? TokenEndpointAuthSigningAlgValuesSupported { get; set; } + + [JsonPropertyName("userinfo_endpoint")] + public string? UserinfoEndpoint { get; set; } + + [JsonPropertyName("check_session_iframe")] + public string? CheckSessionIframe { get; set; } + + [JsonPropertyName("end_session_endpoint")] + public string? EndSessionEndpoint { get; set; } + + [JsonPropertyName("jwks_uri")] + public string? JwksUri { get; set; } + + [JsonPropertyName("registration_endpoint")] + public string? RegistrationEndpoint { get; set; } + + [JsonPropertyName("scopes_supported")] + public IList? ScopesSupported { get; set; } + + [JsonPropertyName("response_types_supported")] + public IList? ResponseTypesSupported { get; set; } + + [JsonPropertyName("acr_values_supported")] + public IList? AcrValuesSupported { get; set; } + + [JsonPropertyName("subject_types_supported")] + public IList? SubjectTypesSupported { get; set; } + + [JsonPropertyName("userinfo_signing_alg_values_supported")] + public IList? UserinfoSigningAlgValuesSupported { get; set; } + + [JsonPropertyName("userinfo_encryption_alg_values_supported")] + public IList? UserinfoEncryptionAlgValuesSupported { get; set; } + + [JsonPropertyName("userinfo_encryption_enc_values_supported")] + public IList? UserinfoEncryptionEncValuesSupported { get; set; } + + [JsonPropertyName("id_token_signing_alg_values_supported")] + public IList? IdTokenSigningAlgValuesSupported { get; set; } + + [JsonPropertyName("id_token_encryption_alg_values_supported")] + public IList? IdTokenEncryptionAlgValuesSupported { get; set; } + + [JsonPropertyName("id_token_encryption_enc_values_supported")] + public IList? IdTokenEncryptionEncValuesSupported { get; set; } + + [JsonPropertyName("request_object_signing_alg_values_supported")] + public IList? RequestObjectSigningAlgValuesSupported { get; set; } + + [JsonPropertyName("display_values_supported")] + public IList? DisplayValuesSupported { get; set; } + + [JsonPropertyName("claim_types_supported")] + public IList? ClaimTypesSupported { get; set; } + + [JsonPropertyName("claims_supported")] + public IList? ClaimsSupported { get; set; } + + [JsonPropertyName("claims_parameter_supported")] + public bool? ClaimsParameterSupported { get; set; } + + [JsonPropertyName("service_documentation")] + public string? ServiceDocumentation { get; set; } + + [JsonPropertyName("ui_locales_supported")] + public IList? UiLocalesSupported { get; set; } + } + + public IResult GetConfiguration() + { + var response = new DiscoveryResponse() + { + Issuer = Settings.HttpsExternalUrl, + AuthorizationEndpoint = Settings.HttpsExternalUrl + "/makaka", + TokenEndpoint = Settings.HttpsExternalUrl + "/token", + TokenEndpointAuthMethodsSupported = ["client_secret_basic", "private_key_jwt"], + TokenEndpointAuthSigningAlgValuesSupported = ["RS256", "ES256"], + + AcrValuesSupported = ["urn:mace:incommon:iap:silver", "urn:mace:incommon:iap:bronze"], + ResponseTypesSupported = ["code", "code id_token", "id_token", "token id_token"], + SubjectTypesSupported = [], + + UserinfoEncryptionEncValuesSupported = ["A128CBC-HS256", "A128GCM"], + IdTokenSigningAlgValuesSupported = ["RS256", "ES256", "HS256"], + IdTokenEncryptionAlgValuesSupported = ["RSA1_5", "A128KW"], + IdTokenEncryptionEncValuesSupported = ["A128CBC-HS256", "A128GCM"], + + RequestObjectSigningAlgValuesSupported = ["none", "RS256", "ES256"], + DisplayValuesSupported = ["page", "popup"], + ClaimTypesSupported = ["normal", "distributed"], + + + ScopesSupported = [], + ClaimsSupported = [], + ClaimsParameterSupported = false, + ServiceDocumentation = null, + UiLocalesSupported = ["ru-RU"], + + }; + + return Results.Json(response); + } + + public IResult Ping() + { + return Results.Ok(); + } + + public IResult LoginViaOauth( + [FromQuery] string state, + [FromQuery] string redirect_uri, + [FromQuery]string response_type, + [FromQuery]string client_id, HttpContext context, + IOpenAuthService openAuthService) + { + + var host = context.Request.Host; + if (!openAuthService.ValidateLoginRequest(response_type, client_id, host.Value)) + return Results.Unauthorized(); + + //TODO LOGIN PAGE URL IN SETTINGS + return Results.Redirect($"https://localhost:5173/login?redirect_to={redirect_uri}"); + } + } +} diff --git a/Yuna.Website/Yuna.Website.Server/GlobalSettings.json b/Yuna.Website/Yuna.Website.Server/GlobalSettings.json index bf1f025..21855de 100644 --- a/Yuna.Website/Yuna.Website.Server/GlobalSettings.json +++ b/Yuna.Website/Yuna.Website.Server/GlobalSettings.json @@ -2,7 +2,8 @@ "AppVariables": { "AccessTokenLifeTimeMinutes": 1, "RefreshTokenLifeTimeDays": 365, - "ReferalCode": "#I_@m_g00d_guy_07_07_2024" + "ReferalCode": "#I_@m_g00d_guy_07_07_2024", + "HttpsExternalHost": "https://smarthome.vasich.keenetic.pro" }, "ConnectionStrings": { "BarcodeService": "http://localhost:7799/barcode", diff --git a/Yuna.Website/Yuna.Website.Server/Infrastructure/Encrypter.cs b/Yuna.Website/Yuna.Website.Server/Infrastructure/Encrypter.cs index d090709..abfb8b6 100644 --- a/Yuna.Website/Yuna.Website.Server/Infrastructure/Encrypter.cs +++ b/Yuna.Website/Yuna.Website.Server/Infrastructure/Encrypter.cs @@ -27,5 +27,13 @@ namespace Yuna.Website.Server.Infrastructure var hashedSalt = _tokenHasher.ComputeHash(bytesStr); return Convert.ToBase64String(hashedSalt); } + + public static string GenerateRandomString(int length) + { + const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + Random random = new Random(); + return new string(Enumerable.Repeat(chars, length) + .Select(s => s[random.Next(s.Length)]).ToArray()); + } } } diff --git a/Yuna.Website/Yuna.Website.Server/Infrastructure/Settings.cs b/Yuna.Website/Yuna.Website.Server/Infrastructure/Settings.cs index b6bae6e..36cb2e4 100644 --- a/Yuna.Website/Yuna.Website.Server/Infrastructure/Settings.cs +++ b/Yuna.Website/Yuna.Website.Server/Infrastructure/Settings.cs @@ -10,6 +10,8 @@ namespace Yuna.Website.Server.Infrastructure public static string ReferalCode { get; private set; } = null!; public static string DbConnectionStr { get; private set; } = null!; + public static string HttpsExternalUrl { get; private set; } = null!; + public static void Init() { var jsonText = File.ReadAllText("globalSettings.json"); @@ -50,6 +52,7 @@ namespace Yuna.Website.Server.Infrastructure private static void LoadAppVariables(JsonElement appVariablesStr) { + HttpsExternalUrl = appVariablesStr.GetProperty("HttpsExternalHost").GetString() ?? throw new Exception("no https exernal host"); ReferalCode = appVariablesStr.GetProperty("ReferalCode").GetString() ?? throw new Exception("No ref code"); AccessTokenLifeTime = TimeSpan.FromSeconds(appVariablesStr.GetProperty("AccessTokenLifeTimeMinutes").GetInt32()); RefreshTokenLifeTime = TimeSpan.FromDays(appVariablesStr.GetProperty("RefreshTokenLifeTimeDays").GetInt32()); diff --git a/Yuna.Website/Yuna.Website.Server/Model/UserBinding.cs b/Yuna.Website/Yuna.Website.Server/Model/UserBinding.cs new file mode 100644 index 0000000..70da23b --- /dev/null +++ b/Yuna.Website/Yuna.Website.Server/Model/UserBinding.cs @@ -0,0 +1,10 @@ +namespace Yuna.Website.Server.Model +{ + public class UserBinding + { + public long Id { get; set; } + public long UserId { get; set; } + public string Code { get; set; } = null!; + public string AccessToken { get; set; } = null!; + } +} diff --git a/Yuna.Website/Yuna.Website.Server/Properties/launchSettings.json b/Yuna.Website/Yuna.Website.Server/Properties/launchSettings.json index bcf7f19..095e15b 100644 --- a/Yuna.Website/Yuna.Website.Server/Properties/launchSettings.json +++ b/Yuna.Website/Yuna.Website.Server/Properties/launchSettings.json @@ -9,7 +9,7 @@ "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy" }, "dotnetRunMessages": true, - "applicationUrl": "http://localhost:5227" + "applicationUrl": "http://192.168.1.2:5227" }, "IIS Express": { "commandName": "IISExpress", @@ -28,6 +28,17 @@ "ASPNETCORE_HTTP_PORTS": "8080" }, "publishAllPorts": true + }, + "https": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://192.168.1.2:5227" } }, "$schema": "http://json.schemastore.org/launchsettings.json", @@ -39,4 +50,4 @@ "sslPort": 0 } } -} +} \ No newline at end of file diff --git a/Yuna.Website/Yuna.Website.Server/Services/OpenAuthService/IOpenAuthService.cs b/Yuna.Website/Yuna.Website.Server/Services/OpenAuthService/IOpenAuthService.cs new file mode 100644 index 0000000..3d705c6 --- /dev/null +++ b/Yuna.Website/Yuna.Website.Server/Services/OpenAuthService/IOpenAuthService.cs @@ -0,0 +1,11 @@ +using Yuna.Website.Server.Model; + +namespace Yuna.Website.Server.Services.OpenAuthService +{ + public interface IOpenAuthService + { + public bool ValidateLoginRequest(string responseType, string clientId, string host); + public UserBinding CreateBinding(User user); + + } +} diff --git a/Yuna.Website/Yuna.Website.Server/Services/OpenAuthService/OpenAuthService.cs b/Yuna.Website/Yuna.Website.Server/Services/OpenAuthService/OpenAuthService.cs new file mode 100644 index 0000000..1b6a0ed --- /dev/null +++ b/Yuna.Website/Yuna.Website.Server/Services/OpenAuthService/OpenAuthService.cs @@ -0,0 +1,47 @@ +using Yuna.Website.Server.Infrastructure; +using Yuna.Website.Server.Model; +using Yuna.Website.Server.Storage.Repositories.UserBindings; + +namespace Yuna.Website.Server.Services.OpenAuthService +{ + public class OpenAuthService : IOpenAuthService + { + private readonly IUserBindingsRepository _userBindingsRepository; + private readonly ILogger _logger; + public OpenAuthService(IUserBindingsRepository userBindingsRepository, ILogger logger) + { + _userBindingsRepository = userBindingsRepository; + _logger = logger; + } + + public UserBinding CreateBinding(User user) + { + //Надо переделать, чтобы код и токен точно были уникальные + var binding = new UserBinding() + { + AccessToken = Encrypter.GenerateRandomString(128), + Code = Encrypter.GenerateRandomString(50), + UserId = user.Id + }; + + _logger.LogInformation("Created OauthBinding:\n" + + "UserId: {0}\n" + + "AccessToken: {1}\n" + + "Code: {2}\n", user.Id, binding.Code, binding.AccessToken); + + return _userBindingsRepository.Create(binding); + } + + public bool ValidateLoginRequest(string responseType, string clientId, string host) + { + _logger.LogInformation("Host: {0}", host); + + var result = responseType.Equals("code") && clientId.Equals("vasich_yandex_server"); + + if (result) _logger.LogInformation("OauthRequest valid"); + else _logger.LogWarning("OauthRequest invalid"); + + return result; + } + } +} diff --git a/Yuna.Website/Yuna.Website.Server/Starter.cs b/Yuna.Website/Yuna.Website.Server/Starter.cs index c450dec..1eb9770 100644 --- a/Yuna.Website/Yuna.Website.Server/Starter.cs +++ b/Yuna.Website/Yuna.Website.Server/Starter.cs @@ -3,12 +3,14 @@ using Yuna.Website.Server.API; using Yuna.Website.Server.Infrastructure; using Yuna.Website.Server.Services.DeviceService; using Yuna.Website.Server.Services.DeviceSkillService; +using Yuna.Website.Server.Services.OpenAuthService; using Yuna.Website.Server.Services.TokenService; using Yuna.Website.Server.Services.UserService; using Yuna.Website.Server.Storage; using Yuna.Website.Server.Storage.Repositories.Device; using Yuna.Website.Server.Storage.Repositories.Prop; using Yuna.Website.Server.Storage.Repositories.User; +using Yuna.Website.Server.Storage.Repositories.UserBindings; namespace Yuna.Website.Server { @@ -60,6 +62,7 @@ namespace Yuna.Website.Server public static void DefineEndpoints(WebApplication app) { + new OpenAuthEndpoints().Define(app); new AuthEndpoints().Define(app); new SkillsEndpoints().Define(app); new DeviceEndpoints().Define(app); @@ -129,10 +132,13 @@ namespace Yuna.Website.Server builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); } public static void DefineAuth(WebApplicationBuilder builder) diff --git a/Yuna.Website/Yuna.Website.Server/Storage/Repositories/UserBindings/IUserBindingsRepository.cs b/Yuna.Website/Yuna.Website.Server/Storage/Repositories/UserBindings/IUserBindingsRepository.cs new file mode 100644 index 0000000..770b80f --- /dev/null +++ b/Yuna.Website/Yuna.Website.Server/Storage/Repositories/UserBindings/IUserBindingsRepository.cs @@ -0,0 +1,10 @@ +using Yuna.Website.Server.Model; + +namespace Yuna.Website.Server.Storage.Repositories.UserBindings +{ + public interface IUserBindingsRepository + { + public UserBinding? GetUserBinding(string code); + public UserBinding Create(UserBinding userBinding); + } +} diff --git a/Yuna.Website/Yuna.Website.Server/Storage/Repositories/UserBindings/UserBindingRepository.cs b/Yuna.Website/Yuna.Website.Server/Storage/Repositories/UserBindings/UserBindingRepository.cs new file mode 100644 index 0000000..59cc18a --- /dev/null +++ b/Yuna.Website/Yuna.Website.Server/Storage/Repositories/UserBindings/UserBindingRepository.cs @@ -0,0 +1,24 @@ +using Yuna.Website.Server.Model; + +namespace Yuna.Website.Server.Storage.Repositories.UserBindings +{ + public class UserBindingRepository : IUserBindingsRepository + { + private static List _inMemoryContext = new(); + + public UserBindingRepository() { } + + public UserBinding Create(UserBinding userBinding) + { + userBinding.Id = _inMemoryContext.Count; + _inMemoryContext.Add(userBinding); + return userBinding; + } + + public UserBinding? GetUserBinding(string code) + { + var binding = _inMemoryContext.FirstOrDefault(x => x.Code.Equals(code)); + return binding; + } + } +} diff --git a/Yuna.Website/yuna.website.client/src/services/Api.ts b/Yuna.Website/yuna.website.client/src/services/Api.ts index ef3d1f3..2dc89f1 100644 --- a/Yuna.Website/yuna.website.client/src/services/Api.ts +++ b/Yuna.Website/yuna.website.client/src/services/Api.ts @@ -1,7 +1,7 @@ import axios from "axios"; export const api = axios.create({ - baseURL: "http://localhost:5227/api/", + baseURL: "https://192.168.1.2:5227/api/", validateStatus: (status) => status < 500, withCredentials: true, headers: { 'Accept': 'application/json' }