From bd9dc7405e0663c6636f36bae133e96a59486936 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Mon, 6 Jan 2025 15:54:45 +0700 Subject: [PATCH] fix login fwlite web (#1338) * use server login when SystemWebView is not enabled, also fix server-based login to support more then one request and cancel requests that hang for 5 minutes * removed --emptyOutDir from `build-app` otherwise the FwLiteWeb server fails to start when both the build and server are started at once due to the missing js files --- .../FwLite/FwLiteShared/Auth/AuthService.cs | 6 ++ .../FwLite/FwLiteShared/Auth/OAuthService.cs | 49 ++++++------ backend/FwLite/FwLiteWeb/Routes/AuthRoutes.cs | 10 +-- frontend/viewer/package.json | 2 +- frontend/viewer/src/HomeView.svelte | 31 ++------ .../viewer/src/lib/auth/LoginButton.svelte | 79 +++++++++++++++++++ .../FwLiteShared/Auth/IAuthService.ts | 1 + 7 files changed, 122 insertions(+), 56 deletions(-) create mode 100644 frontend/viewer/src/lib/auth/LoginButton.svelte diff --git a/backend/FwLite/FwLiteShared/Auth/AuthService.cs b/backend/FwLite/FwLiteShared/Auth/AuthService.cs index c30469cbd..a8bcf218a 100644 --- a/backend/FwLite/FwLiteShared/Auth/AuthService.cs +++ b/backend/FwLite/FwLiteShared/Auth/AuthService.cs @@ -28,6 +28,12 @@ public async Task SignInWebView(LexboxServer server) options.Value.AfterLoginWebView?.Invoke(); } + [JSInvokable] + public bool UseSystemWebView() + { + return options.Value.SystemWebViewLogin; + } + public async Task SignInWebApp(LexboxServer server, string returnUrl) { var result = await clientFactory.GetClient(server).SignIn(returnUrl); diff --git a/backend/FwLite/FwLiteShared/Auth/OAuthService.cs b/backend/FwLite/FwLiteShared/Auth/OAuthService.cs index 4de28e3ad..b67f77032 100644 --- a/backend/FwLite/FwLiteShared/Auth/OAuthService.cs +++ b/backend/FwLite/FwLiteShared/Auth/OAuthService.cs @@ -76,30 +76,35 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) { await foreach (var loginRequest in _requestChannel.Reader.ReadAllAsync(stoppingToken)) { - //this sits here and waits for AcquireAuthorizationCodeAsync to finish, meanwhile the uri passed in to that method is sent back to the caller of SubmitLoginRequest - //which then redirects the browser to that uri, once it's done it's sent back and calls FinishLoginRequest, which sends it's uri to OAuthLoginRequest - //which causes AcquireAuthorizationCodeAsync to return - - try - { - //todo we can get stuck here if the user doesn't complete the login, this basically bricks the login at the moment. We need a timeout or something - //step 2 - var result = await loginRequest.Application.AcquireTokenInteractive(OAuthClient.DefaultScopes) - .WithCustomWebUi(loginRequest) - .ExecuteAsync(stoppingToken); - //step 7, causes step 8 to resume - loginRequest.SetAuthenticationResult(result); - } - catch (Exception e) - { - logger.LogError(e, "Error getting token"); - loginRequest.SetException(e); - } - - if (loginRequest.State is not null) - _oAuthLoginRequests.Remove(loginRequest.State); + //don't await, otherwise we'll block the channel reader and only 1 login will be processed at a time + //cancel the login after 5 minutes, otherwise it'll probably hang forever and abandoned requests will never be cleaned up + _ = Task.Run(() => StartLogin(loginRequest, stoppingToken.Merge(new CancellationTokenSource(TimeSpan.FromMinutes(5)).Token)), stoppingToken); } } + + private async Task StartLogin(OAuthLoginRequest loginRequest, CancellationToken stoppingToken) + { + //this sits here and waits for AcquireAuthorizationCodeAsync to finish, meanwhile the uri passed in to that method is sent back to the caller of SubmitLoginRequest + //which then redirects the browser to that uri, once it's done it's sent back and calls FinishLoginRequest, which sends it's uri to OAuthLoginRequest + //which causes AcquireAuthorizationCodeAsync to return + try + { + //step 2 + var result = await loginRequest.Application.AcquireTokenInteractive(OAuthClient.DefaultScopes) + .WithCustomWebUi(loginRequest) + .ExecuteAsync(stoppingToken); + //step 7, causes step 8 to resume + loginRequest.SetAuthenticationResult(result); + } + catch (Exception e) + { + logger.LogError(e, "Error getting token"); + loginRequest.SetException(e); + } + + if (loginRequest.State is not null) + _oAuthLoginRequests.Remove(loginRequest.State); + } } /// diff --git a/backend/FwLite/FwLiteWeb/Routes/AuthRoutes.cs b/backend/FwLite/FwLiteWeb/Routes/AuthRoutes.cs index d1b727b4a..55bc0711c 100644 --- a/backend/FwLite/FwLiteWeb/Routes/AuthRoutes.cs +++ b/backend/FwLite/FwLiteWeb/Routes/AuthRoutes.cs @@ -18,16 +18,12 @@ public static IEndpointConventionBuilder MapAuthRoutes(this WebApplication app) async (AuthService authService, string authority, IOptions options, [FromHeader] string referer) => { var returnUrl = new Uri(referer).PathAndQuery; - //todo blazor, once we're using blazor this endpoint will only be used for non webview logins if (options.Value.SystemWebViewLogin) { - await authService.SignInWebView(options.Value.GetServerByAuthority(authority)); - return Results.Redirect(returnUrl); - } - else - { - return Results.Redirect(await authService.SignInWebApp(options.Value.GetServerByAuthority(authority), returnUrl)); + throw new NotSupportedException("System web view login is not supported for this endpoint"); } + + return Results.Redirect(await authService.SignInWebApp(options.Value.GetServerByAuthority(authority), returnUrl)); }); group.MapGet("/oauth-callback", async (OAuthService oAuthService, HttpContext context) => diff --git a/frontend/viewer/package.json b/frontend/viewer/package.json index 120e16599..ab26159ec 100644 --- a/frontend/viewer/package.json +++ b/frontend/viewer/package.json @@ -19,7 +19,7 @@ "dev": "vite build -m web-component --watch", "lexbox-dev": "vite build -m web-component", "build": "vite build -m web-component", - "build-app": "vite build --emptyOutDir", + "build-app": "vite build", "preview": "vite preview", "test": "vitest run", "test:ui": "vitest --ui", diff --git a/frontend/viewer/src/HomeView.svelte b/frontend/viewer/src/HomeView.svelte index 21a6497df..9da9937f0 100644 --- a/frontend/viewer/src/HomeView.svelte +++ b/frontend/viewer/src/HomeView.svelte @@ -17,6 +17,7 @@ import {onMount} from 'svelte'; import {useAuthService, useImportFwdataService, useProjectsService} from './lib/services/service-provider'; import type {ILexboxServer, IServerStatus} from '$lib/dotnet-types'; + import LoginButton from '$lib/auth/LoginButton.svelte'; const projectsService = useProjectsService(); const authService = useAuthService(); @@ -86,27 +87,9 @@ throw error; }); - let loadingServer: string = ''; - async function login(server: ILexboxServer) { - loadingServer = server.authority; - try { - await authService.signInWebView(server); - await fetchRemoteProjects(); - serversStatus = await authService.servers(); - } finally { - loadingServer = ''; - } - } - - async function logout(server: ILexboxServer) { - loadingServer = server.authority; - try { - await authService.logout(server); - await fetchRemoteProjects(); - serversStatus = await authService.servers(); - } finally { - loadingServer = ''; - } + async function refreshProjectsAndServers() { + await fetchRemoteProjects(); + serversStatus = await authService.servers(); } @@ -268,11 +251,7 @@ {#if status.loggedInAs}

{status.loggedInAs}

{/if} - {#if status.loggedIn} - - {:else} - - {/if} + refreshProjectsAndServers()} /> {@const serverProjects = remoteProjects[server.authority]?.filter(p => p.crdt) ?? []} {#each serverProjects as project} diff --git a/frontend/viewer/src/lib/auth/LoginButton.svelte b/frontend/viewer/src/lib/auth/LoginButton.svelte new file mode 100644 index 000000000..71bc32f32 --- /dev/null +++ b/frontend/viewer/src/lib/auth/LoginButton.svelte @@ -0,0 +1,79 @@ + + + + +{#if isLoggedIn} + +{:else} + {#if $shouldUseSystemWebView} + + {:else} + + {/if} +{/if} diff --git a/frontend/viewer/src/lib/dotnet-types/generated-types/FwLiteShared/Auth/IAuthService.ts b/frontend/viewer/src/lib/dotnet-types/generated-types/FwLiteShared/Auth/IAuthService.ts index fca224755..3ff3e7a27 100644 --- a/frontend/viewer/src/lib/dotnet-types/generated-types/FwLiteShared/Auth/IAuthService.ts +++ b/frontend/viewer/src/lib/dotnet-types/generated-types/FwLiteShared/Auth/IAuthService.ts @@ -10,6 +10,7 @@ export interface IAuthService { servers() : Promise; signInWebView(server: ILexboxServer) : Promise; + useSystemWebView() : Promise; signInWebApp(server: ILexboxServer, returnUrl: string) : Promise; logout(server: ILexboxServer) : Promise; getLoggedInName(server: ILexboxServer) : Promise;