Skip to content
Merged
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
3 changes: 1 addition & 2 deletions build/update-config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ for part in "${oauth_parts[@]}"; do
value="${value#"${value%%[![:space:]]*}"}"
value="${value%"${value##*[![:space:]]}"}"
case "$key" in
FacebookId|GitHubId|GoogleId|IntercomId|MicrosoftId|SlackId)
GitHubId|GoogleId|IntercomId|MicrosoftId|SlackId)
oauth_ids[$key]="$value" ;;
esac
done
Expand Down Expand Up @@ -43,7 +43,6 @@ mkdir -p _app
write_setting PUBLIC_EXCEPTIONLESS_CLIENT_SETUP_SHOW_SERVER_URL "${EX_ClientSetupShowServerUrl:-true}"
write_setting PUBLIC_EXCEPTIONLESS_SERVER_URL "${EX_ExceptionlessServerUrl:-}"
write_setting PUBLIC_STRIPE_PUBLISHABLE_KEY "${EX_StripePublishableApiKey:-}"
write_setting PUBLIC_FACEBOOK_APPID "${oauth_ids[FacebookId]:-}"
write_setting PUBLIC_GITHUB_APPID "${oauth_ids[GitHubId]:-}"
write_setting PUBLIC_GOOGLE_APPID "${oauth_ids[GoogleId]:-}"
write_setting PUBLIC_MICROSOFT_APPID "${oauth_ids[MicrosoftId]:-}"
Expand Down
2 changes: 1 addition & 1 deletion src/Exceptionless.AppHost/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@
.WithEnvironment("API_HTTPS", worktreePorts.ApiHttpsUrl);
}

// The e2e workflow runs npm ci before starting the AppHost, so a second install would only delay the app.
// The e2e workflow runs npm ci before starting the AppHost, so it skips Aspire's second install.
if (ciE2E)
app.WithNpm(install: false);

Expand Down
2 changes: 0 additions & 2 deletions src/Exceptionless.Core/Bootstrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -374,8 +374,6 @@ private static string GetEnabledIntegrations(AppOptions options)
integrations.Add("Google OAuth");
if (!String.IsNullOrWhiteSpace(options.AuthOptions.MicrosoftId))
integrations.Add("Microsoft OAuth");
if (!String.IsNullOrWhiteSpace(options.AuthOptions.FacebookId))
integrations.Add("Facebook OAuth");
if (!String.IsNullOrWhiteSpace(options.AuthOptions.GitHubId))
integrations.Add("GitHub OAuth");
if (options.AuthOptions.EnableActiveDirectoryAuth)
Expand Down
6 changes: 0 additions & 6 deletions src/Exceptionless.Core/Configuration/AuthOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,6 @@ public class AuthOptions

public string? MicrosoftSecret { get; internal set; }

public string? FacebookId { get; internal set; }

public string? FacebookSecret { get; internal set; }

public string? GitHubId { get; internal set; }

public string? GitHubSecret { get; internal set; }
Expand All @@ -41,8 +37,6 @@ public static AuthOptions ReadFromConfiguration(IConfiguration config)
options.GoogleSecret = oAuth.GetString(nameof(options.GoogleSecret));
options.MicrosoftId = oAuth.GetString(nameof(options.MicrosoftId));
options.MicrosoftSecret = oAuth.GetString(nameof(options.MicrosoftSecret));
options.FacebookId = oAuth.GetString(nameof(options.FacebookId));
options.FacebookSecret = oAuth.GetString(nameof(options.FacebookSecret));
options.GitHubId = oAuth.GetString(nameof(options.GitHubId));
options.GitHubSecret = oAuth.GetString(nameof(options.GitHubSecret));

Expand Down
14 changes: 10 additions & 4 deletions src/Exceptionless.Core/Extensions/UserExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,19 @@ public static void AddOAuthAccount(this User user, string providerName, string p

public static bool RemoveOAuthAccount(this User user, string providerName, string providerUserId)
{
if (user.OAuthAccounts.Count <= 1 && String.IsNullOrEmpty(user.Password))
return false;

var account = user.OAuthAccounts.FirstOrDefault(o => o.Provider == providerName.ToLowerInvariant() && o.ProviderUserId == providerUserId);
var account = user.OAuthAccounts.FirstOrDefault(o =>
String.Equals(o.Provider, providerName, StringComparison.OrdinalIgnoreCase)
&& String.Equals(o.ProviderUserId, providerUserId, StringComparison.Ordinal));
if (account is null)
return true;

var activeOAuthAccounts = user.OAuthAccounts
.Where(o => !String.Equals(o.Provider, "facebook", StringComparison.OrdinalIgnoreCase)
&& !String.Equals(o.Provider, "windowslive", StringComparison.OrdinalIgnoreCase))
.ToArray();
if (String.IsNullOrEmpty(user.Password) && activeOAuthAccounts.Length <= 1 && activeOAuthAccounts.Contains(account))
return false;

return user.OAuthAccounts.Remove(account);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ private sealed record SensitivePropertyRule(
Redact<AppOptions>(nameof(AppOptions.GoogleGeocodingApiKey)),
Redact<AppOptions>(nameof(AppOptions.MaxMindGeoIpKey)),
Redact<AuthOptions>(nameof(AuthOptions.MicrosoftSecret)),
Redact<AuthOptions>(nameof(AuthOptions.FacebookSecret)),
Redact<AuthOptions>(nameof(AuthOptions.GitHubSecret)),
Redact<AuthOptions>(nameof(AuthOptions.GoogleSecret)),
Sanitize<AuthOptions>(
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: 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: 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
Expand Down
22 changes: 2 additions & 20 deletions src/Exceptionless.Web/Api/Endpoints/AuthEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,27 +133,9 @@ headers api_key input box.
}
});

group.MapPost("facebook", 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.FacebookLogin(value, httpContext))).ToHttpResult(resultMapper);
})
.AllowAnonymous()
.Accepts<ExternalAuthInfo>("application/json", "application/*+json")
.Produces<TokenResult>()
.ProducesProblem(StatusCodes.Status403Forbidden)
.ProducesProblem(StatusCodes.Status422UnprocessableEntity)
.WithSummary("Sign in with Facebook")
.WithMetadata(new EndpointDocumentation {
ResponseDescriptions = new() {
["200"] = "User Authentication Token",
["403"] = "Account Creation is currently disabled",
["422"] = "Validation error",
}
});

group.MapPost("live", async (IMediator mediator, IMediatorResultMapper<HttpIResult> resultMapper, HttpContext httpContext, [FromBody] ExternalAuthInfo value) =>
{
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);
Comment thread
niemyjski marked this conversation as resolved.
})
.AllowAnonymous()
.Accepts<ExternalAuthInfo>("application/json", "application/*+json")
Expand Down
53 changes: 33 additions & 20 deletions src/Exceptionless.Web/Api/Handlers/AuthHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,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 @@ -272,16 +274,7 @@ public Task<Result<TokenResult>> Handle(GoogleLogin message)
);
}

public Task<Result<TokenResult>> Handle(FacebookLogin message)
{
return ExternalLoginAsync(message.AuthInfo, message.Context,
authOptions.FacebookId,
authOptions.FacebookSecret,
oauthProviderClient.GetFacebookUserInfoAsync
);
}

public Task<Result<TokenResult>> Handle(LiveLogin message)
public Task<Result<TokenResult>> Handle(MicrosoftLogin message)
{
return ExternalLoginAsync(message.AuthInfo, message.Context,
authOptions.MicrosoftId,
Expand All @@ -302,19 +295,16 @@ public async Task<Result<TokenResult>> Handle(RemoveExternalLogin message)
return Result.BadRequest("Invalid Provider Name or Provider User Id.");
}

if (user.OAuthAccounts.Count <= 1 && String.IsNullOrEmpty(user.Password))
{
logger.LogError("Remove external login failed for {EmailAddress}: You must set a local password before removing your external login", user.EmailAddress);
return Result.BadRequest("You must set a local password before removing your external login.");
}

try
{
if (user.RemoveOAuthAccount(message.ProviderName, message.ProviderUserId.Value))
if (!user.RemoveOAuthAccount(message.ProviderName, message.ProviderUserId.Value))
{
user.AuthenticationVersion = Core.Extensions.StringExtensions.GetNewToken();
await userRepository.SaveAsync(user, o => o.Cache());
logger.LogError("Remove external login failed for {EmailAddress}: You must set a local password before removing your external login", user.EmailAddress);
return Result.BadRequest("You must set a local password before removing your external login.");
}

user.AuthenticationVersion = Core.Extensions.StringExtensions.GetNewToken();
await userRepository.SaveAsync(user, o => o.Cache());
}
catch (Exception ex)
{
Expand Down Expand Up @@ -567,26 +557,35 @@ private async Task<User> 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)
{
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;
}

var user = !String.IsNullOrEmpty(userInfo.Email) ? await userRepository.GetByEmailAddressAsync(userInfo.Email) : null;

if (user is null)
{
if (!await IsAccountCreationEnabledAsync(inviteToken))
Expand All @@ -609,6 +608,20 @@ private async Task<User> 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<bool> IsAccountCreationEnabledAsync(string? token)
{
if (authOptions.EnableAccountCreation)
Expand Down
3 changes: 1 addition & 2 deletions src/Exceptionless.Web/Api/Messages/AuthMessages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ public record LogoutMessage(HttpContext Context);
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
1 change: 0 additions & 1 deletion src/Exceptionless.Web/ClientApp/.env
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ PUBLIC_ENABLE_ACCOUNT_CREATION=true
PUBLIC_EXCEPTIONLESS_API_KEY=
PUBLIC_EXCEPTIONLESS_CLIENT_SETUP_SHOW_SERVER_URL=true
PUBLIC_EXCEPTIONLESS_SERVER_URL=
PUBLIC_FACEBOOK_APPID=
PUBLIC_GITHUB_APPID=
PUBLIC_GOOGLE_APPID=
PUBLIC_INTERCOM_APPID=
Expand Down
1 change: 0 additions & 1 deletion src/Exceptionless.Web/ClientApp/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ export const variables = defineEnvVars({
PUBLIC_EXCEPTIONLESS_CLIENT_SETUP_SHOW_SERVER_URL: { public: true },
PUBLIC_EXCEPTIONLESS_SERVER_URL: { description: 'Server URL shown in client setup instructions and used for UI telemetry.', public: true },
PUBLIC_EXCEPTIONLESS_TELEMETRY_SERVER_URL: { public: true, schema: optional },
PUBLIC_FACEBOOK_APPID: { public: true },
PUBLIC_GITHUB_APPID: { public: true },
PUBLIC_GOOGLE_APPID: { public: true },
PUBLIC_INTERCOM_APPID: { public: true },
Expand Down
Loading
Loading