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
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,13 @@ public IdentityContext(HttpContext httpContext)

public IdentityContext(ClaimsPrincipal principal)
{
_IdentityUserId = principal.FindFirst(ClaimTypes.NameIdentifier) is var idClaim && idClaim is not null
? new Guid(idClaim.Value) : null;
if(principal.Identity!.AuthenticationType != "GitHub"
&& principal.FindFirst(ClaimTypes.NameIdentifier) is var idClaim
&& idClaim is not null)
{
_IdentityUserId = Guid.Parse(idClaim.Value);
}

Name = principal.FindFirst(ClaimTypes.Name)?.Value;
IsAdmin = principal.FindAll(ClaimTypes.Role).Any(x => x.Value == Role.Admin);
IsContentEditor = principal.FindAll(ClaimTypes.Role).Any(x => x.Value == Role.ContentEditor) && !IsAdmin;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using GreenOnSoftware.Application.GithubAuthentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace GreenOnSoftware.Api.Controllers;

[Route("api/[controller]")]
[ApiController]
public class GithubAuthenticationController : ControllerBase
{
private readonly IGithubAuthenticationService _githubAuthenticationService;

public GithubAuthenticationController(IGithubAuthenticationService githubAuthenticationService)
{
_githubAuthenticationService = githubAuthenticationService;
}

[HttpGet("[action]")]
public IActionResult SignIn(string? redirectUrl)
{
if (!string.IsNullOrEmpty(redirectUrl))
{
HttpContext.Session.SetString("GithubAuthorization:RedirectUrl", redirectUrl);
}
var properties = _githubAuthenticationService.CreateAuthenticationProperties("/api/GithubAuthentication/Authenticate");

return Challenge(properties, "GitHub");
}

[Authorize(AuthenticationSchemes = "Identity.Application, Identity.External")]
[HttpGet("[Action]")]
public async Task<IActionResult> Authenticate()
{
var result = await _githubAuthenticationService.AuthenticateAsync();
if (result.HasErrors)
{
return BadRequest(result);
}
string? redirectUrl = HttpContext.Session.GetString("GithubAuthorization:RedirectUrl");
if (string.IsNullOrEmpty(redirectUrl))
{
return Ok(result);
}

HttpContext.Session.Remove("GithubAuthorization:RedirectUrl");

return Redirect(redirectUrl);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="AspNet.Security.OAuth.GitHub" Version="6.0.15" />
<PackageReference Include="AutoMapper" Version="11.0.1" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
<PackageReference Include="FluentValidation.AspNetCore" Version="11.1.2" />
Expand Down
2 changes: 1 addition & 1 deletion dotnet/GreenOnSoftware/GreenOnSoftware.Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
builder.Services.AddDbContext<GreenOnSoftwareDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("GreenOnSoftware")));

builder.Services.AddIdentity();
builder.Services.AddAuthentication(builder.Configuration);

builder.Services.AddCors();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@

namespace GreenOnSoftware.Api.Startup;

public static class IdentityConfig
public static class AuthenticationConfig
{
public static IServiceCollection AddIdentity(this IServiceCollection services)
public static IServiceCollection AddAuthentication(this IServiceCollection services, IConfiguration configuration)
{
services.AddIdentity<User, IdentityRole<Guid>>(options => options.SignIn.RequireConfirmedAccount = false)
.AddEntityFrameworkStores<GreenOnSoftwareDbContext>()
Expand Down Expand Up @@ -53,6 +53,22 @@ public static IServiceCollection AddIdentity(this IServiceCollection services)
};
});

var githubConfig = configuration.GetSection("Github");

var siema = githubConfig["ClientSecret"];

services
.AddAuthentication(options =>
{
options.DefaultChallengeScheme = IdentityConstants.ExternalScheme;
})
.AddGitHub(options => {
options.ClientId = githubConfig["ClientId"];
options.ClientSecret = githubConfig["ClientSecret"];
options.Scope.Add("user:email");
options.SaveTokens = true;
});

return services;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,10 @@
"BlobStorage": {
"ConnectionString": "--secret--",
"Container": "dev"
},

"Github": {
"ClientId": "In secrets.json",
"ClientSecret": "In secrets.json"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,10 @@
"BlobStorage": {
"ConnectionString": "--secret--",
"Container": "dev"
},

"Github": {
"ClientId": "In secrets.json",
"ClientSecret": "In secrets.json"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,10 @@

"CorsOriginsUrls": [
"https://greenonsoftware.com"
]
],

"Github": {
"ClientId": "In secrets.json",
"ClientSecret": "In secrets.json"
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using GreenOnSoftware.Application.Services;
using GreenOnSoftware.Application.GithubAuthentication;
using GreenOnSoftware.Application.Services;
using GreenOnSoftware.Application.Services.Interfaces;
using Microsoft.Extensions.DependencyInjection;

Expand All @@ -13,6 +14,7 @@ public static IServiceCollection AddApplication(this IServiceCollection services
services.AddTransient<IRatingsSessionService, RatingsSessionService>();
services.AddTransient<IArticleUrlIdentifierService, ArticleUrlIdentifierService>();
services.AddTransient<ITagsService, TagsService>();
services.AddTransient<IGithubAuthenticationService, GithubAuthenticationService>();

return services;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
using GreenOnSoftware.Commons.Dtos;
using GreenOnSoftware.Commons.Resources;
using GreenOnSoftware.Core.Identity;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Serilog;
using System.Security.Claims;
using GreenOnSoftware.Commons.Extensions;
using GreenOnSoftware.Commons.Consts;

namespace GreenOnSoftware.Application.GithubAuthentication;

public class GithubAuthenticationService : IGithubAuthenticationService
{
private readonly UserManager<User> _userManager;
private readonly SignInManager<User> _signInManager;

public GithubAuthenticationService(
UserManager<User> userManager,
SignInManager<User> signInManager)
{
_userManager = userManager;
_signInManager = signInManager;
}

public AuthenticationProperties CreateAuthenticationProperties(string redirectUrl)
{

return _signInManager.ConfigureExternalAuthenticationProperties("GitHub", new PathString(redirectUrl));
}

public async Task<Result<GithubTokenDto>> AuthenticateAsync()
{
var response = new Result<GithubTokenDto>();

var externalLoginInfo = await _signInManager.GetExternalLoginInfoAsync();
if (externalLoginInfo is null)
{
response.AddError(ErrorMessages.ActionFailed, ErrorActionNames.ExternalSignIn);
return response;
}
string? applicationUserId = GetAppicationUserId();

var user = await _userManager.FindByLoginAsync(externalLoginInfo.LoginProvider, externalLoginInfo.ProviderKey);
if (user is null)
{
var userResult = await GetApplicationUser(applicationUserId, externalLoginInfo.Principal.Claims);
if (userResult.HasErrors)
{
response.AddErrors(userResult);
response.AddError(ErrorMessages.ActionFailed, ErrorActionNames.ExternalSignIn);
return response;
}
user = userResult.Data;

var addLoginResult = await _userManager.AddLoginAsync(user, externalLoginInfo);
if (!addLoginResult.Succeeded)
{
response.AddError(ErrorMessages.ActionFailed, ErrorActionNames.AddExternalLogin);
return response;
}
}
else
{
if (!string.IsNullOrEmpty(applicationUserId) && applicationUserId != user.Id.ToString())
{
response.AddError(ErrorMessages.AlreadyConnectedWithAnotherAccount);
return response;
}
}

SignInResult signInResult = await _signInManager.ExternalLoginSignInAsync(externalLoginInfo.LoginProvider, externalLoginInfo.ProviderKey, true);

if (!signInResult.Succeeded)
{
response.AddError(ErrorMessages.ActionFailed, ErrorActionNames.ExternalSignIn);
return response;
}

IdentityResult saveTokensResult = await _signInManager.UpdateExternalAuthenticationTokensAsync(externalLoginInfo);

if (!saveTokensResult.Succeeded)
{
response.AddErrors(saveTokensResult.GetErrors());
return response;
}

return response;
}

public async Task<string> GetGithubTokenAsync(string username = null)
{
if (string.IsNullOrEmpty(username))
{
username = _signInManager.Context.User.Claims.Single(x => x.Type == ClaimTypes.Name).Value;
}

User currentUser = await _userManager.FindByNameAsync(username);
string githubToken = await _userManager.GetAuthenticationTokenAsync(currentUser, "Github", "access_token");

return githubToken;
}

private string? GetAppicationUserId()
{
return _signInManager.Context.User
.Identities
.SingleOrDefault(x => x.AuthenticationType == IdentityConstants.ApplicationScheme)
?.Claims
.Single(x => x.Type == ClaimTypes.NameIdentifier)
.Value;
}

private async Task<Result<User>> GetApplicationUser(string? applicationUserId, IEnumerable<Claim> externalClaims)
{
var result = new Result<User>();
if (applicationUserId != null)
{
result.SetData(await _userManager.FindByIdAsync(applicationUserId));
if (result.Data != null)
{
return result;
}
}

var newUser = new User {
Email = externalClaims.Single(x => x.Type == ClaimTypes.Email).Value,
UserName = externalClaims.Single(x => x.Type == ClaimTypes.Name).Value,
};
var addUserResult = await _userManager.CreateAsync(newUser);
if (!addUserResult.Succeeded)
{
result.AddErrors(addUserResult.GetErrors());
return result;
}
var addToRoleResult = await _userManager.AddToRoleAsync(newUser, Role.GeneralUser);
if (!addToRoleResult.Succeeded)
{
result.AddErrors(addToRoleResult.GetErrors());
Log.Error($"Failed to assign role to new user {newUser.UserName}.");
}

result.SetData(newUser);

return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace GreenOnSoftware.Application.GithubAuthentication;

public class GithubTokenDto
{
public string AccessToken { get; set; }
public IEnumerable<string> Scope { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using GreenOnSoftware.Commons.Dtos;
using Microsoft.AspNetCore.Authentication;

namespace GreenOnSoftware.Application.GithubAuthentication;

public interface IGithubAuthenticationService
{
Task<Result<GithubTokenDto>> AuthenticateAsync();
AuthenticationProperties CreateAuthenticationProperties(string redirectUrl);
Task<string> GetGithubTokenAsync(string? username = null);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using GreenOnSoftware.Application.Services.Interfaces;
using GreenOnSoftware.Application.Account.SignInCommand;
using GreenOnSoftware.Application.Services.Interfaces;
using GreenOnSoftware.Commons.Dtos;
using GreenOnSoftware.Commons.Resources;
using Microsoft.AspNetCore.Http;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace GreenOnSoftware.Commons.Consts;

public static class ErrorActionNames
{
public const string AddExternalLogin = "Add external login";
public const string EmailConfirmation = "Email confirmation";
public const string EmailSending = "Email sending";
public const string ExternalSignIn = "External sign in";
}

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

Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@
<data name="ActionFailed" xml:space="preserve">
<value>{0} failed.</value>
</data>
<data name="AlreadyConnectedWithAnotherAccount" xml:space="preserve">
<value>This github account is already connected with another user.</value>
</data>
<data name="ArticleAlreadyExists" xml:space="preserve">
<value>Article already exists.</value>
</data>
Expand Down