Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build/update-config-next.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ IFS=';' read -a oauthParts <<< "$OAuth"
for part in ${oauthParts[@]}
do
key="$( cut -d '=' -f 1 <<< $part )"; echo "key: $key"
value="$( cut -d '=' -f 2- <<< $part )"; echo "value: $value"
value="$( cut -d '=' -f 2- <<< $part )"

if [ "$key" == "FacebookId" ]; then
FacebookAppId=$value
Expand Down
4 changes: 2 additions & 2 deletions build/update-config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ IFS=';' read -a oauthParts <<< "$OAuth"
for part in ${oauthParts[@]}
do
key="$( cut -d '=' -f 1 <<< $part )"; echo "key: $key"
value="$( cut -d '=' -f 2- <<< $part )"; echo "value: $value"
value="$( cut -d '=' -f 2- <<< $part )"

if [ "$key" == "FacebookId" ]; then
FacebookAppId=$value
Expand Down Expand Up @@ -45,7 +45,7 @@ config="
.constant('GITHUB_APPID', '$GitHubAppId')
.constant('GOOGLE_APPID', '$GoogleAppId')
.constant('INTERCOM_APPID', '$IntercomAppId')
.constant('LIVE_APPID', '$MicrosoftAppId')
.constant('MICROSOFT_APPID', '$MicrosoftAppId')
.constant('SLACK_APPID', '$SlackAppId')
.constant('STRIPE_PUBLISHABLE_KEY', '$EX_StripePublishableApiKey')
.constant('SYSTEM_NOTIFICATION_MESSAGE', '$EX_NotificationMessage')
Expand Down
2 changes: 1 addition & 1 deletion src/Exceptionless.Job/appsettings.Production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/Exceptionless.Job/appsettings.Staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.exceptionless.io
Expand Down
4 changes: 2 additions & 2 deletions src/Exceptionless.Web/Api/Endpoints/AuthEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,9 @@ headers api_key input box.
}
});

group.MapPost("live", async (IMediator mediator, IMediatorResultMapper<HttpIResult> resultMapper, HttpContext httpContext, [FromBody] ExternalAuthInfo value) =>
group.MapPost("microsoft", async (IMediator mediator, IMediatorResultMapper<HttpIResult> resultMapper, HttpContext httpContext, [FromBody] ExternalAuthInfo value) =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge BLOCKER: Keep the legacy live authentication endpoint

During rolling deployments, with cached older frontends, or for external clients still using the published contract, requests continue to target POST /api/v2/auth/live; replacing rather than supplementing that route makes every such login return 404 immediately after the server upgrade. Preserve the legacy endpoint and handler as a compatibility path while adding /auth/microsoft, unless explicit approval for the breaking API change is obtained.

AGENTS.md reference: AGENTS.md:L67-L67

Useful? React with 👍 / 👎.

Comment thread
niemyjski marked this conversation as resolved.
{
return (await mediator.InvokeAsync<Result<TokenResult>>(new AuthMessages.LiveLogin(value, httpContext))).ToHttpResult(resultMapper);
return (await mediator.InvokeAsync<Result<TokenResult>>(new AuthMessages.MicrosoftLogin(value, httpContext))).ToHttpResult(resultMapper);
})
.AllowAnonymous()
.Accepts<ExternalAuthInfo>("application/json", "application/*+json")
Expand Down
29 changes: 27 additions & 2 deletions src/Exceptionless.Web/Api/Handlers/AuthHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ public class AuthHandler(
TimeProvider timeProvider,
ILogger<AuthHandler> 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);
Expand Down Expand Up @@ -285,7 +287,7 @@ public Task<Result<TokenResult>> Handle(FacebookLogin message)
);
}

public Task<Result<TokenResult>> Handle(LiveLogin message)
public Task<Result<TokenResult>> Handle(MicrosoftLogin message)
{
return ExternalLoginAsync(message.AuthInfo, message.Context,
authOptions.MicrosoftId,
Expand Down Expand Up @@ -577,22 +579,30 @@ private async Task<User> FromExternalLoginAsync(UserInfo userInfo, HttpContext h
}
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)
{
bool hasChanges = RemoveLegacyMicrosoftOAuthAccounts(existingUser, userInfo.ProviderName);
if (!existingUser.IsEmailAddressVerified)
{
existingUser.MarkEmailAddressVerified();
await userRepository.SaveAsync(existingUser, o => o.Cache());
hasChanges = true;
}

if (hasChanges)
await userRepository.SaveAsync(existingUser, o => o.Cache());

return existingUser;
}

Expand All @@ -610,6 +620,7 @@ private async Task<User> FromExternalLoginAsync(UserInfo userInfo, HttpContext h

user.MarkEmailAddressVerified();
user.AddOAuthAccount(userInfo.ProviderName, userInfo.Id, userInfo.Email);
RemoveLegacyMicrosoftOAuthAccounts(user, userInfo.ProviderName);

if (String.IsNullOrEmpty(user.Id))
await userRepository.AddAsync(user, o => o.Cache());
Expand All @@ -619,6 +630,20 @@ private async Task<User> FromExternalLoginAsync(UserInfo userInfo, HttpContext h
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<bool> IsAccountCreationEnabledAsync(string? token)
{
if (authOptions.EnableAccountCreation)
Expand Down
2 changes: 1 addition & 1 deletion src/Exceptionless.Web/Api/Messages/AuthMessages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> ProviderUserId, HttpContext Context);
public record ChangePassword(ChangePasswordModel Model, HttpContext Context);
public record CheckEmailAddress(string Email, HttpContext Context);
Expand Down
2 changes: 1 addition & 1 deletion src/Exceptionless.Web/ClientApp.angular/app.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
.constant("GITHUB_APPID")
.constant("GOOGLE_APPID")
.constant("INTERCOM_APPID")
.constant("LIVE_APPID")
.constant("MICROSOFT_APPID")
.constant("SLACK_APPID")
.constant("STRIPE_PUBLISHABLE_KEY")
.constant("SYSTEM_NOTIFICATION_MESSAGE")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
FACEBOOK_APPID,
GOOGLE_APPID,
GITHUB_APPID,
LIVE_APPID,
MICROSOFT_APPID,
notificationService,
projectService,
userService,
Expand Down Expand Up @@ -194,7 +194,7 @@

function isExternalLoginEnabled(provider) {
if (!provider) {
return !!FACEBOOK_APPID || !!GITHUB_APPID || !!GOOGLE_APPID || !!LIVE_APPID;
return !!FACEBOOK_APPID || !!GITHUB_APPID || !!GOOGLE_APPID || !!MICROSOFT_APPID;
}

switch (provider) {
Expand All @@ -204,8 +204,8 @@
return !!GITHUB_APPID;
case "google":
return !!GOOGLE_APPID;
case "live":
return !!LIVE_APPID;
case "microsoft":
return !!MICROSOFT_APPID;
default:
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -339,8 +339,8 @@ <h4>{{::'Add an external login' | translate}}</h4>
<button
type="button"
role="button"
ng-click="vm.authenticate('live')"
ng-if="vm.isExternalLoginEnabled('live')"
ng-click="vm.authenticate('microsoft')"
ng-if="vm.isExternalLoginEnabled('microsoft')"
class="btn btn-large image-button icon-login-microsoft"
title="{{::'Log in using your Microsoft account' | translate}}"
></button>
Expand Down
26 changes: 22 additions & 4 deletions src/Exceptionless.Web/ClientApp.angular/app/auth/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,15 @@
"exceptionless.validators",
])
.config(
function ($authProvider, $stateProvider, BASE_URL, FACEBOOK_APPID, GOOGLE_APPID, GITHUB_APPID, LIVE_APPID) {
function (
$authProvider,
$stateProvider,
BASE_URL,
FACEBOOK_APPID,
GOOGLE_APPID,
GITHUB_APPID,
MICROSOFT_APPID
) {
$authProvider.baseUrl = BASE_URL + "/api/v2";
$authProvider.facebook({
clientId: FACEBOOK_APPID,
Expand All @@ -34,9 +42,19 @@
clientId: GITHUB_APPID,
});

$authProvider.live({
clientId: LIVE_APPID,
scope: ["wl.emails"],
$authProvider.oauth2({
name: "microsoft",
url: "/auth/microsoft",
authorizationEndpoint: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
clientId: MICROSOFT_APPID,
redirectUri: window.location.origin,
requiredUrlParams: ["scope", "state"],
scope: ["User.Read"],
scopeDelimiter: " ",
state: function () {
return window.crypto.randomUUID();
Comment thread
niemyjski marked this conversation as resolved.
},
popupOptions: { width: 500, height: 560 },
});

$stateProvider.state("auth", {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
FACEBOOK_APPID,
GOOGLE_APPID,
GITHUB_APPID,
LIVE_APPID,
MICROSOFT_APPID,
ENABLE_ACCOUNT_CREATION,
notificationService,
projectService,
Expand Down Expand Up @@ -64,7 +64,7 @@

function isExternalLoginEnabled(provider) {
if (!provider) {
return !!FACEBOOK_APPID || !!GITHUB_APPID || !!GOOGLE_APPID || !!LIVE_APPID;
return !!FACEBOOK_APPID || !!GITHUB_APPID || !!GOOGLE_APPID || !!MICROSOFT_APPID;
}

switch (provider) {
Expand All @@ -74,8 +74,8 @@
return !!GITHUB_APPID;
case "google":
return !!GOOGLE_APPID;
case "live":
return !!LIVE_APPID;
case "microsoft":
return !!MICROSOFT_APPID;
default:
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ <h4>
<button
type="button"
role="button"
ng-click="vm.authenticate('live')"
ng-if="vm.isExternalLoginEnabled('live')"
ng-click="vm.authenticate('microsoft')"
ng-if="vm.isExternalLoginEnabled('microsoft')"
class="btn btn-large image-button icon-login-microsoft"
title="{{::'Log in using your Microsoft account' | translate}}"
></button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
FACEBOOK_APPID,
GOOGLE_APPID,
GITHUB_APPID,
LIVE_APPID,
MICROSOFT_APPID,
notificationService,
projectService,
stateService,
Expand Down Expand Up @@ -65,7 +65,7 @@

function isExternalLoginEnabled(provider) {
if (!provider) {
return !!FACEBOOK_APPID || !!GITHUB_APPID || !!GOOGLE_APPID || !!LIVE_APPID;
return !!FACEBOOK_APPID || !!GITHUB_APPID || !!GOOGLE_APPID || !!MICROSOFT_APPID;
}

switch (provider) {
Expand All @@ -75,8 +75,8 @@
return !!GITHUB_APPID;
case "google":
return !!GOOGLE_APPID;
case "live":
return !!LIVE_APPID;
case "microsoft":
return !!MICROSOFT_APPID;
default:
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ <h4 ng-if="vm.isExternalLoginEnabled()">{{::'Login with' | translate}}</h4>
<button
type="button"
role="button"
ng-click="vm.authenticate('live')"
ng-if="vm.isExternalLoginEnabled('live')"
ng-click="vm.authenticate('microsoft')"
ng-if="vm.isExternalLoginEnabled('microsoft')"
class="btn btn-large image-button icon-login-microsoft"
title="{{::'Log in using your Microsoft account' | translate}}"
></button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export interface OAuthResponseData {
state: string;
}

export type SupportedOAuthProviders = 'facebook' | 'github' | 'google' | 'live' | 'slack';
export type SupportedOAuthProviders = 'facebook' | 'github' | 'google' | 'microsoft' | 'slack';

const authSerializer = {
deserialize: (value: null | string): null | string => {
Expand Down Expand Up @@ -122,20 +122,20 @@ export async function gotoLogin() {
await goto(redirect, { replaceState: true });
}

export async function liveLogin(redirectUrl?: string) {
export async function microsoftLogin(redirectUrl?: 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: crypto.randomUUID()
},
provider: 'live',
provider: 'microsoft',
redirectUrl,
scope: 'wl.emails'
scope: 'User.Read'
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
githubLogin,
googleClientId,
googleLogin,
liveLogin,
microsoftClientId
microsoftClientId,
microsoftLogin
} from '$features/auth/index.svelte';
import { getMeQuery } from '$features/users/api.svelte';
import X from '@lucide/svelte/icons/x';
Expand Down Expand Up @@ -57,7 +57,7 @@
<H4>Add an external login</H4>
<div class="mt-2 flex flex-wrap gap-2">
{#if microsoftClientId}
<Button aria-label="Link Microsoft account" onclick={() => liveLogin()} variant="outline">
<Button aria-label="Link Microsoft account" onclick={() => microsoftLogin()} variant="outline">
<MicrosoftIcon class="size-4" /> Microsoft
</Button>
{/if}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
githubLogin,
googleClientId,
googleLogin,
liveLogin,
microsoftClientId
microsoftClientId,
microsoftLogin
} from '$features/auth/index.svelte';
import { type LoginFormData, LoginSchema } from '$features/auth/schemas';
import { getSafeRedirectUrl } from '$features/shared/url';
Expand Down Expand Up @@ -161,7 +161,7 @@
</div>
<div class="grid auto-cols-2 grid-flow-col grid-rows-2 gap-4">
{#if microsoftClientId}
<Button aria-label="Login with Microsoft" tabindex={4} onclick={() => liveLogin(redirectUrl)}>
<Button aria-label="Login with Microsoft" tabindex={4} onclick={() => microsoftLogin(redirectUrl)}>
<MicrosoftIcon class="size-4" /> Microsoft
</Button>
{/if}
Expand Down
Loading
Loading