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
3 changes: 3 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ COPY ./src/*.props ./src/
COPY ./tests/*.props ./tests/
COPY ./build/packages/* ./build/packages/

# Copy the Mail Templates
COPY src/Exceptionless.Core/Mail/Templates /app/src/Exceptionless.Core/Mail/Templates

# Copy the main source project files
COPY src/*/*.csproj ./
RUN for file in $(ls *.csproj); do mkdir -p src/${file%.*}/ && mv $file src/${file%.*}/; done
Expand Down
55 changes: 50 additions & 5 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,60 @@ services:
xpack.security.enabled: "false"
action.destructive_requires_name: false
ES_JAVA_OPTS: -Xms1g -Xmx1g
ulimits:
memlock:
soft: -1
hard: -1
networks:
- app_net
ports:
- 9200:9200
volumes:
- esdata:/usr/share/elasticsearch/data
- /home/syncfusion/esdata:/usr/share/elasticsearch/data
restart: unless-stopped

exceptionless:
build:
context: ../
dockerfile: Dockerfile
target: app
image: exceptionless
container_name: exp
depends_on:
- elasticsearch
environment:
EX_ConnectionStrings__Elasticsearch: http://elasticsearch:9200
ports:
- "5200:8080"
networks:
- app_net
volumes:
- /home/syncfusion/appdata:/app/storage
restart: unless-stopped

nginx:
image: nginx:latest
ports:
- "80:80"
- "443:443"
volumes:
- /home/syncfusion/nginx/nginx.conf:/etc/nginx/conf.d/default.conf
- /home/syncfusion/ssl/certs:/etc/ssl/certs
- /home/syncfusion/ssl/private:/etc/ssl/private
networks:
- app_net
depends_on:
- exceptionless

networks:
app_net:
driver: bridge

volumes:
esdata:
driver: local
appdata:
driver: local
kibana:
depends_on:
- elasticsearch
Expand All @@ -32,7 +81,3 @@ services:
ports:
- 8025:8025
- 1025:1025

volumes:
esdata:
driver: local
6 changes: 6 additions & 0 deletions src/Exceptionless.Core/Configuration/AuthOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ public class AuthOptions

public string? LdapConnectionString { get; internal set; }

public string? AADAppId { get; private set; }

public string? AADAppSecret { get; private set; }

public static AuthOptions ReadFromConfiguration(IConfiguration config)
{
var options = new AuthOptions();
Expand All @@ -45,6 +49,8 @@ public static AuthOptions ReadFromConfiguration(IConfiguration config)
options.FacebookSecret = oAuth.GetString(nameof(options.FacebookSecret));
options.GitHubId = oAuth.GetString(nameof(options.GitHubId));
options.GitHubSecret = oAuth.GetString(nameof(options.GitHubSecret));
options.AADAppId = oAuth.GetString(nameof(options.AADAppId));
options.AADAppSecret = oAuth.GetString(nameof(options.AADAppSecret));

return options;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@
return !!GOOGLE_APPID;
case "live":
return !!LIVE_APPID;
case 'oauth2':
return true;
default:
return false;
}
Expand Down
15 changes: 11 additions & 4 deletions src/Exceptionless.Web/ClientApp.angular/app/auth/login.tpl.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,17 @@
autocomplete="on"
>
<div class="form-horizontal col-sm-offset-2">
<h4>
{{::'Log in' | translate}}
<span ng-if="vm.isExternalLoginEnabled()">{{::'with' | translate}}</span>
</h4>
<h4><span>Login with Syncfusion account</span></h4>
<div class="form-group"
style="margin-left: 0px"
ng-if="vm.isExternalLoginEnabled('oauth2')">
<button type="button"
role="button"
ng-click="vm.authenticate('oauth2')"
ng-if="vm.isExternalLoginEnabled('oauth2')"
class="btn btn-large image-button icon-login-microsoft"
title="Log in using your Syncfusion account"></button>
</div>

<div
class="form-group"
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions src/Exceptionless.Web/Controllers/AuthController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,21 @@ public AuthController(AuthOptions authOptions, IOrganizationRepository organizat
/// <response code="422">Validation error</response>
[AllowAnonymous]
[Consumes("application/json")]
[HttpPost("aad")]
public Task<ActionResult<TokenResult>> AADAsync(ExternalAuthInfo value)
{
return ExternalLoginAsync(value,
_authOptions.AADAppId,
_authOptions.AADAppSecret,
(f, c) =>
{
c.Scope = "openid email profile";
return new AADClient(f, c);
}
);
}
[AllowAnonymous]
[Consumes("application/json")]
[HttpPost("login")]
public async Task<ActionResult<TokenResult>> LoginAsync(Login model)
{
Expand Down
135 changes: 135 additions & 0 deletions src/Exceptionless.Web/Utility/AADClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OAuth2.Client;
using OAuth2.Configuration;
using OAuth2.Infrastructure;
using OAuth2.Models;
using RestSharp;
using RestSharp.Authenticators;
using Endpoint = OAuth2.Client.Endpoint;

namespace Exceptionless.Web.Utility
{
public class AADClient : OAuth2Client
{
private const string BaseURI = "https://login.microsoftonline.com";
private static string? TenentId;

private readonly IRequestFactory _factory;
/// <summary>
/// Initializes a new instance of the <see cref="AADClient"/> class.
/// </summary>
/// <param name="factory">The factory.</param>
/// <param name="configuration">The configuration.</param>
public AADClient(IRequestFactory factory, IClientConfiguration configuration)
: base(factory, configuration)
{
_factory = factory;
TenentId = "tenantID";
}

protected override void BeforeGetAccessToken(BeforeAfterRequestArgs args)
{
args.Request.AddObject(new
{
code = args.Parameters.GetOrThrowUnexpectedResponse("code"),
client_id = Configuration.ClientId,
client_secret = Configuration.ClientSecret,
redirect_uri = Configuration.RedirectUri,
grant_type = "authorization_code",
resource = "https://graph.microsoft.com/"
});
}

/// <summary>
/// Should return parsed <see cref="UserInfo"/> from content received from third-party service.
/// </summary>
/// <param name="content">The content which is received from third-party service.</param>
protected override UserInfo ParseUserInfo(string content)
{
var response = JObject.Parse(content);

return new UserInfo
{
Id = response["id"]?.Value<string>() ?? string.Empty, // Ensure null safety with null-coalescing operator
Email = response["userPrincipalName"]?.SafeGet(x => x.Value<string>()) ?? string.Empty, // Ensure null safety
FirstName = response["givenName"]?.Value<string>() ?? string.Empty, // Ensure null safety
LastName = response["surname"]?.Value<string>() ?? string.Empty // Ensure null safety
};
}

/// <summary>
/// Friendly name of provider (OAuth2 service).
/// </summary>
public override string Name
{
get { return "AAD"; }
}

/// <summary>
/// Defines URI of service which issues access code.
/// </summary>
protected override Endpoint AccessCodeServiceEndpoint
{
get { return new Endpoint { BaseUri = BaseURI, Resource = "/" + TenentId + "/oauth2/authorize" }; }
}

/// <summary>
/// Defines URI of service which issues access token.
/// </summary>
protected override Endpoint AccessTokenServiceEndpoint
{
get { return new Endpoint { BaseUri = BaseURI, Resource = "/" + TenentId + "/oauth2/token" }; }
}

/// <summary>
/// Defines URI of service which allows to obtain information about user which is currently logged in.
/// </summary>
protected override Endpoint UserInfoServiceEndpoint
{
get { return new Endpoint { BaseUri = "https://graph.microsoft.com", Resource = "/v1.0/me" }; }
}

/// <summary>
/// Encoding the user input
/// </summary>
public static string HtmlEncode(string name)
{
StringBuilder sbName = new StringBuilder();
sbName.Append(HttpUtility.HtmlEncode(name));
name = sbName.ToString();
return name;
}
protected override async Task<UserInfo> GetUserInfoAsync(CancellationToken cancellationToken = default)
{
var client = _factory.CreateClient(UserInfoServiceEndpoint);
var request = _factory.CreateRequest(UserInfoServiceEndpoint);
request.AddHeader("Authorization", string.Format("bearer {0}", AccessToken));

BeforeGetUserInfo(new BeforeAfterRequestArgs
{
Client = client,
Request = request,
Configuration = Configuration
});

try
{
var response = await client.ExecuteAsync(request, Method.GET, cancellationToken);
var result = ParseUserInfo(response.Content);
result.ProviderName = Name;

return result;
}
catch (Exception)
{
return new UserInfo();
}
}
}
}