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
2 changes: 2 additions & 0 deletions src/Clients/DevDad.SaaSAdmin.API/DevDad.SaaSAdmin.API.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.1" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0" />
<PackageReference Include="DotNetEnv" Version="3.1.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0" />
<PackageReference Include="Microsoft.Identity.Web" Version="3.6.0" />
</ItemGroup>

<ItemGroup>
Expand Down
47 changes: 30 additions & 17 deletions src/Clients/DevDad.SaaSAdmin.API/EndpointExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
using System;
using System.Text.Json;
using System.Threading.Tasks;
using DevDad.SaaSAdmin.AccountManager.Contracts;
using DevDad.SaaSAdmin.API.ApiServices;
using DevDad.SaaSAdmin.API.PublicModels;
using DevDad.SaaSAdmin.iFX;
using DevDad.SaaSAdmin.StoreManager.Contracts;

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

using DevDad.SaaSAdmin.AccountManager.Contracts;
using DevDad.SaaSAdmin.API.ApiServices;
using DevDad.SaaSAdmin.API.PublicModels;
using DevDad.SaaSAdmin.StoreManager.Contracts;
using ThatDeveloperDad.iFX.Serialization;
using Microsoft.AspNetCore.Authorization;
using DevDad.SaaSAdmin.iFX;

namespace DevDad.SaaSAdmin.API;

Expand Down Expand Up @@ -109,34 +111,44 @@ public static class EndpointExtensions
await context.Response.WriteAsync("Hello There from AppEndpoints!");
});

app.MapPost("/loadProfile", async Task<IResult> (LoadProfileRequest requestData, HttpContext httpContext) =>
app.MapPost("/loadProfile",
async Task<IResult> (LoadProfileRequest requestData, HttpContext httpContext) =>
{
IResult result = Results.NoContent();

try
{
IAccountManager? acctManager = componentRegistry.GetService<IAccountManager>();
LoadAccountProfileRequest mgrRequest = new("LoadUserProfile")
LoadAccountProfileRequest mgrRequest = new("LoadOrCreateUserProfile")
{
UserId = requestData.UserEntraId
};

CustomerProfileResponse mgrResponse = await acctManager!.LoadCustomerProfileAsync(mgrRequest);
CustomerProfileResponse mgrResponse = await acctManager!.LoadOrCreateCustomerProfileAsync(mgrRequest);

if(mgrResponse.HasErrors)
if(mgrResponse.Payload == null)
{
result = Results.NotFound<string?>("No profile found for the provided Id.");
logger.LogInformation($"No profile found for UserEntraId {requestData.UserEntraId}");
}
else if(mgrResponse.HasErrors)
{
result = Results.BadRequest(mgrResponse.ErrorReport);
string errorReport = string.Join(Environment.NewLine, mgrResponse.ErrorReport);
logger.LogError(errorReport);
}
else if(mgrResponse.Payload == null)
{
result = Results.NotFound<string?>("No profile found for the provided Id.");
logger.LogInformation($"No profile found for UserEntraId {requestData.UserEntraId}");
}
else
{
result = Results.Ok(mgrResponse.Payload);
LoadProfileResponse apiResponse = new()
{
UserId = mgrResponse.Payload.UserId,
DisplayName = mgrResponse.Payload.DisplayName,
SubscriptionSku = mgrResponse.Payload
.Subscription?.SKU
?? SubscriptionIdentifiers.SKUS_TDMF_FREE
};

result = Results.Ok(apiResponse);
logger.LogInformation($"Profile loaded successfully for UserEntraId {requestData.UserEntraId}");
}
}
Expand All @@ -148,7 +160,8 @@ public static class EndpointExtensions

return result;
})
.Accepts<LoadProfileRequest>("application/json");
.Accepts<LoadProfileRequest>("application/json")
.RequireAuthorization(ApiConstants.AuthorizationPolicies.AllowApiConsumersOnly);

// This endpoint will be called from the Application when a user clicks on an
// Upgrade to Paid Plan button. It will send the basic information to the
Expand Down
102 changes: 77 additions & 25 deletions src/Clients/DevDad.SaaSAdmin.API/Program.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@

using System;
using DevDad.SaaSAdmin.API.ApiServices;
using DotNetEnv;

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Identity.Web;

using DotNetEnv;

using DevDad.SaaSAdmin.API.ApiServices;
using ThatDeveloperDad.iFX;
using Microsoft.IdentityModel.Tokens;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Authorization;
using System.Linq;

namespace DevDad.SaaSAdmin.API;

Expand All @@ -20,33 +29,14 @@ public static void Main(string[] args)
builder.Services.AddOpenApi();
var bootLogger = CreateBootLogger();
IConfiguration systemConfig = LoadSystemConfiguration(bootLogger);
builder = AddUtilityServices(systemConfig, bootLogger, builder);

// Add services to the container.
builder.Services.AddAuthorization(options =>
{
// Need to check the app Environment to determine if we're in Dev or Prod here.
var environment = builder.Environment;

if (environment.IsDevelopment())
{
options.AddPolicy(ApiConstants.AuthorizationPolicies.AllowApiConsumersOnly,
policy => policy.RequireAssertion(_ => true));
}
else
{
options.AddPolicy(ApiConstants.AuthorizationPolicies.AllowApiConsumersOnly,
policy => policy.RequireAuthenticatedUser());
}
});

// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi


builder = AddSecurityServices(systemConfig, bootLogger, builder);
builder = AddUtilityServices(systemConfig, bootLogger, builder);

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthentication();
app.UseAuthorization();

// All the services we'd registered in the Application's DI container are considered
Expand Down Expand Up @@ -84,6 +74,68 @@ public static void Main(string[] args)
app.Run();
}

static WebApplicationBuilder AddSecurityServices(IConfiguration configuration,
ILogger bootLog,
WebApplicationBuilder appBuilder)
{
bootLog.LogTrace("Configuring AuthN and AuthZ ");

appBuilder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(options => {
appBuilder.Configuration.Bind("AzureAd", options);
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidAudience = configuration["AzureAd:Audience"],
ValidIssuer = $"{configuration["AzureAd:Instance"]}{configuration["AzureAd:TenantId"]}/v2.0"
};
},
options => {
appBuilder.Configuration.Bind("AzureAd", options);
});
bootLog.LogTrace("Configured MS Identity & JWT Bearer options.");

// Add services to the container.
appBuilder.Services.AddAuthorization(options =>
{

options.AddPolicy(ApiConstants.AuthorizationPolicies.AllowApiConsumersOnly,
policy =>
policy.RequireAuthenticatedUser()
.RequireAssertion((AuthorizationHandlerContext context)=>
{
bootLog.LogTrace("Evaluationg AuthZ Policy.");
// Validate that the appId on the ConfidentialClientApp
// is in the list of approved apps.
string[] allowedClients = configuration
.GetSection("AzureAd:AllowedClients")
.Get<string[]>()
?? Array.Empty<string>();

string appId = context.User.FindFirst("appid")?.Value??string.Empty;

if(appId == string.Empty)
{ bootLog.LogWarning("No AppId found in the JWT Token. Denying access.");
return false;
}

bool appIdIsAllowed = allowedClients.Contains(appId);
if(!appIdIsAllowed)
{
bootLog.LogWarning($"AppId {appId} is not in the list of allowed clients. Denying access.");
}
else
{
bootLog.LogInformation($"AppId {appId} is in the list of allowed clients. Allowing access.");
}
return appIdIsAllowed;
}));
bootLog.LogTrace("Set policy to Restricted for prod env.");
});

return appBuilder;
}

static WebApplicationBuilder AddUtilityServices(IConfiguration systemConfig,
ILogger bootLog,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System;

namespace DevDad.SaaSAdmin.API.PublicModels;

public class LoadProfileResponse
{
public string UserId { get; set; } = string.Empty;
public string DisplayName {get;set;} = string.Empty;
public string SubscriptionSku { get; set; } = string.Empty;
}
9 changes: 9 additions & 0 deletions src/Clients/DevDad.SaaSAdmin.API/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@
}
},
"AllowedHosts": "*",
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"TenantId": "6f9b52cc-54e3-4c18-a92d-35f0448d1e0d",
"ClientId": "58f44182-7cbf-4293-a78d-2e4528ca298f",
"Audience": "api://58f44182-7cbf-4293-a78d-2e4528ca298f",
"AllowedClients": [
"63ff669d-b2fb-4367-8471-8605957304c6"
]
},
"Architecture":
{
"GlobalBehaviors":[
Expand Down
Loading
Loading