feat: GitHub OAuth mechanism implemented

This commit is contained in:
Mikhail Shabanov committed 2021-04-02 13:20:59 +02:00
1 parent a247406a42
commit 0b5da2f7c6
9 files changed
+299 -9

No files matched your search

@@ -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 HotChocolate.AspNetCore.Authorization;
using HotChocolate.Types;
using WotBlitzStatisticsPro.Common.Model;
using WotBlitzStatisticsPro.Logic;
@@ -7,6 +9,7 @@ namespace WotBlitzStatisticsPro.GraphQl.Mutation
/// <summary>
/// WotBlitzStatistics Mutations
/// </summary>
[ExtendObjectType(Name = "Mutation")]
public class WotBlitzStatisticsMutation
{
private readonly IWargamingDictionaries _wargamingDictionaries;
@@ -24,6 +27,7 @@ namespace WotBlitzStatisticsPro.GraphQl.Mutation
/// </summary>
/// <param name="updateDictionariesRequest">Determines what dictionaries should be updated.</param>
/// <returns>Information about what was updated.</returns>
[Authorize(Policy = "GithubMbogaOnly")]
public Task<UpdateDictionariesResponseItem[]> UpdateDictionaries(
UpdateDictionariesRequest updateDictionariesRequest)
{
+55 -8
View File
@@ -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<IWargamingApiSettings>(wgApiConfig);
services.AddSingleton<IMongoSettings>(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<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"))
.AddType<WotBlitzStatisticsQuery>()
.AddType<AccountInfoResolver>()
.AddType<WotBlitzAchievementsQuery>()
.AddMutationType<WotBlitzStatisticsMutation>()
.AddMutationType(d => d.Name("Mutation"))
.AddType<WotBlitzStatisticsMutation>()
.AddType<AuthenticationMutation>()
.AddType<AccountsSearchItemObjectType>()
.AddType<AccountsSearchObjectType>()
.AddEnumType<MarkOfMastery>()
.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()
.UseAuthorization()
.UseAuthentication()
.UseEndpoints(endpoints =>
{
endpoints.MapGraphQL();
@@ -13,7 +13,10 @@
<PackageReference Include="HotChocolate.AspNetCore" Version="11.0.6" />
<PackageReference Include="HotChocolate.AspNetCore.Authorization" 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="System.IdentityModel.Tokens.Jwt" Version="6.10.0" />
</ItemGroup>
<ItemGroup>
@@ -14,5 +14,9 @@
"Mongo": {
"ConnectionString": "*",
"DatabaseName": "*"
}
},
"GitHubOAuth": {
"ClientId": "YourClientId",
"ClientSecret": "YourSecret"
}
}