Skip to content

Commit

Permalink
fix login fwlite web (#1338)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
hahn-kev authored Jan 6, 2025
1 parent 64ca37e commit bd9dc74
Show file tree
Hide file tree
Showing 7 changed files with 122 additions and 56 deletions.
6 changes: 6 additions & 0 deletions backend/FwLite/FwLiteShared/Auth/AuthService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> SignInWebApp(LexboxServer server, string returnUrl)
{
var result = await clientFactory.GetClient(server).SignIn(returnUrl);
Expand Down
49 changes: 27 additions & 22 deletions backend/FwLite/FwLiteShared/Auth/OAuthService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

/// <summary>
Expand Down
10 changes: 3 additions & 7 deletions backend/FwLite/FwLiteWeb/Routes/AuthRoutes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,12 @@ public static IEndpointConventionBuilder MapAuthRoutes(this WebApplication app)
async (AuthService authService, string authority, IOptions<AuthConfig> 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) =>
Expand Down
2 changes: 1 addition & 1 deletion frontend/viewer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
31 changes: 5 additions & 26 deletions frontend/viewer/src/HomeView.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -268,11 +251,7 @@
{#if status.loggedInAs}
<p class="mr-2 px-2 py-1 text-sm border rounded-full">{status.loggedInAs}</p>
{/if}
{#if status.loggedIn}
<Button loading={loadingServer === server.authority} variant="fill" color="primary" on:click={() => logout(server)} icon={mdiLogout}>Logout</Button>
{:else}
<Button loading={loadingServer === server.authority} variant="fill-light" color="primary" on:click={() => login(server)} icon={mdiLogin}>Login</Button>
{/if}
<LoginButton {server} isLoggedIn={status.loggedIn} on:status={() => refreshProjectsAndServers()} />
</div>
{@const serverProjects = remoteProjects[server.authority]?.filter(p => p.crdt) ?? []}
{#each serverProjects as project}
Expand Down
79 changes: 79 additions & 0 deletions frontend/viewer/src/lib/auth/LoginButton.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<script context="module" lang="ts">
import type {IAuthService} from '$lib/dotnet-types';
import {type Readable, writable, type Writable} from 'svelte/store';
let shouldUseSystemWebViewStore: Writable<boolean> | undefined = undefined;
function useSystemWebView(authService: IAuthService): Readable<boolean> {
if (shouldUseSystemWebViewStore) return shouldUseSystemWebViewStore;
shouldUseSystemWebViewStore = writable(true);
void authService.useSystemWebView().then(r => shouldUseSystemWebViewStore!.set(r));
return shouldUseSystemWebViewStore;
}
</script>

<script lang="ts">
import {mdiLogin, mdiLogout} from '@mdi/js';
import {Button} from 'svelte-ux';
import type {ILexboxServer} from '$lib/dotnet-types';
import {useAuthService} from '$lib/services/service-provider';
import {createEventDispatcher} from 'svelte';
const authService = useAuthService();
const shouldUseSystemWebView = useSystemWebView(authService);
const dispatch = createEventDispatcher<{
status: 'logged-in' | 'logged-out'
}>();
export let isLoggedIn: boolean;
export let server: ILexboxServer;
let loading = false;
async function login(server: ILexboxServer) {
loading = true;
try {
await authService.signInWebView(server);
dispatch('status', 'logged-in');
} finally {
loading = false;
}
}
async function logout(server: ILexboxServer) {
loading = true;
try {
await authService.logout(server);
dispatch('status', 'logged-out');
} finally {
loading = false;
}
}
</script>

{#if isLoggedIn}
<Button {loading}
variant="fill"
color="primary"
on:click={() => logout(server)}
icon={mdiLogout}>
Logout
</Button>
{:else}
{#if $shouldUseSystemWebView}
<Button {loading}
variant="fill-light"
color="primary"
on:click={() => login(server)}
icon={mdiLogin}>
Login
</Button>
{:else}
<Button {loading}
variant="fill-light"
color="primary"
href="/api/auth/login/{server.id}"
icon={mdiLogin}>
Login
</Button>
{/if}
{/if}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface IAuthService
{
servers() : Promise<IServerStatus[]>;
signInWebView(server: ILexboxServer) : Promise<void>;
useSystemWebView() : Promise<boolean>;
signInWebApp(server: ILexboxServer, returnUrl: string) : Promise<string>;
logout(server: ILexboxServer) : Promise<void>;
getLoggedInName(server: ILexboxServer) : Promise<string>;
Expand Down

0 comments on commit bd9dc74

Please sign in to comment.