diff --git a/WotBlitzStatisticsPro.Common/Model/GitHubUser.cs b/WotBlitzStatisticsPro.Common/Model/GitHubUser.cs
new file mode 100644
index 0000000..b5ec940
--- /dev/null
+++ b/WotBlitzStatisticsPro.Common/Model/GitHubUser.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Text.Json.Serialization;
+
+namespace WotBlitzStatisticsPro.Common.Model
+{
+ ///
+ /// Represents authenticated GitHub user
+ ///
+ public class GitHubUser
+ {
+ ///
+ /// User ID
+ ///
+ [JsonPropertyName("id")]
+ public long Id { get; set; }
+
+ ///
+ /// Login
+ ///
+ [JsonPropertyName("login")]
+ public string? Login { get; set; }
+
+ ///
+ /// Avatar URL
+ ///
+ [JsonPropertyName("avatar_url")]
+ public string? Avatar { get; set; }
+
+ ///
+ /// GitHub profile
+ ///
+ [JsonPropertyName("html_url")]
+ public string? ProfileLink { get; set; }
+
+ ///
+ /// HitHub account creation time
+ ///
+ [JsonPropertyName("created_at")]
+ public DateTime CreatedAt { get; set; }
+
+ ///
+ /// Followers count
+ ///
+ [JsonPropertyName("followers")]
+ public int FollowersCount { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/WotBlitzStatisticsPro.GraphQl/GitHubOauth/GitHubOauthAppConfiguration.cs b/WotBlitzStatisticsPro.GraphQl/GitHubOauth/GitHubOauthAppConfiguration.cs
new file mode 100644
index 0000000..fc5899c
--- /dev/null
+++ b/WotBlitzStatisticsPro.GraphQl/GitHubOauth/GitHubOauthAppConfiguration.cs
@@ -0,0 +1,20 @@
+namespace WotBlitzStatisticsPro.GraphQl.GitHubOauth
+{
+ public class GitHubOauthAppConfiguration
+ {
+ ///
+ /// Github OAuth clientId
+ ///
+ public string? ClientId { get; set; }
+
+ ///
+ /// GutHub OAuth client secret
+ ///
+ public string? ClientSecret { get; set; }
+
+ ///
+ /// GutHub user, that allowed to make dictionary updates mutation
+ ///
+ public string AllowedUser { get; set; } = "DrMboga";
+ }
+}
\ No newline at end of file
diff --git a/WotBlitzStatisticsPro.GraphQl/GitHubOauth/GitHubOauthService.cs b/WotBlitzStatisticsPro.GraphQl/GitHubOauth/GitHubOauthService.cs
new file mode 100644
index 0000000..b1bfdb3
--- /dev/null
+++ b/WotBlitzStatisticsPro.GraphQl/GitHubOauth/GitHubOauthService.cs
@@ -0,0 +1,106 @@
+using System;
+using System.Collections.Generic;
+using System.IdentityModel.Tokens.Jwt;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Security.Claims;
+using System.Text;
+using System.Text.Json;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using Microsoft.IdentityModel.Tokens;
+using WotBlitzStatisticsPro.Common.Model;
+
+namespace WotBlitzStatisticsPro.GraphQl.GitHubOauth
+{
+ public class GitHubOauthService: IGitHubOauthService
+ {
+ private readonly IHttpClientFactory _httpClientFactory;
+ private readonly GitHubOauthAppConfiguration _configuration;
+
+ public GitHubOauthService(
+ IHttpClientFactory httpClientFactory,
+ GitHubOauthAppConfiguration configuration)
+ {
+ _httpClientFactory = httpClientFactory;
+ _configuration = configuration;
+ if (_configuration.ClientId == null || _configuration.ClientSecret == null)
+ {
+ throw new ArgumentNullException(nameof(_configuration));
+ }
+ }
+
+ public async Task GetGitHubUser(string oAuthCode)
+ {
+ var token = await GetAccessToken(oAuthCode);
+ return await GetUser(token);
+ }
+
+ public string GenerateToken(GitHubUser user)
+ {
+ var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration.ClientSecret!));
+ var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
+
+ // generate jwt token
+ var claims = new[]
+ {
+ new Claim(ClaimTypes.Name, user.Login?? "unknown"),
+ };
+
+ var token = new JwtSecurityToken(
+ issuer: "https://WotBlitzStatisticsPro.com",
+ audience: "https://WotBlitzStatisticsPro.com",
+ claims: claims,
+ expires: DateTime.Now.AddMinutes(30),
+ signingCredentials: credentials);
+
+ return new JwtSecurityTokenHandler().WriteToken(token);
+ }
+
+ private async Task GetAccessToken(string oAuthCode)
+ {
+ string url = "https://github.com/login/oauth/access_token";
+
+ var parameters = new Dictionary
+ {
+ { "client_id", _configuration.ClientId! },
+ { "client_secret", _configuration.ClientSecret! },
+ { "code", oAuthCode }
+ };
+ var encodedContent = new FormUrlEncodedContent(parameters!);
+
+ var client = _httpClientFactory.CreateClient("github");
+
+ var response = await client.PostAsync(url, encodedContent).ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
+ var stringResponse = await response.Content.ReadAsStringAsync();
+
+ var regex = new Regex("access_token=(.{40})&scope=&token_type=bearer");
+ var match = regex.Match(stringResponse);
+
+ if (match.Success)
+ {
+ return match.Groups[1].Value;
+ }
+
+ throw new ApplicationException($"Can not get access_token from string '{stringResponse}'");
+ }
+
+ private async Task GetUser(string token)
+ {
+ string url = "https://api.github.com/user";
+
+ var client = _httpClientFactory.CreateClient("github");
+ client.DefaultRequestHeaders.Authorization
+ = new AuthenticationHeaderValue("Bearer", token);
+ client.DefaultRequestHeaders.Add("User-Agent", "WotBlitzStatisticsPro");
+
+ var response = await client.GetAsync(url);
+
+ response.EnsureSuccessStatusCode();
+
+ await using var responseStream = await response.Content.ReadAsStreamAsync();
+ return await JsonSerializer.DeserializeAsync(responseStream);
+ }
+ }
+}
\ No newline at end of file
diff --git a/WotBlitzStatisticsPro.GraphQl/GitHubOauth/IGitHubOauthService.cs b/WotBlitzStatisticsPro.GraphQl/GitHubOauth/IGitHubOauthService.cs
new file mode 100644
index 0000000..4743fa7
--- /dev/null
+++ b/WotBlitzStatisticsPro.GraphQl/GitHubOauth/IGitHubOauthService.cs
@@ -0,0 +1,12 @@
+using System.Threading.Tasks;
+using WotBlitzStatisticsPro.Common.Model;
+
+namespace WotBlitzStatisticsPro.GraphQl.GitHubOauth
+{
+ public interface IGitHubOauthService
+ {
+ Task GetGitHubUser(string oAuthCode);
+
+ string GenerateToken(GitHubUser user);
+ }
+}
\ No newline at end of file
diff --git a/WotBlitzStatisticsPro.GraphQl/Mutation/AuthenticationMutation.cs b/WotBlitzStatisticsPro.GraphQl/Mutation/AuthenticationMutation.cs
new file mode 100644
index 0000000..3e9b63e
--- /dev/null
+++ b/WotBlitzStatisticsPro.GraphQl/Mutation/AuthenticationMutation.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Threading.Tasks;
+using HotChocolate.Types;
+using Microsoft.Extensions.Logging;
+using WotBlitzStatisticsPro.GraphQl.GitHubOauth;
+
+
+namespace WotBlitzStatisticsPro.GraphQl.Mutation
+{
+ [ExtendObjectType(Name = "Mutation")]
+ public class AuthenticationMutation
+ {
+ private readonly IGitHubOauthService _guGitHubOauthService;
+ private readonly ILogger _logger;
+
+ public AuthenticationMutation(
+ IGitHubOauthService guGitHubOauthService,
+ ILogger logger)
+ {
+ _guGitHubOauthService = guGitHubOauthService;
+ _logger = logger;
+ }
+
+ ///
+ /// This method accepts GitHub oauth code, and returns JWT token with GutHub user as claim
+ ///
+ /// OAuth code can be generated by this URL https://github.com/login/oauth/authorize?client_id={yourClientId}
+ public async Task GitHubAuthenticate(string oauthCode)
+ {
+ try
+ {
+ var user = await _guGitHubOauthService.GetGitHubUser(oauthCode);
+ if (user == null)
+ {
+ throw new ApplicationException($"Can not find GitHub user by code {oauthCode}");
+ }
+ return _guGitHubOauthService.GenerateToken(user);
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e,"Authentication fails");
+ throw;
+ }
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/WotBlitzStatisticsPro.GraphQl/Mutation/WotBlitzStatisticsMutation.cs b/WotBlitzStatisticsPro.GraphQl/Mutation/WotBlitzStatisticsMutation.cs
index eb49b3a..8d0da36 100644
--- a/WotBlitzStatisticsPro.GraphQl/Mutation/WotBlitzStatisticsMutation.cs
+++ b/WotBlitzStatisticsPro.GraphQl/Mutation/WotBlitzStatisticsMutation.cs
@@ -1,4 +1,6 @@
using System.Threading.Tasks;
+using HotChocolate.AspNetCore.Authorization;
+using HotChocolate.Types;
using WotBlitzStatisticsPro.Common.Model;
using WotBlitzStatisticsPro.Logic;
@@ -7,6 +9,7 @@ namespace WotBlitzStatisticsPro.GraphQl.Mutation
///
/// WotBlitzStatistics Mutations
///
+ [ExtendObjectType(Name = "Mutation")]
public class WotBlitzStatisticsMutation
{
private readonly IWargamingDictionaries _wargamingDictionaries;
@@ -24,6 +27,7 @@ namespace WotBlitzStatisticsPro.GraphQl.Mutation
///
/// Determines what dictionaries should be updated.
/// Information about what was updated.
+ [Authorize(Policy = "GithubMbogaOnly")]
public Task UpdateDictionaries(
UpdateDictionariesRequest updateDictionariesRequest)
{
diff --git a/WotBlitzStatisticsPro.GraphQl/Startup.cs b/WotBlitzStatisticsPro.GraphQl/Startup.cs
index 548748d..395e8b7 100644
--- a/WotBlitzStatisticsPro.GraphQl/Startup.cs
+++ b/WotBlitzStatisticsPro.GraphQl/Startup.cs
@@ -1,11 +1,18 @@
+using System.Security.Claims;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
+using Microsoft.IdentityModel.Tokens;
using WotBlitzStatisticsPro.Common;
using WotBlitzStatisticsPro.Common.Model;
+using WotBlitzStatisticsPro.GraphQl.GitHubOauth;
using WotBlitzStatisticsPro.GraphQl.Mutation;
using WotBlitzStatisticsPro.GraphQl.ObjectTypes;
using WotBlitzStatisticsPro.GraphQl.Query;
@@ -32,33 +39,73 @@ namespace WotBlitzStatisticsPro.GraphQl
var mongoConfig = new MongoSettings();
Configuration.GetSection("Mongo").Bind(mongoConfig);
+ var gitHubOauthConfig = new GitHubOauthAppConfiguration();
+ Configuration.GetSection("GitHubOAuth").Bind(gitHubOauthConfig);
+
services.AddSingleton(wgApiConfig);
services.AddSingleton(mongoConfig);
+ services.AddSingleton(gitHubOauthConfig);
WotBlitzStatisticsLogicInstaller.ConfigureServices(services);
services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy())
.AddMongoDb(mongoConfig.ConnectionString, tags: new[] { "services" });
- // Add GraphQL Services
- services.AddGraphQLServer()
+ // GitHub authorization
+ services.AddHttpClient("github");
+ services.AddTransient();
+ services.AddAuthorization(options =>
+ {
+ options.AddPolicy("GithubMbogaOnly", policy =>
+ policy.RequireClaim(ClaimTypes.Name, gitHubOauthConfig.AllowedUser));
+ });
+
+ services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
+ .AddJwtBearer(options =>
+ {
+ options.TokenValidationParameters = new TokenValidationParameters
+ {
+ ValidateIssuer = true,
+ ValidateAudience = true,
+ ValidateLifetime = true,
+ ValidateIssuerSigningKey = true,
+ ValidIssuer = "https://WotBlitzStatisticsPro.com",
+ ValidAudience = "https://WotBlitzStatisticsPro.com",
+ IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(gitHubOauthConfig.ClientSecret!))
+ };
+ });
+
+
+ // Add GraphQL Services
+ services.AddGraphQLServer()
.AddQueryType(d => d.Name("Query"))
.AddType()
.AddType()
.AddType()
- .AddMutationType()
+ .AddMutationType(d => d.Name("Mutation"))
+ .AddType()
+ .AddType()
.AddType()
.AddType()
.AddEnumType()
.AddEnumType()
- .AddEnumType();
+ .AddEnumType()
+ .AddAuthorization()
+ .AddHttpRequestInterceptor(
+ (context, executor, builder, ct) =>
+ {
+ // your code
+ return ValueTask.CompletedTask;
+ })
+ ;
+ }
- }
-
- public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
+ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
- app
+ app
.UseRouting()
+ .UseAuthorization()
+ .UseAuthentication()
.UseEndpoints(endpoints =>
{
endpoints.MapGraphQL();
diff --git a/WotBlitzStatisticsPro.GraphQl/WotBlitzStatisticsPro.GraphQl.csproj b/WotBlitzStatisticsPro.GraphQl/WotBlitzStatisticsPro.GraphQl.csproj
index f8a0d72..6ef17d2 100644
--- a/WotBlitzStatisticsPro.GraphQl/WotBlitzStatisticsPro.GraphQl.csproj
+++ b/WotBlitzStatisticsPro.GraphQl/WotBlitzStatisticsPro.GraphQl.csproj
@@ -13,7 +13,10 @@
+
+
+
diff --git a/WotBlitzStatisticsPro.GraphQl/appsettings.json b/WotBlitzStatisticsPro.GraphQl/appsettings.json
index 8cd1d47..ad559c2 100644
--- a/WotBlitzStatisticsPro.GraphQl/appsettings.json
+++ b/WotBlitzStatisticsPro.GraphQl/appsettings.json
@@ -14,5 +14,9 @@
"Mongo": {
"ConnectionString": "*",
"DatabaseName": "*"
- }
+ },
+ "GitHubOAuth": {
+ "ClientId": "YourClientId",
+ "ClientSecret": "YourSecret"
+ }
}