diff --git a/.NET Core/EncryptCredentials/EncryptCredentials.sln b/.NET Core/EncryptCredentials/EncryptCredentials.sln index b74ab26a..718e7d3b 100644 --- a/.NET Core/EncryptCredentials/EncryptCredentials.sln +++ b/.NET Core/EncryptCredentials/EncryptCredentials.sln @@ -14,9 +14,6 @@ Global Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {1A13B615-E49B-47EA-B23D-EE164230C682}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1A13B615-E49B-47EA-B23D-EE164230C682}.Debug|Any CPU.Build.0 = Debug|Any CPU @@ -31,4 +28,7 @@ Global {1A13B615-E49B-47EA-B23D-EE164230C682}.Release|x86.ActiveCfg = Release|Any CPU {1A13B615-E49B-47EA-B23D-EE164230C682}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection EndGlobal diff --git a/.NET Core/EncryptCredentials/EncryptCredentials/Controllers/EncryptCredetialsController.cs b/.NET Core/EncryptCredentials/EncryptCredentials/Controllers/EncryptCredetialsController.cs index 9942959f..42b4b11c 100644 --- a/.NET Core/EncryptCredentials/EncryptCredentials/Controllers/EncryptCredetialsController.cs +++ b/.NET Core/EncryptCredentials/EncryptCredentials/Controllers/EncryptCredetialsController.cs @@ -7,12 +7,15 @@ namespace EncryptCredentials.Controllers { using EncryptCredentials.Models; using EncryptCredentials.Services; + using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using Microsoft.PowerBI.Api.Models; using Microsoft.Rest; using System; + [Authorize(Policy = Startup.DatasourceAdministratorPolicy)] + [AutoValidateAntiforgeryToken] public class EncryptCredentialsController : Controller { private readonly PowerBIService powerBIService; diff --git a/.NET Core/EncryptCredentials/EncryptCredentials/Controllers/HomeController.cs b/.NET Core/EncryptCredentials/EncryptCredentials/Controllers/HomeController.cs index a818ea16..bb149909 100644 --- a/.NET Core/EncryptCredentials/EncryptCredentials/Controllers/HomeController.cs +++ b/.NET Core/EncryptCredentials/EncryptCredentials/Controllers/HomeController.cs @@ -7,10 +7,12 @@ namespace EncryptCredentials.Controllers { using EncryptCredentials.Models; using EncryptCredentials.Services; + using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using System; + [Authorize(Policy = Startup.DatasourceAdministratorPolicy)] public class HomeController : Controller { private readonly IOptions azureAd; diff --git a/.NET Core/EncryptCredentials/EncryptCredentials/EncryptCredentials.csproj b/.NET Core/EncryptCredentials/EncryptCredentials/EncryptCredentials.csproj index ff0f6611..77fc92e2 100644 --- a/.NET Core/EncryptCredentials/EncryptCredentials/EncryptCredentials.csproj +++ b/.NET Core/EncryptCredentials/EncryptCredentials/EncryptCredentials.csproj @@ -1,14 +1,14 @@ - netcoreapp3.1 + net8.0 Exe - - - + + + \ No newline at end of file diff --git a/.NET Core/EncryptCredentials/EncryptCredentials/Properties/launchSettings.json b/.NET Core/EncryptCredentials/EncryptCredentials/Properties/launchSettings.json new file mode 100644 index 00000000..10fb222f --- /dev/null +++ b/.NET Core/EncryptCredentials/EncryptCredentials/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "EncryptCredentials": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/.NET Core/EncryptCredentials/EncryptCredentials/Startup.cs b/.NET Core/EncryptCredentials/EncryptCredentials/Startup.cs index bf28a264..df41af06 100644 --- a/.NET Core/EncryptCredentials/EncryptCredentials/Startup.cs +++ b/.NET Core/EncryptCredentials/EncryptCredentials/Startup.cs @@ -7,14 +7,27 @@ namespace EncryptCredentials { using EncryptCredentials.Models; using EncryptCredentials.Services; + using Microsoft.AspNetCore.Authentication; + using Microsoft.AspNetCore.Authentication.Cookies; + using Microsoft.AspNetCore.Authentication.OpenIdConnect; + using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; + using Microsoft.Identity.Web; + using Microsoft.IdentityModel.Protocols.OpenIdConnect; + using System.Threading.Tasks; public class Startup { + public const string DatasourceAdministratorPolicy = "DatasourceAdministrator"; + public const string DatasourceAdministratorRole = "PowerBI.DatasourceAdmin"; + private const string OperatorChallengeScheme = "OperatorChallenge"; + public Startup(IConfiguration configuration) { Configuration = configuration; @@ -25,11 +38,77 @@ public Startup(IConfiguration configuration) // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { + services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp(Configuration.GetSection("OperatorAzureAd")); + + services.AddAuthentication() + .AddPolicyScheme(OperatorChallengeScheme, OperatorChallengeScheme, options => + { + options.ForwardDefaultSelector = context => + context.Request.Path.StartsWithSegments("/encryptcredential") + ? CookieAuthenticationDefaults.AuthenticationScheme + : OpenIdConnectDefaults.AuthenticationScheme; + }); + + services.AddAuthentication(options => + { + options.DefaultChallengeScheme = OperatorChallengeScheme; + }); + + services.Configure(CookieAuthenticationDefaults.AuthenticationScheme, options => + { + options.Cookie.HttpOnly = true; + options.Cookie.SameSite = SameSiteMode.Lax; + options.Cookie.SecurePolicy = CookieSecurePolicy.Always; + options.Events.OnRedirectToLogin = context => + { + if (context.Request.Path.StartsWithSegments("/encryptcredential")) + { + context.Response.StatusCode = StatusCodes.Status401Unauthorized; + return Task.CompletedTask; + } + + context.Response.Redirect(context.RedirectUri); + return Task.CompletedTask; + }; + options.Events.OnRedirectToAccessDenied = context => + { + if (context.Request.Path.StartsWithSegments("/encryptcredential")) + { + context.Response.StatusCode = StatusCodes.Status403Forbidden; + return Task.CompletedTask; + } + + context.Response.Redirect(context.RedirectUri); + return Task.CompletedTask; + }; + }); + + var datasourceAdministratorPolicy = new AuthorizationPolicyBuilder() + .RequireAuthenticatedUser() + .RequireRole(DatasourceAdministratorRole) + .Build(); + + services.AddAuthorization(options => + { + options.AddPolicy(DatasourceAdministratorPolicy, datasourceAdministratorPolicy); + options.FallbackPolicy = datasourceAdministratorPolicy; + }); + + services.Configure(OpenIdConnectDefaults.AuthenticationScheme, options => + { + options.ResponseType = OpenIdConnectResponseType.Code; + options.UsePkce = true; + }); + // Register AadService and PbiEmbedService for dependency injection services.AddScoped(typeof(AadService)) .AddScoped(typeof(PowerBIService)); - services.AddControllersWithViews(); + services.AddControllersWithViews(options => + { + options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()); + }); // Loading appsettings.json in C# Model classes services.Configure(Configuration.GetSection("AzureAd")); @@ -53,6 +132,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseRouting(); + app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => diff --git a/.NET Core/EncryptCredentials/EncryptCredentials/Views/Home/Index.cshtml b/.NET Core/EncryptCredentials/EncryptCredentials/Views/Home/Index.cshtml index c10ada0a..f2deb6f1 100644 --- a/.NET Core/EncryptCredentials/EncryptCredentials/Views/Home/Index.cshtml +++ b/.NET Core/EncryptCredentials/EncryptCredentials/Views/Home/Index.cshtml @@ -14,6 +14,7 @@ Licensed under the MIT license. --> + @Html.AntiForgeryToken()
Encrypt Power BI Data Source Credentials diff --git a/.NET Core/EncryptCredentials/EncryptCredentials/appsettings.json b/.NET Core/EncryptCredentials/EncryptCredentials/appsettings.json index f20bf934..21ebe158 100644 --- a/.NET Core/EncryptCredentials/EncryptCredentials/appsettings.json +++ b/.NET Core/EncryptCredentials/EncryptCredentials/appsettings.json @@ -11,6 +11,13 @@ "PbiPassword": "", "ClientSecret": "" }, + "OperatorAzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "", + "ClientId": "", + "ClientSecret": "", + "CallbackPath": "/signin-oidc" + }, "Logging": { "LogLevel": { "Default": "Information", diff --git a/.NET Core/EncryptCredentials/EncryptCredentials/wwwroot/js/index.js b/.NET Core/EncryptCredentials/EncryptCredentials/wwwroot/js/index.js index d98d8efd..9a14d501 100644 --- a/.NET Core/EncryptCredentials/EncryptCredentials/wwwroot/js/index.js +++ b/.NET Core/EncryptCredentials/EncryptCredentials/wwwroot/js/index.js @@ -19,6 +19,12 @@ $(function () { // Freezing the contents for endpoint objects Object.freeze(Endpoints); + $.ajaxSetup({ + headers: { + "RequestVerificationToken": $("input[name='__RequestVerificationToken']").val() + } + }); + // Cache constants const ENABLED = "btn-primary"; const DISABLED = "btn-secondary"; diff --git a/.NET Core/EncryptCredentials/README.md b/.NET Core/EncryptCredentials/README.md index 845b6f17..70099a3f 100644 --- a/.NET Core/EncryptCredentials/README.md +++ b/.NET Core/EncryptCredentials/README.md @@ -2,17 +2,24 @@ ## Requirements -1. [.NET Core 3.1](https://aka.ms/netcore31) SDK or higher. +1. [.NET 8](https://dotnet.microsoft.com/download/dotnet/8.0) SDK or higher. -2. IDE/code editor. We recommend using Visual Studio Code or Visual Studio 2019 (or a later version). -
-> **Note:** Visual Studio version >=16.5 is required to use .NET Core SDK 3.1. +2. IDE/code editor. We recommend using Visual Studio Code or Visual Studio 2022 (version 17.8 or later). -### Set up a Power BI app +### Set up the applications Follow the steps on [aka.ms/EmbedForCustomer](https://aka.ms/embedforcustomer) +Create a separate Microsoft Entra app registration for users who operate this sample: + +1. Add a web redirect URI for `https://localhost:5001/signin-oidc`. +2. Define an app role with the value `PowerBI.DatasourceAdmin` and allow users or groups as members. +3. Assign only the users or groups that are allowed to manage Power BI datasource credentials to that role. +4. Create a client secret and configure the tenant ID, client ID, and secret in the `OperatorAzureAd` section. Prefer environment variables, user secrets, or a secret store instead of writing the secret to `appsettings.json`. + +The operator app registration authenticates and authorizes incoming users. Keep it separate from the privileged Power BI identity configured in the `AzureAd` section. + ### Run the application on localhost 1. Open the [EncryptCredentials.sln](./EncryptCredentials.sln) file in Visual Studio. If you are using Visual Studio Code, open [EncryptCredentials](./EncryptCredentials) folder.