Compare commits
3
Commits
89316e247f
...
2c4fccb22d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c4fccb22d | ||
|
|
f5d5b0f68f | ||
|
|
7e27b0742b |
No files matched your search
+15
@@ -0,0 +1,15 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Rider ignored files
|
||||||
|
/modules.xml
|
||||||
|
/contentModel.xml
|
||||||
|
/projectSettingsUpdater.xml
|
||||||
|
/.idea.ModeusSchedule.MSAuth.iml
|
||||||
|
# Ignored default folder with query files
|
||||||
|
/queries/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# Макросервис авторизации в Modeus через Microsoft
|
# Макросервис авторизации в Modeus через Microsoft
|
||||||
|
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
[](https://dotnet.microsoft.com/download/dotnet/9.0)
|
[](https://dotnet.microsoft.com/download/dotnet/10.0)
|
||||||
|
|
||||||
## Зачем нужен этот сервис
|
## Зачем нужен этот сервис
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.Playwright;
|
using Microsoft.Playwright;
|
||||||
|
|
||||||
namespace ModeusSchedule.MSAuth.BrowserScripts;
|
namespace ModeusSchedule.MSAuth.BrowserScripts;
|
||||||
@@ -17,11 +16,14 @@ public static class MicrosoftLoginHelper
|
|||||||
{
|
{
|
||||||
logger.LogInformation("Начало входа в Microsoft. Url: {LoginUrl}, пользователь: {Username}", loginUrl, username);
|
logger.LogInformation("Начало входа в Microsoft. Url: {LoginUrl}, пользователь: {Username}", loginUrl, username);
|
||||||
|
|
||||||
await page.GotoAsync(loginUrl, new PageGotoOptions { WaitUntil = WaitUntilState.DOMContentLoaded });
|
// Готовность формы проверяем ниже по полям: сторонние скрипты могут задерживать DOMContentLoaded.
|
||||||
|
await page.GotoAsync(loginUrl, new PageGotoOptions { WaitUntil = WaitUntilState.Commit });
|
||||||
|
|
||||||
logger.LogDebug("Страница логина загружена, ожидаем переход на домен Microsoft.");
|
logger.LogDebug("Началась загрузка страницы логина, ожидаем переход на домен Microsoft.");
|
||||||
|
|
||||||
await page.WaitForURLAsync(new Regex("login\\.(microsoftonline|live)\\.com", RegexOptions.IgnoreCase), new PageWaitForURLOptions { Timeout = 60_000 });
|
// WaitForURL при уже совпавшем URL дополнительно ожидает событие загрузки страницы.
|
||||||
|
await page.WaitForFunctionAsync(@"() => /^login\.(microsoftonline|live)\.com$/i.test(location.hostname)",
|
||||||
|
options: new PageWaitForFunctionOptions { Timeout = 60_000 });
|
||||||
|
|
||||||
var useAnotherAccount = page.Locator("div#otherTile, #otherTileText, div[data-test-id='useAnotherAccount']").First;
|
var useAnotherAccount = page.Locator("div#otherTile, #otherTileText, div[data-test-id='useAnotherAccount']").First;
|
||||||
try
|
try
|
||||||
@@ -36,7 +38,7 @@ public static class MicrosoftLoginHelper
|
|||||||
}
|
}
|
||||||
|
|
||||||
var emailInput = page.Locator("input[name='loginfmt'], input#i0116");
|
var emailInput = page.Locator("input[name='loginfmt'], input#i0116");
|
||||||
await emailInput.WaitForAsync(new LocatorWaitForOptions { State = WaitForSelectorState.Visible, Timeout = 30_000 });
|
await emailInput.WaitForAsync(new LocatorWaitForOptions { State = WaitForSelectorState.Visible, Timeout = 60_000 });
|
||||||
logger.LogDebug("Поле ввода email найдено, вводим логин.");
|
logger.LogDebug("Поле ввода email найдено, вводим логин.");
|
||||||
await emailInput.FillAsync(username);
|
await emailInput.FillAsync(username);
|
||||||
|
|
||||||
@@ -122,6 +124,30 @@ public static class MicrosoftLoginHelper
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
// Microsoft может предложить добавить еще один способ проверки после успешного MFA.
|
||||||
|
// Ссылка определяется по адресу, чтобы обработка не зависела от языка страницы.
|
||||||
|
var skipMfaRegistration = page.Locator("a[href*='skipmfaregistration=1']").First;
|
||||||
|
var canSkipMfaRegistration = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await skipMfaRegistration.WaitForAsync(new()
|
||||||
|
{
|
||||||
|
State = WaitForSelectorState.Visible,
|
||||||
|
Timeout = 8_000
|
||||||
|
});
|
||||||
|
canSkipMfaRegistration = true;
|
||||||
|
}
|
||||||
|
catch (TimeoutException)
|
||||||
|
{
|
||||||
|
logger.LogDebug("Предложение настроить дополнительный способ MFA не появилось — пропускаем шаг.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canSkipMfaRegistration)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Откладываем настройку дополнительного способа MFA.");
|
||||||
|
await skipMfaRegistration.ClickAsync();
|
||||||
|
}
|
||||||
|
|
||||||
await page.WaitForSelectorAsync("button, input[type='submit'], a", new PageWaitForSelectorOptions { Timeout = 8000 });
|
await page.WaitForSelectorAsync("button, input[type='submit'], a", new PageWaitForSelectorOptions { Timeout = 8000 });
|
||||||
|
|
||||||
var locator = page.Locator("#idSIButton9, #idBtn_Back").First;
|
var locator = page.Locator("#idSIButton9, #idBtn_Back").First;
|
||||||
|
|||||||
+22
-17
@@ -1,24 +1,29 @@
|
|||||||
FROM mcr.microsoft.com/dotnet/sdk:9.0-alpine3.22 AS build
|
FROM mcr.microsoft.com/dotnet/sdk:10.0-noble AS restore
|
||||||
ARG BUILD_CONFIGURATION=Release
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY . .
|
COPY ModeusSchedule.MSAuth.csproj .
|
||||||
RUN dotnet restore "ModeusSchedule.MSAuth.csproj"
|
RUN dotnet restore "ModeusSchedule.MSAuth.csproj" && \
|
||||||
RUN dotnet publish "ModeusSchedule.MSAuth.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
set -- /root/.nuget/packages/microsoft.playwright/*/.playwright && \
|
||||||
|
test "$#" -eq 1 && \
|
||||||
|
mkdir -p /opt/playwright/node && \
|
||||||
|
cp -a "$1/package" /opt/playwright/ && \
|
||||||
|
cp -a "$1/node/linux-x64" /opt/playwright/node/ && \
|
||||||
|
cp -a "$1/node/LICENSE" /opt/playwright/node/ && \
|
||||||
|
chmod +x /opt/playwright/node/linux-x64/node
|
||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS final
|
FROM restore AS build
|
||||||
|
ARG BUILD_CONFIGURATION=Release
|
||||||
|
COPY . .
|
||||||
|
RUN dotnet publish "ModeusSchedule.MSAuth.csproj" -c "$BUILD_CONFIGURATION" --no-restore -o /app/publish /p:UseAppHost=false && \
|
||||||
|
rm -rf /app/publish/.playwright /app/publish/playwright.ps1
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:10.0-noble AS final
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apt-get update && \
|
|
||||||
apt-get install -y --no-install-recommends wget && \
|
|
||||||
wget -q https://github.com/PowerShell/PowerShell/releases/download/v7.5.2/powershell_7.5.2-1.deb_amd64.deb && \
|
|
||||||
apt-get install -y ./powershell_7.5.2-1.deb_amd64.deb && \
|
|
||||||
rm -f powershell_7.5.2-1.deb_amd64.deb && \
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
|
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
|
||||||
|
|
||||||
COPY --from=build /app/publish .
|
COPY --from=restore /opt/playwright ./.playwright
|
||||||
RUN pwsh ./playwright.ps1 install --with-deps chromium
|
RUN ./.playwright/node/linux-x64/node ./.playwright/package/cli.js install --with-deps --only-shell chromium && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
ENTRYPOINT ["dotnet", "ModeusSchedule.MSAuth.dll"]
|
COPY --from=build /app/publish .
|
||||||
|
ENTRYPOINT ["dotnet", "ModeusSchedule.MSAuth.dll"]
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Playwright" Version="1.48.0" />
|
<PackageReference Include="Microsoft.Playwright" Version="1.63.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Microsoft.Playwright;
|
using Microsoft.Playwright;
|
||||||
@@ -50,6 +51,19 @@ public class MicrosoftAuthService(ILogger<MicrosoftAuthService> logger, IConfigu
|
|||||||
ViewportSize = null
|
ViewportSize = null
|
||||||
});
|
});
|
||||||
var page = await context.NewPageAsync();
|
var page = await context.NewPageAsync();
|
||||||
|
var pendingRequests = new ConcurrentDictionary<IRequest, byte>();
|
||||||
|
EventHandler<IRequest> requestStarted = (_, request) => pendingRequests.TryAdd(request, 0);
|
||||||
|
EventHandler<IRequest> requestFinished = (sender, request) => pendingRequests.TryRemove(request, out _);
|
||||||
|
EventHandler<IRequest> requestFailed = (sender, request) =>
|
||||||
|
{
|
||||||
|
pendingRequests.TryRemove(request, out _);
|
||||||
|
if (request.Failure == "net::ERR_ABORTED") return;
|
||||||
|
logger.LogWarning("Ошибка загрузки ресурса браузером. Домен: {Host}, тип: {ResourceType}, причина: {Failure}",
|
||||||
|
new Uri(request.Url).Host, request.ResourceType, request.Failure);
|
||||||
|
};
|
||||||
|
page.Request += requestStarted;
|
||||||
|
page.RequestFinished += requestFinished;
|
||||||
|
page.RequestFailed += requestFailed;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -80,11 +94,20 @@ public class MicrosoftAuthService(ILogger<MicrosoftAuthService> logger, IConfigu
|
|||||||
}
|
}
|
||||||
catch (Exception ex) when (ex is not MicrosoftAuthInProgressException)
|
catch (Exception ex) when (ex is not MicrosoftAuthInProgressException)
|
||||||
{
|
{
|
||||||
logger.LogError(ex, "Ошибка при получении JWT через Microsoft авторизацию");
|
logger.LogError(ex, "Ошибка при получении JWT через Microsoft авторизацию. Текущий домен браузера: {Host}",
|
||||||
|
page.Url == "about:blank" ? "about:blank" : new Uri(page.Url).Host);
|
||||||
|
if (!pendingRequests.IsEmpty)
|
||||||
|
{
|
||||||
|
var resources = pendingRequests.Keys.Select(request => $"{new Uri(request.Url).Host} ({request.ResourceType})").Distinct();
|
||||||
|
logger.LogWarning("Незавершенные запросы браузера: {Resources}", string.Join(", ", resources));
|
||||||
|
}
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
page.Request -= requestStarted;
|
||||||
|
page.RequestFinished -= requestFinished;
|
||||||
|
page.RequestFailed -= requestFailed;
|
||||||
await context.CloseAsync();
|
await context.CloseAsync();
|
||||||
await browser.CloseAsync();
|
await browser.CloseAsync();
|
||||||
|
|
||||||
@@ -123,6 +146,12 @@ public class MicrosoftAuthService(ILogger<MicrosoftAuthService> logger, IConfigu
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (_browsersEnsured) return;
|
if (_browsersEnsured) return;
|
||||||
|
if (string.Equals(Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER"), "true", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
// В Docker браузер и его зависимости уже установлены при сборке образа.
|
||||||
|
_browsersEnsured = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Устанавливаем Chromium, если не установлен
|
// Устанавливаем Chromium, если не установлен
|
||||||
|
|||||||
Reference in new issue
Block a user