diff --git a/Dockerfile b/Dockerfile index 3d4ddf6651..8e6766c33f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 8b8f295ee6..b49bee93c3 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -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 @@ -32,7 +81,3 @@ services: ports: - 8025:8025 - 1025:1025 - -volumes: - esdata: - driver: local diff --git a/src/Exceptionless.Core/Configuration/AuthOptions.cs b/src/Exceptionless.Core/Configuration/AuthOptions.cs index f394c32f99..cd74449702 100644 --- a/src/Exceptionless.Core/Configuration/AuthOptions.cs +++ b/src/Exceptionless.Core/Configuration/AuthOptions.cs @@ -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(); @@ -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; } diff --git a/src/Exceptionless.Web/ClientApp.angular/app/auth/login-controller.js b/src/Exceptionless.Web/ClientApp.angular/app/auth/login-controller.js index 7ac079b77c..3bcc27086d 100644 --- a/src/Exceptionless.Web/ClientApp.angular/app/auth/login-controller.js +++ b/src/Exceptionless.Web/ClientApp.angular/app/auth/login-controller.js @@ -71,6 +71,8 @@ return !!GOOGLE_APPID; case "live": return !!LIVE_APPID; + case 'oauth2': + return true; default: return false; } diff --git a/src/Exceptionless.Web/ClientApp.angular/app/auth/login.tpl.html b/src/Exceptionless.Web/ClientApp.angular/app/auth/login.tpl.html index f26f7dc9dc..5de727c761 100644 --- a/src/Exceptionless.Web/ClientApp.angular/app/auth/login.tpl.html +++ b/src/Exceptionless.Web/ClientApp.angular/app/auth/login.tpl.html @@ -19,10 +19,17 @@ autocomplete="on" >
-

- {{::'Log in' | translate}} - {{::'with' | translate}} -

+

Login with Syncfusion account

+
+ +
Validation error [AllowAnonymous] [Consumes("application/json")] + [HttpPost("aad")] + public Task> 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> LoginAsync(Login model) { diff --git a/src/Exceptionless.Web/Utility/AADClient.cs b/src/Exceptionless.Web/Utility/AADClient.cs new file mode 100644 index 0000000000..d2db9741f3 --- /dev/null +++ b/src/Exceptionless.Web/Utility/AADClient.cs @@ -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; + /// + /// Initializes a new instance of the class. + /// + /// The factory. + /// The configuration. + 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/" + }); + } + + /// + /// Should return parsed from content received from third-party service. + /// + /// The content which is received from third-party service. + protected override UserInfo ParseUserInfo(string content) + { + var response = JObject.Parse(content); + + return new UserInfo + { + Id = response["id"]?.Value() ?? string.Empty, // Ensure null safety with null-coalescing operator + Email = response["userPrincipalName"]?.SafeGet(x => x.Value()) ?? string.Empty, // Ensure null safety + FirstName = response["givenName"]?.Value() ?? string.Empty, // Ensure null safety + LastName = response["surname"]?.Value() ?? string.Empty // Ensure null safety + }; + } + + /// + /// Friendly name of provider (OAuth2 service). + /// + public override string Name + { + get { return "AAD"; } + } + + /// + /// Defines URI of service which issues access code. + /// + protected override Endpoint AccessCodeServiceEndpoint + { + get { return new Endpoint { BaseUri = BaseURI, Resource = "/" + TenentId + "/oauth2/authorize" }; } + } + + /// + /// Defines URI of service which issues access token. + /// + protected override Endpoint AccessTokenServiceEndpoint + { + get { return new Endpoint { BaseUri = BaseURI, Resource = "/" + TenentId + "/oauth2/token" }; } + } + + /// + /// Defines URI of service which allows to obtain information about user which is currently logged in. + /// + protected override Endpoint UserInfoServiceEndpoint + { + get { return new Endpoint { BaseUri = "https://graph.microsoft.com", Resource = "/v1.0/me" }; } + } + + /// + /// Encoding the user input + /// + public static string HtmlEncode(string name) + { + StringBuilder sbName = new StringBuilder(); + sbName.Append(HttpUtility.HtmlEncode(name)); + name = sbName.ToString(); + return name; + } + protected override async Task 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(); + } + } + } +}