From e7a3ee23892f23ab631c2cf585fd5da4a4db80d2 Mon Sep 17 00:00:00 2001 From: Oscar Morales Date: Wed, 2 Jul 2025 22:35:57 -0600 Subject: [PATCH] Add google authentication --- .../Controllers/AuthenticationController.cs | 25 ++- .../Controllers/ModuleController.cs | 4 + .../Controllers/PermissionController.cs | 12 +- .../Controllers/UserController.cs | 24 +-- .../Core.Thalos.BFF.Api.csproj | 6 + Core.Thalos.BFF.Api/Program.cs | 198 ++++++++++++------ Core.Thalos.BFF.Api/Services/Constant.cs | 7 + .../GoogleAccessTokenAuthenticationHandler.cs | 61 ++++++ .../Services/IGoogleAuthHelper.cs | 37 ++++ .../Services/IGoogleAuthorization.cs | 42 ++++ Core.Thalos.BFF.Api/Services/Token.cs | 4 + Core.Thalos.BFF.Api/appsettings.Local.json | 8 + 12 files changed, 341 insertions(+), 87 deletions(-) create mode 100644 Core.Thalos.BFF.Api/Services/Constant.cs create mode 100644 Core.Thalos.BFF.Api/Services/GoogleAccessTokenAuthenticationHandler.cs create mode 100644 Core.Thalos.BFF.Api/Services/IGoogleAuthHelper.cs create mode 100644 Core.Thalos.BFF.Api/Services/IGoogleAuthorization.cs create mode 100644 Core.Thalos.BFF.Api/Services/Token.cs diff --git a/Core.Thalos.BFF.Api/Controllers/AuthenticationController.cs b/Core.Thalos.BFF.Api/Controllers/AuthenticationController.cs index f247b08..e9489d7 100644 --- a/Core.Thalos.BFF.Api/Controllers/AuthenticationController.cs +++ b/Core.Thalos.BFF.Api/Controllers/AuthenticationController.cs @@ -3,10 +3,16 @@ using Core.Thalos.Adapters; using Core.Thalos.Adapters.Common.Constants; using Core.Thalos.Adapters.Contracts; using Core.Thalos.Application.UseCases.Users.Input; +using Core.Thalos.BFF.Api.Services; using Core.Thalos.External.Clients.Thalos.Requests.Users; +using Google.Apis.Auth.OAuth2; using LSA.Dashboard.External.Clients.Dashboard; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Graph; +using Newtonsoft.Json.Linq; +using System.Reflection; +using System.Text.Json; namespace Core.Thalos.BFF.Api.Controllers { @@ -18,8 +24,23 @@ namespace Core.Thalos.BFF.Api.Controllers [Produces(MimeTypes.ApplicationJson)] [Consumes(MimeTypes.ApplicationJson)] [ApiController] - public class AuthenticationController(IThalosServiceClient thalosServiceClient, ILogger logger, ITokenService tokenService) : BaseController(logger) + public class AuthenticationController( + IThalosServiceClient thalosServiceClient, + ILogger logger, + ITokenService tokenService, + IGoogleAuthorization googleAuthorization) : BaseController(logger) { + [HttpGet] + public IActionResult Authorize() => Ok(googleAuthorization.GetAuthorizationUrl()); + + [HttpGet("callback")] + public async Task Callback(string code) + { + var userCredential = await googleAuthorization.ExchangeCodeForToken(code); + + return Ok(new Token(userCredential!.Token.IdToken)); + } + /// /// Get token for user. /// @@ -30,7 +51,7 @@ namespace Core.Thalos.BFF.Api.Controllers [HttpGet] [Route(Routes.GenerateToken)] [ProducesResponseType(typeof(UserAdapter), StatusCodes.Status200OK)] - [Authorize(AuthenticationSchemes = Schemes.AzureScheme)] + [Authorize] public async Task GenerateTokenService(CancellationToken cancellationToken) { try diff --git a/Core.Thalos.BFF.Api/Controllers/ModuleController.cs b/Core.Thalos.BFF.Api/Controllers/ModuleController.cs index dd9865e..73b2f0d 100644 --- a/Core.Thalos.BFF.Api/Controllers/ModuleController.cs +++ b/Core.Thalos.BFF.Api/Controllers/ModuleController.cs @@ -2,6 +2,7 @@ using Core.Thalos.Adapters; using Core.Thalos.Adapters.Attributes; using Core.Thalos.Adapters.Common.Constants; +using Core.Thalos.BFF.Api.Services; using Core.Thalos.External.Clients.Thalos.Requests.Permissions; using Lib.Architecture.BuildingBlocks; using LSA.Dashboard.External.Clients.Dashboard; @@ -32,6 +33,7 @@ namespace Core.Thalos.BFF.Api.Controllers [ProducesResponseType(typeof(Notification), StatusCodes.Status412PreconditionFailed)] [ProducesResponseType(typeof(Notification), StatusCodes.Status422UnprocessableEntity)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] + [Authorize(AuthenticationSchemes = Constant.Scheme)] //[Permission("ModuleManagement.Read, RoleManagement.Read")] public async Task GetAllModulesService(CancellationToken cancellationToken) { @@ -65,6 +67,7 @@ namespace Core.Thalos.BFF.Api.Controllers [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] + [AllowAnonymous] //[Permission("ModuleManagement.Read")] public async Task GetAllModulesByListAsync([FromBody] GetAllModulesByListRequest request, CancellationToken cancellationToken) { @@ -98,6 +101,7 @@ namespace Core.Thalos.BFF.Api.Controllers [ProducesResponseType(typeof(Notification), StatusCodes.Status412PreconditionFailed)] [ProducesResponseType(typeof(Notification), StatusCodes.Status422UnprocessableEntity)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] + [AllowAnonymous] //[Permission("ModuleManagement.Write")] public async Task CreateModuleService(CreateModuleRequest newModule, CancellationToken cancellationToken) { diff --git a/Core.Thalos.BFF.Api/Controllers/PermissionController.cs b/Core.Thalos.BFF.Api/Controllers/PermissionController.cs index f8e38ea..02675eb 100644 --- a/Core.Thalos.BFF.Api/Controllers/PermissionController.cs +++ b/Core.Thalos.BFF.Api/Controllers/PermissionController.cs @@ -33,7 +33,7 @@ namespace Core.Thalos.BFF.Api.Controllers [ProducesResponseType(typeof(Notification), StatusCodes.Status412PreconditionFailed)] [ProducesResponseType(typeof(Notification), StatusCodes.Status422UnprocessableEntity)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - [Permission("PermissionManagement.Read, RoleManagement.Read")] + ////[Permission("PermissionManagement.Read, RoleManagement.Read")] public async Task GetAllPermissionsService(CancellationToken cancellationToken) { try @@ -66,7 +66,7 @@ namespace Core.Thalos.BFF.Api.Controllers [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - [Permission("PermissionManagement.Read")] + //[Permission("PermissionManagement.Read")] public async Task GetAllPermissionsByListAsync([FromBody] GetAllPermissionsByListRequest request, CancellationToken cancellationToken) { try @@ -99,7 +99,7 @@ namespace Core.Thalos.BFF.Api.Controllers [ProducesResponseType(typeof(Notification), StatusCodes.Status412PreconditionFailed)] [ProducesResponseType(typeof(Notification), StatusCodes.Status422UnprocessableEntity)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - [Permission("PermissionManagement.Write")] + //[Permission("PermissionManagement.Write")] public async Task CreatePermissionService(CreatePermissionRequest newPermission, CancellationToken cancellationToken) { try @@ -131,7 +131,7 @@ namespace Core.Thalos.BFF.Api.Controllers [ProducesResponseType(typeof(Notification), StatusCodes.Status412PreconditionFailed)] [ProducesResponseType(typeof(Notification), StatusCodes.Status422UnprocessableEntity)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - [Permission("PermissionManagement.Read")] + //[Permission("PermissionManagement.Read")] public async Task GetPermissionByIdService(GetPermissionRequest request, CancellationToken cancellationToken) { try @@ -159,7 +159,7 @@ namespace Core.Thalos.BFF.Api.Controllers [ProducesResponseType(typeof(Notification), StatusCodes.Status412PreconditionFailed)] [ProducesResponseType(typeof(Notification), StatusCodes.Status422UnprocessableEntity)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - [Permission("PermissionManagement.Write")] + //[Permission("PermissionManagement.Write")] public async Task UpdatePermissionService(UpdatePermissionRequest newPermission, CancellationToken cancellationToken) { try @@ -193,7 +193,7 @@ namespace Core.Thalos.BFF.Api.Controllers [ProducesResponseType(typeof(Notification), StatusCodes.Status412PreconditionFailed)] [ProducesResponseType(typeof(Notification), StatusCodes.Status422UnprocessableEntity)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - [Permission("PermissionManagement.Write")] + //[Permission("PermissionManagement.Write")] public async Task ChangePermissionStatusService([FromBody] ChangePermissionStatusRequest request, CancellationToken cancellationToken) { try diff --git a/Core.Thalos.BFF.Api/Controllers/UserController.cs b/Core.Thalos.BFF.Api/Controllers/UserController.cs index 8a14c92..abb4b3f 100644 --- a/Core.Thalos.BFF.Api/Controllers/UserController.cs +++ b/Core.Thalos.BFF.Api/Controllers/UserController.cs @@ -32,7 +32,7 @@ namespace Core.Thalos.BFF.Api.Controllers [ProducesResponseType(typeof(Notification), StatusCodes.Status422UnprocessableEntity)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] //[Authorize(AuthenticationSchemes = Schemes.DefaultScheme)] - [Permission("UserManagement.Read")] + //[Permission("UserManagement.Read")] public async Task GetAllUsersService(CancellationToken cancellationToken) { try @@ -59,7 +59,7 @@ namespace Core.Thalos.BFF.Api.Controllers [ProducesResponseType(typeof(Notification), StatusCodes.Status422UnprocessableEntity)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] //[Authorize(AuthenticationSchemes = Schemes.DefaultScheme)] - [Permission("UserManagement.Write")] + //[Permission("UserManagement.Write")] public async Task CreateUserService(CreateUserRequest newUser, CancellationToken cancellationToken) { try @@ -98,7 +98,7 @@ namespace Core.Thalos.BFF.Api.Controllers [ProducesResponseType(typeof(Notification), StatusCodes.Status422UnprocessableEntity)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] //[Authorize(AuthenticationSchemes = Schemes.DefaultScheme)] - [Permission("UserManagement.Read")] + //[Permission("UserManagement.Read")] public async Task GetUserByIdService(GetUserRequest request, CancellationToken cancellationToken) { try @@ -127,7 +127,7 @@ namespace Core.Thalos.BFF.Api.Controllers [ProducesResponseType(typeof(Notification), StatusCodes.Status422UnprocessableEntity)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] //[Authorize(AuthenticationSchemes = Schemes.DefaultScheme)] - [Permission("UserManagement.Read")] + //[Permission("UserManagement.Read")] public async Task GetUserByEmailService(GetUserByEmailRequest request, CancellationToken cancellationToken) { try @@ -156,7 +156,7 @@ namespace Core.Thalos.BFF.Api.Controllers [ProducesResponseType(typeof(Notification), StatusCodes.Status422UnprocessableEntity)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] //[Authorize(AuthenticationSchemes = Schemes.DefaultScheme)] - [Permission("UserManagement.Write")] + //[Permission("UserManagement.Write")] public async Task UpdateUserService(UpdateUserRequest request, CancellationToken cancellationToken) { try @@ -194,7 +194,7 @@ namespace Core.Thalos.BFF.Api.Controllers [ProducesResponseType(typeof(Notification), StatusCodes.Status412PreconditionFailed)] [ProducesResponseType(typeof(Notification), StatusCodes.Status422UnprocessableEntity)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - [Authorize(AuthenticationSchemes = $"{Schemes.AzureScheme}, {Schemes.DefaultScheme}")] + //[Authorize(AuthenticationSchemes = $"{Schemes.AzureScheme}, {Schemes.DefaultScheme}")] public async Task LoginUserService([FromBody] LoginUserRequest request, CancellationToken cancellationToken) { try @@ -222,7 +222,7 @@ namespace Core.Thalos.BFF.Api.Controllers [ProducesResponseType(typeof(Notification), StatusCodes.Status412PreconditionFailed)] [ProducesResponseType(typeof(Notification), StatusCodes.Status422UnprocessableEntity)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - [Authorize(AuthenticationSchemes = $"{Schemes.AzureScheme}, {Schemes.DefaultScheme}")] + //[Authorize(AuthenticationSchemes = $"{Schemes.AzureScheme}, {Schemes.DefaultScheme}")] public async Task LogoutUserService([FromBody] LogoutUserRequest request, CancellationToken cancellationToken) { try @@ -253,7 +253,7 @@ namespace Core.Thalos.BFF.Api.Controllers [ProducesResponseType(typeof(Notification), StatusCodes.Status422UnprocessableEntity)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] //[Authorize(AuthenticationSchemes = Schemes.DefaultScheme)] - [Permission("UserManagement.Write")] + //[Permission("UserManagement.Write")] public async Task ChangeUserStatusService([FromBody] ChangeUserStatusRequest request, CancellationToken cancellationToken) { try @@ -284,7 +284,7 @@ namespace Core.Thalos.BFF.Api.Controllers [ProducesResponseType(typeof(Notification), StatusCodes.Status422UnprocessableEntity)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] //[Authorize(AuthenticationSchemes = Schemes.DefaultScheme)] - [Permission("UserManagement.Write")] + //[Permission("UserManagement.Write")] public async Task AddCompanyToUserService([FromBody] AddCompanyToUserRequest request, CancellationToken cancellationToken) { try @@ -316,7 +316,7 @@ namespace Core.Thalos.BFF.Api.Controllers [ProducesResponseType(typeof(Notification), StatusCodes.Status422UnprocessableEntity)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] //[Authorize(AuthenticationSchemes = Schemes.DefaultScheme)] - [Permission("UserManagement.Write")] + //[Permission("UserManagement.Write")] public async Task RemoveCompanyFromUserService([FromBody] RemoveCompanyFromUserRequest request, CancellationToken cancellationToken) { try @@ -348,7 +348,7 @@ namespace Core.Thalos.BFF.Api.Controllers [ProducesResponseType(typeof(Notification), StatusCodes.Status422UnprocessableEntity)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] //[Authorize(AuthenticationSchemes = Schemes.DefaultScheme)] - [Permission("UserManagement.Write")] + //[Permission("UserManagement.Write")] public async Task AddProjectToUserService([FromBody] AddProjectToUserRequest request, CancellationToken cancellationToken) { @@ -381,7 +381,7 @@ namespace Core.Thalos.BFF.Api.Controllers [ProducesResponseType(typeof(Notification), StatusCodes.Status422UnprocessableEntity)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] //[Authorize(AuthenticationSchemes = Schemes.DefaultScheme)] - [Permission("UserManagement.Write")] + //[Permission("UserManagement.Write")] public async Task RemoveProjectFromUserService([FromBody] RemoveProjectFromUserRequest request, CancellationToken cancellationToken) { try diff --git a/Core.Thalos.BFF.Api/Core.Thalos.BFF.Api.csproj b/Core.Thalos.BFF.Api/Core.Thalos.BFF.Api.csproj index 9d06f39..2b3fb76 100644 --- a/Core.Thalos.BFF.Api/Core.Thalos.BFF.Api.csproj +++ b/Core.Thalos.BFF.Api/Core.Thalos.BFF.Api.csproj @@ -10,6 +10,12 @@ + + + + + + diff --git a/Core.Thalos.BFF.Api/Program.cs b/Core.Thalos.BFF.Api/Program.cs index b90e44d..baf72ad 100644 --- a/Core.Thalos.BFF.Api/Program.cs +++ b/Core.Thalos.BFF.Api/Program.cs @@ -1,127 +1,192 @@ -using Asp.Versioning; +using Asp.Versioning; using Azure.Identity; using Core.Blueprint.Logging.Configuration; +using Core.Thalos.Adapters.Contracts; using Core.Thalos.Adapters.Extensions; +using Core.Thalos.Adapters.Services; +using Core.Thalos.BFF.Api.Services; using Core.Thalos.External.ClientConfiguration; +using Google.Protobuf.WellKnownTypes; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.ResponseCompression; using Microsoft.Extensions.Configuration.AzureAppConfiguration; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Interfaces; +using Microsoft.OpenApi.Models; using OpenTelemetry.Logs; using OpenTelemetry.Resources; using Swashbuckle.AspNetCore.SwaggerUI; using System.IO.Compression; using System.Reflection; +using System.Security.Claims; var builder = WebApplication.CreateBuilder(args); -builder.Services.AddEndpointsApiExplorer(); builder.Configuration .AddUserSecrets(Assembly.GetExecutingAssembly()) .AddEnvironmentVariables(); +// 🔑 Google Auth config +var googleClientId = builder.Configuration["Authentication:Google:ClientId"]; +var googleClientSecret = builder.Configuration["Authentication:Google:ClientSecret"]; +var redirectUri = builder.Configuration["Authentication:Google:RedirectUri"]; + +// 🧩 Authentication +builder.Services.AddAuthentication(options => +{ + options.DefaultAuthenticateScheme = Constant.Scheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; +}) +.AddScheme(Constant.Scheme, null) +.AddGoogle(options => +{ + options.ClientId = googleClientId!; + options.ClientSecret = googleClientSecret!; + //options.SaveTokens = true; + options.CallbackPath = $"/{redirectUri}"; +}); + +builder.Services.AddAuthorization(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddEndpointsApiExplorer(); + +// 🧩 Swagger + OAuth2 +builder.Services.AddSwaggerGen(opts => +{ + const string schemeName = "oauth2"; + + opts.SwaggerDoc("v1", + new OpenApiInfo { Title = "Core.Thalos.BFF", Version = "v1" }); + + opts.AddSecurityDefinition(schemeName, new OpenApiSecurityScheme + { + Type = SecuritySchemeType.OAuth2, + Scheme = "bearer", // tells Swagger-UI to build an Authorization header + BearerFormat = "JWT", + Name = "Authorization", + In = ParameterLocation.Header, + + /* ⚠️ The key line – tell Swagger-UI to pick id_token instead of access_token */ + Extensions = new Dictionary + { + ["x-tokenName"] = new OpenApiString("id_token") + }, + + Flows = new OpenApiOAuthFlows + { + AuthorizationCode = new OpenApiOAuthFlow + { + AuthorizationUrl = new Uri("https://accounts.google.com/o/oauth2/v2/auth"), + TokenUrl = new Uri("https://oauth2.googleapis.com/token"), + Scopes = new Dictionary + { + { "openid", "OpenID Connect" }, + { "email", "Access email" }, + { "profile", "Access profile" } + } + } + } + }); + + // every operation requires the scheme + opts.AddSecurityRequirement(new OpenApiSecurityRequirement + { + [new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { Type = ReferenceType.SecurityScheme, Id = schemeName } + } + ] = new[] { "openid", "email", "profile" } + }); +}); + +// 🎯 Existing configs (unchanged) builder.Services.AddResponseCompression(); builder.Services.AddProblemDetails(); builder.Services.AddLogs(builder); builder.Services.AddMemoryCache(); -builder.Services.AddResponseCaching(configureOptions => { configureOptions.UseCaseSensitivePaths = true; }); -builder.Logging.AddOpenTelemetry(options => +builder.Services.AddResponseCaching(options => { options.UseCaseSensitivePaths = true; }); + +builder.Logging.AddOpenTelemetry(logging => { - options.IncludeScopes = true; - options.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("core.thalos.bff.api")).AddConsoleExporter(); + logging.IncludeScopes = true; + logging.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("core.thalos.bff.api")) + .AddConsoleExporter(); }); builder.Host.ConfigureServices((context, services) => { - builder.Services.AddHsts(options => + services.AddHsts(options => { options.Preload = true; options.IncludeSubDomains = true; options.MaxAge = TimeSpan.FromDays(60); }); - builder.Services.AddResponseCaching(configureOptions => - { - configureOptions.UseCaseSensitivePaths = true; - configureOptions.MaximumBodySize = 2048; - }); - builder.Services.AddHttpsRedirection(options => - { - options.RedirectStatusCode = 308; - }); - services.AddHttpLogging(http => - { - http.CombineLogs = true; - }); + services.AddHttpsRedirection(options => { options.RedirectStatusCode = 308; }); services.AddAntiforgery(); + services.AddHttpLogging(http => http.CombineLogs = true); + services.AddCors(options => options.AddPolicy("AllowAll", policy => policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod())); - services.AddCors(options => + services.AddMvc().AddJsonOptions(opt => { - options.AddPolicy("AllowAll", policyBuilder => - policyBuilder.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod()); + opt.JsonSerializerOptions.WriteIndented = true; + opt.JsonSerializerOptions.MaxDepth = 20; + opt.JsonSerializerOptions.NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowNamedFloatingPointLiterals; }); - services.AddMvc().AddJsonOptions(options => + + services.Configure(opt => opt.Level = CompressionLevel.SmallestSize); + services.Configure(opt => opt.Level = CompressionLevel.SmallestSize); + services.AddResponseCompression(opt => { - options.JsonSerializerOptions.WriteIndented = true; - options.JsonSerializerOptions.MaxDepth = 20; - options.JsonSerializerOptions.NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowNamedFloatingPointLiterals; + opt.EnableForHttps = true; + opt.Providers.Add(); + opt.Providers.Add(); }); - services.Configure(options => - { - options.Level = CompressionLevel.SmallestSize; - }); - services.Configure(options => - { - options.Level = CompressionLevel.SmallestSize; - }); - services.AddResponseCompression(options => - { - options.EnableForHttps = true; - options.Providers.Add(); - options.Providers.Add(); - }); - services.AddResponseCaching(); + services.AddControllers(); - services.AddEndpointsApiExplorer(); - services.AddSwaggerGen(); - services.AddLogging(); - services.AddProblemDetails(); services.AddHttpContextAccessor(); - services.AddTransient(); // Register the TrackingIdHandler + services.AddTransient(); services.RegisterExternalLayer(builder.Configuration); services.AddApiVersioning(options => options.ReportApiVersions = true) - .AddApiExplorer(options => - { - options.GroupNameFormat = "'v'VVV"; - options.SubstituteApiVersionInUrl = true; - }); + .AddApiExplorer(opt => + { + opt.GroupNameFormat = "'v'VVV"; + opt.SubstituteApiVersionInUrl = true; + }); }); -builder.Services.AddCors(options => -{ - options.AddDefaultPolicy( - builder => - { - builder.AllowAnyOrigin() - .AllowAnyHeader() - .AllowAnyMethod(); - }); -}); +builder.Services.AddScoped(); -//*************************************************************************// var app = builder.Build(); +app.UseDeveloperExceptionPage(); -app.UseCors(options => options.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin()); app.UseSwagger(); + +app.UseCors(options => options.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod().WithOrigins("https://localhost:7239")); app.UseSwaggerUI(options => { - foreach (var version in app.DescribeApiVersions().Select(version => version.GroupName)) + foreach (var version in app.DescribeApiVersions().Select(v => v.GroupName)) options.SwaggerEndpoint($"/swagger/{version}/swagger.json", version); options.DisplayRequestDuration(); options.EnableTryItOutByDefault(); options.DocExpansion(DocExpansion.None); + + options.OAuthClientId(googleClientId); + options.OAuthClientSecret(googleClientSecret); + options.OAuthUsePkce(); + options.OAuthScopes("openid", "email", "profile"); }); + app.UseResponseCompression(); app.UseResponseCaching(); app.UseHttpsRedirection(); @@ -132,5 +197,4 @@ app.MapControllers(); app.UseHsts(); app.UseAntiforgery(); app.UseLogging(builder.Configuration); - -app.Run(); \ No newline at end of file +app.Run(); diff --git a/Core.Thalos.BFF.Api/Services/Constant.cs b/Core.Thalos.BFF.Api/Services/Constant.cs new file mode 100644 index 0000000..2464462 --- /dev/null +++ b/Core.Thalos.BFF.Api/Services/Constant.cs @@ -0,0 +1,7 @@ +namespace Core.Thalos.BFF.Api.Services +{ + public static class Constant + { + public const string Scheme = "GoogleAccessToken"; + } +} diff --git a/Core.Thalos.BFF.Api/Services/GoogleAccessTokenAuthenticationHandler.cs b/Core.Thalos.BFF.Api/Services/GoogleAccessTokenAuthenticationHandler.cs new file mode 100644 index 0000000..1c151f1 --- /dev/null +++ b/Core.Thalos.BFF.Api/Services/GoogleAccessTokenAuthenticationHandler.cs @@ -0,0 +1,61 @@ +using Google.Apis.Auth; +using Google.Apis.Auth.OAuth2.Flows; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Options; +using System.Security.Claims; +using System.Text.Encodings.Web; + +namespace Core.Thalos.BFF.Api.Services +{ + public class GoogleAccessTokenAuthenticationHandler(IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder, + IConfiguration config) : AuthenticationHandler(options, logger, encoder) + { + protected override async Task HandleAuthenticateAsync() + { + if (!Request.Headers.ContainsKey("Authorization")) + return AuthenticateResult.Fail("Missing Authorization header"); + + var authHeader = Request.Headers.Authorization.ToString(); + if (!authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) + return AuthenticateResult.Fail("Invalid Authorization header"); + + var idToken = authHeader["Bearer ".Length..].Trim(); + + GoogleJsonWebSignature.Payload payload; + try + { + payload = await GoogleJsonWebSignature.ValidateAsync( + idToken, + new GoogleJsonWebSignature.ValidationSettings + { + Audience = new[] { config["Authentication:Google:ClientId"]! } + }); + } + catch (InvalidJwtException) + { + return AuthenticateResult.Fail("Invalid Google token"); + } + + var claims = new List + { + new Claim(ClaimTypes.NameIdentifier, payload.Subject), + new Claim(ClaimTypes.Email, payload.Email), + new Claim(ClaimTypes.Name, payload.Name ?? "") + }; + + var identity = new ClaimsIdentity(claims, Constant.Scheme); + var principal = new ClaimsPrincipal(identity); + + var userEmail = principal.FindFirst(ClaimTypes.Email)?.Value; + + if (string.IsNullOrEmpty(userEmail) || + !userEmail.EndsWith("@agilewebs.com", StringComparison.OrdinalIgnoreCase)) + return AuthenticateResult.Fail("Unauthorized Access"); + + var ticket = new AuthenticationTicket(principal, Constant.Scheme); + return AuthenticateResult.Success(ticket); + } + } +} diff --git a/Core.Thalos.BFF.Api/Services/IGoogleAuthHelper.cs b/Core.Thalos.BFF.Api/Services/IGoogleAuthHelper.cs new file mode 100644 index 0000000..4cb1464 --- /dev/null +++ b/Core.Thalos.BFF.Api/Services/IGoogleAuthHelper.cs @@ -0,0 +1,37 @@ +using Google.Apis.Auth.OAuth2; +using Google.Apis.Oauth2.v2; + +namespace Core.Thalos.BFF.Api.Services +{ + public interface IGoogleAuthHelper + { + string[] GetScopes(); + string ScopeToString(); + ClientSecrets GetClientSecrets(); + } + + public class GoogleAuthHelperService(IConfiguration config) : IGoogleAuthHelper + { + public ClientSecrets GetClientSecrets() + { + string clientId = config["Authentication:Google:ClientId"]!; + string clientSecret = config["Authentication:Google:ClientSecret"]!; + + return new() { ClientId = clientId, ClientSecret = clientSecret }; + } + + public string[] GetScopes() + { + var scopes = new[] + { + Oauth2Service.Scope.Openid, + Oauth2Service.Scope.UserinfoEmail, + Oauth2Service.Scope.UserinfoProfile + }; + + return scopes; + } + + public string ScopeToString() => string.Join(", ", GetScopes()); + } +} diff --git a/Core.Thalos.BFF.Api/Services/IGoogleAuthorization.cs b/Core.Thalos.BFF.Api/Services/IGoogleAuthorization.cs new file mode 100644 index 0000000..a60bf70 --- /dev/null +++ b/Core.Thalos.BFF.Api/Services/IGoogleAuthorization.cs @@ -0,0 +1,42 @@ +using Google.Apis.Auth.OAuth2; +using Google.Apis.Auth.OAuth2.Flows; + +namespace Core.Thalos.BFF.Api.Services +{ + public interface IGoogleAuthorization + { + string GetAuthorizationUrl(); + Task ExchangeCodeForToken(string code); + } + + public class GoogleAuthorizationService( + IGoogleAuthHelper googleHelper, IConfiguration config) : IGoogleAuthorization + { + private string RedirectUrl = config["Authentication:Google:RedirectUri"]!; + + public async Task ExchangeCodeForToken(string code) + { + var flow = new GoogleAuthorizationCodeFlow( + new GoogleAuthorizationCodeFlow.Initializer + { + ClientSecrets = googleHelper.GetClientSecrets(), + Scopes = googleHelper.GetScopes() + }); + + var token = await flow.ExchangeCodeForTokenAsync( + "user", code, RedirectUrl, CancellationToken.None); + + return new UserCredential(flow, "user", token); + } + + public string GetAuthorizationUrl() => + new GoogleAuthorizationCodeFlow( + new GoogleAuthorizationCodeFlow.Initializer + { + + ClientSecrets = googleHelper.GetClientSecrets(), + Scopes = googleHelper.GetScopes(), + Prompt = "consent" + }).CreateAuthorizationCodeRequest(RedirectUrl).Build().ToString(); + } +} diff --git a/Core.Thalos.BFF.Api/Services/Token.cs b/Core.Thalos.BFF.Api/Services/Token.cs new file mode 100644 index 0000000..7883ff4 --- /dev/null +++ b/Core.Thalos.BFF.Api/Services/Token.cs @@ -0,0 +1,4 @@ +namespace Core.Thalos.BFF.Api.Services +{ + public record Token(string IdToken); +} diff --git a/Core.Thalos.BFF.Api/appsettings.Local.json b/Core.Thalos.BFF.Api/appsettings.Local.json index f9eaec1..f57e6bc 100644 --- a/Core.Thalos.BFF.Api/appsettings.Local.json +++ b/Core.Thalos.BFF.Api/appsettings.Local.json @@ -7,6 +7,14 @@ }, "LocalGateways": { "ThalosService": "https://localhost:7253/api" + }, + "Authentication": { + "Google": { + "ClientId": "128345072002-mtfdgpcur44o9tbd7q6e0bb9qnp2crfp.apps.googleusercontent.com", + "ClientSecret": "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", + "ApplicationName": "Thalos", + "RedirectUri": "https://localhost:7239/api/v1/Authentication/callback" + } } }