Skip to content
Merged
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
1 change: 1 addition & 0 deletions Domain/Entities/User.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public User()
public required string Email { get; set; }
public required Provider Provider { get; set; }
public required string ProviderKey { get; set; }
public string ProviderRefreshToken { get; set; } = null;
public required string Name { get; set; }
public bool IsActive { get; set; }
public bool IsEmailVerified { get; set; } = false;
Expand Down
236 changes: 142 additions & 94 deletions IoCConfig/ConfigureServicesExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
using Service;
using Domain.Models;
using Domain.Services;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text.Json;
using System.Threading.Tasks;
using DataAccess;
using MongoDB.Driver;
using Domain.Models;
using Domain.Repositories;
using Domain.Services;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.Google;
using Microsoft.IdentityModel.Tokens;
using System.Security.Cryptography;
using System;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.DataProtection;
using System.IO;
using System.Threading.Tasks;
using System.Text.Json;
using System.Security.Claims;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using MongoDB.Driver;
using Service;

namespace IoCConfig
{
Expand All @@ -26,91 +28,123 @@ public static class ConfigureServicesExtensions
public static void AddCustomCors(this IServiceCollection services, IConfiguration configuration)
{
services.AddCors(options =>
options.AddPolicy("CorsPolicy",
builder => builder
.WithOrigins(configuration["Cors:Origins"])
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials()
));
options.AddPolicy(
"CorsPolicy",
builder =>
builder
.WithOrigins(configuration["Cors:Origins"])
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials()
)
);
}

public static void AddCustomAuthentication(this IServiceCollection services, IConfiguration configuration)
public static void AddCustomAuthentication(
this IServiceCollection services,
IConfiguration configuration
)
{
var rsa = RSA.Create();
rsa.ImportRSAPrivateKey(Convert.FromBase64String(configuration["Jwt:PrivateKey"] ?? ""), out _);
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = GoogleDefaults.AuthenticationScheme;
})
.AddCookie()
.AddGoogle(options =>
{
options.ClientId = configuration["OAuth:GoogleClientId"] ?? "";
options.ClientSecret = configuration["OAuth:GoogleClientSecret"] ?? "";
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Events.OnCreatingTicket = ctx =>
var scopes = configuration.GetSection("OAuth:Scopes").Get<List<string>>() ?? new List<string>();
rsa.ImportRSAPrivateKey(
Convert.FromBase64String(configuration["Jwt:PrivateKey"] ?? ""),
out _
);
services
.AddAuthentication(options =>
{
if (ctx.User.TryGetProperty("picture", out var value))
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = GoogleDefaults.AuthenticationScheme;
})
.AddCookie()
.AddGoogle(options =>
{
options.ClientId = configuration["OAuth:GoogleClientId"] ?? "";
options.ClientSecret = configuration["OAuth:GoogleClientSecret"] ?? "";
options.SaveTokens = true;
options.AccessType = "offline";
foreach (string scope in scopes)
{
ctx.Identity?.AddClaim(new Claim(ClaimTypes.Uri, value.ToString()));
options.Scope.Add(scope);
}
return Task.CompletedTask;
};
options.Events.OnRemoteFailure = ctx =>
{
ctx.Response.Redirect(configuration["OAuth:GoogleCallbackURL"]);
ctx.HandleResponse();
return Task.CompletedTask;
};
})
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = configuration["Jwt:Issuer"],
ValidAudience = configuration["Jwt:Audience"],
IssuerSigningKey = new RsaSecurityKey(rsa)
};

options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Events.OnCreatingTicket = ctx =>
{
if (context.Request.Cookies.TryGetValue(configuration["Jwt:CookieName"], out var encryptedToken))
if (ctx.User.TryGetProperty("picture", out var value))
{
var dataProtector = context.HttpContext.RequestServices
.GetRequiredService<IDataProtectionProvider>()
.CreateProtector(configuration["Jwt:DataProtectionPurpose"]);
ctx.Identity?.AddClaim(new Claim(ClaimTypes.Uri, value.ToString()));
}
return Task.CompletedTask;
};
options.Events.OnRemoteFailure = ctx =>
{
ctx.Response.Redirect(configuration["OAuth:GoogleCallbackURL"]);
ctx.HandleResponse();
return Task.CompletedTask;
};
})
.AddJwtBearer(
JwtBearerDefaults.AuthenticationScheme,
options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = configuration["Jwt:Issuer"],
ValidAudience = configuration["Jwt:Audience"],
IssuerSigningKey = new RsaSecurityKey(rsa),
};

try
{
var authCookie = JsonSerializer.Deserialize<AuthCookie>(dataProtector.Unprotect(encryptedToken));
context.Token = authCookie.AccessToken;
}
catch
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
context.Fail("Invalid or tampered token");
}
}
if (
context.Request.Cookies.TryGetValue(
configuration["Jwt:CookieName"],
out var encryptedToken
)
)
{
var dataProtector = context
.HttpContext.RequestServices.GetRequiredService<IDataProtectionProvider>()
.CreateProtector(configuration["Jwt:DataProtectionPurpose"]);

return Task.CompletedTask;
try
{
var authCookie = JsonSerializer.Deserialize<AuthCookie>(
dataProtector.Unprotect(encryptedToken)
);
context.Token = authCookie.AccessToken;
}
catch
{
context.Fail("Invalid or tampered token");
}
}

return Task.CompletedTask;
},
};
}
};
});
);
}

public static void AddCustomDataProtection(this IServiceCollection services, IConfiguration configuration)
public static void AddCustomDataProtection(
this IServiceCollection services,
IConfiguration configuration
)
{
services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(configuration["Jwt:DataProtectionKeysPath"]))
.SetApplicationName(configuration["Jwt:DataProtectionApplicationName"]);
services
.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(configuration["Jwt:DataProtectionKeysPath"]))
.SetApplicationName(configuration["Jwt:DataProtectionApplicationName"]);
}

public static void AddCustomServices(this IServiceCollection services)
{
services.AddScoped<IJwtTokenService, JwtTokenService>();
Expand All @@ -121,7 +155,10 @@ public static void AddCustomServices(this IServiceCollection services)
services.AddHttpContextAccessor();
}

public static void AddTurnstileService(this IServiceCollection services, IConfiguration configuration)
public static void AddTurnstileService(
this IServiceCollection services,
IConfiguration configuration
)
{
var turnstileOptions = new TurnstileOptions();
configuration.GetSection("Turnstile").Bind(turnstileOptions);
Expand All @@ -132,32 +169,43 @@ public static void AddTurnstileService(this IServiceCollection services, IConfig
});
}

public static void AddCustomOptions(this IServiceCollection services, IConfiguration configuration)
public static void AddCustomOptions(
this IServiceCollection services,
IConfiguration configuration
)
{
services.AddOptions<JwtOptions>().Bind(configuration.GetSection("Jwt"));
services.AddOptions<OAuthOptions>().Bind(configuration.GetSection("OAuth"));
services.AddOptions<SMTPOptions>().Bind(configuration.GetSection("SMTP"));
services.AddOptions<EmailTemplateOptions>().Bind(configuration.GetSection("EmailTemplate"));
services.AddOptions<Domain.Models.DataProtectionOptions>().Bind(configuration.GetSection("DataProtection"));
services
.AddOptions<Domain.Models.DataProtectionOptions>()
.Bind(configuration.GetSection("DataProtection"));
}

public static void AddCustomSwagger(this IServiceCollection services)
{

services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo
{
Title = "Micro IDP API Document",
Version = "v1"
});
options.SwaggerDoc(
"v1",
new OpenApiInfo { Title = "Micro IDP API Document", Version = "v1" }
);
});
}

public static void AddCustomMongoDbService(this IServiceCollection services, IConfiguration configuration)
public static void AddCustomMongoDbService(
this IServiceCollection services,
IConfiguration configuration
)
{
services.AddSingleton<IMongoClient>(s => new MongoClient(configuration.GetConnectionString("MongoDb")));
services.AddScoped<IMongoDbContext>(s => new MongoDbContext(s.GetRequiredService<IMongoClient>(), configuration["DbName"]));
services.AddSingleton<IMongoClient>(s => new MongoClient(
configuration.GetConnectionString("MongoDb")
));
services.AddScoped<IMongoDbContext>(s => new MongoDbContext(
s.GetRequiredService<IMongoClient>(),
configuration["DbName"]
));
}
}
}
4 changes: 3 additions & 1 deletion Service/JwtTokenService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ public JwtTokensData CreateJwtTokens(User user)
new Claim(ClaimTypes.Name, user.Name, ClaimValueTypes.String, jwtIssuer),
new Claim(ClaimTypes.Email, user.Email, ClaimValueTypes.String, jwtIssuer),
new Claim(ClaimTypes.SerialNumber, user.SerialNumber, ClaimValueTypes.String, jwtIssuer),
new Claim(ClaimTypes.UserData, user.Id.ToString(), ClaimValueTypes.String, jwtIssuer)
new Claim(ClaimTypes.UserData, user.Id.ToString(), ClaimValueTypes.String, jwtIssuer),
new Claim("ProviderKey", user.ProviderKey, ClaimValueTypes.String, jwtIssuer),
new Claim("ProviderRefreshToken", user.ProviderRefreshToken, ClaimValueTypes.String, jwtIssuer)
];

foreach (Role role in user.Roles)
Expand Down
12 changes: 10 additions & 2 deletions WebApi/Controllers/AuthController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,11 @@ public async Task<ActionResult<ApiResponseViewModel>> ForgotPassword(EmailViewMo
return Ok(new ApiResponseViewModel { Success = true, Message = "reset_password_email_sent_successfully" });
}

if (user.Provider != Provider.Password)
{
return BadRequest(new ApiResponseViewModel { Success = false, Message = "password_reset_not_allowed_for_oauth_users" });
}

var resetPasswordCode = new ResetPasswordCode
{
Email = user.Email,
Expand Down Expand Up @@ -360,6 +365,7 @@ public IActionResult GoogleLogin()
{
RedirectUri = _oAuthOptions.GoogleCallbackURL
};
properties.Parameters.Add("prompt", "consent");
return Challenge(properties, GoogleDefaults.AuthenticationScheme);
}

Expand All @@ -373,11 +379,12 @@ public async Task<ActionResult<ApiResponseViewModel<AuthResponseViewModel>>> Goo
return BadRequest(new ApiResponseViewModel
{
Success = false,
Message =
"google_authentication_failed."
Message = "google_authentication_failed."
});
}

var refreshToken = authenticateResult.Properties.GetTokenValue("refresh_token");

var claims = authenticateResult.Principal.Claims;

var email = claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value;
Expand All @@ -401,6 +408,7 @@ public async Task<ActionResult<ApiResponseViewModel<AuthResponseViewModel>>> Goo
Email = email,
ProviderKey = _securityService.GetSha256Hash(nameIdentifier),
Provider = Provider.Google,
ProviderRefreshToken = refreshToken,
IsActive = true,
Roles = [new Role { Name = "User" }],
IsEmailVerified = true,
Expand Down
4 changes: 2 additions & 2 deletions WebApi/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using IoCConfig;
using IoCConfig;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Mvc;
using Serilog;
Expand Down Expand Up @@ -56,4 +56,4 @@
app.UseSerilogRequestLogging();
app.MapControllers();

app.Run();
app.Run();
2 changes: 1 addition & 1 deletion WebApi/ViewModels/AuthResponseViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace WebApi.ViewModels;
namespace WebApi.ViewModels;

public class AuthResponseViewModel
{
Expand Down
5 changes: 3 additions & 2 deletions WebApi/appsettings.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
{
"Serilog": {
"Using": ["Serilog.Sinks.Console", "Serilog.Formatting.Compact"],
"MinimumLevel": {
Expand Down Expand Up @@ -38,7 +38,8 @@
"OAuth": {
"GoogleClientId": "",
"GoogleClientSecret": "",
"GoogleCallbackURL": ""
"GoogleCallbackURL": "",
"Scopes": [""]
},
"ConnectionStrings": {
"MongoDb": "mongodb://USERNAME:PASSWORD@localhost:27017"
Expand Down
Loading