From aeab9548b89ab8ef39351188c8df193b1e1d926e Mon Sep 17 00:00:00 2001 From: Oscar Morales Date: Tue, 15 Jul 2025 13:52:09 -0600 Subject: [PATCH] Add google and jwt authentication and authorization --- .../Common/Constants/Policies.cs | 24 +++++ .../Common/Constants/Roles.cs | 15 +++ .../Google/GoogleAuthorization.cs | 38 ++++++++ .../Google/IGoogleAuthorization.cs | 10 ++ .../Jwt/PermissionAuthorizationAdapter.cs | 9 ++ .../Jwt/PermissionsAuthorizationHandler.cs | 18 ++++ .../Extensions/AuthenticationExtension.cs | 92 +++++++++++++++++++ .../Extensions/SwaggerExtension.cs | 84 +++++++++++++++++ .../GoogleAccessTokenAuthenticationHandler.cs | 63 +++++++++++++ .../Helpers/GoogleAuthHelper.cs | 31 +++++++ .../Helpers/IGoogleAuthHelper.cs | 11 +++ .../Common/Constants/Policies.cs | 24 +++++ .../Common/Constants/Roles.cs | 15 +++ .../Common/Constants/Schemes.cs | 5 + .../Core.Thalos.BuildingBlocks.csproj | 5 +- 15 files changed, 443 insertions(+), 1 deletion(-) create mode 100644 Core.Cerberos.Adapters/Common/Constants/Policies.cs create mode 100644 Core.Cerberos.Adapters/Common/Constants/Roles.cs create mode 100644 Core.Thalos.BuildingBlocks/Authentication/Authorization/Google/GoogleAuthorization.cs create mode 100644 Core.Thalos.BuildingBlocks/Authentication/Authorization/Google/IGoogleAuthorization.cs create mode 100644 Core.Thalos.BuildingBlocks/Authentication/Authorization/Jwt/PermissionAuthorizationAdapter.cs create mode 100644 Core.Thalos.BuildingBlocks/Authentication/Authorization/Jwt/PermissionsAuthorizationHandler.cs create mode 100644 Core.Thalos.BuildingBlocks/Authentication/Extensions/AuthenticationExtension.cs create mode 100644 Core.Thalos.BuildingBlocks/Authentication/Extensions/SwaggerExtension.cs create mode 100644 Core.Thalos.BuildingBlocks/Authentication/Handlers/GoogleAccessTokenAuthenticationHandler.cs create mode 100644 Core.Thalos.BuildingBlocks/Authentication/Helpers/GoogleAuthHelper.cs create mode 100644 Core.Thalos.BuildingBlocks/Authentication/Helpers/IGoogleAuthHelper.cs create mode 100644 Core.Thalos.BuildingBlocks/Common/Constants/Policies.cs create mode 100644 Core.Thalos.BuildingBlocks/Common/Constants/Roles.cs diff --git a/Core.Cerberos.Adapters/Common/Constants/Policies.cs b/Core.Cerberos.Adapters/Common/Constants/Policies.cs new file mode 100644 index 0000000..aa7c248 --- /dev/null +++ b/Core.Cerberos.Adapters/Common/Constants/Policies.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +namespace Core.Thalos.BuildingBlocks.Common.Constants +{ + /// + /// Constants for policy. + /// + public class Policies + { + /// + /// Defines the access policy for reading mobile-related data. + /// This policy grants read-only permissions for retrieving mobile device information, + /// user mobile settings, or related data as per the application's authorization scope. + /// + public const string Read = "Read"; + + /// + /// Defines the access policy for writing mobile-related data. + /// This policy grants permissions to modify, update, or store mobile device information, + /// user mobile settings, or related data as per the application's authorization scope. + /// + public const string Write = "Write"; + } +} diff --git a/Core.Cerberos.Adapters/Common/Constants/Roles.cs b/Core.Cerberos.Adapters/Common/Constants/Roles.cs new file mode 100644 index 0000000..023b651 --- /dev/null +++ b/Core.Cerberos.Adapters/Common/Constants/Roles.cs @@ -0,0 +1,15 @@ +namespace Core.Thalos.BuildingBlocks.Common.Constants +{ + public class Roles + { + /// + /// The role for Guest. + /// + public const string Guest = "684909c4826cd093b4f61c11"; + + /// + /// The role for Admin. + /// + public const string Admin = "68407642ec46a0e6fe1e8ec9"; + } +} diff --git a/Core.Thalos.BuildingBlocks/Authentication/Authorization/Google/GoogleAuthorization.cs b/Core.Thalos.BuildingBlocks/Authentication/Authorization/Google/GoogleAuthorization.cs new file mode 100644 index 0000000..199a870 --- /dev/null +++ b/Core.Thalos.BuildingBlocks/Authentication/Authorization/Google/GoogleAuthorization.cs @@ -0,0 +1,38 @@ +using Core.Thalos.BuildingBlocks.Authentication.Helpers; +using Google.Apis.Auth.OAuth2; +using Google.Apis.Auth.OAuth2.Flows; +using Microsoft.Extensions.Configuration; + +namespace Core.Thalos.BuildingBlocks.Authentication.Authorization.Google +{ + public class GoogleAuthorization( + 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.BuildingBlocks/Authentication/Authorization/Google/IGoogleAuthorization.cs b/Core.Thalos.BuildingBlocks/Authentication/Authorization/Google/IGoogleAuthorization.cs new file mode 100644 index 0000000..9827aab --- /dev/null +++ b/Core.Thalos.BuildingBlocks/Authentication/Authorization/Google/IGoogleAuthorization.cs @@ -0,0 +1,10 @@ +using Google.Apis.Auth.OAuth2; + +namespace Core.Thalos.BuildingBlocks.Authentication.Authorization.Google +{ + public interface IGoogleAuthorization + { + string GetAuthorizationUrl(); + Task ExchangeCodeForToken(string code); + } +} diff --git a/Core.Thalos.BuildingBlocks/Authentication/Authorization/Jwt/PermissionAuthorizationAdapter.cs b/Core.Thalos.BuildingBlocks/Authentication/Authorization/Jwt/PermissionAuthorizationAdapter.cs new file mode 100644 index 0000000..0bbb5d0 --- /dev/null +++ b/Core.Thalos.BuildingBlocks/Authentication/Authorization/Jwt/PermissionAuthorizationAdapter.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Core.Thalos.BuildingBlocks.Authentication.Authorization.Jwt +{ + public class PermissionAuthorizationAdapter(string[] permission) : IAuthorizationRequirement + { + public string[] Permission { get; } = permission ?? throw new ArgumentNullException(nameof(permission)); + } +} diff --git a/Core.Thalos.BuildingBlocks/Authentication/Authorization/Jwt/PermissionsAuthorizationHandler.cs b/Core.Thalos.BuildingBlocks/Authentication/Authorization/Jwt/PermissionsAuthorizationHandler.cs new file mode 100644 index 0000000..eaca04c --- /dev/null +++ b/Core.Thalos.BuildingBlocks/Authentication/Authorization/Jwt/PermissionsAuthorizationHandler.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Core.Thalos.BuildingBlocks.Authentication.Authorization.Jwt +{ + public class PermissionsAuthorizationHandler : AuthorizationHandler + { + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionAuthorizationAdapter requirement) + { + PermissionAuthorizationAdapter requirement2 = requirement; + if (context.User.Claims.Any((x) => x.Type == "roleId" && requirement2.Permission.Contains(x.Value))) + { + context.Succeed(requirement2); + } + + return Task.CompletedTask; + } + } +} diff --git a/Core.Thalos.BuildingBlocks/Authentication/Extensions/AuthenticationExtension.cs b/Core.Thalos.BuildingBlocks/Authentication/Extensions/AuthenticationExtension.cs new file mode 100644 index 0000000..f2b1daf --- /dev/null +++ b/Core.Thalos.BuildingBlocks/Authentication/Extensions/AuthenticationExtension.cs @@ -0,0 +1,92 @@ +using Core.Thalos.Adapters.Common.Constants; +using Core.Thalos.Adapters.Options; +using Core.Thalos.BuildingBlocks.Authentication.Authorization.Google; +using Core.Thalos.BuildingBlocks.Authentication.Authorization.Jwt; +using Core.Thalos.BuildingBlocks.Authentication.Handlers; +using Core.Thalos.BuildingBlocks.Authentication.Helpers; +using Core.Thalos.BuildingBlocks.Common.Constants; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; +using System.Text; + +namespace Core.Thalos.BuildingBlocks.Authentication.Extensions +{ + /// + /// Extension methods for configuring authentication with various Google and JWT setups. + /// + public static class AuthenticationExtension + { + /// + /// Configures authentication using Google Auth for an API that requires downstream API access. + /// + /// The to add the services to. + /// The containing Google Auth configuration settings. + public static void ConfigureAuthentication(this IServiceCollection services, IConfiguration configuration) + { + var secretKey = configuration.GetSection("SecretKey").Value ?? string.Empty; + var _signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secretKey)); + var jwtAppSettingsOptions = configuration.GetSection(nameof(JwtIssuerOptions)); + var jwtIssuerOptions = jwtAppSettingsOptions.Get(); + + var googleClientId = configuration["Authentication:Google:ClientId"]; + var googleClientSecret = configuration["Authentication:Google:ClientSecret"]; + var redirectUri = configuration["Authentication:Google:RedirectUri"]; + + services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = Schemes.GoogleScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddScheme(Schemes.GoogleScheme, null) + .AddGoogle(options => + { + options.ClientId = googleClientId!; + options.ClientSecret = googleClientSecret!; + //options.SaveTokens = true; + options.CallbackPath = $"/{redirectUri}"; + }) + .AddJwtBearer(Schemes.DefaultScheme, x => + { + x.TokenValidationParameters = new TokenValidationParameters + { + ValidIssuer = jwtIssuerOptions?.Issuer, + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidAudience = jwtIssuerOptions?.Audience, + IssuerSigningKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(configuration["SecretKey"] ?? string.Empty)) + }; + }); + + services.Configure(options => + { + options.Issuer = jwtAppSettingsOptions[nameof(jwtIssuerOptions.Issuer)] ?? string.Empty; + options.Audience = jwtAppSettingsOptions[nameof(jwtIssuerOptions.Audience)] ?? string.Empty; + options.SigningCredentials = new SigningCredentials(_signingKey, SecurityAlgorithms.HmacSha256); + }); + + services.AddSingleton(jwtAppSettingsOptions); + + string[] roles = { Roles.Guest, Roles.Admin }; + + services.AddAuthorization(options => + { + options.AddPolicy(Policies.Read, policy => policy.Requirements.Add(new PermissionAuthorizationAdapter(roles))); + options.AddPolicy(Policies.Write, policy => policy.Requirements.Add(new PermissionAuthorizationAdapter(roles))); + }); + + services.AddTransient(); + + services.AddAuthorization(); + services.AddScoped(); + services.AddScoped(); + } + } +} diff --git a/Core.Thalos.BuildingBlocks/Authentication/Extensions/SwaggerExtension.cs b/Core.Thalos.BuildingBlocks/Authentication/Extensions/SwaggerExtension.cs new file mode 100644 index 0000000..e6b9379 --- /dev/null +++ b/Core.Thalos.BuildingBlocks/Authentication/Extensions/SwaggerExtension.cs @@ -0,0 +1,84 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Interfaces; +using Microsoft.OpenApi.Models; + +namespace Core.Thalos.BuildingBlocks.Authentication.Extensions +{ + public static class SwaggerExtension + { + public static void AddSwaggerGen(this IServiceCollection services, IConfiguration configuration) + { + services.AddSwaggerGen(opts => + { + const string schemeName = "oauth2"; + + opts.AddSecurityDefinition(schemeName, new OpenApiSecurityScheme + { + Type = SecuritySchemeType.OAuth2, + Scheme = "bearer", + BearerFormat = "JWT", + Name = "Authorization", + In = ParameterLocation.Header, + + 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" } + }); + + opts.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + Description = "JWT Authorization header using the Bearer scheme", + Name = "Authorization", + In = ParameterLocation.Header, + Type = SecuritySchemeType.Http, + Scheme = "bearer", + BearerFormat = "JWT" + }); + + opts.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + Array.Empty() + } + }); + }); + } + } +} diff --git a/Core.Thalos.BuildingBlocks/Authentication/Handlers/GoogleAccessTokenAuthenticationHandler.cs b/Core.Thalos.BuildingBlocks/Authentication/Handlers/GoogleAccessTokenAuthenticationHandler.cs new file mode 100644 index 0000000..83361a2 --- /dev/null +++ b/Core.Thalos.BuildingBlocks/Authentication/Handlers/GoogleAccessTokenAuthenticationHandler.cs @@ -0,0 +1,63 @@ +using Core.Thalos.Adapters.Common.Constants; +using Google.Apis.Auth; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.Security.Claims; +using System.Text.Encodings.Web; + +namespace Core.Thalos.BuildingBlocks.Authentication.Handlers +{ + 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, Schemes.GoogleScheme); + 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, Schemes.GoogleScheme); + return AuthenticateResult.Success(ticket); + } + } +} diff --git a/Core.Thalos.BuildingBlocks/Authentication/Helpers/GoogleAuthHelper.cs b/Core.Thalos.BuildingBlocks/Authentication/Helpers/GoogleAuthHelper.cs new file mode 100644 index 0000000..9470ef2 --- /dev/null +++ b/Core.Thalos.BuildingBlocks/Authentication/Helpers/GoogleAuthHelper.cs @@ -0,0 +1,31 @@ +using Google.Apis.Auth.OAuth2; +using Google.Apis.Oauth2.v2; +using Microsoft.Extensions.Configuration; + +namespace Core.Thalos.BuildingBlocks.Authentication.Helpers +{ + public class GoogleAuthHelper(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.BuildingBlocks/Authentication/Helpers/IGoogleAuthHelper.cs b/Core.Thalos.BuildingBlocks/Authentication/Helpers/IGoogleAuthHelper.cs new file mode 100644 index 0000000..cac6350 --- /dev/null +++ b/Core.Thalos.BuildingBlocks/Authentication/Helpers/IGoogleAuthHelper.cs @@ -0,0 +1,11 @@ +using Google.Apis.Auth.OAuth2; + +namespace Core.Thalos.BuildingBlocks.Authentication.Helpers +{ + public interface IGoogleAuthHelper + { + string[] GetScopes(); + string ScopeToString(); + ClientSecrets GetClientSecrets(); + } +} diff --git a/Core.Thalos.BuildingBlocks/Common/Constants/Policies.cs b/Core.Thalos.BuildingBlocks/Common/Constants/Policies.cs new file mode 100644 index 0000000..aa7c248 --- /dev/null +++ b/Core.Thalos.BuildingBlocks/Common/Constants/Policies.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +namespace Core.Thalos.BuildingBlocks.Common.Constants +{ + /// + /// Constants for policy. + /// + public class Policies + { + /// + /// Defines the access policy for reading mobile-related data. + /// This policy grants read-only permissions for retrieving mobile device information, + /// user mobile settings, or related data as per the application's authorization scope. + /// + public const string Read = "Read"; + + /// + /// Defines the access policy for writing mobile-related data. + /// This policy grants permissions to modify, update, or store mobile device information, + /// user mobile settings, or related data as per the application's authorization scope. + /// + public const string Write = "Write"; + } +} diff --git a/Core.Thalos.BuildingBlocks/Common/Constants/Roles.cs b/Core.Thalos.BuildingBlocks/Common/Constants/Roles.cs new file mode 100644 index 0000000..023b651 --- /dev/null +++ b/Core.Thalos.BuildingBlocks/Common/Constants/Roles.cs @@ -0,0 +1,15 @@ +namespace Core.Thalos.BuildingBlocks.Common.Constants +{ + public class Roles + { + /// + /// The role for Guest. + /// + public const string Guest = "684909c4826cd093b4f61c11"; + + /// + /// The role for Admin. + /// + public const string Admin = "68407642ec46a0e6fe1e8ec9"; + } +} diff --git a/Core.Thalos.BuildingBlocks/Common/Constants/Schemes.cs b/Core.Thalos.BuildingBlocks/Common/Constants/Schemes.cs index b0e176b..268a42a 100644 --- a/Core.Thalos.BuildingBlocks/Common/Constants/Schemes.cs +++ b/Core.Thalos.BuildingBlocks/Common/Constants/Schemes.cs @@ -14,5 +14,10 @@ /// The azure scheme. /// public const string AzureScheme = "AzureScheme"; + + /// + /// The google scheme. + /// + public const string GoogleScheme = "GoogleScheme"; } } diff --git a/Core.Thalos.BuildingBlocks/Core.Thalos.BuildingBlocks.csproj b/Core.Thalos.BuildingBlocks/Core.Thalos.BuildingBlocks.csproj index d947258..cf01555 100644 --- a/Core.Thalos.BuildingBlocks/Core.Thalos.BuildingBlocks.csproj +++ b/Core.Thalos.BuildingBlocks/Core.Thalos.BuildingBlocks.csproj @@ -8,13 +8,16 @@ net8.0 enable enable - 1.0.2 + 1.0.5 $(Date:yyyyMMddHHmm) + + + -- 2.49.1