From 3cab3dcd36d74339eaaf7230fd4d874573ebfed5 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 21 Oct 2026 21:54:27 -0500 Subject: [PATCH 01/12] Replace Windows Live authentication with Microsoft --- .../appsettings.Production.yml | 2 +- src/Exceptionless.Job/appsettings.Staging.yml | 2 +- .../Api/Endpoints/AuthEndpoints.cs | 6 +- .../Api/Handlers/AuthHandler.cs | 47 +++- .../Api/Handlers/UserHandler.cs | 5 +- .../Api/Messages/AuthMessages.cs | 2 +- .../src/lib/features/auth/index.svelte.ts | 46 +++- .../src/lib/features/auth/microsoft.test.ts | 137 ++++++++++ .../account/external-logins/+page.svelte | 6 +- .../src/routes/(auth)/login/+page.svelte | 6 +- .../src/routes/(auth)/signup/+page.svelte | 13 +- .../Security/OAuthProviderClient.cs | 4 +- .../appsettings.Production.yml | 2 +- src/Exceptionless.Web/appsettings.Staging.yml | 2 +- .../Api/Data/endpoint-manifest.json | 20 +- .../Exceptionless.Tests/Api/Data/openapi.json | 4 +- .../Api/Endpoints/AuthEndpointTests.cs | 16 +- .../Endpoints/MicrosoftAuthEndpointTests.cs | 258 ++++++++++++++++++ .../Api/Endpoints/UserEndpointTests.cs | 26 ++ .../Api/Handlers/AuthHandlerTests.cs | 79 +++++- .../Mail/CountingMailer.cs | 4 + .../Utility/TestOAuthProviderClient.cs | 2 +- 22 files changed, 619 insertions(+), 70 deletions(-) create mode 100644 src/Exceptionless.Web/ClientApp/src/lib/features/auth/microsoft.test.ts create mode 100644 tests/Exceptionless.Tests/Api/Endpoints/MicrosoftAuthEndpointTests.cs diff --git a/src/Exceptionless.Job/appsettings.Production.yml b/src/Exceptionless.Job/appsettings.Production.yml index b01a5221e1..54a50b1916 100644 --- a/src/Exceptionless.Job/appsettings.Production.yml +++ b/src/Exceptionless.Job/appsettings.Production.yml @@ -7,7 +7,7 @@ ConnectionStrings: # Storage: '' # Email: 'smtps://user:password@domain.com:587' # LDAP: '' - OAuth: FacebookId=395178683904310;GitHubId=7ef1dd5bfbc4ccf7f5ef;GoogleId=809763155066-enkkdmt4ierc33q9cft9nf5d5c02h30q.apps.googleusercontent.com;MicrosoftId=000000004C137E8B;SlackId=34500115540.177239122322; + OAuth: FacebookId=395178683904310;GitHubId=7ef1dd5bfbc4ccf7f5ef;GoogleId=809763155066-enkkdmt4ierc33q9cft9nf5d5c02h30q.apps.googleusercontent.com;MicrosoftId=;SlackId=34500115540.177239122322; # Base url for the ui used to build links in emails and other places. BaseURL: https://be.exceptionless.io diff --git a/src/Exceptionless.Job/appsettings.Staging.yml b/src/Exceptionless.Job/appsettings.Staging.yml index 608f6972a1..c3b0be8d15 100644 --- a/src/Exceptionless.Job/appsettings.Staging.yml +++ b/src/Exceptionless.Job/appsettings.Staging.yml @@ -6,7 +6,7 @@ ConnectionStrings: # MessageBus: provider=redis; # Queue: provider=redis; # Storage: provider=folder;path=.\storage= - OAuth: FacebookId=395178683904310;GitHubId=7ef1dd5bfbc4ccf7f5ef;GoogleId=809763155066-enkkdmt4ierc33q9cft9nf5d5c02h30q.apps.googleusercontent.com;MicrosoftId=000000004C137E8B;SlackId=34500115540.177239122322; + OAuth: FacebookId=395178683904310;GitHubId=7ef1dd5bfbc4ccf7f5ef;GoogleId=809763155066-enkkdmt4ierc33q9cft9nf5d5c02h30q.apps.googleusercontent.com;MicrosoftId=;SlackId=34500115540.177239122322; # Base url for the ui used to build links in emails and other places. BaseURL: https://dev-app.exceptionless.io diff --git a/src/Exceptionless.Web/Api/Endpoints/AuthEndpoints.cs b/src/Exceptionless.Web/Api/Endpoints/AuthEndpoints.cs index 7b1fc2b41a..afd5f85c18 100644 --- a/src/Exceptionless.Web/Api/Endpoints/AuthEndpoints.cs +++ b/src/Exceptionless.Web/Api/Endpoints/AuthEndpoints.cs @@ -151,9 +151,9 @@ headers api_key input box. } }); - group.MapPost("live", async (IMediator mediator, IMediatorResultMapper resultMapper, HttpContext httpContext, [FromBody] ExternalAuthInfo value) => + group.MapPost("microsoft", async (IMediator mediator, IMediatorResultMapper resultMapper, HttpContext httpContext, [FromBody] ExternalAuthInfo value) => { - return (await mediator.InvokeAsync>(new AuthMessages.LiveLogin(value, httpContext))).ToHttpResult(resultMapper); + return (await mediator.InvokeAsync>(new AuthMessages.MicrosoftLogin(value, httpContext))).ToHttpResult(resultMapper); }) .AllowAnonymous() .Accepts("application/json", "application/*+json") @@ -164,7 +164,7 @@ headers api_key input box. .WithMetadata(new EndpointDocumentation { ResponseDescriptions = new() { ["200"] = "User Authentication Token", - ["403"] = "Account Creation is currently disabled", + ["403"] = "Account creation is disabled or the existing account must be signed into before linking Microsoft", ["422"] = "Validation error", } }); diff --git a/src/Exceptionless.Web/Api/Handlers/AuthHandler.cs b/src/Exceptionless.Web/Api/Handlers/AuthHandler.cs index fc37943515..4bc0146072 100644 --- a/src/Exceptionless.Web/Api/Handlers/AuthHandler.cs +++ b/src/Exceptionless.Web/Api/Handlers/AuthHandler.cs @@ -37,6 +37,8 @@ public class AuthHandler( TimeProvider timeProvider, ILogger logger) { + private const string LegacyMicrosoftOAuthProvider = "WindowsLive"; + private const string MicrosoftOAuthProvider = "Microsoft"; private readonly ScopedCacheClient _cache = new(cacheClient, "Auth"); private static bool _isFirstUserChecked; private static readonly TimeSpan IntercomJwtLifetime = TimeSpan.FromMinutes(60); @@ -281,7 +283,7 @@ public Task> Handle(FacebookLogin message) ); } - public Task> Handle(LiveLogin message) + public Task> Handle(MicrosoftLogin message) { return ExternalLoginAsync(message.AuthInfo, message.Context, authOptions.MicrosoftId, @@ -522,7 +524,11 @@ private async Task> ExternalLoginAsync(ExternalAuthInfo auth User? user; try { - user = await FromExternalLoginAsync(userInfo, authInfo.InviteToken, httpContext); + var result = await FromExternalLoginAsync(userInfo, authInfo.InviteToken, httpContext); + if (!result.IsSuccess) + return Result.FromResult(result); + + user = result.Value; } catch (ApplicationException ex) { @@ -542,18 +548,22 @@ private async Task> ExternalLoginAsync(ExternalAuthInfo auth return new TokenResult { Token = await GetOrCreateAuthenticationTokenAsync(user) }; } - private async Task FromExternalLoginAsync(UserInfo userInfo, string? inviteToken, HttpContext httpContext) + private async Task> FromExternalLoginAsync(UserInfo userInfo, string? inviteToken, HttpContext httpContext) { ArgumentException.ThrowIfNullOrWhiteSpace(userInfo.Id); ArgumentException.ThrowIfNullOrWhiteSpace(userInfo.ProviderName); ArgumentException.ThrowIfNullOrWhiteSpace(userInfo.Email); + bool isMicrosoft = String.Equals(userInfo.ProviderName, MicrosoftOAuthProvider, StringComparison.OrdinalIgnoreCase); var existingUser = await userRepository.GetUserByOAuthProviderAsync(userInfo.ProviderName, userInfo.Id); using var _ = logger.BeginScope(new ExceptionlessState().Tag("External Login").Tag(userInfo.ProviderName).Identity(userInfo.Email).SetHttpContext(httpContext)); if (String.IsNullOrWhiteSpace(inviteToken) && httpContext.User.IsUserAuthType()) { var currentUser = httpContext.Request.GetUser(); + if (isMicrosoft && !currentUser.IsEmailAddressVerified) + return Result.Forbidden("Verify your account email address before linking Microsoft from your account settings."); + if (existingUser is not null) { if (!String.Equals(existingUser.Id, currentUser.Id, StringComparison.Ordinal)) @@ -567,26 +577,39 @@ private async Task FromExternalLoginAsync(UserInfo userInfo, string? invit } else { + if (RemoveLegacyMicrosoftOAuthAccounts(currentUser, userInfo.ProviderName)) + return await userRepository.SaveAsync(currentUser, o => o.Cache()); + return currentUser; } } currentUser.AddOAuthAccount(userInfo.ProviderName, userInfo.Id, userInfo.Email); + RemoveLegacyMicrosoftOAuthAccounts(currentUser, userInfo.ProviderName); return await userRepository.SaveAsync(currentUser, o => o.Cache()); } if (existingUser is not null) { - if (!existingUser.IsEmailAddressVerified) + bool hasChanges = RemoveLegacyMicrosoftOAuthAccounts(existingUser, userInfo.ProviderName); + if (!isMicrosoft && !existingUser.IsEmailAddressVerified) { existingUser.MarkEmailAddressVerified(); - await userRepository.SaveAsync(existingUser, o => o.Cache()); + hasChanges = true; } + if (hasChanges) + await userRepository.SaveAsync(existingUser, o => o.Cache()); + return existingUser; } + // Microsoft Graph mail is editable. Never create or match an account using it. + if (isMicrosoft) + return Result.Forbidden("Create and verify an account, or sign in to your existing account, then link Microsoft from your account settings."); + var user = !String.IsNullOrEmpty(userInfo.Email) ? await userRepository.GetByEmailAddressAsync(userInfo.Email) : null; + if (user is null) { if (!await IsAccountCreationEnabledAsync(inviteToken)) @@ -609,6 +632,20 @@ private async Task FromExternalLoginAsync(UserInfo userInfo, string? invit return user; } + private static bool RemoveLegacyMicrosoftOAuthAccounts(User user, string providerName) + { + if (!String.Equals(providerName, MicrosoftOAuthProvider, StringComparison.OrdinalIgnoreCase)) + return false; + + var legacyAccounts = user.OAuthAccounts + .Where(account => String.Equals(account.Provider, LegacyMicrosoftOAuthProvider, StringComparison.OrdinalIgnoreCase)) + .ToArray(); + foreach (var account in legacyAccounts) + user.OAuthAccounts.Remove(account); + + return legacyAccounts.Length > 0; + } + private async Task IsAccountCreationEnabledAsync(string? token) { if (authOptions.EnableAccountCreation) diff --git a/src/Exceptionless.Web/Api/Handlers/UserHandler.cs b/src/Exceptionless.Web/Api/Handlers/UserHandler.cs index 9a8a6e9787..65c293bb7b 100644 --- a/src/Exceptionless.Web/Api/Handlers/UserHandler.cs +++ b/src/Exceptionless.Web/Api/Handlers/UserHandler.cs @@ -284,7 +284,10 @@ public async Task> Handle(UpdateEmailAddress me user.ResetPasswordResetToken(); user.EmailAddress = email; - user.IsEmailAddressVerified = user.OAuthAccounts.Any(oa => String.Equals(oa.EmailAddress(), email, StringComparison.InvariantCultureIgnoreCase)); + // Microsoft Graph mail is editable and does not establish email ownership. + user.IsEmailAddressVerified = user.OAuthAccounts.Any(oa => + !String.Equals(oa.Provider, "Microsoft", StringComparison.OrdinalIgnoreCase) + && String.Equals(oa.EmailAddress(), email, StringComparison.InvariantCultureIgnoreCase)); if (user.IsEmailAddressVerified) user.MarkEmailAddressVerified(); else diff --git a/src/Exceptionless.Web/Api/Messages/AuthMessages.cs b/src/Exceptionless.Web/Api/Messages/AuthMessages.cs index 068710fd2d..cd43fd7a93 100644 --- a/src/Exceptionless.Web/Api/Messages/AuthMessages.cs +++ b/src/Exceptionless.Web/Api/Messages/AuthMessages.cs @@ -9,7 +9,7 @@ public record SignupMessage(Signup Model, HttpContext Context); public record GitHubLogin(ExternalAuthInfo AuthInfo, HttpContext Context); public record GoogleLogin(ExternalAuthInfo AuthInfo, HttpContext Context); public record FacebookLogin(ExternalAuthInfo AuthInfo, HttpContext Context); -public record LiveLogin(ExternalAuthInfo AuthInfo, HttpContext Context); +public record MicrosoftLogin(ExternalAuthInfo AuthInfo, HttpContext Context); public record RemoveExternalLogin(string ProviderName, ValueFromBody ProviderUserId, HttpContext Context); public record ChangePassword(ChangePasswordModel Model, HttpContext Context); public record CheckEmailAddress(string Email, HttpContext Context); diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/auth/index.svelte.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/auth/index.svelte.ts index 2d7aff7947..6bbf006859 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/auth/index.svelte.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/auth/index.svelte.ts @@ -9,9 +9,11 @@ import { import { goto } from '$app/navigation'; import { resolve } from '$app/paths'; import { useFetchClient } from '@foundatiofx/fetchclient'; +import { toast } from 'svelte-sonner'; import { gotoRedirect } from '#features/navigation/redirect.ts'; import { getVisibleUrl } from '#features/navigation/visible-url.ts'; +import { getProblemMessage } from '#shared/validation.ts'; import type { TokenResult } from './models'; @@ -54,7 +56,7 @@ export interface OAuthResponseData { state: string; } -export type SupportedOAuthProviders = 'facebook' | 'github' | 'google' | 'live' | 'slack'; +export type SupportedOAuthProviders = 'facebook' | 'github' | 'google' | 'microsoft' | 'slack'; export const enableAccountCreation = PUBLIC_ENABLE_ACCOUNT_CREATION === 'true'; export const facebookClientId = PUBLIC_FACEBOOK_APPID; @@ -133,21 +135,21 @@ export async function gotoLogin() { }); } -export async function liveLogin(redirectUrl?: string, inviteToken?: null | string) { +export async function microsoftLogin(redirectUrl?: string, inviteToken?: null | string) { if (!microsoftClientId) { - throw new Error('Live client id not set'); + throw new Error('Microsoft client id not set'); } await oauthLogin({ - authUrl: 'https://login.live.com/oauth20_authorize.srf', + authUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize', clientId: microsoftClientId, extraParams: { - display: 'popup' + state: createOAuthState() }, inviteToken, - provider: 'live', + provider: 'microsoft', redirectUrl, - scope: 'wl.emails' + scope: 'User.Read' }); } @@ -175,21 +177,37 @@ export async function slackOAuthLogin(): Promise { // OAuth helpers +function createOAuthState() { + if (typeof crypto.randomUUID === 'function') { + return crypto.randomUUID(); + } + + return Array.from(crypto.getRandomValues(new Uint8Array(16)), (value) => value.toString(16).padStart(2, '0')).join(''); +} + async function oauthLogin(options: OAuthLoginOptions) { const data = await openOAuthPopup(options); const client = useFetchClient(); - const response = await client.postJSON(`auth/${options.provider}`, { - clientId: options.clientId, - code: data.code, - inviteToken: options.inviteToken, - redirectUri: window.location.origin, - state: data.state - }); + const response = await client.postJSON( + `auth/${options.provider}`, + { + clientId: options.clientId, + code: data.code, + inviteToken: options.inviteToken, + redirectUri: window.location.origin, + state: data.state + }, + { + expectedStatusCodes: [401, 403, 422] + } + ); if (response.ok && response.data?.token) { accessToken.current = response.data.token; await gotoRedirect(options.redirectUrl || resolve('/(app)')); + } else { + toast.error(getProblemMessage(response.problem, 'Unable to sign in. Please try again.')); } } diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/auth/microsoft.test.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/auth/microsoft.test.ts new file mode 100644 index 0000000000..715f2fa732 --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/auth/microsoft.test.ts @@ -0,0 +1,137 @@ +import { FetchClient } from '@foundatiofx/fetchclient'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const { accessToken, goto, postJSON, toastError } = vi.hoisted(() => ({ + accessToken: { current: null as null | string }, + goto: vi.fn(), + postJSON: vi.fn(), + toastError: vi.fn() +})); + +vi.mock('$app/navigation', () => ({ goto })); +vi.mock('$app/paths', () => ({ resolve: (path: string) => path })); +vi.mock('$app/state', () => ({ page: {} })); +vi.mock('$env/dynamic/public', () => ({ env: { PUBLIC_MICROSOFT_APPID: 'microsoft-client-id' } })); +vi.mock('@foundatiofx/fetchclient', async (importOriginal) => ({ ...(await importOriginal()), useFetchClient: () => ({ postJSON }) })); +vi.mock('svelte-sonner', () => ({ toast: { error: toastError } })); +vi.mock('./api.svelte', () => ({})); +vi.mock('./state.svelte', () => ({ accessToken })); +vi.mock('./validators', () => ({ validateEmailAvailability: vi.fn() })); + +import { microsoftLogin } from './index.svelte'; + +describe('microsoftLogin', () => { + const popup = { close: vi.fn(), closed: false, focus: vi.fn(), location: new URL('https://login.microsoftonline.com/') }; + const open = vi.fn<(url: string) => typeof popup>().mockReturnValue(popup); + + beforeEach(() => { + vi.useFakeTimers(); + vi.clearAllMocks(); + accessToken.current = null; + popup.location = new URL('https://login.microsoftonline.com/'); + postJSON.mockResolvedValue({ data: { token: 'session-token' }, ok: true }); + vi.stubGlobal('window', { location: new URL('http://localhost:7131/login'), open, outerHeight: 900, outerWidth: 1200, screenX: 0, screenY: 0 }); + }); + + afterEach(() => { + vi.useRealTimers(); + vi.unstubAllGlobals(); + }); + + it.each([true, false])('validates state and preserves invitation and redirect with randomUUID available: %s', async (hasRandomUUID) => { + const getRandomValues = vi.fn((bytes: Uint8Array) => bytes.fill(10)); + vi.stubGlobal('crypto', { getRandomValues, randomUUID: hasRandomUUID ? () => 'oauth-state' : undefined }); + + const login = microsoftLogin('/organization/invited', 'invitation-token'); + const authorizationUrl = new URL(open.mock.calls[0]![0]); + expect(authorizationUrl.origin + authorizationUrl.pathname).toBe('https://login.microsoftonline.com/common/oauth2/v2.0/authorize'); + expect(authorizationUrl.searchParams.get('client_id')).toBe('microsoft-client-id'); + expect(authorizationUrl.searchParams.get('scope')).toBe('User.Read'); + expect(authorizationUrl.searchParams.get('response_type')).toBe('code'); + expect(authorizationUrl.searchParams.get('redirect_uri')).toBe('http://localhost:7131'); + const state = authorizationUrl.searchParams.get('state'); + expect(state).toBe(hasRandomUUID ? 'oauth-state' : '0a'.repeat(16)); + + popup.location = new URL(`http://localhost:7131/?code=authorization-code&state=${state}`); + await vi.advanceTimersByTimeAsync(500); + await login; + + expect(postJSON).toHaveBeenCalledExactlyOnceWith( + 'auth/microsoft', + { + clientId: 'microsoft-client-id', + code: 'authorization-code', + inviteToken: 'invitation-token', + redirectUri: 'http://localhost:7131', + state + }, + { expectedStatusCodes: [401, 403, 422] } + ); + expect(accessToken.current).toBe('session-token'); + expect(goto).toHaveBeenCalledExactlyOnceWith('/organization/invited'); + expect(popup.close).toHaveBeenCalledOnce(); + }); + + it.each([ + [ + { status: 403, title: 'Sign in to your existing account first, then link Microsoft from your account settings.' }, + 'Sign in to your existing account first, then link Microsoft from your account settings.' + ], + [undefined, 'Unable to sign in. Please try again.'] + ])('displays an unsuccessful exchange without changing the session', async (problem, message) => { + vi.stubGlobal('crypto', { randomUUID: () => 'expected-state' }); + accessToken.current = 'existing-session'; + postJSON.mockResolvedValue({ ok: false, problem }); + const login = microsoftLogin(); + popup.location = new URL('http://localhost:7131/?code=authorization-code&state=expected-state'); + await vi.advanceTimersByTimeAsync(500); + await login; + + expect(toastError).toHaveBeenCalledExactlyOnceWith(message); + expect(accessToken.current).toBe('existing-session'); + expect(goto).not.toHaveBeenCalled(); + expect(popup.close).toHaveBeenCalledOnce(); + }); + + it('displays the 403 with the application error callback enabled', async () => { + const message = 'Sign in to your existing account first, then link Microsoft from your account settings.'; + const client = new FetchClient({ + baseUrl: 'http://localhost/api/v2', + defaultRequestOptions: { + errorCallback: (response) => { + throw response.problem ?? response; + } + }, + fetch: async () => + new Response(JSON.stringify({ status: 403, title: message }), { + headers: { 'Content-Type': 'application/problem+json' }, + status: 403 + }) + }); + postJSON.mockImplementation(client.postJSON.bind(client)); + vi.stubGlobal('crypto', { randomUUID: () => 'expected-state' }); + const login = microsoftLogin(); + const completed = expect(login).resolves.toBeUndefined(); + popup.location = new URL('http://localhost:7131/?code=authorization-code&state=expected-state'); + await vi.advanceTimersByTimeAsync(500); + await completed; + expect(toastError).toHaveBeenCalledExactlyOnceWith(message); + expect(accessToken.current).toBeNull(); + expect(goto).not.toHaveBeenCalled(); + }); + + it.each(['state=wrong-state', '', 'error=access_denied'])('does not exchange the code when the callback contains %s', async (query) => { + vi.stubGlobal('crypto', { randomUUID: () => 'expected-state' }); + const login = microsoftLogin(); + const rejected = expect(login).rejects.toThrow(query.includes('error=') ? 'access_denied' : 'Invalid state'); + + popup.location = new URL(`http://localhost:7131/?code=authorization-code&${query}`); + await vi.advanceTimersByTimeAsync(500); + await rejected; + + expect(postJSON).not.toHaveBeenCalled(); + expect(accessToken.current).toBeNull(); + expect(goto).not.toHaveBeenCalled(); + expect(popup.close).toHaveBeenCalledOnce(); + }); +}); diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(app)/account/external-logins/+page.svelte b/src/Exceptionless.Web/ClientApp/src/routes/(app)/account/external-logins/+page.svelte index 399d725359..fda0466923 100644 --- a/src/Exceptionless.Web/ClientApp/src/routes/(app)/account/external-logins/+page.svelte +++ b/src/Exceptionless.Web/ClientApp/src/routes/(app)/account/external-logins/+page.svelte @@ -19,8 +19,8 @@ githubLogin, googleClientId, googleLogin, - liveLogin, - microsoftClientId + microsoftClientId, + microsoftLogin } from '#features/auth/index.svelte.ts'; import { getMeQuery } from '#features/users/api.svelte.ts'; @@ -58,7 +58,7 @@

Add an external login

{#if microsoftClientId} - {/if} diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(auth)/login/+page.svelte b/src/Exceptionless.Web/ClientApp/src/routes/(auth)/login/+page.svelte index 31c36255e4..2cf27e7924 100644 --- a/src/Exceptionless.Web/ClientApp/src/routes/(auth)/login/+page.svelte +++ b/src/Exceptionless.Web/ClientApp/src/routes/(auth)/login/+page.svelte @@ -28,9 +28,9 @@ githubLogin, googleClientId, googleLogin, - liveLogin, logout, - microsoftClientId + microsoftClientId, + microsoftLogin } from '#features/auth/index.svelte.ts'; import { type LoginFormData, LoginSchema } from '#features/auth/schemas.ts'; import { gotoRedirect } from '#features/navigation/redirect.ts'; @@ -178,7 +178,7 @@
{#if microsoftClientId} - {/if} diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(auth)/signup/+page.svelte b/src/Exceptionless.Web/ClientApp/src/routes/(auth)/signup/+page.svelte index 22400385b2..da5c6fd263 100644 --- a/src/Exceptionless.Web/ClientApp/src/routes/(auth)/signup/+page.svelte +++ b/src/Exceptionless.Web/ClientApp/src/routes/(auth)/signup/+page.svelte @@ -8,7 +8,6 @@ import FacebookIcon from '#comp/icons/FacebookIcon.svelte'; import GitHubIcon from '#comp/icons/GitHubIcon.svelte'; import GoogleIcon from '#comp/icons/GoogleIcon.svelte'; - import MicrosoftIcon from '#comp/icons/MicrosoftIcon.svelte'; import Logo from '#comp/logo.svelte'; import { A, P } from '#comp/typography/index.ts'; import { Button } from '#comp/ui/button/index.ts'; @@ -20,16 +19,13 @@ import { signup } from '#features/auth/api.svelte.ts'; import { accessToken, - enableOAuthLogin, facebookClientId, facebookLogin, gitHubClientId, githubLogin, googleClientId, googleLogin, - liveLogin, - logout, - microsoftClientId + logout } from '#features/auth/index.svelte.ts'; import { type SignupFormData, SignupSchema } from '#features/auth/schemas.ts'; import { validateEmailAvailability } from '#features/auth/validators.ts'; @@ -78,14 +74,9 @@
- {#if enableOAuthLogin} + {#if googleClientId || facebookClientId || gitHubClientId}

Sign up with

- {#if microsoftClientId} - - {/if} {#if googleClientId}