Skip to content
Open
Show file tree
Hide file tree
Changes from all 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) =>
Comment thread
niemyjski marked this conversation as resolved.
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
39 changes: 35 additions & 4 deletions src/Exceptionless.Web/ClientApp.angular/app/auth/auth.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
(function () {
"use strict";

function createOAuthState() {
if (typeof window.crypto.randomUUID === "function") {
return window.crypto.randomUUID();
}

var bytes = window.crypto.getRandomValues(new Uint8Array(16));
return Array.prototype.map
.call(bytes, function (value) {
return ("0" + value.toString(16)).slice(-2);
})
.join("");
}

angular
.module("app.auth", [
"directives.inputMatch",
Expand All @@ -20,7 +33,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 +55,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 createOAuthState();
},
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 @@ -44,7 +44,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 = env.PUBLIC_ENABLE_ACCOUNT_CREATION === 'true';
export const facebookClientId = env.PUBLIC_FACEBOOK_APPID;
Expand Down Expand Up @@ -118,20 +118,20 @@ export async function gotoLogin() {
});
}

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: createOAuthState()
},
provider: 'live',
provider: 'microsoft',
redirectUrl,
scope: 'wl.emails'
scope: 'User.Read'
});
}

Expand Down Expand Up @@ -159,6 +159,14 @@ export async function slackOAuthLogin(): Promise<string> {

// 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);

Expand Down
Loading
Loading