mirror of
https://github.com/DrMboga/WotBlitzStatisticsBlazor.git
synced 2026-10-23 19:30:06 +02:00
feat: GitHub OAuth mechanism implemented
This commit is contained in:
1 parent
a247406a42
commit
0b5da2f7c6
9 files changed
+299
-9
No files matched your search
@@ -0,0 +1,47 @@
|
|||||||
|
using System;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace WotBlitzStatisticsPro.Common.Model
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents authenticated GitHub user
|
||||||
|
/// </summary>
|
||||||
|
public class GitHubUser
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// User ID
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Login
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("login")]
|
||||||
|
public string? Login { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Avatar URL
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("avatar_url")]
|
||||||
|
public string? Avatar { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// GitHub profile
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("html_url")]
|
||||||
|
public string? ProfileLink { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// HitHub account creation time
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("created_at")]
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Followers count
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("followers")]
|
||||||
|
public int FollowersCount { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
namespace WotBlitzStatisticsPro.GraphQl.GitHubOauth
|
||||||
|
{
|
||||||
|
public class GitHubOauthAppConfiguration
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Github OAuth clientId
|
||||||
|
/// </summary>
|
||||||
|
public string? ClientId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// GutHub OAuth client secret
|
||||||
|
/// </summary>
|
||||||
|
public string? ClientSecret { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// GutHub user, that allowed to make dictionary updates mutation
|
||||||
|
/// </summary>
|
||||||
|
public string AllowedUser { get; set; } = "DrMboga";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<GitHubUser?> 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<string> GetAccessToken(string oAuthCode)
|
||||||
|
{
|
||||||
|
string url = "https://github.com/login/oauth/access_token";
|
||||||
|
|
||||||
|
var parameters = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "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<GitHubUser?> 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<GitHubUser?>(responseStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using WotBlitzStatisticsPro.Common.Model;
|
||||||
|
|
||||||
|
namespace WotBlitzStatisticsPro.GraphQl.GitHubOauth
|
||||||
|
{
|
||||||
|
public interface IGitHubOauthService
|
||||||
|
{
|
||||||
|
Task<GitHubUser?> GetGitHubUser(string oAuthCode);
|
||||||
|
|
||||||
|
string GenerateToken(GitHubUser user);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<AuthenticationMutation> _logger;
|
||||||
|
|
||||||
|
public AuthenticationMutation(
|
||||||
|
IGitHubOauthService guGitHubOauthService,
|
||||||
|
ILogger<AuthenticationMutation> logger)
|
||||||
|
{
|
||||||
|
_guGitHubOauthService = guGitHubOauthService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This method accepts GitHub oauth code, and returns JWT token with GutHub user as claim
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="oauthCode">OAuth code can be generated by this URL https://github.com/login/oauth/authorize?client_id={yourClientId}</param>
|
||||||
|
public async Task<string> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using HotChocolate.AspNetCore.Authorization;
|
||||||
|
using HotChocolate.Types;
|
||||||
using WotBlitzStatisticsPro.Common.Model;
|
using WotBlitzStatisticsPro.Common.Model;
|
||||||
using WotBlitzStatisticsPro.Logic;
|
using WotBlitzStatisticsPro.Logic;
|
||||||
|
|
||||||
@@ -7,6 +9,7 @@ namespace WotBlitzStatisticsPro.GraphQl.Mutation
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// WotBlitzStatistics Mutations
|
/// WotBlitzStatistics Mutations
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[ExtendObjectType(Name = "Mutation")]
|
||||||
public class WotBlitzStatisticsMutation
|
public class WotBlitzStatisticsMutation
|
||||||
{
|
{
|
||||||
private readonly IWargamingDictionaries _wargamingDictionaries;
|
private readonly IWargamingDictionaries _wargamingDictionaries;
|
||||||
@@ -24,6 +27,7 @@ namespace WotBlitzStatisticsPro.GraphQl.Mutation
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="updateDictionariesRequest">Determines what dictionaries should be updated.</param>
|
/// <param name="updateDictionariesRequest">Determines what dictionaries should be updated.</param>
|
||||||
/// <returns>Information about what was updated.</returns>
|
/// <returns>Information about what was updated.</returns>
|
||||||
|
[Authorize(Policy = "GithubMbogaOnly")]
|
||||||
public Task<UpdateDictionariesResponseItem[]> UpdateDictionaries(
|
public Task<UpdateDictionariesResponseItem[]> UpdateDictionaries(
|
||||||
UpdateDictionariesRequest updateDictionariesRequest)
|
UpdateDictionariesRequest updateDictionariesRequest)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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.Builder;
|
||||||
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using WotBlitzStatisticsPro.Common;
|
using WotBlitzStatisticsPro.Common;
|
||||||
using WotBlitzStatisticsPro.Common.Model;
|
using WotBlitzStatisticsPro.Common.Model;
|
||||||
|
using WotBlitzStatisticsPro.GraphQl.GitHubOauth;
|
||||||
using WotBlitzStatisticsPro.GraphQl.Mutation;
|
using WotBlitzStatisticsPro.GraphQl.Mutation;
|
||||||
using WotBlitzStatisticsPro.GraphQl.ObjectTypes;
|
using WotBlitzStatisticsPro.GraphQl.ObjectTypes;
|
||||||
using WotBlitzStatisticsPro.GraphQl.Query;
|
using WotBlitzStatisticsPro.GraphQl.Query;
|
||||||
@@ -32,33 +39,73 @@ namespace WotBlitzStatisticsPro.GraphQl
|
|||||||
var mongoConfig = new MongoSettings();
|
var mongoConfig = new MongoSettings();
|
||||||
Configuration.GetSection("Mongo").Bind(mongoConfig);
|
Configuration.GetSection("Mongo").Bind(mongoConfig);
|
||||||
|
|
||||||
|
var gitHubOauthConfig = new GitHubOauthAppConfiguration();
|
||||||
|
Configuration.GetSection("GitHubOAuth").Bind(gitHubOauthConfig);
|
||||||
|
|
||||||
services.AddSingleton<IWargamingApiSettings>(wgApiConfig);
|
services.AddSingleton<IWargamingApiSettings>(wgApiConfig);
|
||||||
services.AddSingleton<IMongoSettings>(mongoConfig);
|
services.AddSingleton<IMongoSettings>(mongoConfig);
|
||||||
|
services.AddSingleton(gitHubOauthConfig);
|
||||||
WotBlitzStatisticsLogicInstaller.ConfigureServices(services);
|
WotBlitzStatisticsLogicInstaller.ConfigureServices(services);
|
||||||
|
|
||||||
services.AddHealthChecks()
|
services.AddHealthChecks()
|
||||||
.AddCheck("self", () => HealthCheckResult.Healthy())
|
.AddCheck("self", () => HealthCheckResult.Healthy())
|
||||||
.AddMongoDb(mongoConfig.ConnectionString, tags: new[] { "services" });
|
.AddMongoDb(mongoConfig.ConnectionString, tags: new[] { "services" });
|
||||||
|
|
||||||
// Add GraphQL Services
|
// GitHub authorization
|
||||||
services.AddGraphQLServer()
|
services.AddHttpClient("github");
|
||||||
|
services.AddTransient<IGitHubOauthService, GitHubOauthService>();
|
||||||
|
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"))
|
.AddQueryType(d => d.Name("Query"))
|
||||||
.AddType<WotBlitzStatisticsQuery>()
|
.AddType<WotBlitzStatisticsQuery>()
|
||||||
.AddType<AccountInfoResolver>()
|
.AddType<AccountInfoResolver>()
|
||||||
.AddType<WotBlitzAchievementsQuery>()
|
.AddType<WotBlitzAchievementsQuery>()
|
||||||
.AddMutationType<WotBlitzStatisticsMutation>()
|
.AddMutationType(d => d.Name("Mutation"))
|
||||||
|
.AddType<WotBlitzStatisticsMutation>()
|
||||||
|
.AddType<AuthenticationMutation>()
|
||||||
.AddType<AccountsSearchItemObjectType>()
|
.AddType<AccountsSearchItemObjectType>()
|
||||||
.AddType<AccountsSearchObjectType>()
|
.AddType<AccountsSearchObjectType>()
|
||||||
.AddEnumType<MarkOfMastery>()
|
.AddEnumType<MarkOfMastery>()
|
||||||
.AddEnumType<RealmType>()
|
.AddEnumType<RealmType>()
|
||||||
.AddEnumType<RequestLanguage>();
|
.AddEnumType<RequestLanguage>()
|
||||||
|
.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()
|
.UseRouting()
|
||||||
|
.UseAuthorization()
|
||||||
|
.UseAuthentication()
|
||||||
.UseEndpoints(endpoints =>
|
.UseEndpoints(endpoints =>
|
||||||
{
|
{
|
||||||
endpoints.MapGraphQL();
|
endpoints.MapGraphQL();
|
||||||
|
|||||||
@@ -13,7 +13,10 @@
|
|||||||
<PackageReference Include="HotChocolate.AspNetCore" Version="11.0.6" />
|
<PackageReference Include="HotChocolate.AspNetCore" Version="11.0.6" />
|
||||||
<PackageReference Include="HotChocolate.AspNetCore.Authorization" Version="11.0.6" />
|
<PackageReference Include="HotChocolate.AspNetCore.Authorization" Version="11.0.6" />
|
||||||
<PackageReference Include="HotChocolate.Types.Filters" Version="11.0.6" />
|
<PackageReference Include="HotChocolate.Types.Filters" Version="11.0.6" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.4" />
|
||||||
|
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="6.10.0" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||||
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.10.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -14,5 +14,9 @@
|
|||||||
"Mongo": {
|
"Mongo": {
|
||||||
"ConnectionString": "*",
|
"ConnectionString": "*",
|
||||||
"DatabaseName": "*"
|
"DatabaseName": "*"
|
||||||
}
|
},
|
||||||
|
"GitHubOAuth": {
|
||||||
|
"ClientId": "YourClientId",
|
||||||
|
"ClientSecret": "YourSecret"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user