diff --git a/1-web-apps/README.md b/1-web-apps/README.md new file mode 100644 index 0000000..834eae7 --- /dev/null +++ b/1-web-apps/README.md @@ -0,0 +1,53 @@ +# 🌐 Web App Samples + +This folder contains a collection of .NET web application samples demonstrating authentication and authorization scenarios using the Microsoft identity platform. Each sample showcases a different web application type or authentication flow, using up-to-date libraries and best practices. + +## 📋 Samples Overview + +| 📁 Folder Name | 🔑 Authentication Libraries Used | 🏷️ .NET Version | +|-----------------------------------------------------|--------------------------------------------------------------|------------------| +| [web-app-aspnet-core](./web-app-aspnetcore) | Microsoft.Identity.Web, Microsoft.Identity.Client (MSAL.NET) | .NET 8.0 | +| [web-app-blazor-server](./web-app-blazor-server) | Microsoft.Identity.Web, Microsoft.Identity.Client (MSAL.NET) | .NET 8.0 | + +> [!NOTE] +> All samples use the latest supported versions of the Microsoft identity libraries and are configured for secure, modern authentication scenarios. + +--- + +## 🚀 Getting Started + +Follow these steps to set up your environment and run any of the web app samples in this folder. + +### ☑️ Prerequisites + +You will need the following to run any of these samples: + + - [.NET 8.0 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) + - A Microsoft Entra tenant and app registration (see each sample's README for details) + - An editor or IDE such as [Visual Studio](https://visualstudio.microsoft.com/) or [Visual Studio Code](https://code.visualstudio.com/) + +### 📥 Clone the Repository + +1. Navigate to where you want to have the sample located, and enter the following: + + ```sh + git clone https://github.com/your-org/ms-identity-docs-code-dotnet.git + ``` + +2. Navigate to the web app folder in the sample you have downloaded by using the following command; + + ```sh + cd ms-identity-docs-code-dotnet/1-web-apps + ``` + +--- + +## 📚 Resources + +- [Microsoft Identity Platform Documentation](https://learn.microsoft.com/entra/identity-platform/) +- [Microsoft.Identity.Web Library](https://learn.microsoft.com/entra/msal/dotnet/microsoft-identity-web/) +- [MSAL.NET Library](https://learn.microsoft.com/entra/identity-platform/msal-overview) +- [Microsoft Entra App Registration](https://learn.microsoft.com/entra/identity-platform/quickstart-register-app) +- [Securing ASP.NET Core with Microsoft Identity](https://learn.microsoft.com/aspnet/core/security/authentication/identity) + +--- \ No newline at end of file diff --git a/web-app-aspnet/Pages/Error.cshtml b/1-web-apps/web-app-aspnetcore/Pages/Error.cshtml similarity index 97% rename from web-app-aspnet/Pages/Error.cshtml rename to 1-web-apps/web-app-aspnetcore/Pages/Error.cshtml index 09da0d2..6f92b95 100644 --- a/web-app-aspnet/Pages/Error.cshtml +++ b/1-web-apps/web-app-aspnetcore/Pages/Error.cshtml @@ -1,26 +1,26 @@ -@page -@model ErrorModel -@{ - ViewData["Title"] = "Error"; -} - -

Error.

-

An error occurred while processing your request.

- -@if (Model.ShowRequestId) -{ -

- Request ID: @Model.RequestId -

-} - -

Development Mode

-

- Swapping to the Development environment displays detailed information about the error that occurred. -

-

- The Development environment shouldn't be enabled for deployed applications. - It can result in displaying sensitive information from exceptions to end users. - For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development - and restarting the app. -

+@page +@model ErrorModel +@{ + ViewData["Title"] = "Error"; +} + +

Error.

+

An error occurred while processing your request.

+ +@if (Model.ShowRequestId) +{ +

+ Request ID: @Model.RequestId +

+} + +

Development Mode

+

+ Swapping to the Development environment displays detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

diff --git a/web-app-aspnet/Pages/Error.cshtml.cs b/1-web-apps/web-app-aspnetcore/Pages/Error.cshtml.cs similarity index 95% rename from web-app-aspnet/Pages/Error.cshtml.cs rename to 1-web-apps/web-app-aspnetcore/Pages/Error.cshtml.cs index 540eff0..9cccf75 100644 --- a/web-app-aspnet/Pages/Error.cshtml.cs +++ b/1-web-apps/web-app-aspnetcore/Pages/Error.cshtml.cs @@ -1,27 +1,27 @@ -using System.Diagnostics; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.RazorPages; - -namespace sign_in_webapp.Pages; - -[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] -[IgnoreAntiforgeryToken] -public class ErrorModel : PageModel -{ - public string? RequestId { get; set; } - - public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); - - private readonly ILogger _logger; - - public ErrorModel(ILogger logger) - { - _logger = logger; - } - - public void OnGet() - { - RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; - } -} - +using System.Diagnostics; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace sign_in_webapp.Pages; + +[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] +[IgnoreAntiforgeryToken] +public class ErrorModel : PageModel +{ + public string? RequestId { get; set; } + + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + private readonly ILogger _logger; + + public ErrorModel(ILogger logger) + { + _logger = logger; + } + + public void OnGet() + { + RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; + } +} + diff --git a/web-app-aspnet/Pages/Index.cshtml b/1-web-apps/web-app-aspnetcore/Pages/Index.cshtml similarity index 83% rename from web-app-aspnet/Pages/Index.cshtml rename to 1-web-apps/web-app-aspnetcore/Pages/Index.cshtml index 6382291..8f4bfa3 100644 --- a/web-app-aspnet/Pages/Index.cshtml +++ b/1-web-apps/web-app-aspnetcore/Pages/Index.cshtml @@ -1,18 +1,18 @@ -@page -@model IndexModel -@{ - ViewData["Title"] = "Home page"; -} - -
-

Welcome

-

Learn about building Web apps with ASP.NET Core.

-
- -@* *@ -

Before rendering the page, the Page Model was able to make a call to Microsoft Graph's /me API for your user and received the following:

- -

@ViewData["ApiResult"]

- -

Refreshing this page will continue to use the cached access token acquired for Microsoft Graph, which is valid for future page views will attempt to refresh this token as it nears its expiration.

-@*
*@ +@page +@model IndexModel +@{ + ViewData["Title"] = "Home page"; +} + +
+

Welcome

+

Learn about building Web apps with ASP.NET Core.

+
+ +@* *@ +

Before rendering the page, the Page Model was able to make a call to Microsoft Graph's /me API for your user and received the following:

+ +

@ViewData["ApiResult"]

+ +

Refreshing this page will continue to use the cached access token acquired for Microsoft Graph, which is valid for future page views will attempt to refresh this token as it nears its expiration.

+@*
*@ diff --git a/web-app-aspnet/Pages/Index.cshtml.cs b/1-web-apps/web-app-aspnetcore/Pages/Index.cshtml.cs similarity index 97% rename from web-app-aspnet/Pages/Index.cshtml.cs rename to 1-web-apps/web-app-aspnetcore/Pages/Index.cshtml.cs index 9d5200b..2ff6f27 100644 --- a/web-app-aspnet/Pages/Index.cshtml.cs +++ b/1-web-apps/web-app-aspnetcore/Pages/Index.cshtml.cs @@ -1,36 +1,36 @@ -using System.Text.Json; -using Microsoft.AspNetCore.Mvc.RazorPages; -using Microsoft.Identity.Web; -using Microsoft.Identity.Abstractions; - -namespace sign_in_webapp.Pages; - -[AuthorizeForScopes(ScopeKeySection = "DownstreamApis:MicrosoftGraph:Scopes")] -public class IndexModel : PageModel -{ - private readonly ILogger _logger; - - private readonly IDownstreamApi _downstreamWebApi; - - public IndexModel(ILogger logger, - IDownstreamApi downstreamWebApi) - { - _logger = logger; - _downstreamWebApi = downstreamWebApi; - } - - public async Task OnGet() - { - using var response = await _downstreamWebApi.CallApiForUserAsync("MicrosoftGraph").ConfigureAwait(false); - if (response.StatusCode == System.Net.HttpStatusCode.OK) - { - var apiResult = await response.Content.ReadFromJsonAsync().ConfigureAwait(false); - ViewData["ApiResult"] = JsonSerializer.Serialize(apiResult, new JsonSerializerOptions { WriteIndented = true }); - } - else - { - var error = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - throw new HttpRequestException($"Invalid status code in the HttpResponseMessage: {response.StatusCode}: {error}"); - } - } -} +using System.Text.Json; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Identity.Web; +using Microsoft.Identity.Abstractions; + +namespace sign_in_webapp.Pages; + +[AuthorizeForScopes(ScopeKeySection = "DownstreamApis:MicrosoftGraph:Scopes")] +public class IndexModel : PageModel +{ + private readonly ILogger _logger; + + private readonly IDownstreamApi _downstreamWebApi; + + public IndexModel(ILogger logger, + IDownstreamApi downstreamWebApi) + { + _logger = logger; + _downstreamWebApi = downstreamWebApi; + } + + public async Task OnGet() + { + using var response = await _downstreamWebApi.CallApiForUserAsync("MicrosoftGraph").ConfigureAwait(false); + if (response.StatusCode == System.Net.HttpStatusCode.OK) + { + var apiResult = await response.Content.ReadFromJsonAsync().ConfigureAwait(false); + ViewData["ApiResult"] = JsonSerializer.Serialize(apiResult, new JsonSerializerOptions { WriteIndented = true }); + } + else + { + var error = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new HttpRequestException($"Invalid status code in the HttpResponseMessage: {response.StatusCode}: {error}"); + } + } +} diff --git a/web-app-aspnet/Pages/Privacy.cshtml b/1-web-apps/web-app-aspnetcore/Pages/Privacy.cshtml similarity index 95% rename from web-app-aspnet/Pages/Privacy.cshtml rename to 1-web-apps/web-app-aspnetcore/Pages/Privacy.cshtml index 5c16860..46ba966 100644 --- a/web-app-aspnet/Pages/Privacy.cshtml +++ b/1-web-apps/web-app-aspnetcore/Pages/Privacy.cshtml @@ -1,8 +1,8 @@ -@page -@model PrivacyModel -@{ - ViewData["Title"] = "Privacy Policy"; -} -

@ViewData["Title"]

- -

Use this page to detail your site's privacy policy.

+@page +@model PrivacyModel +@{ + ViewData["Title"] = "Privacy Policy"; +} +

@ViewData["Title"]

+ +

Use this page to detail your site's privacy policy.

diff --git a/web-app-aspnet/Pages/Privacy.cshtml.cs b/1-web-apps/web-app-aspnetcore/Pages/Privacy.cshtml.cs similarity index 94% rename from web-app-aspnet/Pages/Privacy.cshtml.cs rename to 1-web-apps/web-app-aspnetcore/Pages/Privacy.cshtml.cs index f473ef5..bf67afc 100644 --- a/web-app-aspnet/Pages/Privacy.cshtml.cs +++ b/1-web-apps/web-app-aspnetcore/Pages/Privacy.cshtml.cs @@ -1,19 +1,19 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.RazorPages; - -namespace sign_in_webapp.Pages; - -public class PrivacyModel : PageModel -{ - private readonly ILogger _logger; - - public PrivacyModel(ILogger logger) - { - _logger = logger; - } - - public void OnGet() - { - } -} - +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace sign_in_webapp.Pages; + +public class PrivacyModel : PageModel +{ + private readonly ILogger _logger; + + public PrivacyModel(ILogger logger) + { + _logger = logger; + } + + public void OnGet() + { + } +} + diff --git a/web-app-aspnet/Pages/Shared/_Layout.cshtml b/1-web-apps/web-app-aspnetcore/Pages/Shared/_Layout.cshtml similarity index 97% rename from web-app-aspnet/Pages/Shared/_Layout.cshtml rename to 1-web-apps/web-app-aspnetcore/Pages/Shared/_Layout.cshtml index 2b33266..dac0efe 100644 --- a/web-app-aspnet/Pages/Shared/_Layout.cshtml +++ b/1-web-apps/web-app-aspnetcore/Pages/Shared/_Layout.cshtml @@ -1,54 +1,54 @@ - - - - - - @ViewData["Title"] - sign_in_webapp - - - - - -
- -
-
-
- @RenderBody() -
-
- -
-
- © sign_in_webapp - Privacy -
-
- - - - - - @await RenderSectionAsync("Scripts", required: false) - - + + + + + + @ViewData["Title"] - sign_in_webapp + + + + + +
+ +
+
+
+ @RenderBody() +
+
+ +
+
+ © sign_in_webapp - Privacy +
+
+ + + + + + @await RenderSectionAsync("Scripts", required: false) + + diff --git a/web-app-aspnet/Pages/Shared/_Layout.cshtml.css b/1-web-apps/web-app-aspnetcore/Pages/Shared/_Layout.cshtml.css similarity index 82% rename from web-app-aspnet/Pages/Shared/_Layout.cshtml.css rename to 1-web-apps/web-app-aspnetcore/Pages/Shared/_Layout.cshtml.css index c04d2df..c187c02 100644 --- a/web-app-aspnet/Pages/Shared/_Layout.cshtml.css +++ b/1-web-apps/web-app-aspnetcore/Pages/Shared/_Layout.cshtml.css @@ -1,48 +1,48 @@ -/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification -for details on configuring this project to bundle and minify static web assets. */ - -a.navbar-brand { - white-space: normal; - text-align: center; - word-break: break-all; -} - -a { - color: #0077cc; -} - -.btn-primary { - color: #fff; - background-color: #1b6ec2; - border-color: #1861ac; -} - -.nav-pills .nav-link.active, .nav-pills .show > .nav-link { - color: #fff; - background-color: #1b6ec2; - border-color: #1861ac; -} - -.border-top { - border-top: 1px solid #e5e5e5; -} -.border-bottom { - border-bottom: 1px solid #e5e5e5; -} - -.box-shadow { - box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); -} - -button.accept-policy { - font-size: 1rem; - line-height: inherit; -} - -.footer { - position: absolute; - bottom: 0; - width: 100%; - white-space: nowrap; - line-height: 60px; -} +/* Please see documentation at https://learn.microsoft.com/aspnet/core/client-side/bundling-and-minification +for details on configuring this project to bundle and minify static web assets. */ + +a.navbar-brand { + white-space: normal; + text-align: center; + word-break: break-all; +} + +a { + color: #0077cc; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.nav-pills .nav-link.active, .nav-pills .show > .nav-link { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.border-top { + border-top: 1px solid #e5e5e5; +} +.border-bottom { + border-bottom: 1px solid #e5e5e5; +} + +.box-shadow { + box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); +} + +button.accept-policy { + font-size: 1rem; + line-height: inherit; +} + +.footer { + position: absolute; + bottom: 0; + width: 100%; + white-space: nowrap; + line-height: 60px; +} diff --git a/web-app-aspnet/Pages/Shared/_LoginPartial.cshtml b/1-web-apps/web-app-aspnetcore/Pages/Shared/_LoginPartial.cshtml similarity index 96% rename from web-app-aspnet/Pages/Shared/_LoginPartial.cshtml rename to 1-web-apps/web-app-aspnetcore/Pages/Shared/_LoginPartial.cshtml index 88e6b6e..7754394 100644 --- a/web-app-aspnet/Pages/Shared/_LoginPartial.cshtml +++ b/1-web-apps/web-app-aspnetcore/Pages/Shared/_LoginPartial.cshtml @@ -1,19 +1,19 @@ -@using System.Security.Principal - - +@using System.Security.Principal + + diff --git a/web-app-aspnet/Pages/Shared/_ValidationScriptsPartial.cshtml b/1-web-apps/web-app-aspnetcore/Pages/Shared/_ValidationScriptsPartial.cshtml similarity index 98% rename from web-app-aspnet/Pages/Shared/_ValidationScriptsPartial.cshtml rename to 1-web-apps/web-app-aspnetcore/Pages/Shared/_ValidationScriptsPartial.cshtml index ff9c793..5a16d80 100644 --- a/web-app-aspnet/Pages/Shared/_ValidationScriptsPartial.cshtml +++ b/1-web-apps/web-app-aspnetcore/Pages/Shared/_ValidationScriptsPartial.cshtml @@ -1,2 +1,2 @@ - - + + diff --git a/web-app-aspnet/Pages/_ViewImports.cshtml b/1-web-apps/web-app-aspnetcore/Pages/_ViewImports.cshtml similarity index 97% rename from web-app-aspnet/Pages/_ViewImports.cshtml rename to 1-web-apps/web-app-aspnetcore/Pages/_ViewImports.cshtml index f052820..f874786 100644 --- a/web-app-aspnet/Pages/_ViewImports.cshtml +++ b/1-web-apps/web-app-aspnetcore/Pages/_ViewImports.cshtml @@ -1,3 +1,3 @@ -@using sign_in_webapp -@namespace sign_in_webapp.Pages -@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@using sign_in_webapp +@namespace sign_in_webapp.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/web-app-aspnet/Pages/_ViewStart.cshtml b/1-web-apps/web-app-aspnetcore/Pages/_ViewStart.cshtml similarity index 91% rename from web-app-aspnet/Pages/_ViewStart.cshtml rename to 1-web-apps/web-app-aspnetcore/Pages/_ViewStart.cshtml index 6e88aa3..a5f1004 100644 --- a/web-app-aspnet/Pages/_ViewStart.cshtml +++ b/1-web-apps/web-app-aspnetcore/Pages/_ViewStart.cshtml @@ -1,3 +1,3 @@ -@{ - Layout = "_Layout"; -} +@{ + Layout = "_Layout"; +} diff --git a/web-app-aspnet/Program.cs b/1-web-apps/web-app-aspnetcore/Program.cs similarity index 97% rename from web-app-aspnet/Program.cs rename to 1-web-apps/web-app-aspnetcore/Program.cs index 714c892..4ccfd48 100644 --- a/web-app-aspnet/Program.cs +++ b/1-web-apps/web-app-aspnetcore/Program.cs @@ -1,44 +1,44 @@ -// -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc.Authorization; -using Microsoft.Identity.Web; -using Microsoft.Identity.Web.UI; -// - -// -WebApplicationBuilder builder = WebApplication.CreateBuilder(args); -IEnumerable? initialScopes = builder.Configuration.GetSection("DownstreamApis:MicrosoftGraph:Scopes").Get>(); - -builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration, "AzureAd") - .EnableTokenAcquisitionToCallDownstreamApi(initialScopes) - .AddInMemoryTokenCaches(); -builder.Services.AddDownstreamApis(builder.Configuration.GetSection("DownstreamApis")); - -// - -// -builder.Services.AddRazorPages().AddMvcOptions(options => - { - var policy = new AuthorizationPolicyBuilder() - .RequireAuthenticatedUser() - .Build(); - options.Filters.Add(new AuthorizeFilter(policy)); - }).AddMicrosoftIdentityUI(); -// - -// -WebApplication app = builder.Build(); - -app.UseAuthentication(); -app.UseAuthorization(); -// - -app.UseHttpsRedirection(); -app.UseStaticFiles(); - -app.UseRouting(); - -app.MapRazorPages(); -app.MapControllers(); - -app.Run(); +// +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc.Authorization; +using Microsoft.Identity.Web; +using Microsoft.Identity.Web.UI; +// + +// +WebApplicationBuilder builder = WebApplication.CreateBuilder(args); +IEnumerable? initialScopes = builder.Configuration.GetSection("DownstreamApis:MicrosoftGraph:Scopes").Get>(); + +builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration, "AzureAd") + .EnableTokenAcquisitionToCallDownstreamApi(initialScopes) + .AddInMemoryTokenCaches(); +builder.Services.AddDownstreamApis(builder.Configuration.GetSection("DownstreamApis")); + +// + +// +builder.Services.AddRazorPages().AddMvcOptions(options => + { + var policy = new AuthorizationPolicyBuilder() + .RequireAuthenticatedUser() + .Build(); + options.Filters.Add(new AuthorizeFilter(policy)); + }).AddMicrosoftIdentityUI(); +// + +// +WebApplication app = builder.Build(); + +app.UseAuthentication(); +app.UseAuthorization(); +// + +app.UseHttpsRedirection(); +app.UseStaticFiles(); + +app.UseRouting(); + +app.MapRazorPages(); +app.MapControllers(); + +app.Run(); diff --git a/web-app-aspnet/Properties/launchSettings.json b/1-web-apps/web-app-aspnetcore/Properties/launchSettings.json similarity index 100% rename from web-app-aspnet/Properties/launchSettings.json rename to 1-web-apps/web-app-aspnetcore/Properties/launchSettings.json diff --git a/web-app-aspnet/README.md b/1-web-apps/web-app-aspnetcore/README.md similarity index 79% rename from web-app-aspnet/README.md rename to 1-web-apps/web-app-aspnetcore/README.md index ef631f6..7e7fac5 100644 --- a/web-app-aspnet/README.md +++ b/1-web-apps/web-app-aspnetcore/README.md @@ -1,11 +1,11 @@ --- -# Metadata required by https://docs.microsoft.com/samples/browse/ -# Metadata properties: https://review.docs.microsoft.com/help/contribute/samples/process/onboarding?branch=main#add-metadata-to-readme +# Metadata required by https://learn.microsoft.com/samples/browse/ +# Metadata properties: https://review.learn.microsoft.com/help/contribute/samples/process/onboarding?branch=main#add-metadata-to-readme languages: - csharp page_type: sample name: "ASP.NET Core 8.0 Web App Sign-in user" -description: "This is a ASP.NET Core 8.0 Web App that sign-in users. The code in this sample is used by one or more articles on docs.microsoft.com." +description: "This is a ASP.NET Core 8.0 Web App that sign-in users. The code in this sample is used by one or more articles on learn.microsoft.com." products: - azure - entra-id @@ -15,13 +15,13 @@ urlFragment: ms-identity-docs-code-csharp-sign-in # ASP.NET Core 8.0 Web App - Sign-in user | Microsoft identity platform -This web app, built with ASP.NET Core 8.0 Razor, has added sign-in features. It uses the [OpenID Connect](https://docs.microsoft.com/en-us/entra/identity-platform/v2-protocols-oidc) and [ASP.NET Core Identity](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/identity?view=aspnetcore-8.0) for authentication. This allows users to sign in with their Microsoft Entra accounts. Once signed in, the app can access protected resources on the user’s behalf. +This web app, built with ASP.NET Core 8.0 Razor, has added sign-in features. It uses the [OpenID Connect](https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols-oidc) and [ASP.NET Core Identity](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/identity?view=aspnetcore-8.0) for authentication. This allows users to sign in with their Microsoft Entra accounts. Once signed in, the app can access protected resources on the user’s behalf. ## Quickstart and tutorial guides for this sample - For a quickstart experience that gets you started fast, see [Quickstart: Sign in users and call the Microsoft Graph API from an ASP.NET Core web app](https://learn.microsoft.com/entra/identity-platform/quickstart-web-app-dotnet-core-sign-in). -- For a in-depth tutorial that walks you through this sample from start to finish, see [Tutorial: Sign in users and call the Microsoft Graph API from an ASP.NET Core web app](https://docs.microsoft.com/entra/identity-platform/tutorial-web-app-dotnet-register-app). +- For a in-depth tutorial that walks you through this sample from start to finish, see [Tutorial: Sign in users and call the Microsoft Graph API from an ASP.NET Core web app](https://learn.microsoft.com/entra/identity-platform/tutorial-web-app-dotnet-register-app). ## Prerequisites @@ -32,7 +32,7 @@ This web app, built with ASP.NET Core 8.0 Razor, has added sign-in features. It ### 1. Register the web API application in your Microsoft Entra ID -First, complete the steps in [Register an application with the Microsoft identity platform](https://docs.microsoft.com/entra/identity-platform/tutorial-web-app-dotnet-register-app) to register a web application in the Microsoft identity platform. +First, complete the steps in [Register an application with the Microsoft identity platform](https://learn.microsoft.com/entra/identity-platform/tutorial-web-app-dotnet-register-app) to register a web application in the Microsoft identity platform. Use the following settings for your app registration: @@ -86,13 +86,13 @@ Use the following settings for your app registration: 1. Select Sign out -![A screenshot of an ASP.NET Core 8.0 Web App indicating the user signed-out and allowing click "Sign in" to signin again.](./app-signedout.png) +![A screenshot of an ASP.NET Core 8.0 Web App indicating the user signed-out and allowing click "Sign in" to signin again.](./media/app-signedout.png) ## About the code The ASP.NET Core 8.0 Web App will allow users to sign-in, so it can retrieve a Security Token scoped specifically for the Microsoft Graph API, and will use that token to access the user's information. For more information about the proposed scenario, please take a look at the following diagram: -:link: For more information about how to proctect your projects, please let's take a look at https://docs.microsoft.com/en-us/azure/active-directory/develop/sample-v2-code. To know more about how this sample has been generated, please visit https://docs.microsoft.com/en-us/aspnet/core/tutorials/razor-pages/?view=aspnetcore-8.0 +:link: For more information about how to proctect your projects, please let's take a look at https://learn.microsoft.com/en-us/entra/identity-platform/sample-v2-code. To know more about how this sample has been generated, please visit https://learn.microsoft.com/en-us/aspnet/core/tutorials/razor-pages/?view=aspnetcore-8.0 ## Reporting problems @@ -107,7 +107,7 @@ If you can't get the sample working, you've checked [Stack Overflow](http://stac > :warning: WARNING: Any issue _not_ limited to running this or another sample app will be closed without being addressed. -For all other requests, see [Support and help options for developers | Microsoft identity platform](https://docs.microsoft.com/azure/active-directory/develop/developer-support-help-options). +For all other requests, see [Support and help options for developers | Microsoft identity platform](https://learn.microsoft.com/entra/identity-platform/developer-support-help-options). ## Contributing diff --git a/web-app-aspnet/WebApp.csproj b/1-web-apps/web-app-aspnetcore/WebApp.csproj similarity index 100% rename from web-app-aspnet/WebApp.csproj rename to 1-web-apps/web-app-aspnetcore/WebApp.csproj diff --git a/web-app-aspnet/appsettings.json b/1-web-apps/web-app-aspnetcore/appsettings.json similarity index 96% rename from web-app-aspnet/appsettings.json rename to 1-web-apps/web-app-aspnetcore/appsettings.json index 12f26ac..636506e 100644 --- a/web-app-aspnet/appsettings.json +++ b/1-web-apps/web-app-aspnetcore/appsettings.json @@ -1,31 +1,31 @@ -{ -"AzureAd": { - "Instance": "https://login.microsoftonline.com/", - "TenantId": "Enter the tenant ID obtained from the Microsoft Entra admin center", - "ClientId": "Enter the client ID obtained from the Microsoft Entra admin center", - "ClientCredentials": [ - { - "SourceType": "StoreWithThumbprint", - "CertificateStorePath": "CurrentUser/My", - "CertificateThumbprint": "Enter the certificate thumbprint obtained the Microsoft Entra admin center" - } - ], - "CallbackPath": "/signin-oidc" -}, - "DownstreamApis": { - "MicrosoftGraph" :{ - "BaseUrl": "https://graph.microsoft.com/v1.0/", - "RelativePath": "me", - "Scopes": [ - "user.read" - ] - } - }, - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*" -} +{ +"AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "Enter the tenant ID obtained from the Microsoft Entra admin center", + "ClientId": "Enter the client ID obtained from the Microsoft Entra admin center", + "ClientCredentials": [ + { + "SourceType": "StoreWithThumbprint", + "CertificateStorePath": "CurrentUser/My", + "CertificateThumbprint": "Enter the certificate thumbprint obtained the Microsoft Entra admin center" + } + ], + "CallbackPath": "/signin-oidc" +}, + "DownstreamApis": { + "MicrosoftGraph" :{ + "BaseUrl": "https://graph.microsoft.com/v1.0/", + "RelativePath": "me", + "Scopes": [ + "user.read" + ] + } + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/web-app-aspnet/media/app-signedin.png b/1-web-apps/web-app-aspnetcore/media/app-signedin.png similarity index 100% rename from web-app-aspnet/media/app-signedin.png rename to 1-web-apps/web-app-aspnetcore/media/app-signedin.png diff --git a/web-app-aspnet/media/app-signedout.png b/1-web-apps/web-app-aspnetcore/media/app-signedout.png similarity index 100% rename from web-app-aspnet/media/app-signedout.png rename to 1-web-apps/web-app-aspnetcore/media/app-signedout.png diff --git a/web-app-aspnet/wwwroot/css/site.css b/1-web-apps/web-app-aspnetcore/wwwroot/css/site.css similarity index 91% rename from web-app-aspnet/wwwroot/css/site.css rename to 1-web-apps/web-app-aspnetcore/wwwroot/css/site.css index 50d5b60..f27e5ad 100644 --- a/web-app-aspnet/wwwroot/css/site.css +++ b/1-web-apps/web-app-aspnetcore/wwwroot/css/site.css @@ -1,18 +1,18 @@ -html { - font-size: 14px; -} - -@media (min-width: 768px) { - html { - font-size: 16px; - } -} - -html { - position: relative; - min-height: 100%; -} - -body { - margin-bottom: 60px; +html { + font-size: 14px; +} + +@media (min-width: 768px) { + html { + font-size: 16px; + } +} + +html { + position: relative; + min-height: 100%; +} + +body { + margin-bottom: 60px; } \ No newline at end of file diff --git a/spa-blazor-wasm/media/favicon.ico b/1-web-apps/web-app-aspnetcore/wwwroot/favicon.ico similarity index 100% rename from spa-blazor-wasm/media/favicon.ico rename to 1-web-apps/web-app-aspnetcore/wwwroot/favicon.ico diff --git a/web-app-aspnet/wwwroot/js/site.js b/1-web-apps/web-app-aspnetcore/wwwroot/js/site.js similarity index 50% rename from web-app-aspnet/wwwroot/js/site.js rename to 1-web-apps/web-app-aspnetcore/wwwroot/js/site.js index b2f58e1..0937657 100644 --- a/web-app-aspnet/wwwroot/js/site.js +++ b/1-web-apps/web-app-aspnetcore/wwwroot/js/site.js @@ -1,4 +1,4 @@ -// Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification -// for details on configuring this project to bundle and minify static web assets. - -// Write your JavaScript code. +// Please see documentation at https://learn.microsoft.com/aspnet/core/client-side/bundling-and-minification +// for details on configuring this project to bundle and minify static web assets. + +// Write your JavaScript code. diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/LICENSE b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/LICENSE similarity index 98% rename from web-app-aspnet/wwwroot/lib/bootstrap/LICENSE rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/LICENSE index 1f4dd66..72dda23 100644 --- a/web-app-aspnet/wwwroot/lib/bootstrap/LICENSE +++ b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/LICENSE @@ -1,22 +1,22 @@ -The MIT License (MIT) - -Copyright (c) 2011-2021 Twitter, Inc. -Copyright (c) 2011-2021 The Bootstrap Authors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +The MIT License (MIT) + +Copyright (c) 2011-2021 Twitter, Inc. +Copyright (c) 2011-2021 The Bootstrap Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css.map b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css.map similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css.map rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css.map diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css.map b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css.map similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css.map rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css.map diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css.map b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css.map similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css.map rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css.map diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css.map b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css.map similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css.map rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css.map diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap.css b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap.css similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap.css rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap.css diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map diff --git a/spa-blazor-wasm/wwwroot/css/bootstrap/bootstrap.min.css b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css similarity index 100% rename from spa-blazor-wasm/wwwroot/css/bootstrap/bootstrap.min.css rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css diff --git a/spa-blazor-wasm/wwwroot/css/bootstrap/bootstrap.min.css.map b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map similarity index 100% rename from spa-blazor-wasm/wwwroot/css/bootstrap/bootstrap.min.css.map rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css.map b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css.map similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css.map rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css.map diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css.map b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css.map similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css.map rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css.map diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js.map b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js.map similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js.map rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js.map diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js.map b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js.map similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js.map rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js.map diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/js/bootstrap.js b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/js/bootstrap.js similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/js/bootstrap.js rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/js/bootstrap.js diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/js/bootstrap.js.map b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/js/bootstrap.js.map similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/js/bootstrap.js.map rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/js/bootstrap.js.map diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js.map b/1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js.map similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js.map rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js.map diff --git a/web-app-aspnet/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt b/1-web-apps/web-app-aspnetcore/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt similarity index 97% rename from web-app-aspnet/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt index 78109a8..0bdc196 100644 --- a/web-app-aspnet/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt +++ b/1-web-apps/web-app-aspnetcore/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt @@ -1,12 +1,12 @@ -Copyright (c) .NET Foundation. All rights reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); you may not use -these files except in compliance with the License. You may obtain a copy of the -License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed -under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR -CONDITIONS OF ANY KIND, either express or implied. See the License for the -specific language governing permissions and limitations under the License. +Copyright (c) .NET Foundation. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +these files except in compliance with the License. You may obtain a copy of the +License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. diff --git a/web-app-aspnet/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js b/1-web-apps/web-app-aspnetcore/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js similarity index 100% rename from web-app-aspnet/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js diff --git a/web-app-aspnet/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js b/1-web-apps/web-app-aspnetcore/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js similarity index 100% rename from web-app-aspnet/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js diff --git a/web-app-aspnet/wwwroot/lib/jquery-validation/LICENSE.md b/1-web-apps/web-app-aspnetcore/wwwroot/lib/jquery-validation/LICENSE.md similarity index 98% rename from web-app-aspnet/wwwroot/lib/jquery-validation/LICENSE.md rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/jquery-validation/LICENSE.md index bd38e93..dc377cc 100644 --- a/web-app-aspnet/wwwroot/lib/jquery-validation/LICENSE.md +++ b/1-web-apps/web-app-aspnetcore/wwwroot/lib/jquery-validation/LICENSE.md @@ -1,22 +1,22 @@ -The MIT License (MIT) -===================== - -Copyright Jörn Zaefferer - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +The MIT License (MIT) +===================== + +Copyright Jörn Zaefferer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/web-app-aspnet/wwwroot/lib/jquery-validation/dist/additional-methods.js b/1-web-apps/web-app-aspnetcore/wwwroot/lib/jquery-validation/dist/additional-methods.js similarity index 97% rename from web-app-aspnet/wwwroot/lib/jquery-validation/dist/additional-methods.js rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/jquery-validation/dist/additional-methods.js index 480ecd1..e129bc0 100644 --- a/web-app-aspnet/wwwroot/lib/jquery-validation/dist/additional-methods.js +++ b/1-web-apps/web-app-aspnetcore/wwwroot/lib/jquery-validation/dist/additional-methods.js @@ -1,1158 +1,1158 @@ -/*! - * jQuery Validation Plugin v1.17.0 - * - * https://jqueryvalidation.org/ - * - * Copyright (c) 2017 Jörn Zaefferer - * Released under the MIT license - */ -(function( factory ) { - if ( typeof define === "function" && define.amd ) { - define( ["jquery", "./jquery.validate"], factory ); - } else if (typeof module === "object" && module.exports) { - module.exports = factory( require( "jquery" ) ); - } else { - factory( jQuery ); - } -}(function( $ ) { - -( function() { - - function stripHtml( value ) { - - // Remove html tags and space chars - return value.replace( /<.[^<>]*?>/g, " " ).replace( / | /gi, " " ) - - // Remove punctuation - .replace( /[.(),;:!?%#$'\"_+=\/\-“”’]*/g, "" ); - } - - $.validator.addMethod( "maxWords", function( value, element, params ) { - return this.optional( element ) || stripHtml( value ).match( /\b\w+\b/g ).length <= params; - }, $.validator.format( "Please enter {0} words or less." ) ); - - $.validator.addMethod( "minWords", function( value, element, params ) { - return this.optional( element ) || stripHtml( value ).match( /\b\w+\b/g ).length >= params; - }, $.validator.format( "Please enter at least {0} words." ) ); - - $.validator.addMethod( "rangeWords", function( value, element, params ) { - var valueStripped = stripHtml( value ), - regex = /\b\w+\b/g; - return this.optional( element ) || valueStripped.match( regex ).length >= params[ 0 ] && valueStripped.match( regex ).length <= params[ 1 ]; - }, $.validator.format( "Please enter between {0} and {1} words." ) ); - -}() ); - -// Accept a value from a file input based on a required mimetype -$.validator.addMethod( "accept", function( value, element, param ) { - - // Split mime on commas in case we have multiple types we can accept - var typeParam = typeof param === "string" ? param.replace( /\s/g, "" ) : "image/*", - optionalValue = this.optional( element ), - i, file, regex; - - // Element is optional - if ( optionalValue ) { - return optionalValue; - } - - if ( $( element ).attr( "type" ) === "file" ) { - - // Escape string to be used in the regex - // see: https://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex - // Escape also "/*" as "/.*" as a wildcard - typeParam = typeParam - .replace( /[\-\[\]\/\{\}\(\)\+\?\.\\\^\$\|]/g, "\\$&" ) - .replace( /,/g, "|" ) - .replace( /\/\*/g, "/.*" ); - - // Check if the element has a FileList before checking each file - if ( element.files && element.files.length ) { - regex = new RegExp( ".?(" + typeParam + ")$", "i" ); - for ( i = 0; i < element.files.length; i++ ) { - file = element.files[ i ]; - - // Grab the mimetype from the loaded file, verify it matches - if ( !file.type.match( regex ) ) { - return false; - } - } - } - } - - // Either return true because we've validated each file, or because the - // browser does not support element.files and the FileList feature - return true; -}, $.validator.format( "Please enter a value with a valid mimetype." ) ); - -$.validator.addMethod( "alphanumeric", function( value, element ) { - return this.optional( element ) || /^\w+$/i.test( value ); -}, "Letters, numbers, and underscores only please" ); - -/* - * Dutch bank account numbers (not 'giro' numbers) have 9 digits - * and pass the '11 check'. - * We accept the notation with spaces, as that is common. - * acceptable: 123456789 or 12 34 56 789 - */ -$.validator.addMethod( "bankaccountNL", function( value, element ) { - if ( this.optional( element ) ) { - return true; - } - if ( !( /^[0-9]{9}|([0-9]{2} ){3}[0-9]{3}$/.test( value ) ) ) { - return false; - } - - // Now '11 check' - var account = value.replace( / /g, "" ), // Remove spaces - sum = 0, - len = account.length, - pos, factor, digit; - for ( pos = 0; pos < len; pos++ ) { - factor = len - pos; - digit = account.substring( pos, pos + 1 ); - sum = sum + factor * digit; - } - return sum % 11 === 0; -}, "Please specify a valid bank account number" ); - -$.validator.addMethod( "bankorgiroaccountNL", function( value, element ) { - return this.optional( element ) || - ( $.validator.methods.bankaccountNL.call( this, value, element ) ) || - ( $.validator.methods.giroaccountNL.call( this, value, element ) ); -}, "Please specify a valid bank or giro account number" ); - -/** - * BIC is the business identifier code (ISO 9362). This BIC check is not a guarantee for authenticity. - * - * BIC pattern: BBBBCCLLbbb (8 or 11 characters long; bbb is optional) - * - * Validation is case-insensitive. Please make sure to normalize input yourself. - * - * BIC definition in detail: - * - First 4 characters - bank code (only letters) - * - Next 2 characters - ISO 3166-1 alpha-2 country code (only letters) - * - Next 2 characters - location code (letters and digits) - * a. shall not start with '0' or '1' - * b. second character must be a letter ('O' is not allowed) or digit ('0' for test (therefore not allowed), '1' denoting passive participant, '2' typically reverse-billing) - * - Last 3 characters - branch code, optional (shall not start with 'X' except in case of 'XXX' for primary office) (letters and digits) - */ -$.validator.addMethod( "bic", function( value, element ) { - return this.optional( element ) || /^([A-Z]{6}[A-Z2-9][A-NP-Z1-9])(X{3}|[A-WY-Z0-9][A-Z0-9]{2})?$/.test( value.toUpperCase() ); -}, "Please specify a valid BIC code" ); - -/* - * Código de identificación fiscal ( CIF ) is the tax identification code for Spanish legal entities - * Further rules can be found in Spanish on http://es.wikipedia.org/wiki/C%C3%B3digo_de_identificaci%C3%B3n_fiscal - * - * Spanish CIF structure: - * - * [ T ][ P ][ P ][ N ][ N ][ N ][ N ][ N ][ C ] - * - * Where: - * - * T: 1 character. Kind of Organization Letter: [ABCDEFGHJKLMNPQRSUVW] - * P: 2 characters. Province. - * N: 5 characters. Secuencial Number within the province. - * C: 1 character. Control Digit: [0-9A-J]. - * - * [ T ]: Kind of Organizations. Possible values: - * - * A. Corporations - * B. LLCs - * C. General partnerships - * D. Companies limited partnerships - * E. Communities of goods - * F. Cooperative Societies - * G. Associations - * H. Communities of homeowners in horizontal property regime - * J. Civil Societies - * K. Old format - * L. Old format - * M. Old format - * N. Nonresident entities - * P. Local authorities - * Q. Autonomous bodies, state or not, and the like, and congregations and religious institutions - * R. Congregations and religious institutions (since 2008 ORDER EHA/451/2008) - * S. Organs of State Administration and regions - * V. Agrarian Transformation - * W. Permanent establishments of non-resident in Spain - * - * [ C ]: Control Digit. It can be a number or a letter depending on T value: - * [ T ] --> [ C ] - * ------ ---------- - * A Number - * B Number - * E Number - * H Number - * K Letter - * P Letter - * Q Letter - * S Letter - * - */ -$.validator.addMethod( "cifES", function( value, element ) { - "use strict"; - - if ( this.optional( element ) ) { - return true; - } - - var cifRegEx = new RegExp( /^([ABCDEFGHJKLMNPQRSUVW])(\d{7})([0-9A-J])$/gi ); - var letter = value.substring( 0, 1 ), // [ T ] - number = value.substring( 1, 8 ), // [ P ][ P ][ N ][ N ][ N ][ N ][ N ] - control = value.substring( 8, 9 ), // [ C ] - all_sum = 0, - even_sum = 0, - odd_sum = 0, - i, n, - control_digit, - control_letter; - - function isOdd( n ) { - return n % 2 === 0; - } - - // Quick format test - if ( value.length !== 9 || !cifRegEx.test( value ) ) { - return false; - } - - for ( i = 0; i < number.length; i++ ) { - n = parseInt( number[ i ], 10 ); - - // Odd positions - if ( isOdd( i ) ) { - - // Odd positions are multiplied first. - n *= 2; - - // If the multiplication is bigger than 10 we need to adjust - odd_sum += n < 10 ? n : n - 9; - - // Even positions - // Just sum them - } else { - even_sum += n; - } - } - - all_sum = even_sum + odd_sum; - control_digit = ( 10 - ( all_sum ).toString().substr( -1 ) ).toString(); - control_digit = parseInt( control_digit, 10 ) > 9 ? "0" : control_digit; - control_letter = "JABCDEFGHI".substr( control_digit, 1 ).toString(); - - // Control must be a digit - if ( letter.match( /[ABEH]/ ) ) { - return control === control_digit; - - // Control must be a letter - } else if ( letter.match( /[KPQS]/ ) ) { - return control === control_letter; - } - - // Can be either - return control === control_digit || control === control_letter; - -}, "Please specify a valid CIF number." ); - -/* - * Brazillian CPF number (Cadastrado de Pessoas Físicas) is the equivalent of a Brazilian tax registration number. - * CPF numbers have 11 digits in total: 9 numbers followed by 2 check numbers that are being used for validation. - */ -$.validator.addMethod( "cpfBR", function( value ) { - - // Removing special characters from value - value = value.replace( /([~!@#$%^&*()_+=`{}\[\]\-|\\:;'<>,.\/? ])+/g, "" ); - - // Checking value to have 11 digits only - if ( value.length !== 11 ) { - return false; - } - - var sum = 0, - firstCN, secondCN, checkResult, i; - - firstCN = parseInt( value.substring( 9, 10 ), 10 ); - secondCN = parseInt( value.substring( 10, 11 ), 10 ); - - checkResult = function( sum, cn ) { - var result = ( sum * 10 ) % 11; - if ( ( result === 10 ) || ( result === 11 ) ) { - result = 0; - } - return ( result === cn ); - }; - - // Checking for dump data - if ( value === "" || - value === "00000000000" || - value === "11111111111" || - value === "22222222222" || - value === "33333333333" || - value === "44444444444" || - value === "55555555555" || - value === "66666666666" || - value === "77777777777" || - value === "88888888888" || - value === "99999999999" - ) { - return false; - } - - // Step 1 - using first Check Number: - for ( i = 1; i <= 9; i++ ) { - sum = sum + parseInt( value.substring( i - 1, i ), 10 ) * ( 11 - i ); - } - - // If first Check Number (CN) is valid, move to Step 2 - using second Check Number: - if ( checkResult( sum, firstCN ) ) { - sum = 0; - for ( i = 1; i <= 10; i++ ) { - sum = sum + parseInt( value.substring( i - 1, i ), 10 ) * ( 12 - i ); - } - return checkResult( sum, secondCN ); - } - return false; - -}, "Please specify a valid CPF number" ); - -// https://jqueryvalidation.org/creditcard-method/ -// based on https://en.wikipedia.org/wiki/Luhn_algorithm -$.validator.addMethod( "creditcard", function( value, element ) { - if ( this.optional( element ) ) { - return "dependency-mismatch"; - } - - // Accept only spaces, digits and dashes - if ( /[^0-9 \-]+/.test( value ) ) { - return false; - } - - var nCheck = 0, - nDigit = 0, - bEven = false, - n, cDigit; - - value = value.replace( /\D/g, "" ); - - // Basing min and max length on - // https://developer.ean.com/general_info/Valid_Credit_Card_Types - if ( value.length < 13 || value.length > 19 ) { - return false; - } - - for ( n = value.length - 1; n >= 0; n-- ) { - cDigit = value.charAt( n ); - nDigit = parseInt( cDigit, 10 ); - if ( bEven ) { - if ( ( nDigit *= 2 ) > 9 ) { - nDigit -= 9; - } - } - - nCheck += nDigit; - bEven = !bEven; - } - - return ( nCheck % 10 ) === 0; -}, "Please enter a valid credit card number." ); - -/* NOTICE: Modified version of Castle.Components.Validator.CreditCardValidator - * Redistributed under the the Apache License 2.0 at http://www.apache.org/licenses/LICENSE-2.0 - * Valid Types: mastercard, visa, amex, dinersclub, enroute, discover, jcb, unknown, all (overrides all other settings) - */ -$.validator.addMethod( "creditcardtypes", function( value, element, param ) { - if ( /[^0-9\-]+/.test( value ) ) { - return false; - } - - value = value.replace( /\D/g, "" ); - - var validTypes = 0x0000; - - if ( param.mastercard ) { - validTypes |= 0x0001; - } - if ( param.visa ) { - validTypes |= 0x0002; - } - if ( param.amex ) { - validTypes |= 0x0004; - } - if ( param.dinersclub ) { - validTypes |= 0x0008; - } - if ( param.enroute ) { - validTypes |= 0x0010; - } - if ( param.discover ) { - validTypes |= 0x0020; - } - if ( param.jcb ) { - validTypes |= 0x0040; - } - if ( param.unknown ) { - validTypes |= 0x0080; - } - if ( param.all ) { - validTypes = 0x0001 | 0x0002 | 0x0004 | 0x0008 | 0x0010 | 0x0020 | 0x0040 | 0x0080; - } - if ( validTypes & 0x0001 && /^(5[12345])/.test( value ) ) { // Mastercard - return value.length === 16; - } - if ( validTypes & 0x0002 && /^(4)/.test( value ) ) { // Visa - return value.length === 16; - } - if ( validTypes & 0x0004 && /^(3[47])/.test( value ) ) { // Amex - return value.length === 15; - } - if ( validTypes & 0x0008 && /^(3(0[012345]|[68]))/.test( value ) ) { // Dinersclub - return value.length === 14; - } - if ( validTypes & 0x0010 && /^(2(014|149))/.test( value ) ) { // Enroute - return value.length === 15; - } - if ( validTypes & 0x0020 && /^(6011)/.test( value ) ) { // Discover - return value.length === 16; - } - if ( validTypes & 0x0040 && /^(3)/.test( value ) ) { // Jcb - return value.length === 16; - } - if ( validTypes & 0x0040 && /^(2131|1800)/.test( value ) ) { // Jcb - return value.length === 15; - } - if ( validTypes & 0x0080 ) { // Unknown - return true; - } - return false; -}, "Please enter a valid credit card number." ); - -/** - * Validates currencies with any given symbols by @jameslouiz - * Symbols can be optional or required. Symbols required by default - * - * Usage examples: - * currency: ["£", false] - Use false for soft currency validation - * currency: ["$", false] - * currency: ["RM", false] - also works with text based symbols such as "RM" - Malaysia Ringgit etc - * - * - * - * Soft symbol checking - * currencyInput: { - * currency: ["$", false] - * } - * - * Strict symbol checking (default) - * currencyInput: { - * currency: "$" - * //OR - * currency: ["$", true] - * } - * - * Multiple Symbols - * currencyInput: { - * currency: "$,£,¢" - * } - */ -$.validator.addMethod( "currency", function( value, element, param ) { - var isParamString = typeof param === "string", - symbol = isParamString ? param : param[ 0 ], - soft = isParamString ? true : param[ 1 ], - regex; - - symbol = symbol.replace( /,/g, "" ); - symbol = soft ? symbol + "]" : symbol + "]?"; - regex = "^[" + symbol + "([1-9]{1}[0-9]{0,2}(\\,[0-9]{3})*(\\.[0-9]{0,2})?|[1-9]{1}[0-9]{0,}(\\.[0-9]{0,2})?|0(\\.[0-9]{0,2})?|(\\.[0-9]{1,2})?)$"; - regex = new RegExp( regex ); - return this.optional( element ) || regex.test( value ); - -}, "Please specify a valid currency" ); - -$.validator.addMethod( "dateFA", function( value, element ) { - return this.optional( element ) || /^[1-4]\d{3}\/((0?[1-6]\/((3[0-1])|([1-2][0-9])|(0?[1-9])))|((1[0-2]|(0?[7-9]))\/(30|([1-2][0-9])|(0?[1-9]))))$/.test( value ); -}, $.validator.messages.date ); - -/** - * Return true, if the value is a valid date, also making this formal check dd/mm/yyyy. - * - * @example $.validator.methods.date("01/01/1900") - * @result true - * - * @example $.validator.methods.date("01/13/1990") - * @result false - * - * @example $.validator.methods.date("01.01.1900") - * @result false - * - * @example - * @desc Declares an optional input element whose value must be a valid date. - * - * @name $.validator.methods.dateITA - * @type Boolean - * @cat Plugins/Validate/Methods - */ -$.validator.addMethod( "dateITA", function( value, element ) { - var check = false, - re = /^\d{1,2}\/\d{1,2}\/\d{4}$/, - adata, gg, mm, aaaa, xdata; - if ( re.test( value ) ) { - adata = value.split( "/" ); - gg = parseInt( adata[ 0 ], 10 ); - mm = parseInt( adata[ 1 ], 10 ); - aaaa = parseInt( adata[ 2 ], 10 ); - xdata = new Date( Date.UTC( aaaa, mm - 1, gg, 12, 0, 0, 0 ) ); - if ( ( xdata.getUTCFullYear() === aaaa ) && ( xdata.getUTCMonth() === mm - 1 ) && ( xdata.getUTCDate() === gg ) ) { - check = true; - } else { - check = false; - } - } else { - check = false; - } - return this.optional( element ) || check; -}, $.validator.messages.date ); - -$.validator.addMethod( "dateNL", function( value, element ) { - return this.optional( element ) || /^(0?[1-9]|[12]\d|3[01])[\.\/\-](0?[1-9]|1[012])[\.\/\-]([12]\d)?(\d\d)$/.test( value ); -}, $.validator.messages.date ); - -// Older "accept" file extension method. Old docs: http://docs.jquery.com/Plugins/Validation/Methods/accept -$.validator.addMethod( "extension", function( value, element, param ) { - param = typeof param === "string" ? param.replace( /,/g, "|" ) : "png|jpe?g|gif"; - return this.optional( element ) || value.match( new RegExp( "\\.(" + param + ")$", "i" ) ); -}, $.validator.format( "Please enter a value with a valid extension." ) ); - -/** - * Dutch giro account numbers (not bank numbers) have max 7 digits - */ -$.validator.addMethod( "giroaccountNL", function( value, element ) { - return this.optional( element ) || /^[0-9]{1,7}$/.test( value ); -}, "Please specify a valid giro account number" ); - -/** - * IBAN is the international bank account number. - * It has a country - specific format, that is checked here too - * - * Validation is case-insensitive. Please make sure to normalize input yourself. - */ -$.validator.addMethod( "iban", function( value, element ) { - - // Some quick simple tests to prevent needless work - if ( this.optional( element ) ) { - return true; - } - - // Remove spaces and to upper case - var iban = value.replace( / /g, "" ).toUpperCase(), - ibancheckdigits = "", - leadingZeroes = true, - cRest = "", - cOperator = "", - countrycode, ibancheck, charAt, cChar, bbanpattern, bbancountrypatterns, ibanregexp, i, p; - - // Check for IBAN code length. - // It contains: - // country code ISO 3166-1 - two letters, - // two check digits, - // Basic Bank Account Number (BBAN) - up to 30 chars - var minimalIBANlength = 5; - if ( iban.length < minimalIBANlength ) { - return false; - } - - // Check the country code and find the country specific format - countrycode = iban.substring( 0, 2 ); - bbancountrypatterns = { - "AL": "\\d{8}[\\dA-Z]{16}", - "AD": "\\d{8}[\\dA-Z]{12}", - "AT": "\\d{16}", - "AZ": "[\\dA-Z]{4}\\d{20}", - "BE": "\\d{12}", - "BH": "[A-Z]{4}[\\dA-Z]{14}", - "BA": "\\d{16}", - "BR": "\\d{23}[A-Z][\\dA-Z]", - "BG": "[A-Z]{4}\\d{6}[\\dA-Z]{8}", - "CR": "\\d{17}", - "HR": "\\d{17}", - "CY": "\\d{8}[\\dA-Z]{16}", - "CZ": "\\d{20}", - "DK": "\\d{14}", - "DO": "[A-Z]{4}\\d{20}", - "EE": "\\d{16}", - "FO": "\\d{14}", - "FI": "\\d{14}", - "FR": "\\d{10}[\\dA-Z]{11}\\d{2}", - "GE": "[\\dA-Z]{2}\\d{16}", - "DE": "\\d{18}", - "GI": "[A-Z]{4}[\\dA-Z]{15}", - "GR": "\\d{7}[\\dA-Z]{16}", - "GL": "\\d{14}", - "GT": "[\\dA-Z]{4}[\\dA-Z]{20}", - "HU": "\\d{24}", - "IS": "\\d{22}", - "IE": "[\\dA-Z]{4}\\d{14}", - "IL": "\\d{19}", - "IT": "[A-Z]\\d{10}[\\dA-Z]{12}", - "KZ": "\\d{3}[\\dA-Z]{13}", - "KW": "[A-Z]{4}[\\dA-Z]{22}", - "LV": "[A-Z]{4}[\\dA-Z]{13}", - "LB": "\\d{4}[\\dA-Z]{20}", - "LI": "\\d{5}[\\dA-Z]{12}", - "LT": "\\d{16}", - "LU": "\\d{3}[\\dA-Z]{13}", - "MK": "\\d{3}[\\dA-Z]{10}\\d{2}", - "MT": "[A-Z]{4}\\d{5}[\\dA-Z]{18}", - "MR": "\\d{23}", - "MU": "[A-Z]{4}\\d{19}[A-Z]{3}", - "MC": "\\d{10}[\\dA-Z]{11}\\d{2}", - "MD": "[\\dA-Z]{2}\\d{18}", - "ME": "\\d{18}", - "NL": "[A-Z]{4}\\d{10}", - "NO": "\\d{11}", - "PK": "[\\dA-Z]{4}\\d{16}", - "PS": "[\\dA-Z]{4}\\d{21}", - "PL": "\\d{24}", - "PT": "\\d{21}", - "RO": "[A-Z]{4}[\\dA-Z]{16}", - "SM": "[A-Z]\\d{10}[\\dA-Z]{12}", - "SA": "\\d{2}[\\dA-Z]{18}", - "RS": "\\d{18}", - "SK": "\\d{20}", - "SI": "\\d{15}", - "ES": "\\d{20}", - "SE": "\\d{20}", - "CH": "\\d{5}[\\dA-Z]{12}", - "TN": "\\d{20}", - "TR": "\\d{5}[\\dA-Z]{17}", - "AE": "\\d{3}\\d{16}", - "GB": "[A-Z]{4}\\d{14}", - "VG": "[\\dA-Z]{4}\\d{16}" - }; - - bbanpattern = bbancountrypatterns[ countrycode ]; - - // As new countries will start using IBAN in the - // future, we only check if the countrycode is known. - // This prevents false negatives, while almost all - // false positives introduced by this, will be caught - // by the checksum validation below anyway. - // Strict checking should return FALSE for unknown - // countries. - if ( typeof bbanpattern !== "undefined" ) { - ibanregexp = new RegExp( "^[A-Z]{2}\\d{2}" + bbanpattern + "$", "" ); - if ( !( ibanregexp.test( iban ) ) ) { - return false; // Invalid country specific format - } - } - - // Now check the checksum, first convert to digits - ibancheck = iban.substring( 4, iban.length ) + iban.substring( 0, 4 ); - for ( i = 0; i < ibancheck.length; i++ ) { - charAt = ibancheck.charAt( i ); - if ( charAt !== "0" ) { - leadingZeroes = false; - } - if ( !leadingZeroes ) { - ibancheckdigits += "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ".indexOf( charAt ); - } - } - - // Calculate the result of: ibancheckdigits % 97 - for ( p = 0; p < ibancheckdigits.length; p++ ) { - cChar = ibancheckdigits.charAt( p ); - cOperator = "" + cRest + "" + cChar; - cRest = cOperator % 97; - } - return cRest === 1; -}, "Please specify a valid IBAN" ); - -$.validator.addMethod( "integer", function( value, element ) { - return this.optional( element ) || /^-?\d+$/.test( value ); -}, "A positive or negative non-decimal number please" ); - -$.validator.addMethod( "ipv4", function( value, element ) { - return this.optional( element ) || /^(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)$/i.test( value ); -}, "Please enter a valid IP v4 address." ); - -$.validator.addMethod( "ipv6", function( value, element ) { - return this.optional( element ) || /^((([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}:[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){5}:([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(([0-9A-Fa-f]{1,4}:){0,5}:((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(::([0-9A-Fa-f]{1,4}:){0,5}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|([0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})|(::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){1,7}:))$/i.test( value ); -}, "Please enter a valid IP v6 address." ); - -$.validator.addMethod( "lettersonly", function( value, element ) { - return this.optional( element ) || /^[a-z]+$/i.test( value ); -}, "Letters only please" ); - -$.validator.addMethod( "letterswithbasicpunc", function( value, element ) { - return this.optional( element ) || /^[a-z\-.,()'"\s]+$/i.test( value ); -}, "Letters or punctuation only please" ); - -$.validator.addMethod( "mobileNL", function( value, element ) { - return this.optional( element ) || /^((\+|00(\s|\s?\-\s?)?)31(\s|\s?\-\s?)?(\(0\)[\-\s]?)?|0)6((\s|\s?\-\s?)?[0-9]){8}$/.test( value ); -}, "Please specify a valid mobile number" ); - -/* For UK phone functions, do the following server side processing: - * Compare original input with this RegEx pattern: - * ^\(?(?:(?:00\)?[\s\-]?\(?|\+)(44)\)?[\s\-]?\(?(?:0\)?[\s\-]?\(?)?|0)([1-9]\d{1,4}\)?[\s\d\-]+)$ - * Extract $1 and set $prefix to '+44' if $1 is '44', otherwise set $prefix to '0' - * Extract $2 and remove hyphens, spaces and parentheses. Phone number is combined $prefix and $2. - * A number of very detailed GB telephone number RegEx patterns can also be found at: - * http://www.aa-asterisk.org.uk/index.php/Regular_Expressions_for_Validating_and_Formatting_GB_Telephone_Numbers - */ -$.validator.addMethod( "mobileUK", function( phone_number, element ) { - phone_number = phone_number.replace( /\(|\)|\s+|-/g, "" ); - return this.optional( element ) || phone_number.length > 9 && - phone_number.match( /^(?:(?:(?:00\s?|\+)44\s?|0)7(?:[1345789]\d{2}|624)\s?\d{3}\s?\d{3})$/ ); -}, "Please specify a valid mobile number" ); - -$.validator.addMethod( "netmask", function( value, element ) { - return this.optional( element ) || /^(254|252|248|240|224|192|128)\.0\.0\.0|255\.(254|252|248|240|224|192|128|0)\.0\.0|255\.255\.(254|252|248|240|224|192|128|0)\.0|255\.255\.255\.(254|252|248|240|224|192|128|0)/i.test( value ); -}, "Please enter a valid netmask." ); - -/* - * The NIE (Número de Identificación de Extranjero) is a Spanish tax identification number assigned by the Spanish - * authorities to any foreigner. - * - * The NIE is the equivalent of a Spaniards Número de Identificación Fiscal (NIF) which serves as a fiscal - * identification number. The CIF number (Certificado de Identificación Fiscal) is equivalent to the NIF, but applies to - * companies rather than individuals. The NIE consists of an 'X' or 'Y' followed by 7 or 8 digits then another letter. - */ -$.validator.addMethod( "nieES", function( value, element ) { - "use strict"; - - if ( this.optional( element ) ) { - return true; - } - - var nieRegEx = new RegExp( /^[MXYZ]{1}[0-9]{7,8}[TRWAGMYFPDXBNJZSQVHLCKET]{1}$/gi ); - var validChars = "TRWAGMYFPDXBNJZSQVHLCKET", - letter = value.substr( value.length - 1 ).toUpperCase(), - number; - - value = value.toString().toUpperCase(); - - // Quick format test - if ( value.length > 10 || value.length < 9 || !nieRegEx.test( value ) ) { - return false; - } - - // X means same number - // Y means number + 10000000 - // Z means number + 20000000 - value = value.replace( /^[X]/, "0" ) - .replace( /^[Y]/, "1" ) - .replace( /^[Z]/, "2" ); - - number = value.length === 9 ? value.substr( 0, 8 ) : value.substr( 0, 9 ); - - return validChars.charAt( parseInt( number, 10 ) % 23 ) === letter; - -}, "Please specify a valid NIE number." ); - -/* - * The Número de Identificación Fiscal ( NIF ) is the way tax identification used in Spain for individuals - */ -$.validator.addMethod( "nifES", function( value, element ) { - "use strict"; - - if ( this.optional( element ) ) { - return true; - } - - value = value.toUpperCase(); - - // Basic format test - if ( !value.match( "((^[A-Z]{1}[0-9]{7}[A-Z0-9]{1}$|^[T]{1}[A-Z0-9]{8}$)|^[0-9]{8}[A-Z]{1}$)" ) ) { - return false; - } - - // Test NIF - if ( /^[0-9]{8}[A-Z]{1}$/.test( value ) ) { - return ( "TRWAGMYFPDXBNJZSQVHLCKE".charAt( value.substring( 8, 0 ) % 23 ) === value.charAt( 8 ) ); - } - - // Test specials NIF (starts with K, L or M) - if ( /^[KLM]{1}/.test( value ) ) { - return ( value[ 8 ] === "TRWAGMYFPDXBNJZSQVHLCKE".charAt( value.substring( 8, 1 ) % 23 ) ); - } - - return false; - -}, "Please specify a valid NIF number." ); - -/* - * Numer identyfikacji podatkowej ( NIP ) is the way tax identification used in Poland for companies - */ -$.validator.addMethod( "nipPL", function( value ) { - "use strict"; - - value = value.replace( /[^0-9]/g, "" ); - - if ( value.length !== 10 ) { - return false; - } - - var arrSteps = [ 6, 5, 7, 2, 3, 4, 5, 6, 7 ]; - var intSum = 0; - for ( var i = 0; i < 9; i++ ) { - intSum += arrSteps[ i ] * value[ i ]; - } - var int2 = intSum % 11; - var intControlNr = ( int2 === 10 ) ? 0 : int2; - - return ( intControlNr === parseInt( value[ 9 ], 10 ) ); -}, "Please specify a valid NIP number." ); - -$.validator.addMethod( "notEqualTo", function( value, element, param ) { - return this.optional( element ) || !$.validator.methods.equalTo.call( this, value, element, param ); -}, "Please enter a different value, values must not be the same." ); - -$.validator.addMethod( "nowhitespace", function( value, element ) { - return this.optional( element ) || /^\S+$/i.test( value ); -}, "No white space please" ); - -/** -* Return true if the field value matches the given format RegExp -* -* @example $.validator.methods.pattern("AR1004",element,/^AR\d{4}$/) -* @result true -* -* @example $.validator.methods.pattern("BR1004",element,/^AR\d{4}$/) -* @result false -* -* @name $.validator.methods.pattern -* @type Boolean -* @cat Plugins/Validate/Methods -*/ -$.validator.addMethod( "pattern", function( value, element, param ) { - if ( this.optional( element ) ) { - return true; - } - if ( typeof param === "string" ) { - param = new RegExp( "^(?:" + param + ")$" ); - } - return param.test( value ); -}, "Invalid format." ); - -/** - * Dutch phone numbers have 10 digits (or 11 and start with +31). - */ -$.validator.addMethod( "phoneNL", function( value, element ) { - return this.optional( element ) || /^((\+|00(\s|\s?\-\s?)?)31(\s|\s?\-\s?)?(\(0\)[\-\s]?)?|0)[1-9]((\s|\s?\-\s?)?[0-9]){8}$/.test( value ); -}, "Please specify a valid phone number." ); - -/* For UK phone functions, do the following server side processing: - * Compare original input with this RegEx pattern: - * ^\(?(?:(?:00\)?[\s\-]?\(?|\+)(44)\)?[\s\-]?\(?(?:0\)?[\s\-]?\(?)?|0)([1-9]\d{1,4}\)?[\s\d\-]+)$ - * Extract $1 and set $prefix to '+44' if $1 is '44', otherwise set $prefix to '0' - * Extract $2 and remove hyphens, spaces and parentheses. Phone number is combined $prefix and $2. - * A number of very detailed GB telephone number RegEx patterns can also be found at: - * http://www.aa-asterisk.org.uk/index.php/Regular_Expressions_for_Validating_and_Formatting_GB_Telephone_Numbers - */ - -// Matches UK landline + mobile, accepting only 01-3 for landline or 07 for mobile to exclude many premium numbers -$.validator.addMethod( "phonesUK", function( phone_number, element ) { - phone_number = phone_number.replace( /\(|\)|\s+|-/g, "" ); - return this.optional( element ) || phone_number.length > 9 && - phone_number.match( /^(?:(?:(?:00\s?|\+)44\s?|0)(?:1\d{8,9}|[23]\d{9}|7(?:[1345789]\d{8}|624\d{6})))$/ ); -}, "Please specify a valid uk phone number" ); - -/* For UK phone functions, do the following server side processing: - * Compare original input with this RegEx pattern: - * ^\(?(?:(?:00\)?[\s\-]?\(?|\+)(44)\)?[\s\-]?\(?(?:0\)?[\s\-]?\(?)?|0)([1-9]\d{1,4}\)?[\s\d\-]+)$ - * Extract $1 and set $prefix to '+44' if $1 is '44', otherwise set $prefix to '0' - * Extract $2 and remove hyphens, spaces and parentheses. Phone number is combined $prefix and $2. - * A number of very detailed GB telephone number RegEx patterns can also be found at: - * http://www.aa-asterisk.org.uk/index.php/Regular_Expressions_for_Validating_and_Formatting_GB_Telephone_Numbers - */ -$.validator.addMethod( "phoneUK", function( phone_number, element ) { - phone_number = phone_number.replace( /\(|\)|\s+|-/g, "" ); - return this.optional( element ) || phone_number.length > 9 && - phone_number.match( /^(?:(?:(?:00\s?|\+)44\s?)|(?:\(?0))(?:\d{2}\)?\s?\d{4}\s?\d{4}|\d{3}\)?\s?\d{3}\s?\d{3,4}|\d{4}\)?\s?(?:\d{5}|\d{3}\s?\d{3})|\d{5}\)?\s?\d{4,5})$/ ); -}, "Please specify a valid phone number" ); - -/** - * Matches US phone number format - * - * where the area code may not start with 1 and the prefix may not start with 1 - * allows '-' or ' ' as a separator and allows parens around area code - * some people may want to put a '1' in front of their number - * - * 1(212)-999-2345 or - * 212 999 2344 or - * 212-999-0983 - * - * but not - * 111-123-5434 - * and not - * 212 123 4567 - */ -$.validator.addMethod( "phoneUS", function( phone_number, element ) { - phone_number = phone_number.replace( /\s+/g, "" ); - return this.optional( element ) || phone_number.length > 9 && - phone_number.match( /^(\+?1-?)?(\([2-9]([02-9]\d|1[02-9])\)|[2-9]([02-9]\d|1[02-9]))-?[2-9]([02-9]\d|1[02-9])-?\d{4}$/ ); -}, "Please specify a valid phone number" ); - -/* -* Valida CEPs do brasileiros: -* -* Formatos aceitos: -* 99999-999 -* 99.999-999 -* 99999999 -*/ -$.validator.addMethod( "postalcodeBR", function( cep_value, element ) { - return this.optional( element ) || /^\d{2}.\d{3}-\d{3}?$|^\d{5}-?\d{3}?$/.test( cep_value ); -}, "Informe um CEP válido." ); - -/** - * Matches a valid Canadian Postal Code - * - * @example jQuery.validator.methods.postalCodeCA( "H0H 0H0", element ) - * @result true - * - * @example jQuery.validator.methods.postalCodeCA( "H0H0H0", element ) - * @result false - * - * @name jQuery.validator.methods.postalCodeCA - * @type Boolean - * @cat Plugins/Validate/Methods - */ -$.validator.addMethod( "postalCodeCA", function( value, element ) { - return this.optional( element ) || /^[ABCEGHJKLMNPRSTVXY]\d[ABCEGHJKLMNPRSTVWXYZ] *\d[ABCEGHJKLMNPRSTVWXYZ]\d$/i.test( value ); -}, "Please specify a valid postal code" ); - -/* Matches Italian postcode (CAP) */ -$.validator.addMethod( "postalcodeIT", function( value, element ) { - return this.optional( element ) || /^\d{5}$/.test( value ); -}, "Please specify a valid postal code" ); - -$.validator.addMethod( "postalcodeNL", function( value, element ) { - return this.optional( element ) || /^[1-9][0-9]{3}\s?[a-zA-Z]{2}$/.test( value ); -}, "Please specify a valid postal code" ); - -// Matches UK postcode. Does not match to UK Channel Islands that have their own postcodes (non standard UK) -$.validator.addMethod( "postcodeUK", function( value, element ) { - return this.optional( element ) || /^((([A-PR-UWYZ][0-9])|([A-PR-UWYZ][0-9][0-9])|([A-PR-UWYZ][A-HK-Y][0-9])|([A-PR-UWYZ][A-HK-Y][0-9][0-9])|([A-PR-UWYZ][0-9][A-HJKSTUW])|([A-PR-UWYZ][A-HK-Y][0-9][ABEHMNPRVWXY]))\s?([0-9][ABD-HJLNP-UW-Z]{2})|(GIR)\s?(0AA))$/i.test( value ); -}, "Please specify a valid UK postcode" ); - -/* - * Lets you say "at least X inputs that match selector Y must be filled." - * - * The end result is that neither of these inputs: - * - * - * - * - * ...will validate unless at least one of them is filled. - * - * partnumber: {require_from_group: [1,".productinfo"]}, - * description: {require_from_group: [1,".productinfo"]} - * - * options[0]: number of fields that must be filled in the group - * options[1]: CSS selector that defines the group of conditionally required fields - */ -$.validator.addMethod( "require_from_group", function( value, element, options ) { - var $fields = $( options[ 1 ], element.form ), - $fieldsFirst = $fields.eq( 0 ), - validator = $fieldsFirst.data( "valid_req_grp" ) ? $fieldsFirst.data( "valid_req_grp" ) : $.extend( {}, this ), - isValid = $fields.filter( function() { - return validator.elementValue( this ); - } ).length >= options[ 0 ]; - - // Store the cloned validator for future validation - $fieldsFirst.data( "valid_req_grp", validator ); - - // If element isn't being validated, run each require_from_group field's validation rules - if ( !$( element ).data( "being_validated" ) ) { - $fields.data( "being_validated", true ); - $fields.each( function() { - validator.element( this ); - } ); - $fields.data( "being_validated", false ); - } - return isValid; -}, $.validator.format( "Please fill at least {0} of these fields." ) ); - -/* - * Lets you say "either at least X inputs that match selector Y must be filled, - * OR they must all be skipped (left blank)." - * - * The end result, is that none of these inputs: - * - * - * - * - * - * ...will validate unless either at least two of them are filled, - * OR none of them are. - * - * partnumber: {skip_or_fill_minimum: [2,".productinfo"]}, - * description: {skip_or_fill_minimum: [2,".productinfo"]}, - * color: {skip_or_fill_minimum: [2,".productinfo"]} - * - * options[0]: number of fields that must be filled in the group - * options[1]: CSS selector that defines the group of conditionally required fields - * - */ -$.validator.addMethod( "skip_or_fill_minimum", function( value, element, options ) { - var $fields = $( options[ 1 ], element.form ), - $fieldsFirst = $fields.eq( 0 ), - validator = $fieldsFirst.data( "valid_skip" ) ? $fieldsFirst.data( "valid_skip" ) : $.extend( {}, this ), - numberFilled = $fields.filter( function() { - return validator.elementValue( this ); - } ).length, - isValid = numberFilled === 0 || numberFilled >= options[ 0 ]; - - // Store the cloned validator for future validation - $fieldsFirst.data( "valid_skip", validator ); - - // If element isn't being validated, run each skip_or_fill_minimum field's validation rules - if ( !$( element ).data( "being_validated" ) ) { - $fields.data( "being_validated", true ); - $fields.each( function() { - validator.element( this ); - } ); - $fields.data( "being_validated", false ); - } - return isValid; -}, $.validator.format( "Please either skip these fields or fill at least {0} of them." ) ); - -/* Validates US States and/or Territories by @jdforsythe - * Can be case insensitive or require capitalization - default is case insensitive - * Can include US Territories or not - default does not - * Can include US Military postal abbreviations (AA, AE, AP) - default does not - * - * Note: "States" always includes DC (District of Colombia) - * - * Usage examples: - * - * This is the default - case insensitive, no territories, no military zones - * stateInput: { - * caseSensitive: false, - * includeTerritories: false, - * includeMilitary: false - * } - * - * Only allow capital letters, no territories, no military zones - * stateInput: { - * caseSensitive: false - * } - * - * Case insensitive, include territories but not military zones - * stateInput: { - * includeTerritories: true - * } - * - * Only allow capital letters, include territories and military zones - * stateInput: { - * caseSensitive: true, - * includeTerritories: true, - * includeMilitary: true - * } - * - */ -$.validator.addMethod( "stateUS", function( value, element, options ) { - var isDefault = typeof options === "undefined", - caseSensitive = ( isDefault || typeof options.caseSensitive === "undefined" ) ? false : options.caseSensitive, - includeTerritories = ( isDefault || typeof options.includeTerritories === "undefined" ) ? false : options.includeTerritories, - includeMilitary = ( isDefault || typeof options.includeMilitary === "undefined" ) ? false : options.includeMilitary, - regex; - - if ( !includeTerritories && !includeMilitary ) { - regex = "^(A[KLRZ]|C[AOT]|D[CE]|FL|GA|HI|I[ADLN]|K[SY]|LA|M[ADEINOST]|N[CDEHJMVY]|O[HKR]|PA|RI|S[CD]|T[NX]|UT|V[AT]|W[AIVY])$"; - } else if ( includeTerritories && includeMilitary ) { - regex = "^(A[AEKLPRSZ]|C[AOT]|D[CE]|FL|G[AU]|HI|I[ADLN]|K[SY]|LA|M[ADEINOPST]|N[CDEHJMVY]|O[HKR]|P[AR]|RI|S[CD]|T[NX]|UT|V[AIT]|W[AIVY])$"; - } else if ( includeTerritories ) { - regex = "^(A[KLRSZ]|C[AOT]|D[CE]|FL|G[AU]|HI|I[ADLN]|K[SY]|LA|M[ADEINOPST]|N[CDEHJMVY]|O[HKR]|P[AR]|RI|S[CD]|T[NX]|UT|V[AIT]|W[AIVY])$"; - } else { - regex = "^(A[AEKLPRZ]|C[AOT]|D[CE]|FL|GA|HI|I[ADLN]|K[SY]|LA|M[ADEINOST]|N[CDEHJMVY]|O[HKR]|PA|RI|S[CD]|T[NX]|UT|V[AT]|W[AIVY])$"; - } - - regex = caseSensitive ? new RegExp( regex ) : new RegExp( regex, "i" ); - return this.optional( element ) || regex.test( value ); -}, "Please specify a valid state" ); - -// TODO check if value starts with <, otherwise don't try stripping anything -$.validator.addMethod( "strippedminlength", function( value, element, param ) { - return $( value ).text().length >= param; -}, $.validator.format( "Please enter at least {0} characters" ) ); - -$.validator.addMethod( "time", function( value, element ) { - return this.optional( element ) || /^([01]\d|2[0-3]|[0-9])(:[0-5]\d){1,2}$/.test( value ); -}, "Please enter a valid time, between 00:00 and 23:59" ); - -$.validator.addMethod( "time12h", function( value, element ) { - return this.optional( element ) || /^((0?[1-9]|1[012])(:[0-5]\d){1,2}(\ ?[AP]M))$/i.test( value ); -}, "Please enter a valid time in 12-hour am/pm format" ); - -// Same as url, but TLD is optional -$.validator.addMethod( "url2", function( value, element ) { - return this.optional( element ) || /^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)*(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i.test( value ); -}, $.validator.messages.url ); - -/** - * Return true, if the value is a valid vehicle identification number (VIN). - * - * Works with all kind of text inputs. - * - * @example - * @desc Declares a required input element whose value must be a valid vehicle identification number. - * - * @name $.validator.methods.vinUS - * @type Boolean - * @cat Plugins/Validate/Methods - */ -$.validator.addMethod( "vinUS", function( v ) { - if ( v.length !== 17 ) { - return false; - } - - var LL = [ "A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" ], - VL = [ 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 7, 9, 2, 3, 4, 5, 6, 7, 8, 9 ], - FL = [ 8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2 ], - rs = 0, - i, n, d, f, cd, cdv; - - for ( i = 0; i < 17; i++ ) { - f = FL[ i ]; - d = v.slice( i, i + 1 ); - if ( i === 8 ) { - cdv = d; - } - if ( !isNaN( d ) ) { - d *= f; - } else { - for ( n = 0; n < LL.length; n++ ) { - if ( d.toUpperCase() === LL[ n ] ) { - d = VL[ n ]; - d *= f; - if ( isNaN( cdv ) && n === 8 ) { - cdv = LL[ n ]; - } - break; - } - } - } - rs += d; - } - cd = rs % 11; - if ( cd === 10 ) { - cd = "X"; - } - if ( cd === cdv ) { - return true; - } - return false; -}, "The specified vehicle identification number (VIN) is invalid." ); - -$.validator.addMethod( "zipcodeUS", function( value, element ) { - return this.optional( element ) || /^\d{5}(-\d{4})?$/.test( value ); -}, "The specified US ZIP Code is invalid" ); - -$.validator.addMethod( "ziprange", function( value, element ) { - return this.optional( element ) || /^90[2-5]\d\{2\}-\d{4}$/.test( value ); -}, "Your ZIP-code must be in the range 902xx-xxxx to 905xx-xxxx" ); -return $; +/*! + * jQuery Validation Plugin v1.17.0 + * + * https://jqueryvalidation.org/ + * + * Copyright (c) 2017 Jörn Zaefferer + * Released under the MIT license + */ +(function( factory ) { + if ( typeof define === "function" && define.amd ) { + define( ["jquery", "./jquery.validate"], factory ); + } else if (typeof module === "object" && module.exports) { + module.exports = factory( require( "jquery" ) ); + } else { + factory( jQuery ); + } +}(function( $ ) { + +( function() { + + function stripHtml( value ) { + + // Remove html tags and space chars + return value.replace( /<.[^<>]*?>/g, " " ).replace( / | /gi, " " ) + + // Remove punctuation + .replace( /[.(),;:!?%#$'\"_+=\/\-“”’]*/g, "" ); + } + + $.validator.addMethod( "maxWords", function( value, element, params ) { + return this.optional( element ) || stripHtml( value ).match( /\b\w+\b/g ).length <= params; + }, $.validator.format( "Please enter {0} words or less." ) ); + + $.validator.addMethod( "minWords", function( value, element, params ) { + return this.optional( element ) || stripHtml( value ).match( /\b\w+\b/g ).length >= params; + }, $.validator.format( "Please enter at least {0} words." ) ); + + $.validator.addMethod( "rangeWords", function( value, element, params ) { + var valueStripped = stripHtml( value ), + regex = /\b\w+\b/g; + return this.optional( element ) || valueStripped.match( regex ).length >= params[ 0 ] && valueStripped.match( regex ).length <= params[ 1 ]; + }, $.validator.format( "Please enter between {0} and {1} words." ) ); + +}() ); + +// Accept a value from a file input based on a required mimetype +$.validator.addMethod( "accept", function( value, element, param ) { + + // Split mime on commas in case we have multiple types we can accept + var typeParam = typeof param === "string" ? param.replace( /\s/g, "" ) : "image/*", + optionalValue = this.optional( element ), + i, file, regex; + + // Element is optional + if ( optionalValue ) { + return optionalValue; + } + + if ( $( element ).attr( "type" ) === "file" ) { + + // Escape string to be used in the regex + // see: https://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex + // Escape also "/*" as "/.*" as a wildcard + typeParam = typeParam + .replace( /[\-\[\]\/\{\}\(\)\+\?\.\\\^\$\|]/g, "\\$&" ) + .replace( /,/g, "|" ) + .replace( /\/\*/g, "/.*" ); + + // Check if the element has a FileList before checking each file + if ( element.files && element.files.length ) { + regex = new RegExp( ".?(" + typeParam + ")$", "i" ); + for ( i = 0; i < element.files.length; i++ ) { + file = element.files[ i ]; + + // Grab the mimetype from the loaded file, verify it matches + if ( !file.type.match( regex ) ) { + return false; + } + } + } + } + + // Either return true because we've validated each file, or because the + // browser does not support element.files and the FileList feature + return true; +}, $.validator.format( "Please enter a value with a valid mimetype." ) ); + +$.validator.addMethod( "alphanumeric", function( value, element ) { + return this.optional( element ) || /^\w+$/i.test( value ); +}, "Letters, numbers, and underscores only please" ); + +/* + * Dutch bank account numbers (not 'giro' numbers) have 9 digits + * and pass the '11 check'. + * We accept the notation with spaces, as that is common. + * acceptable: 123456789 or 12 34 56 789 + */ +$.validator.addMethod( "bankaccountNL", function( value, element ) { + if ( this.optional( element ) ) { + return true; + } + if ( !( /^[0-9]{9}|([0-9]{2} ){3}[0-9]{3}$/.test( value ) ) ) { + return false; + } + + // Now '11 check' + var account = value.replace( / /g, "" ), // Remove spaces + sum = 0, + len = account.length, + pos, factor, digit; + for ( pos = 0; pos < len; pos++ ) { + factor = len - pos; + digit = account.substring( pos, pos + 1 ); + sum = sum + factor * digit; + } + return sum % 11 === 0; +}, "Please specify a valid bank account number" ); + +$.validator.addMethod( "bankorgiroaccountNL", function( value, element ) { + return this.optional( element ) || + ( $.validator.methods.bankaccountNL.call( this, value, element ) ) || + ( $.validator.methods.giroaccountNL.call( this, value, element ) ); +}, "Please specify a valid bank or giro account number" ); + +/** + * BIC is the business identifier code (ISO 9362). This BIC check is not a guarantee for authenticity. + * + * BIC pattern: BBBBCCLLbbb (8 or 11 characters long; bbb is optional) + * + * Validation is case-insensitive. Please make sure to normalize input yourself. + * + * BIC definition in detail: + * - First 4 characters - bank code (only letters) + * - Next 2 characters - ISO 3166-1 alpha-2 country code (only letters) + * - Next 2 characters - location code (letters and digits) + * a. shall not start with '0' or '1' + * b. second character must be a letter ('O' is not allowed) or digit ('0' for test (therefore not allowed), '1' denoting passive participant, '2' typically reverse-billing) + * - Last 3 characters - branch code, optional (shall not start with 'X' except in case of 'XXX' for primary office) (letters and digits) + */ +$.validator.addMethod( "bic", function( value, element ) { + return this.optional( element ) || /^([A-Z]{6}[A-Z2-9][A-NP-Z1-9])(X{3}|[A-WY-Z0-9][A-Z0-9]{2})?$/.test( value.toUpperCase() ); +}, "Please specify a valid BIC code" ); + +/* + * Código de identificación fiscal ( CIF ) is the tax identification code for Spanish legal entities + * Further rules can be found in Spanish on http://es.wikipedia.org/wiki/C%C3%B3digo_de_identificaci%C3%B3n_fiscal + * + * Spanish CIF structure: + * + * [ T ][ P ][ P ][ N ][ N ][ N ][ N ][ N ][ C ] + * + * Where: + * + * T: 1 character. Kind of Organization Letter: [ABCDEFGHJKLMNPQRSUVW] + * P: 2 characters. Province. + * N: 5 characters. Secuencial Number within the province. + * C: 1 character. Control Digit: [0-9A-J]. + * + * [ T ]: Kind of Organizations. Possible values: + * + * A. Corporations + * B. LLCs + * C. General partnerships + * D. Companies limited partnerships + * E. Communities of goods + * F. Cooperative Societies + * G. Associations + * H. Communities of homeowners in horizontal property regime + * J. Civil Societies + * K. Old format + * L. Old format + * M. Old format + * N. Nonresident entities + * P. Local authorities + * Q. Autonomous bodies, state or not, and the like, and congregations and religious institutions + * R. Congregations and religious institutions (since 2008 ORDER EHA/451/2008) + * S. Organs of State Administration and regions + * V. Agrarian Transformation + * W. Permanent establishments of non-resident in Spain + * + * [ C ]: Control Digit. It can be a number or a letter depending on T value: + * [ T ] --> [ C ] + * ------ ---------- + * A Number + * B Number + * E Number + * H Number + * K Letter + * P Letter + * Q Letter + * S Letter + * + */ +$.validator.addMethod( "cifES", function( value, element ) { + "use strict"; + + if ( this.optional( element ) ) { + return true; + } + + var cifRegEx = new RegExp( /^([ABCDEFGHJKLMNPQRSUVW])(\d{7})([0-9A-J])$/gi ); + var letter = value.substring( 0, 1 ), // [ T ] + number = value.substring( 1, 8 ), // [ P ][ P ][ N ][ N ][ N ][ N ][ N ] + control = value.substring( 8, 9 ), // [ C ] + all_sum = 0, + even_sum = 0, + odd_sum = 0, + i, n, + control_digit, + control_letter; + + function isOdd( n ) { + return n % 2 === 0; + } + + // Quick format test + if ( value.length !== 9 || !cifRegEx.test( value ) ) { + return false; + } + + for ( i = 0; i < number.length; i++ ) { + n = parseInt( number[ i ], 10 ); + + // Odd positions + if ( isOdd( i ) ) { + + // Odd positions are multiplied first. + n *= 2; + + // If the multiplication is bigger than 10 we need to adjust + odd_sum += n < 10 ? n : n - 9; + + // Even positions + // Just sum them + } else { + even_sum += n; + } + } + + all_sum = even_sum + odd_sum; + control_digit = ( 10 - ( all_sum ).toString().substr( -1 ) ).toString(); + control_digit = parseInt( control_digit, 10 ) > 9 ? "0" : control_digit; + control_letter = "JABCDEFGHI".substr( control_digit, 1 ).toString(); + + // Control must be a digit + if ( letter.match( /[ABEH]/ ) ) { + return control === control_digit; + + // Control must be a letter + } else if ( letter.match( /[KPQS]/ ) ) { + return control === control_letter; + } + + // Can be either + return control === control_digit || control === control_letter; + +}, "Please specify a valid CIF number." ); + +/* + * Brazillian CPF number (Cadastrado de Pessoas Físicas) is the equivalent of a Brazilian tax registration number. + * CPF numbers have 11 digits in total: 9 numbers followed by 2 check numbers that are being used for validation. + */ +$.validator.addMethod( "cpfBR", function( value ) { + + // Removing special characters from value + value = value.replace( /([~!@#$%^&*()_+=`{}\[\]\-|\\:;'<>,.\/? ])+/g, "" ); + + // Checking value to have 11 digits only + if ( value.length !== 11 ) { + return false; + } + + var sum = 0, + firstCN, secondCN, checkResult, i; + + firstCN = parseInt( value.substring( 9, 10 ), 10 ); + secondCN = parseInt( value.substring( 10, 11 ), 10 ); + + checkResult = function( sum, cn ) { + var result = ( sum * 10 ) % 11; + if ( ( result === 10 ) || ( result === 11 ) ) { + result = 0; + } + return ( result === cn ); + }; + + // Checking for dump data + if ( value === "" || + value === "00000000000" || + value === "11111111111" || + value === "22222222222" || + value === "33333333333" || + value === "44444444444" || + value === "55555555555" || + value === "66666666666" || + value === "77777777777" || + value === "88888888888" || + value === "99999999999" + ) { + return false; + } + + // Step 1 - using first Check Number: + for ( i = 1; i <= 9; i++ ) { + sum = sum + parseInt( value.substring( i - 1, i ), 10 ) * ( 11 - i ); + } + + // If first Check Number (CN) is valid, move to Step 2 - using second Check Number: + if ( checkResult( sum, firstCN ) ) { + sum = 0; + for ( i = 1; i <= 10; i++ ) { + sum = sum + parseInt( value.substring( i - 1, i ), 10 ) * ( 12 - i ); + } + return checkResult( sum, secondCN ); + } + return false; + +}, "Please specify a valid CPF number" ); + +// https://jqueryvalidation.org/creditcard-method/ +// based on https://en.wikipedia.org/wiki/Luhn_algorithm +$.validator.addMethod( "creditcard", function( value, element ) { + if ( this.optional( element ) ) { + return "dependency-mismatch"; + } + + // Accept only spaces, digits and dashes + if ( /[^0-9 \-]+/.test( value ) ) { + return false; + } + + var nCheck = 0, + nDigit = 0, + bEven = false, + n, cDigit; + + value = value.replace( /\D/g, "" ); + + // Basing min and max length on + // https://developer.ean.com/general_info/Valid_Credit_Card_Types + if ( value.length < 13 || value.length > 19 ) { + return false; + } + + for ( n = value.length - 1; n >= 0; n-- ) { + cDigit = value.charAt( n ); + nDigit = parseInt( cDigit, 10 ); + if ( bEven ) { + if ( ( nDigit *= 2 ) > 9 ) { + nDigit -= 9; + } + } + + nCheck += nDigit; + bEven = !bEven; + } + + return ( nCheck % 10 ) === 0; +}, "Please enter a valid credit card number." ); + +/* NOTICE: Modified version of Castle.Components.Validator.CreditCardValidator + * Redistributed under the the Apache License 2.0 at http://www.apache.org/licenses/LICENSE-2.0 + * Valid Types: mastercard, visa, amex, dinersclub, enroute, discover, jcb, unknown, all (overrides all other settings) + */ +$.validator.addMethod( "creditcardtypes", function( value, element, param ) { + if ( /[^0-9\-]+/.test( value ) ) { + return false; + } + + value = value.replace( /\D/g, "" ); + + var validTypes = 0x0000; + + if ( param.mastercard ) { + validTypes |= 0x0001; + } + if ( param.visa ) { + validTypes |= 0x0002; + } + if ( param.amex ) { + validTypes |= 0x0004; + } + if ( param.dinersclub ) { + validTypes |= 0x0008; + } + if ( param.enroute ) { + validTypes |= 0x0010; + } + if ( param.discover ) { + validTypes |= 0x0020; + } + if ( param.jcb ) { + validTypes |= 0x0040; + } + if ( param.unknown ) { + validTypes |= 0x0080; + } + if ( param.all ) { + validTypes = 0x0001 | 0x0002 | 0x0004 | 0x0008 | 0x0010 | 0x0020 | 0x0040 | 0x0080; + } + if ( validTypes & 0x0001 && /^(5[12345])/.test( value ) ) { // Mastercard + return value.length === 16; + } + if ( validTypes & 0x0002 && /^(4)/.test( value ) ) { // Visa + return value.length === 16; + } + if ( validTypes & 0x0004 && /^(3[47])/.test( value ) ) { // Amex + return value.length === 15; + } + if ( validTypes & 0x0008 && /^(3(0[012345]|[68]))/.test( value ) ) { // Dinersclub + return value.length === 14; + } + if ( validTypes & 0x0010 && /^(2(014|149))/.test( value ) ) { // Enroute + return value.length === 15; + } + if ( validTypes & 0x0020 && /^(6011)/.test( value ) ) { // Discover + return value.length === 16; + } + if ( validTypes & 0x0040 && /^(3)/.test( value ) ) { // Jcb + return value.length === 16; + } + if ( validTypes & 0x0040 && /^(2131|1800)/.test( value ) ) { // Jcb + return value.length === 15; + } + if ( validTypes & 0x0080 ) { // Unknown + return true; + } + return false; +}, "Please enter a valid credit card number." ); + +/** + * Validates currencies with any given symbols by @jameslouiz + * Symbols can be optional or required. Symbols required by default + * + * Usage examples: + * currency: ["£", false] - Use false for soft currency validation + * currency: ["$", false] + * currency: ["RM", false] - also works with text based symbols such as "RM" - Malaysia Ringgit etc + * + * + * + * Soft symbol checking + * currencyInput: { + * currency: ["$", false] + * } + * + * Strict symbol checking (default) + * currencyInput: { + * currency: "$" + * //OR + * currency: ["$", true] + * } + * + * Multiple Symbols + * currencyInput: { + * currency: "$,£,¢" + * } + */ +$.validator.addMethod( "currency", function( value, element, param ) { + var isParamString = typeof param === "string", + symbol = isParamString ? param : param[ 0 ], + soft = isParamString ? true : param[ 1 ], + regex; + + symbol = symbol.replace( /,/g, "" ); + symbol = soft ? symbol + "]" : symbol + "]?"; + regex = "^[" + symbol + "([1-9]{1}[0-9]{0,2}(\\,[0-9]{3})*(\\.[0-9]{0,2})?|[1-9]{1}[0-9]{0,}(\\.[0-9]{0,2})?|0(\\.[0-9]{0,2})?|(\\.[0-9]{1,2})?)$"; + regex = new RegExp( regex ); + return this.optional( element ) || regex.test( value ); + +}, "Please specify a valid currency" ); + +$.validator.addMethod( "dateFA", function( value, element ) { + return this.optional( element ) || /^[1-4]\d{3}\/((0?[1-6]\/((3[0-1])|([1-2][0-9])|(0?[1-9])))|((1[0-2]|(0?[7-9]))\/(30|([1-2][0-9])|(0?[1-9]))))$/.test( value ); +}, $.validator.messages.date ); + +/** + * Return true, if the value is a valid date, also making this formal check dd/mm/yyyy. + * + * @example $.validator.methods.date("01/01/1900") + * @result true + * + * @example $.validator.methods.date("01/13/1990") + * @result false + * + * @example $.validator.methods.date("01.01.1900") + * @result false + * + * @example + * @desc Declares an optional input element whose value must be a valid date. + * + * @name $.validator.methods.dateITA + * @type Boolean + * @cat Plugins/Validate/Methods + */ +$.validator.addMethod( "dateITA", function( value, element ) { + var check = false, + re = /^\d{1,2}\/\d{1,2}\/\d{4}$/, + adata, gg, mm, aaaa, xdata; + if ( re.test( value ) ) { + adata = value.split( "/" ); + gg = parseInt( adata[ 0 ], 10 ); + mm = parseInt( adata[ 1 ], 10 ); + aaaa = parseInt( adata[ 2 ], 10 ); + xdata = new Date( Date.UTC( aaaa, mm - 1, gg, 12, 0, 0, 0 ) ); + if ( ( xdata.getUTCFullYear() === aaaa ) && ( xdata.getUTCMonth() === mm - 1 ) && ( xdata.getUTCDate() === gg ) ) { + check = true; + } else { + check = false; + } + } else { + check = false; + } + return this.optional( element ) || check; +}, $.validator.messages.date ); + +$.validator.addMethod( "dateNL", function( value, element ) { + return this.optional( element ) || /^(0?[1-9]|[12]\d|3[01])[\.\/\-](0?[1-9]|1[012])[\.\/\-]([12]\d)?(\d\d)$/.test( value ); +}, $.validator.messages.date ); + +// Older "accept" file extension method. Old docs: http://docs.jquery.com/Plugins/Validation/Methods/accept +$.validator.addMethod( "extension", function( value, element, param ) { + param = typeof param === "string" ? param.replace( /,/g, "|" ) : "png|jpe?g|gif"; + return this.optional( element ) || value.match( new RegExp( "\\.(" + param + ")$", "i" ) ); +}, $.validator.format( "Please enter a value with a valid extension." ) ); + +/** + * Dutch giro account numbers (not bank numbers) have max 7 digits + */ +$.validator.addMethod( "giroaccountNL", function( value, element ) { + return this.optional( element ) || /^[0-9]{1,7}$/.test( value ); +}, "Please specify a valid giro account number" ); + +/** + * IBAN is the international bank account number. + * It has a country - specific format, that is checked here too + * + * Validation is case-insensitive. Please make sure to normalize input yourself. + */ +$.validator.addMethod( "iban", function( value, element ) { + + // Some quick simple tests to prevent needless work + if ( this.optional( element ) ) { + return true; + } + + // Remove spaces and to upper case + var iban = value.replace( / /g, "" ).toUpperCase(), + ibancheckdigits = "", + leadingZeroes = true, + cRest = "", + cOperator = "", + countrycode, ibancheck, charAt, cChar, bbanpattern, bbancountrypatterns, ibanregexp, i, p; + + // Check for IBAN code length. + // It contains: + // country code ISO 3166-1 - two letters, + // two check digits, + // Basic Bank Account Number (BBAN) - up to 30 chars + var minimalIBANlength = 5; + if ( iban.length < minimalIBANlength ) { + return false; + } + + // Check the country code and find the country specific format + countrycode = iban.substring( 0, 2 ); + bbancountrypatterns = { + "AL": "\\d{8}[\\dA-Z]{16}", + "AD": "\\d{8}[\\dA-Z]{12}", + "AT": "\\d{16}", + "AZ": "[\\dA-Z]{4}\\d{20}", + "BE": "\\d{12}", + "BH": "[A-Z]{4}[\\dA-Z]{14}", + "BA": "\\d{16}", + "BR": "\\d{23}[A-Z][\\dA-Z]", + "BG": "[A-Z]{4}\\d{6}[\\dA-Z]{8}", + "CR": "\\d{17}", + "HR": "\\d{17}", + "CY": "\\d{8}[\\dA-Z]{16}", + "CZ": "\\d{20}", + "DK": "\\d{14}", + "DO": "[A-Z]{4}\\d{20}", + "EE": "\\d{16}", + "FO": "\\d{14}", + "FI": "\\d{14}", + "FR": "\\d{10}[\\dA-Z]{11}\\d{2}", + "GE": "[\\dA-Z]{2}\\d{16}", + "DE": "\\d{18}", + "GI": "[A-Z]{4}[\\dA-Z]{15}", + "GR": "\\d{7}[\\dA-Z]{16}", + "GL": "\\d{14}", + "GT": "[\\dA-Z]{4}[\\dA-Z]{20}", + "HU": "\\d{24}", + "IS": "\\d{22}", + "IE": "[\\dA-Z]{4}\\d{14}", + "IL": "\\d{19}", + "IT": "[A-Z]\\d{10}[\\dA-Z]{12}", + "KZ": "\\d{3}[\\dA-Z]{13}", + "KW": "[A-Z]{4}[\\dA-Z]{22}", + "LV": "[A-Z]{4}[\\dA-Z]{13}", + "LB": "\\d{4}[\\dA-Z]{20}", + "LI": "\\d{5}[\\dA-Z]{12}", + "LT": "\\d{16}", + "LU": "\\d{3}[\\dA-Z]{13}", + "MK": "\\d{3}[\\dA-Z]{10}\\d{2}", + "MT": "[A-Z]{4}\\d{5}[\\dA-Z]{18}", + "MR": "\\d{23}", + "MU": "[A-Z]{4}\\d{19}[A-Z]{3}", + "MC": "\\d{10}[\\dA-Z]{11}\\d{2}", + "MD": "[\\dA-Z]{2}\\d{18}", + "ME": "\\d{18}", + "NL": "[A-Z]{4}\\d{10}", + "NO": "\\d{11}", + "PK": "[\\dA-Z]{4}\\d{16}", + "PS": "[\\dA-Z]{4}\\d{21}", + "PL": "\\d{24}", + "PT": "\\d{21}", + "RO": "[A-Z]{4}[\\dA-Z]{16}", + "SM": "[A-Z]\\d{10}[\\dA-Z]{12}", + "SA": "\\d{2}[\\dA-Z]{18}", + "RS": "\\d{18}", + "SK": "\\d{20}", + "SI": "\\d{15}", + "ES": "\\d{20}", + "SE": "\\d{20}", + "CH": "\\d{5}[\\dA-Z]{12}", + "TN": "\\d{20}", + "TR": "\\d{5}[\\dA-Z]{17}", + "AE": "\\d{3}\\d{16}", + "GB": "[A-Z]{4}\\d{14}", + "VG": "[\\dA-Z]{4}\\d{16}" + }; + + bbanpattern = bbancountrypatterns[ countrycode ]; + + // As new countries will start using IBAN in the + // future, we only check if the countrycode is known. + // This prevents false negatives, while almost all + // false positives introduced by this, will be caught + // by the checksum validation below anyway. + // Strict checking should return FALSE for unknown + // countries. + if ( typeof bbanpattern !== "undefined" ) { + ibanregexp = new RegExp( "^[A-Z]{2}\\d{2}" + bbanpattern + "$", "" ); + if ( !( ibanregexp.test( iban ) ) ) { + return false; // Invalid country specific format + } + } + + // Now check the checksum, first convert to digits + ibancheck = iban.substring( 4, iban.length ) + iban.substring( 0, 4 ); + for ( i = 0; i < ibancheck.length; i++ ) { + charAt = ibancheck.charAt( i ); + if ( charAt !== "0" ) { + leadingZeroes = false; + } + if ( !leadingZeroes ) { + ibancheckdigits += "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ".indexOf( charAt ); + } + } + + // Calculate the result of: ibancheckdigits % 97 + for ( p = 0; p < ibancheckdigits.length; p++ ) { + cChar = ibancheckdigits.charAt( p ); + cOperator = "" + cRest + "" + cChar; + cRest = cOperator % 97; + } + return cRest === 1; +}, "Please specify a valid IBAN" ); + +$.validator.addMethod( "integer", function( value, element ) { + return this.optional( element ) || /^-?\d+$/.test( value ); +}, "A positive or negative non-decimal number please" ); + +$.validator.addMethod( "ipv4", function( value, element ) { + return this.optional( element ) || /^(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)$/i.test( value ); +}, "Please enter a valid IP v4 address." ); + +$.validator.addMethod( "ipv6", function( value, element ) { + return this.optional( element ) || /^((([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}:[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){5}:([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(([0-9A-Fa-f]{1,4}:){0,5}:((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(::([0-9A-Fa-f]{1,4}:){0,5}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|([0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})|(::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){1,7}:))$/i.test( value ); +}, "Please enter a valid IP v6 address." ); + +$.validator.addMethod( "lettersonly", function( value, element ) { + return this.optional( element ) || /^[a-z]+$/i.test( value ); +}, "Letters only please" ); + +$.validator.addMethod( "letterswithbasicpunc", function( value, element ) { + return this.optional( element ) || /^[a-z\-.,()'"\s]+$/i.test( value ); +}, "Letters or punctuation only please" ); + +$.validator.addMethod( "mobileNL", function( value, element ) { + return this.optional( element ) || /^((\+|00(\s|\s?\-\s?)?)31(\s|\s?\-\s?)?(\(0\)[\-\s]?)?|0)6((\s|\s?\-\s?)?[0-9]){8}$/.test( value ); +}, "Please specify a valid mobile number" ); + +/* For UK phone functions, do the following server side processing: + * Compare original input with this RegEx pattern: + * ^\(?(?:(?:00\)?[\s\-]?\(?|\+)(44)\)?[\s\-]?\(?(?:0\)?[\s\-]?\(?)?|0)([1-9]\d{1,4}\)?[\s\d\-]+)$ + * Extract $1 and set $prefix to '+44' if $1 is '44', otherwise set $prefix to '0' + * Extract $2 and remove hyphens, spaces and parentheses. Phone number is combined $prefix and $2. + * A number of very detailed GB telephone number RegEx patterns can also be found at: + * http://www.aa-asterisk.org.uk/index.php/Regular_Expressions_for_Validating_and_Formatting_GB_Telephone_Numbers + */ +$.validator.addMethod( "mobileUK", function( phone_number, element ) { + phone_number = phone_number.replace( /\(|\)|\s+|-/g, "" ); + return this.optional( element ) || phone_number.length > 9 && + phone_number.match( /^(?:(?:(?:00\s?|\+)44\s?|0)7(?:[1345789]\d{2}|624)\s?\d{3}\s?\d{3})$/ ); +}, "Please specify a valid mobile number" ); + +$.validator.addMethod( "netmask", function( value, element ) { + return this.optional( element ) || /^(254|252|248|240|224|192|128)\.0\.0\.0|255\.(254|252|248|240|224|192|128|0)\.0\.0|255\.255\.(254|252|248|240|224|192|128|0)\.0|255\.255\.255\.(254|252|248|240|224|192|128|0)/i.test( value ); +}, "Please enter a valid netmask." ); + +/* + * The NIE (Número de Identificación de Extranjero) is a Spanish tax identification number assigned by the Spanish + * authorities to any foreigner. + * + * The NIE is the equivalent of a Spaniards Número de Identificación Fiscal (NIF) which serves as a fiscal + * identification number. The CIF number (Certificado de Identificación Fiscal) is equivalent to the NIF, but applies to + * companies rather than individuals. The NIE consists of an 'X' or 'Y' followed by 7 or 8 digits then another letter. + */ +$.validator.addMethod( "nieES", function( value, element ) { + "use strict"; + + if ( this.optional( element ) ) { + return true; + } + + var nieRegEx = new RegExp( /^[MXYZ]{1}[0-9]{7,8}[TRWAGMYFPDXBNJZSQVHLCKET]{1}$/gi ); + var validChars = "TRWAGMYFPDXBNJZSQVHLCKET", + letter = value.substr( value.length - 1 ).toUpperCase(), + number; + + value = value.toString().toUpperCase(); + + // Quick format test + if ( value.length > 10 || value.length < 9 || !nieRegEx.test( value ) ) { + return false; + } + + // X means same number + // Y means number + 10000000 + // Z means number + 20000000 + value = value.replace( /^[X]/, "0" ) + .replace( /^[Y]/, "1" ) + .replace( /^[Z]/, "2" ); + + number = value.length === 9 ? value.substr( 0, 8 ) : value.substr( 0, 9 ); + + return validChars.charAt( parseInt( number, 10 ) % 23 ) === letter; + +}, "Please specify a valid NIE number." ); + +/* + * The Número de Identificación Fiscal ( NIF ) is the way tax identification used in Spain for individuals + */ +$.validator.addMethod( "nifES", function( value, element ) { + "use strict"; + + if ( this.optional( element ) ) { + return true; + } + + value = value.toUpperCase(); + + // Basic format test + if ( !value.match( "((^[A-Z]{1}[0-9]{7}[A-Z0-9]{1}$|^[T]{1}[A-Z0-9]{8}$)|^[0-9]{8}[A-Z]{1}$)" ) ) { + return false; + } + + // Test NIF + if ( /^[0-9]{8}[A-Z]{1}$/.test( value ) ) { + return ( "TRWAGMYFPDXBNJZSQVHLCKE".charAt( value.substring( 8, 0 ) % 23 ) === value.charAt( 8 ) ); + } + + // Test specials NIF (starts with K, L or M) + if ( /^[KLM]{1}/.test( value ) ) { + return ( value[ 8 ] === "TRWAGMYFPDXBNJZSQVHLCKE".charAt( value.substring( 8, 1 ) % 23 ) ); + } + + return false; + +}, "Please specify a valid NIF number." ); + +/* + * Numer identyfikacji podatkowej ( NIP ) is the way tax identification used in Poland for companies + */ +$.validator.addMethod( "nipPL", function( value ) { + "use strict"; + + value = value.replace( /[^0-9]/g, "" ); + + if ( value.length !== 10 ) { + return false; + } + + var arrSteps = [ 6, 5, 7, 2, 3, 4, 5, 6, 7 ]; + var intSum = 0; + for ( var i = 0; i < 9; i++ ) { + intSum += arrSteps[ i ] * value[ i ]; + } + var int2 = intSum % 11; + var intControlNr = ( int2 === 10 ) ? 0 : int2; + + return ( intControlNr === parseInt( value[ 9 ], 10 ) ); +}, "Please specify a valid NIP number." ); + +$.validator.addMethod( "notEqualTo", function( value, element, param ) { + return this.optional( element ) || !$.validator.methods.equalTo.call( this, value, element, param ); +}, "Please enter a different value, values must not be the same." ); + +$.validator.addMethod( "nowhitespace", function( value, element ) { + return this.optional( element ) || /^\S+$/i.test( value ); +}, "No white space please" ); + +/** +* Return true if the field value matches the given format RegExp +* +* @example $.validator.methods.pattern("AR1004",element,/^AR\d{4}$/) +* @result true +* +* @example $.validator.methods.pattern("BR1004",element,/^AR\d{4}$/) +* @result false +* +* @name $.validator.methods.pattern +* @type Boolean +* @cat Plugins/Validate/Methods +*/ +$.validator.addMethod( "pattern", function( value, element, param ) { + if ( this.optional( element ) ) { + return true; + } + if ( typeof param === "string" ) { + param = new RegExp( "^(?:" + param + ")$" ); + } + return param.test( value ); +}, "Invalid format." ); + +/** + * Dutch phone numbers have 10 digits (or 11 and start with +31). + */ +$.validator.addMethod( "phoneNL", function( value, element ) { + return this.optional( element ) || /^((\+|00(\s|\s?\-\s?)?)31(\s|\s?\-\s?)?(\(0\)[\-\s]?)?|0)[1-9]((\s|\s?\-\s?)?[0-9]){8}$/.test( value ); +}, "Please specify a valid phone number." ); + +/* For UK phone functions, do the following server side processing: + * Compare original input with this RegEx pattern: + * ^\(?(?:(?:00\)?[\s\-]?\(?|\+)(44)\)?[\s\-]?\(?(?:0\)?[\s\-]?\(?)?|0)([1-9]\d{1,4}\)?[\s\d\-]+)$ + * Extract $1 and set $prefix to '+44' if $1 is '44', otherwise set $prefix to '0' + * Extract $2 and remove hyphens, spaces and parentheses. Phone number is combined $prefix and $2. + * A number of very detailed GB telephone number RegEx patterns can also be found at: + * http://www.aa-asterisk.org.uk/index.php/Regular_Expressions_for_Validating_and_Formatting_GB_Telephone_Numbers + */ + +// Matches UK landline + mobile, accepting only 01-3 for landline or 07 for mobile to exclude many premium numbers +$.validator.addMethod( "phonesUK", function( phone_number, element ) { + phone_number = phone_number.replace( /\(|\)|\s+|-/g, "" ); + return this.optional( element ) || phone_number.length > 9 && + phone_number.match( /^(?:(?:(?:00\s?|\+)44\s?|0)(?:1\d{8,9}|[23]\d{9}|7(?:[1345789]\d{8}|624\d{6})))$/ ); +}, "Please specify a valid uk phone number" ); + +/* For UK phone functions, do the following server side processing: + * Compare original input with this RegEx pattern: + * ^\(?(?:(?:00\)?[\s\-]?\(?|\+)(44)\)?[\s\-]?\(?(?:0\)?[\s\-]?\(?)?|0)([1-9]\d{1,4}\)?[\s\d\-]+)$ + * Extract $1 and set $prefix to '+44' if $1 is '44', otherwise set $prefix to '0' + * Extract $2 and remove hyphens, spaces and parentheses. Phone number is combined $prefix and $2. + * A number of very detailed GB telephone number RegEx patterns can also be found at: + * http://www.aa-asterisk.org.uk/index.php/Regular_Expressions_for_Validating_and_Formatting_GB_Telephone_Numbers + */ +$.validator.addMethod( "phoneUK", function( phone_number, element ) { + phone_number = phone_number.replace( /\(|\)|\s+|-/g, "" ); + return this.optional( element ) || phone_number.length > 9 && + phone_number.match( /^(?:(?:(?:00\s?|\+)44\s?)|(?:\(?0))(?:\d{2}\)?\s?\d{4}\s?\d{4}|\d{3}\)?\s?\d{3}\s?\d{3,4}|\d{4}\)?\s?(?:\d{5}|\d{3}\s?\d{3})|\d{5}\)?\s?\d{4,5})$/ ); +}, "Please specify a valid phone number" ); + +/** + * Matches US phone number format + * + * where the area code may not start with 1 and the prefix may not start with 1 + * allows '-' or ' ' as a separator and allows parens around area code + * some people may want to put a '1' in front of their number + * + * 1(212)-999-2345 or + * 212 999 2344 or + * 212-999-0983 + * + * but not + * 111-123-5434 + * and not + * 212 123 4567 + */ +$.validator.addMethod( "phoneUS", function( phone_number, element ) { + phone_number = phone_number.replace( /\s+/g, "" ); + return this.optional( element ) || phone_number.length > 9 && + phone_number.match( /^(\+?1-?)?(\([2-9]([02-9]\d|1[02-9])\)|[2-9]([02-9]\d|1[02-9]))-?[2-9]([02-9]\d|1[02-9])-?\d{4}$/ ); +}, "Please specify a valid phone number" ); + +/* +* Valida CEPs do brasileiros: +* +* Formatos aceitos: +* 99999-999 +* 99.999-999 +* 99999999 +*/ +$.validator.addMethod( "postalcodeBR", function( cep_value, element ) { + return this.optional( element ) || /^\d{2}.\d{3}-\d{3}?$|^\d{5}-?\d{3}?$/.test( cep_value ); +}, "Informe um CEP válido." ); + +/** + * Matches a valid Canadian Postal Code + * + * @example jQuery.validator.methods.postalCodeCA( "H0H 0H0", element ) + * @result true + * + * @example jQuery.validator.methods.postalCodeCA( "H0H0H0", element ) + * @result false + * + * @name jQuery.validator.methods.postalCodeCA + * @type Boolean + * @cat Plugins/Validate/Methods + */ +$.validator.addMethod( "postalCodeCA", function( value, element ) { + return this.optional( element ) || /^[ABCEGHJKLMNPRSTVXY]\d[ABCEGHJKLMNPRSTVWXYZ] *\d[ABCEGHJKLMNPRSTVWXYZ]\d$/i.test( value ); +}, "Please specify a valid postal code" ); + +/* Matches Italian postcode (CAP) */ +$.validator.addMethod( "postalcodeIT", function( value, element ) { + return this.optional( element ) || /^\d{5}$/.test( value ); +}, "Please specify a valid postal code" ); + +$.validator.addMethod( "postalcodeNL", function( value, element ) { + return this.optional( element ) || /^[1-9][0-9]{3}\s?[a-zA-Z]{2}$/.test( value ); +}, "Please specify a valid postal code" ); + +// Matches UK postcode. Does not match to UK Channel Islands that have their own postcodes (non standard UK) +$.validator.addMethod( "postcodeUK", function( value, element ) { + return this.optional( element ) || /^((([A-PR-UWYZ][0-9])|([A-PR-UWYZ][0-9][0-9])|([A-PR-UWYZ][A-HK-Y][0-9])|([A-PR-UWYZ][A-HK-Y][0-9][0-9])|([A-PR-UWYZ][0-9][A-HJKSTUW])|([A-PR-UWYZ][A-HK-Y][0-9][ABEHMNPRVWXY]))\s?([0-9][ABD-HJLNP-UW-Z]{2})|(GIR)\s?(0AA))$/i.test( value ); +}, "Please specify a valid UK postcode" ); + +/* + * Lets you say "at least X inputs that match selector Y must be filled." + * + * The end result is that neither of these inputs: + * + * + * + * + * ...will validate unless at least one of them is filled. + * + * partnumber: {require_from_group: [1,".productinfo"]}, + * description: {require_from_group: [1,".productinfo"]} + * + * options[0]: number of fields that must be filled in the group + * options[1]: CSS selector that defines the group of conditionally required fields + */ +$.validator.addMethod( "require_from_group", function( value, element, options ) { + var $fields = $( options[ 1 ], element.form ), + $fieldsFirst = $fields.eq( 0 ), + validator = $fieldsFirst.data( "valid_req_grp" ) ? $fieldsFirst.data( "valid_req_grp" ) : $.extend( {}, this ), + isValid = $fields.filter( function() { + return validator.elementValue( this ); + } ).length >= options[ 0 ]; + + // Store the cloned validator for future validation + $fieldsFirst.data( "valid_req_grp", validator ); + + // If element isn't being validated, run each require_from_group field's validation rules + if ( !$( element ).data( "being_validated" ) ) { + $fields.data( "being_validated", true ); + $fields.each( function() { + validator.element( this ); + } ); + $fields.data( "being_validated", false ); + } + return isValid; +}, $.validator.format( "Please fill at least {0} of these fields." ) ); + +/* + * Lets you say "either at least X inputs that match selector Y must be filled, + * OR they must all be skipped (left blank)." + * + * The end result, is that none of these inputs: + * + * + * + * + * + * ...will validate unless either at least two of them are filled, + * OR none of them are. + * + * partnumber: {skip_or_fill_minimum: [2,".productinfo"]}, + * description: {skip_or_fill_minimum: [2,".productinfo"]}, + * color: {skip_or_fill_minimum: [2,".productinfo"]} + * + * options[0]: number of fields that must be filled in the group + * options[1]: CSS selector that defines the group of conditionally required fields + * + */ +$.validator.addMethod( "skip_or_fill_minimum", function( value, element, options ) { + var $fields = $( options[ 1 ], element.form ), + $fieldsFirst = $fields.eq( 0 ), + validator = $fieldsFirst.data( "valid_skip" ) ? $fieldsFirst.data( "valid_skip" ) : $.extend( {}, this ), + numberFilled = $fields.filter( function() { + return validator.elementValue( this ); + } ).length, + isValid = numberFilled === 0 || numberFilled >= options[ 0 ]; + + // Store the cloned validator for future validation + $fieldsFirst.data( "valid_skip", validator ); + + // If element isn't being validated, run each skip_or_fill_minimum field's validation rules + if ( !$( element ).data( "being_validated" ) ) { + $fields.data( "being_validated", true ); + $fields.each( function() { + validator.element( this ); + } ); + $fields.data( "being_validated", false ); + } + return isValid; +}, $.validator.format( "Please either skip these fields or fill at least {0} of them." ) ); + +/* Validates US States and/or Territories by @jdforsythe + * Can be case insensitive or require capitalization - default is case insensitive + * Can include US Territories or not - default does not + * Can include US Military postal abbreviations (AA, AE, AP) - default does not + * + * Note: "States" always includes DC (District of Colombia) + * + * Usage examples: + * + * This is the default - case insensitive, no territories, no military zones + * stateInput: { + * caseSensitive: false, + * includeTerritories: false, + * includeMilitary: false + * } + * + * Only allow capital letters, no territories, no military zones + * stateInput: { + * caseSensitive: false + * } + * + * Case insensitive, include territories but not military zones + * stateInput: { + * includeTerritories: true + * } + * + * Only allow capital letters, include territories and military zones + * stateInput: { + * caseSensitive: true, + * includeTerritories: true, + * includeMilitary: true + * } + * + */ +$.validator.addMethod( "stateUS", function( value, element, options ) { + var isDefault = typeof options === "undefined", + caseSensitive = ( isDefault || typeof options.caseSensitive === "undefined" ) ? false : options.caseSensitive, + includeTerritories = ( isDefault || typeof options.includeTerritories === "undefined" ) ? false : options.includeTerritories, + includeMilitary = ( isDefault || typeof options.includeMilitary === "undefined" ) ? false : options.includeMilitary, + regex; + + if ( !includeTerritories && !includeMilitary ) { + regex = "^(A[KLRZ]|C[AOT]|D[CE]|FL|GA|HI|I[ADLN]|K[SY]|LA|M[ADEINOST]|N[CDEHJMVY]|O[HKR]|PA|RI|S[CD]|T[NX]|UT|V[AT]|W[AIVY])$"; + } else if ( includeTerritories && includeMilitary ) { + regex = "^(A[AEKLPRSZ]|C[AOT]|D[CE]|FL|G[AU]|HI|I[ADLN]|K[SY]|LA|M[ADEINOPST]|N[CDEHJMVY]|O[HKR]|P[AR]|RI|S[CD]|T[NX]|UT|V[AIT]|W[AIVY])$"; + } else if ( includeTerritories ) { + regex = "^(A[KLRSZ]|C[AOT]|D[CE]|FL|G[AU]|HI|I[ADLN]|K[SY]|LA|M[ADEINOPST]|N[CDEHJMVY]|O[HKR]|P[AR]|RI|S[CD]|T[NX]|UT|V[AIT]|W[AIVY])$"; + } else { + regex = "^(A[AEKLPRZ]|C[AOT]|D[CE]|FL|GA|HI|I[ADLN]|K[SY]|LA|M[ADEINOST]|N[CDEHJMVY]|O[HKR]|PA|RI|S[CD]|T[NX]|UT|V[AT]|W[AIVY])$"; + } + + regex = caseSensitive ? new RegExp( regex ) : new RegExp( regex, "i" ); + return this.optional( element ) || regex.test( value ); +}, "Please specify a valid state" ); + +// TODO check if value starts with <, otherwise don't try stripping anything +$.validator.addMethod( "strippedminlength", function( value, element, param ) { + return $( value ).text().length >= param; +}, $.validator.format( "Please enter at least {0} characters" ) ); + +$.validator.addMethod( "time", function( value, element ) { + return this.optional( element ) || /^([01]\d|2[0-3]|[0-9])(:[0-5]\d){1,2}$/.test( value ); +}, "Please enter a valid time, between 00:00 and 23:59" ); + +$.validator.addMethod( "time12h", function( value, element ) { + return this.optional( element ) || /^((0?[1-9]|1[012])(:[0-5]\d){1,2}(\ ?[AP]M))$/i.test( value ); +}, "Please enter a valid time in 12-hour am/pm format" ); + +// Same as url, but TLD is optional +$.validator.addMethod( "url2", function( value, element ) { + return this.optional( element ) || /^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)*(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i.test( value ); +}, $.validator.messages.url ); + +/** + * Return true, if the value is a valid vehicle identification number (VIN). + * + * Works with all kind of text inputs. + * + * @example + * @desc Declares a required input element whose value must be a valid vehicle identification number. + * + * @name $.validator.methods.vinUS + * @type Boolean + * @cat Plugins/Validate/Methods + */ +$.validator.addMethod( "vinUS", function( v ) { + if ( v.length !== 17 ) { + return false; + } + + var LL = [ "A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" ], + VL = [ 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 7, 9, 2, 3, 4, 5, 6, 7, 8, 9 ], + FL = [ 8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2 ], + rs = 0, + i, n, d, f, cd, cdv; + + for ( i = 0; i < 17; i++ ) { + f = FL[ i ]; + d = v.slice( i, i + 1 ); + if ( i === 8 ) { + cdv = d; + } + if ( !isNaN( d ) ) { + d *= f; + } else { + for ( n = 0; n < LL.length; n++ ) { + if ( d.toUpperCase() === LL[ n ] ) { + d = VL[ n ]; + d *= f; + if ( isNaN( cdv ) && n === 8 ) { + cdv = LL[ n ]; + } + break; + } + } + } + rs += d; + } + cd = rs % 11; + if ( cd === 10 ) { + cd = "X"; + } + if ( cd === cdv ) { + return true; + } + return false; +}, "The specified vehicle identification number (VIN) is invalid." ); + +$.validator.addMethod( "zipcodeUS", function( value, element ) { + return this.optional( element ) || /^\d{5}(-\d{4})?$/.test( value ); +}, "The specified US ZIP Code is invalid" ); + +$.validator.addMethod( "ziprange", function( value, element ) { + return this.optional( element ) || /^90[2-5]\d\{2\}-\d{4}$/.test( value ); +}, "Your ZIP-code must be in the range 902xx-xxxx to 905xx-xxxx" ); +return $; })); \ No newline at end of file diff --git a/web-app-aspnet/wwwroot/lib/jquery-validation/dist/additional-methods.min.js b/1-web-apps/web-app-aspnetcore/wwwroot/lib/jquery-validation/dist/additional-methods.min.js similarity index 99% rename from web-app-aspnet/wwwroot/lib/jquery-validation/dist/additional-methods.min.js rename to 1-web-apps/web-app-aspnetcore/wwwroot/lib/jquery-validation/dist/additional-methods.min.js index 5d127b8..6767f24 100644 --- a/web-app-aspnet/wwwroot/lib/jquery-validation/dist/additional-methods.min.js +++ b/1-web-apps/web-app-aspnetcore/wwwroot/lib/jquery-validation/dist/additional-methods.min.js @@ -1,4 +1,4 @@ -/*! jQuery Validation Plugin - v1.17.0 - 7/29/2017 - * https://jqueryvalidation.org/ - * Copyright (c) 2017 Jörn Zaefferer; Licensed MIT */ +/*! jQuery Validation Plugin - v1.17.0 - 7/29/2017 + * https://jqueryvalidation.org/ + * Copyright (c) 2017 Jörn Zaefferer; Licensed MIT */ !function(a){"function"==typeof define&&define.amd?define(["jquery","./jquery.validate.min"],a):"object"==typeof module&&module.exports?module.exports=a(require("jquery")):a(jQuery)}(function(a){return function(){function b(a){return a.replace(/<.[^<>]*?>/g," ").replace(/ | /gi," ").replace(/[.(),;:!?%#$'\"_+=\/\-“”’]*/g,"")}a.validator.addMethod("maxWords",function(a,c,d){return this.optional(c)||b(a).match(/\b\w+\b/g).length<=d},a.validator.format("Please enter {0} words or less.")),a.validator.addMethod("minWords",function(a,c,d){return this.optional(c)||b(a).match(/\b\w+\b/g).length>=d},a.validator.format("Please enter at least {0} words.")),a.validator.addMethod("rangeWords",function(a,c,d){var e=b(a),f=/\b\w+\b/g;return this.optional(c)||e.match(f).length>=d[0]&&e.match(f).length<=d[1]},a.validator.format("Please enter between {0} and {1} words."))}(),a.validator.addMethod("accept",function(b,c,d){var e,f,g,h="string"==typeof d?d.replace(/\s/g,""):"image/*",i=this.optional(c);if(i)return i;if("file"===a(c).attr("type")&&(h=h.replace(/[\-\[\]\/\{\}\(\)\+\?\.\\\^\$\|]/g,"\\$&").replace(/,/g,"|").replace(/\/\*/g,"/.*"),c.files&&c.files.length))for(g=new RegExp(".?("+h+")$","i"),e=0;e9?"0":f,g="JABCDEFGHI".substr(f,1).toString(),i.match(/[ABEH]/)?k===f:i.match(/[KPQS]/)?k===g:k===f||k===g},"Please specify a valid CIF number."),a.validator.addMethod("cpfBR",function(a){if(a=a.replace(/([~!@#$%^&*()_+=`{}\[\]\-|\\:;'<>,.\/? ])+/g,""),11!==a.length)return!1;var b,c,d,e,f=0;if(b=parseInt(a.substring(9,10),10),c=parseInt(a.substring(10,11),10),d=function(a,b){var c=10*a%11;return 10!==c&&11!==c||(c=0),c===b},""===a||"00000000000"===a||"11111111111"===a||"22222222222"===a||"33333333333"===a||"44444444444"===a||"55555555555"===a||"66666666666"===a||"77777777777"===a||"88888888888"===a||"99999999999"===a)return!1;for(e=1;e<=9;e++)f+=parseInt(a.substring(e-1,e),10)*(11-e);if(d(f,b)){for(f=0,e=1;e<=10;e++)f+=parseInt(a.substring(e-1,e),10)*(12-e);return d(f,c)}return!1},"Please specify a valid CPF number"),a.validator.addMethod("creditcard",function(a,b){if(this.optional(b))return"dependency-mismatch";if(/[^0-9 \-]+/.test(a))return!1;var c,d,e=0,f=0,g=!1;if(a=a.replace(/\D/g,""),a.length<13||a.length>19)return!1;for(c=a.length-1;c>=0;c--)d=a.charAt(c),f=parseInt(d,10),g&&(f*=2)>9&&(f-=9),e+=f,g=!g;return e%10===0},"Please enter a valid credit card number."),a.validator.addMethod("creditcardtypes",function(a,b,c){if(/[^0-9\-]+/.test(a))return!1;a=a.replace(/\D/g,"");var d=0;return c.mastercard&&(d|=1),c.visa&&(d|=2),c.amex&&(d|=4),c.dinersclub&&(d|=8),c.enroute&&(d|=16),c.discover&&(d|=32),c.jcb&&(d|=64),c.unknown&&(d|=128),c.all&&(d=255),1&d&&/^(5[12345])/.test(a)?16===a.length:2&d&&/^(4)/.test(a)?16===a.length:4&d&&/^(3[47])/.test(a)?15===a.length:8&d&&/^(3(0[012345]|[68]))/.test(a)?14===a.length:16&d&&/^(2(014|149))/.test(a)?15===a.length:32&d&&/^(6011)/.test(a)?16===a.length:64&d&&/^(3)/.test(a)?16===a.length:64&d&&/^(2131|1800)/.test(a)?15===a.length:!!(128&d)},"Please enter a valid credit card number."),a.validator.addMethod("currency",function(a,b,c){var d,e="string"==typeof c,f=e?c:c[0],g=!!e||c[1];return f=f.replace(/,/g,""),f=g?f+"]":f+"]?",d="^["+f+"([1-9]{1}[0-9]{0,2}(\\,[0-9]{3})*(\\.[0-9]{0,2})?|[1-9]{1}[0-9]{0,}(\\.[0-9]{0,2})?|0(\\.[0-9]{0,2})?|(\\.[0-9]{1,2})?)$",d=new RegExp(d),this.optional(b)||d.test(a)},"Please specify a valid currency"),a.validator.addMethod("dateFA",function(a,b){return this.optional(b)||/^[1-4]\d{3}\/((0?[1-6]\/((3[0-1])|([1-2][0-9])|(0?[1-9])))|((1[0-2]|(0?[7-9]))\/(30|([1-2][0-9])|(0?[1-9]))))$/.test(a)},a.validator.messages.date),a.validator.addMethod("dateITA",function(a,b){var c,d,e,f,g,h=!1,i=/^\d{1,2}\/\d{1,2}\/\d{4}$/;return i.test(a)?(c=a.split("/"),d=parseInt(c[0],10),e=parseInt(c[1],10),f=parseInt(c[2],10),g=new Date(Date.UTC(f,e-1,d,12,0,0,0)),h=g.getUTCFullYear()===f&&g.getUTCMonth()===e-1&&g.getUTCDate()===d):h=!1,this.optional(b)||h},a.validator.messages.date),a.validator.addMethod("dateNL",function(a,b){return this.optional(b)||/^(0?[1-9]|[12]\d|3[01])[\.\/\-](0?[1-9]|1[012])[\.\/\-]([12]\d)?(\d\d)$/.test(a)},a.validator.messages.date),a.validator.addMethod("extension",function(a,b,c){return c="string"==typeof c?c.replace(/,/g,"|"):"png|jpe?g|gif",this.optional(b)||a.match(new RegExp("\\.("+c+")$","i"))},a.validator.format("Please enter a value with a valid extension.")),a.validator.addMethod("giroaccountNL",function(a,b){return this.optional(b)||/^[0-9]{1,7}$/.test(a)},"Please specify a valid giro account number"),a.validator.addMethod("iban",function(a,b){if(this.optional(b))return!0;var c,d,e,f,g,h,i,j,k,l=a.replace(/ /g,"").toUpperCase(),m="",n=!0,o="",p="",q=5;if(l.length9&&a.match(/^(?:(?:(?:00\s?|\+)44\s?|0)7(?:[1345789]\d{2}|624)\s?\d{3}\s?\d{3})$/)},"Please specify a valid mobile number"),a.validator.addMethod("netmask",function(a,b){return this.optional(b)||/^(254|252|248|240|224|192|128)\.0\.0\.0|255\.(254|252|248|240|224|192|128|0)\.0\.0|255\.255\.(254|252|248|240|224|192|128|0)\.0|255\.255\.255\.(254|252|248|240|224|192|128|0)/i.test(a)},"Please enter a valid netmask."),a.validator.addMethod("nieES",function(a,b){"use strict";if(this.optional(b))return!0;var c,d=new RegExp(/^[MXYZ]{1}[0-9]{7,8}[TRWAGMYFPDXBNJZSQVHLCKET]{1}$/gi),e="TRWAGMYFPDXBNJZSQVHLCKET",f=a.substr(a.length-1).toUpperCase();return a=a.toString().toUpperCase(),!(a.length>10||a.length<9||!d.test(a))&&(a=a.replace(/^[X]/,"0").replace(/^[Y]/,"1").replace(/^[Z]/,"2"),c=9===a.length?a.substr(0,8):a.substr(0,9),e.charAt(parseInt(c,10)%23)===f)},"Please specify a valid NIE number."),a.validator.addMethod("nifES",function(a,b){"use strict";return!!this.optional(b)||(a=a.toUpperCase(),!!a.match("((^[A-Z]{1}[0-9]{7}[A-Z0-9]{1}$|^[T]{1}[A-Z0-9]{8}$)|^[0-9]{8}[A-Z]{1}$)")&&(/^[0-9]{8}[A-Z]{1}$/.test(a)?"TRWAGMYFPDXBNJZSQVHLCKE".charAt(a.substring(8,0)%23)===a.charAt(8):!!/^[KLM]{1}/.test(a)&&a[8]==="TRWAGMYFPDXBNJZSQVHLCKE".charAt(a.substring(8,1)%23)))},"Please specify a valid NIF number."),a.validator.addMethod("nipPL",function(a){"use strict";if(a=a.replace(/[^0-9]/g,""),10!==a.length)return!1;for(var b=[6,5,7,2,3,4,5,6,7],c=0,d=0;d<9;d++)c+=b[d]*a[d];var e=c%11,f=10===e?0:e;return f===parseInt(a[9],10)},"Please specify a valid NIP number."),a.validator.addMethod("notEqualTo",function(b,c,d){return this.optional(c)||!a.validator.methods.equalTo.call(this,b,c,d)},"Please enter a different value, values must not be the same."),a.validator.addMethod("nowhitespace",function(a,b){return this.optional(b)||/^\S+$/i.test(a)},"No white space please"),a.validator.addMethod("pattern",function(a,b,c){return!!this.optional(b)||("string"==typeof c&&(c=new RegExp("^(?:"+c+")$")),c.test(a))},"Invalid format."),a.validator.addMethod("phoneNL",function(a,b){return this.optional(b)||/^((\+|00(\s|\s?\-\s?)?)31(\s|\s?\-\s?)?(\(0\)[\-\s]?)?|0)[1-9]((\s|\s?\-\s?)?[0-9]){8}$/.test(a)},"Please specify a valid phone number."),a.validator.addMethod("phonesUK",function(a,b){return a=a.replace(/\(|\)|\s+|-/g,""),this.optional(b)||a.length>9&&a.match(/^(?:(?:(?:00\s?|\+)44\s?|0)(?:1\d{8,9}|[23]\d{9}|7(?:[1345789]\d{8}|624\d{6})))$/)},"Please specify a valid uk phone number"),a.validator.addMethod("phoneUK",function(a,b){return a=a.replace(/\(|\)|\s+|-/g,""),this.optional(b)||a.length>9&&a.match(/^(?:(?:(?:00\s?|\+)44\s?)|(?:\(?0))(?:\d{2}\)?\s?\d{4}\s?\d{4}|\d{3}\)?\s?\d{3}\s?\d{3,4}|\d{4}\)?\s?(?:\d{5}|\d{3}\s?\d{3})|\d{5}\)?\s?\d{4,5})$/)},"Please specify a valid phone number"),a.validator.addMethod("phoneUS",function(a,b){return a=a.replace(/\s+/g,""),this.optional(b)||a.length>9&&a.match(/^(\+?1-?)?(\([2-9]([02-9]\d|1[02-9])\)|[2-9]([02-9]\d|1[02-9]))-?[2-9]([02-9]\d|1[02-9])-?\d{4}$/)},"Please specify a valid phone number"),a.validator.addMethod("postalcodeBR",function(a,b){return this.optional(b)||/^\d{2}.\d{3}-\d{3}?$|^\d{5}-?\d{3}?$/.test(a)},"Informe um CEP válido."),a.validator.addMethod("postalCodeCA",function(a,b){return this.optional(b)||/^[ABCEGHJKLMNPRSTVXY]\d[ABCEGHJKLMNPRSTVWXYZ] *\d[ABCEGHJKLMNPRSTVWXYZ]\d$/i.test(a)},"Please specify a valid postal code"),a.validator.addMethod("postalcodeIT",function(a,b){return this.optional(b)||/^\d{5}$/.test(a)},"Please specify a valid postal code"),a.validator.addMethod("postalcodeNL",function(a,b){return this.optional(b)||/^[1-9][0-9]{3}\s?[a-zA-Z]{2}$/.test(a)},"Please specify a valid postal code"),a.validator.addMethod("postcodeUK",function(a,b){return this.optional(b)||/^((([A-PR-UWYZ][0-9])|([A-PR-UWYZ][0-9][0-9])|([A-PR-UWYZ][A-HK-Y][0-9])|([A-PR-UWYZ][A-HK-Y][0-9][0-9])|([A-PR-UWYZ][0-9][A-HJKSTUW])|([A-PR-UWYZ][A-HK-Y][0-9][ABEHMNPRVWXY]))\s?([0-9][ABD-HJLNP-UW-Z]{2})|(GIR)\s?(0AA))$/i.test(a)},"Please specify a valid UK postcode"),a.validator.addMethod("require_from_group",function(b,c,d){var e=a(d[1],c.form),f=e.eq(0),g=f.data("valid_req_grp")?f.data("valid_req_grp"):a.extend({},this),h=e.filter(function(){return g.elementValue(this)}).length>=d[0];return f.data("valid_req_grp",g),a(c).data("being_validated")||(e.data("being_validated",!0),e.each(function(){g.element(this)}),e.data("being_validated",!1)),h},a.validator.format("Please fill at least {0} of these fields.")),a.validator.addMethod("skip_or_fill_minimum",function(b,c,d){var e=a(d[1],c.form),f=e.eq(0),g=f.data("valid_skip")?f.data("valid_skip"):a.extend({},this),h=e.filter(function(){return g.elementValue(this)}).length,i=0===h||h>=d[0];return f.data("valid_skip",g),a(c).data("being_validated")||(e.data("being_validated",!0),e.each(function(){g.element(this)}),e.data("being_validated",!1)),i},a.validator.format("Please either skip these fields or fill at least {0} of them.")),a.validator.addMethod("stateUS",function(a,b,c){var d,e="undefined"==typeof c,f=!e&&"undefined"!=typeof c.caseSensitive&&c.caseSensitive,g=!e&&"undefined"!=typeof c.includeTerritories&&c.includeTerritories,h=!e&&"undefined"!=typeof c.includeMilitary&&c.includeMilitary;return d=g||h?g&&h?"^(A[AEKLPRSZ]|C[AOT]|D[CE]|FL|G[AU]|HI|I[ADLN]|K[SY]|LA|M[ADEINOPST]|N[CDEHJMVY]|O[HKR]|P[AR]|RI|S[CD]|T[NX]|UT|V[AIT]|W[AIVY])$":g?"^(A[KLRSZ]|C[AOT]|D[CE]|FL|G[AU]|HI|I[ADLN]|K[SY]|LA|M[ADEINOPST]|N[CDEHJMVY]|O[HKR]|P[AR]|RI|S[CD]|T[NX]|UT|V[AIT]|W[AIVY])$":"^(A[AEKLPRZ]|C[AOT]|D[CE]|FL|GA|HI|I[ADLN]|K[SY]|LA|M[ADEINOST]|N[CDEHJMVY]|O[HKR]|PA|RI|S[CD]|T[NX]|UT|V[AT]|W[AIVY])$":"^(A[KLRZ]|C[AOT]|D[CE]|FL|GA|HI|I[ADLN]|K[SY]|LA|M[ADEINOST]|N[CDEHJMVY]|O[HKR]|PA|RI|S[CD]|T[NX]|UT|V[AT]|W[AIVY])$",d=f?new RegExp(d):new RegExp(d,"i"),this.optional(b)||d.test(a)},"Please specify a valid state"),a.validator.addMethod("strippedminlength",function(b,c,d){return a(b).text().length>=d},a.validator.format("Please enter at least {0} characters")),a.validator.addMethod("time",function(a,b){return this.optional(b)||/^([01]\d|2[0-3]|[0-9])(:[0-5]\d){1,2}$/.test(a)},"Please enter a valid time, between 00:00 and 23:59"),a.validator.addMethod("time12h",function(a,b){return this.optional(b)||/^((0?[1-9]|1[012])(:[0-5]\d){1,2}(\ ?[AP]M))$/i.test(a)},"Please enter a valid time in 12-hour am/pm format"),a.validator.addMethod("url2",function(a,b){return this.optional(b)||/^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)*(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i.test(a)},a.validator.messages.url),a.validator.addMethod("vinUS",function(a){if(17!==a.length)return!1;var b,c,d,e,f,g,h=["A","B","C","D","E","F","G","H","J","K","L","M","N","P","R","S","T","U","V","W","X","Y","Z"],i=[1,2,3,4,5,6,7,8,1,2,3,4,5,7,9,2,3,4,5,6,7,8,9],j=[8,7,6,5,4,3,2,10,0,9,8,7,6,5,4,3,2],k=0;for(b=0;b<17;b++){if(e=j[b],d=a.slice(b,b+1),8===b&&(g=d),isNaN(d)){for(c=0;c - - - - - - -

Sorry, there's nothing at this address.

-
-
-
- + + + + + + + +

Sorry, there's nothing at this address.

+
+
+
+
diff --git a/web-app-blazor-server/BlazorServerWebApp.csproj b/1-web-apps/web-app-blazor-server/BlazorServerWebApp.csproj similarity index 97% rename from web-app-blazor-server/BlazorServerWebApp.csproj rename to 1-web-apps/web-app-blazor-server/BlazorServerWebApp.csproj index f0c1a79..dfd978d 100644 --- a/web-app-blazor-server/BlazorServerWebApp.csproj +++ b/1-web-apps/web-app-blazor-server/BlazorServerWebApp.csproj @@ -1,12 +1,12 @@ - - - net8.0 - enable - enable - - - - - - + + + net8.0 + enable + enable + + + + + + \ No newline at end of file diff --git a/web-app-blazor-server/Pages/Index.razor b/1-web-apps/web-app-blazor-server/Pages/Index.razor similarity index 97% rename from web-app-blazor-server/Pages/Index.razor rename to 1-web-apps/web-app-blazor-server/Pages/Index.razor index be1931e..3de029e 100644 --- a/web-app-blazor-server/Pages/Index.razor +++ b/1-web-apps/web-app-blazor-server/Pages/Index.razor @@ -1,48 +1,48 @@ -@using Microsoft.Identity.Abstractions; -@using Microsoft.Identity.Web; -@using System.Text.Json -@inject IDownstreamApi downstreamApi -@inject MicrosoftIdentityConsentAndConditionalAccessHandler ConsentHandler - -@page "/" - -ASP.NET Core 6.0 Blazor Server | user sign-in, protected web API access (Microsoft Graph) | Microsoft - identity platform - -

Welcome to User Sign In ASP.NET Core Blazor Server

- - - - @if (graphApiResponse != null) - { -

Before rendering the page, the controller was able to make a call to - Microsoft Graph's /me API for your user and received the - following:

- -

-

@JsonSerializer.Serialize(graphApiResponse, new JsonSerializerOptions { WriteIndented = true })
-

- -

Refreshing this page will continue to use the cached access token acquired for Microsoft Graph, which is valid - for future page views will attempt to refresh this token as it nears its expiration.

- } - - @code { - private JsonDocument graphApiResponse = null; - - protected override async Task OnInitializedAsync() - { - try - { - using var response = await downstreamApi.CallApiForUserAsync("GraphApi").ConfigureAwait(false); - response.EnsureSuccessStatusCode(); - graphApiResponse = await response.Content.ReadFromJsonAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - ConsentHandler.HandleException(ex); - } - } - } -
-
+@using Microsoft.Identity.Abstractions; +@using Microsoft.Identity.Web; +@using System.Text.Json +@inject IDownstreamApi downstreamApi +@inject MicrosoftIdentityConsentAndConditionalAccessHandler ConsentHandler + +@page "/" + +ASP.NET Core 6.0 Blazor Server | user sign-in, protected web API access (Microsoft Graph) | Microsoft + identity platform + +

Welcome to User Sign In ASP.NET Core Blazor Server

+ + + + @if (graphApiResponse != null) + { +

Before rendering the page, the controller was able to make a call to + Microsoft Graph's /me API for your user and received the + following:

+ +

+

@JsonSerializer.Serialize(graphApiResponse, new JsonSerializerOptions { WriteIndented = true })
+

+ +

Refreshing this page will continue to use the cached access token acquired for Microsoft Graph, which is valid + for future page views will attempt to refresh this token as it nears its expiration.

+ } + + @code { + private JsonDocument graphApiResponse = null; + + protected override async Task OnInitializedAsync() + { + try + { + using var response = await downstreamApi.CallApiForUserAsync("GraphApi").ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + graphApiResponse = await response.Content.ReadFromJsonAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + ConsentHandler.HandleException(ex); + } + } + } +
+
diff --git a/web-app-blazor-server/Pages/_Host.cshtml b/1-web-apps/web-app-blazor-server/Pages/_Host.cshtml similarity index 96% rename from web-app-blazor-server/Pages/_Host.cshtml rename to 1-web-apps/web-app-blazor-server/Pages/_Host.cshtml index 7c241ae..ff53046 100644 --- a/web-app-blazor-server/Pages/_Host.cshtml +++ b/1-web-apps/web-app-blazor-server/Pages/_Host.cshtml @@ -1,8 +1,8 @@ -@page "/" -@namespace BlazorServerWebApp.Pages -@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers -@{ - Layout = "_Layout"; -} - - +@page "/" +@namespace BlazorServerWebApp.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = "_Layout"; +} + + diff --git a/web-app-blazor-server/Pages/_Layout.cshtml b/1-web-apps/web-app-blazor-server/Pages/_Layout.cshtml similarity index 97% rename from web-app-blazor-server/Pages/_Layout.cshtml rename to 1-web-apps/web-app-blazor-server/Pages/_Layout.cshtml index 43b2991..3b968d0 100644 --- a/web-app-blazor-server/Pages/_Layout.cshtml +++ b/1-web-apps/web-app-blazor-server/Pages/_Layout.cshtml @@ -1,32 +1,32 @@ -@using Microsoft.AspNetCore.Components.Web -@namespace BlazorServerWebApp.Pages -@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers - - - - - - - - - - - - - - @RenderBody() - -
- - An error has occurred. This application may no longer respond until reloaded. - - - An unhandled exception has occurred. See browser dev tools for details. - - Reload - 🗙 -
- - - - +@using Microsoft.AspNetCore.Components.Web +@namespace BlazorServerWebApp.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers + + + + + + + + + + + + + + @RenderBody() + +
+ + An error has occurred. This application may no longer respond until reloaded. + + + An unhandled exception has occurred. See browser dev tools for details. + + Reload + 🗙 +
+ + + + diff --git a/web-app-blazor-server/Program.cs b/1-web-apps/web-app-blazor-server/Program.cs similarity index 96% rename from web-app-blazor-server/Program.cs rename to 1-web-apps/web-app-blazor-server/Program.cs index 1fcba52..8e028d1 100644 --- a/web-app-blazor-server/Program.cs +++ b/1-web-apps/web-app-blazor-server/Program.cs @@ -1,41 +1,41 @@ -using Microsoft.Identity.Web; -using Microsoft.Identity.Web.UI; - -WebApplicationBuilder builder = WebApplication.CreateBuilder(args); -IEnumerable? initialScopes = builder.Configuration["DownstreamApi:Scopes"]?.Split(' '); - -builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration, "AzureAd") - .EnableTokenAcquisitionToCallDownstreamApi(initialScopes) - .AddDownstreamApi("GraphApi", builder.Configuration.GetSection("GraphApi")) - .AddInMemoryTokenCaches(); - -builder.Services.AddControllersWithViews() - .AddMicrosoftIdentityUI(); - -builder.Services.AddAuthorization(options => -{ - options.FallbackPolicy = options.DefaultPolicy; -}); - -builder.Services.AddRazorPages(); -builder.Services.AddServerSideBlazor() - .AddMicrosoftIdentityConsentHandler(); - -WebApplication app = builder.Build(); - -app.UseHttpsRedirection(); -app.UseStaticFiles(); - -app.UseRouting(); - -app.UseAuthentication(); -app.UseAuthorization(); - -app.UseEndpoints(endpoints => -{ - endpoints.MapControllers(); - endpoints.MapBlazorHub(); - endpoints.MapFallbackToPage("/_Host"); -}); - -app.Run(); +using Microsoft.Identity.Web; +using Microsoft.Identity.Web.UI; + +WebApplicationBuilder builder = WebApplication.CreateBuilder(args); +IEnumerable? initialScopes = builder.Configuration["DownstreamApi:Scopes"]?.Split(' '); + +builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration, "AzureAd") + .EnableTokenAcquisitionToCallDownstreamApi(initialScopes) + .AddDownstreamApi("GraphApi", builder.Configuration.GetSection("GraphApi")) + .AddInMemoryTokenCaches(); + +builder.Services.AddControllersWithViews() + .AddMicrosoftIdentityUI(); + +builder.Services.AddAuthorization(options => +{ + options.FallbackPolicy = options.DefaultPolicy; +}); + +builder.Services.AddRazorPages(); +builder.Services.AddServerSideBlazor() + .AddMicrosoftIdentityConsentHandler(); + +WebApplication app = builder.Build(); + +app.UseHttpsRedirection(); +app.UseStaticFiles(); + +app.UseRouting(); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.UseEndpoints(endpoints => +{ + endpoints.MapControllers(); + endpoints.MapBlazorHub(); + endpoints.MapFallbackToPage("/_Host"); +}); + +app.Run(); diff --git a/web-app-blazor-server/Properties/launchSettings.json b/1-web-apps/web-app-blazor-server/Properties/launchSettings.json similarity index 96% rename from web-app-blazor-server/Properties/launchSettings.json rename to 1-web-apps/web-app-blazor-server/Properties/launchSettings.json index 85d71fd..5e0ab55 100644 --- a/web-app-blazor-server/Properties/launchSettings.json +++ b/1-web-apps/web-app-blazor-server/Properties/launchSettings.json @@ -1,13 +1,13 @@ -{ - "profiles": { - "BlazorServerWebApp": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "applicationUrl": "https://localhost:5001;http://localhost:5000", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } -} +{ + "profiles": { + "BlazorServerWebApp": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/web-app-blazor-server/README.md b/1-web-apps/web-app-blazor-server/README.md similarity index 90% rename from web-app-blazor-server/README.md rename to 1-web-apps/web-app-blazor-server/README.md index 2636ad7..bc56bcc 100644 --- a/web-app-blazor-server/README.md +++ b/1-web-apps/web-app-blazor-server/README.md @@ -1,6 +1,6 @@ --- -# Metadata required by https://docs.microsoft.com/samples/browse/ -# Metadata properties: https://review.docs.microsoft.com/help/contribute/samples/process/onboarding?branch=main#add-metadata-to-readme +# Metadata required by https://learn.microsoft.com/samples/browse/ +# Metadata properties: https://review.learn.microsoft.com/help/contribute/samples/process/onboarding?branch=main#add-metadata-to-readme languages: - csharp page_type: sample @@ -33,7 +33,7 @@ This ASP.NET Core Blazor Server application application authenticates a user and ### 1. Register the app -First, complete the steps in [Register an application with the Microsoft identity platform](https://docs.microsoft.com/azure/active-directory/develop/quickstart-register-app) to register the application. +First, complete the steps in [Register an application with the Microsoft identity platform](https://learn.microsoft.com/entra/identity-platform/quickstart-register-app) to register the application. Use these settings in your app registration. @@ -85,7 +85,7 @@ In _appsettings.json_, update each variable with values from the app registratio This ASP.NET Core 6 Blazor Server application is created using the .NET Blazor Server App template. The app is adding sign-in to protect itself, and as a consequence this is requiring the user to be authenticated in Microsoft Entra ID. -When this .NET Blazor Server starts and before listening for any HTTP requests, it bootstraps the application using a single-surface API provided as part of **Microsoft.Identity.Web** and **Microsoft.Idenitty.Web.UI**. The former is tying ASP.NET Core, its authentication middleware for sign-in, and the [Microsoft Authentication Library (MSAL) for .NET](https://github.com/azuread/microsoft-authentication-library-for-dotnet), while the latter adds UI components and controllers to facilitate user sign-in, sign-out, and other account experiences. All the details required for authentication are being gathered from a configuration section named `AzureAd` as well as others entries used during the call to a protected API, like the scopes. As for authorization, it is using the default policy options. Additionally, special services are injected [specifically for Blazor for re-signing, consent and Conditional Access purposes](https://github.com/AzureAD/microsoft-identity-web/wiki/Managing-incremental-consent-and-conditional-access#in-blazor-server). The recommended pattern to acquire tokens is first attempting to acquire them [non-interactevelly (or silent from cache), and then interactively](https://docs.microsoft.com/azure/active-directory/develop/msal-authentication-flows#interactive-and-non-interactive-authentication). Therefore, it is required to store the tokens for them to be cached. In this tutorial tokens are being stored in memory. +When this .NET Blazor Server starts and before listening for any HTTP requests, it bootstraps the application using a single-surface API provided as part of **Microsoft.Identity.Web** and **Microsoft.Idenitty.Web.UI**. The former is tying ASP.NET Core, its authentication middleware for sign-in, and the [Microsoft Authentication Library (MSAL) for .NET](https://github.com/azuread/microsoft-authentication-library-for-dotnet), while the latter adds UI components and controllers to facilitate user sign-in, sign-out, and other account experiences. All the details required for authentication are being gathered from a configuration section named `AzureAd` as well as others entries used during the call to a protected API, like the scopes. As for authorization, it is using the default policy options. Additionally, special services are injected [specifically for Blazor for re-signing, consent and Conditional Access purposes](https://github.com/AzureAD/microsoft-identity-web/wiki/Managing-incremental-consent-and-conditional-access#in-blazor-server). The recommended pattern to acquire tokens is first attempting to acquire them [non-interactevelly (or silent from cache), and then interactively](https://learn.microsoft.com/entra/identity-platform/msal-authentication-flows#interactive-and-non-interactive-authentication). Therefore, it is required to store the tokens for them to be cached. In this tutorial tokens are being stored in memory. When users navigate to the home page, the application initiates an authentication flow, more specifically an **Authorization code type flow (OAuth 2 authorization code grant)**. During this authentication flow, the user is prompted for their credentials, by Microsoft Entra ID, if the token has to be acquired interactively, and then asked to consent to the permissions. Upon successful authentication, this web app is making a call to the Microsoft Graph /me endpoint from the **Index** page by using an injected **IDownstreamWebApi** service. This helper facilitates making an HTTP GET request to the protected web API adding the cached or newly acquired user's access token in the HTTP Authorization header. The app displays that you've successfully logged in using your Microsoft Entra credentials, and the Microsoft Graph API response. @@ -104,7 +104,7 @@ If you can't get the sample working, you've checked [Stack Overflow](https://sta > :warning: WARNING: Any issue in this repository _not_ limited to running one of its sample apps will be closed without being addressed. -For all other requests, see [Support and help options for developers | Microsoft identity platform](https://docs.microsoft.com/azure/active-directory/develop/developer-support-help-options). +For all other requests, see [Support and help options for developers | Microsoft identity platform](https://learn.microsoft.com/entra/identity-platform/developer-support-help-options). ## Contributing diff --git a/web-app-blazor-server/Shared/LoginDisplay.razor b/1-web-apps/web-app-blazor-server/Shared/LoginDisplay.razor similarity index 96% rename from web-app-blazor-server/Shared/LoginDisplay.razor rename to 1-web-apps/web-app-blazor-server/Shared/LoginDisplay.razor index 76165cc..fb77623 100644 --- a/web-app-blazor-server/Shared/LoginDisplay.razor +++ b/1-web-apps/web-app-blazor-server/Shared/LoginDisplay.razor @@ -1,9 +1,9 @@ - - - Hello, @context.User.Identity.Name! - Sign Out - - - Sign In - - + + + Hello, @context.User.Identity.Name! + Sign Out + + + Sign In + + diff --git a/web-app-blazor-server/Shared/MainLayout.razor b/1-web-apps/web-app-blazor-server/Shared/MainLayout.razor similarity index 94% rename from web-app-blazor-server/Shared/MainLayout.razor rename to 1-web-apps/web-app-blazor-server/Shared/MainLayout.razor index 94d6a16..0222a55 100644 --- a/web-app-blazor-server/Shared/MainLayout.razor +++ b/1-web-apps/web-app-blazor-server/Shared/MainLayout.razor @@ -1,19 +1,19 @@ -@inherits LayoutComponentBase - -Menu - -
- - -
-
- -
- -
- @Body -
-
-
+@inherits LayoutComponentBase + +Menu + +
+ + +
+
+ +
+ +
+ @Body +
+
+
diff --git a/web-app-blazor-server/Shared/MainLayout.razor.css b/1-web-apps/web-app-blazor-server/Shared/MainLayout.razor.css similarity index 94% rename from web-app-blazor-server/Shared/MainLayout.razor.css rename to 1-web-apps/web-app-blazor-server/Shared/MainLayout.razor.css index 699f17c..551e4b2 100644 --- a/web-app-blazor-server/Shared/MainLayout.razor.css +++ b/1-web-apps/web-app-blazor-server/Shared/MainLayout.razor.css @@ -1,70 +1,70 @@ -.page { - position: relative; - display: flex; - flex-direction: column; -} - -main { - flex: 1; -} - -.sidebar { - background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); -} - -.top-row { - background-color: #f7f7f7; - border-bottom: 1px solid #d6d5d5; - justify-content: flex-end; - height: 3.5rem; - display: flex; - align-items: center; -} - - .top-row ::deep a, .top-row .btn-link { - white-space: nowrap; - margin-left: 1.5rem; - } - - .top-row a:first-child { - overflow: hidden; - text-overflow: ellipsis; - } - -@media (max-width: 640.98px) { - .top-row:not(.auth) { - display: none; - } - - .top-row.auth { - justify-content: space-between; - } - - .top-row a, .top-row .btn-link { - margin-left: 0; - } -} - -@media (min-width: 641px) { - .page { - flex-direction: row; - } - - .sidebar { - width: 250px; - height: 100vh; - position: sticky; - top: 0; - } - - .top-row { - position: sticky; - top: 0; - z-index: 1; - } - - .top-row, article { - padding-left: 2rem !important; - padding-right: 1.5rem !important; - } -} +.page { + position: relative; + display: flex; + flex-direction: column; +} + +main { + flex: 1; +} + +.sidebar { + background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); +} + +.top-row { + background-color: #f7f7f7; + border-bottom: 1px solid #d6d5d5; + justify-content: flex-end; + height: 3.5rem; + display: flex; + align-items: center; +} + + .top-row ::deep a, .top-row .btn-link { + white-space: nowrap; + margin-left: 1.5rem; + } + + .top-row a:first-child { + overflow: hidden; + text-overflow: ellipsis; + } + +@media (max-width: 640.98px) { + .top-row:not(.auth) { + display: none; + } + + .top-row.auth { + justify-content: space-between; + } + + .top-row a, .top-row .btn-link { + margin-left: 0; + } +} + +@media (min-width: 641px) { + .page { + flex-direction: row; + } + + .sidebar { + width: 250px; + height: 100vh; + position: sticky; + top: 0; + } + + .top-row { + position: sticky; + top: 0; + z-index: 1; + } + + .top-row, article { + padding-left: 2rem !important; + padding-right: 1.5rem !important; + } +} diff --git a/web-app-blazor-server/Shared/NavMenu.razor b/1-web-apps/web-app-blazor-server/Shared/NavMenu.razor similarity index 96% rename from web-app-blazor-server/Shared/NavMenu.razor rename to 1-web-apps/web-app-blazor-server/Shared/NavMenu.razor index e85392e..ec02dcb 100644 --- a/web-app-blazor-server/Shared/NavMenu.razor +++ b/1-web-apps/web-app-blazor-server/Shared/NavMenu.razor @@ -1,29 +1,29 @@ - - -
- -
- -@code { - private bool collapseNavMenu = true; - - private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null; - - private void ToggleNavMenu() - { - collapseNavMenu = !collapseNavMenu; - } -} + + +
+ +
+ +@code { + private bool collapseNavMenu = true; + + private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null; + + private void ToggleNavMenu() + { + collapseNavMenu = !collapseNavMenu; + } +} diff --git a/spa-blazor-wasm/Shared/NavMenu.razor.css b/1-web-apps/web-app-blazor-server/Shared/NavMenu.razor.css similarity index 94% rename from spa-blazor-wasm/Shared/NavMenu.razor.css rename to 1-web-apps/web-app-blazor-server/Shared/NavMenu.razor.css index e681f23..acc5f9f 100644 --- a/spa-blazor-wasm/Shared/NavMenu.razor.css +++ b/1-web-apps/web-app-blazor-server/Shared/NavMenu.razor.css @@ -1,62 +1,62 @@ -.navbar-toggler { - background-color: rgba(255, 255, 255, 0.1); -} - -.top-row { - height: 3.5rem; - background-color: rgba(0,0,0,0.4); -} - -.navbar-brand { - font-size: 1.1rem; -} - -.oi { - width: 2rem; - font-size: 1.1rem; - vertical-align: text-top; - top: -2px; -} - -.nav-item { - font-size: 0.9rem; - padding-bottom: 0.5rem; -} - - .nav-item:first-of-type { - padding-top: 1rem; - } - - .nav-item:last-of-type { - padding-bottom: 1rem; - } - - .nav-item ::deep a { - color: #d7d7d7; - border-radius: 4px; - height: 3rem; - display: flex; - align-items: center; - line-height: 3rem; - } - -.nav-item ::deep a.active { - background-color: rgba(255,255,255,0.25); - color: white; -} - -.nav-item ::deep a:hover { - background-color: rgba(255,255,255,0.1); - color: white; -} - -@media (min-width: 641px) { - .navbar-toggler { - display: none; - } - - .collapse { - /* Never collapse the sidebar for wide screens */ - display: block; - } -} +.navbar-toggler { + background-color: rgba(255, 255, 255, 0.1); +} + +.top-row { + height: 3.5rem; + background-color: rgba(0,0,0,0.4); +} + +.navbar-brand { + font-size: 1.1rem; +} + +.oi { + width: 2rem; + font-size: 1.1rem; + vertical-align: text-top; + top: -2px; +} + +.nav-item { + font-size: 0.9rem; + padding-bottom: 0.5rem; +} + + .nav-item:first-of-type { + padding-top: 1rem; + } + + .nav-item:last-of-type { + padding-bottom: 1rem; + } + + .nav-item ::deep a { + color: #d7d7d7; + border-radius: 4px; + height: 3rem; + display: flex; + align-items: center; + line-height: 3rem; + } + +.nav-item ::deep a.active { + background-color: rgba(255,255,255,0.25); + color: white; +} + +.nav-item ::deep a:hover { + background-color: rgba(255,255,255,0.1); + color: white; +} + +@media (min-width: 641px) { + .navbar-toggler { + display: none; + } + + .collapse { + /* Never collapse the sidebar for wide screens */ + display: block; + } +} diff --git a/web-app-blazor-server/_Imports.razor b/1-web-apps/web-app-blazor-server/_Imports.razor similarity index 97% rename from web-app-blazor-server/_Imports.razor rename to 1-web-apps/web-app-blazor-server/_Imports.razor index 7a5dc59..826dab7 100644 --- a/web-app-blazor-server/_Imports.razor +++ b/1-web-apps/web-app-blazor-server/_Imports.razor @@ -1,10 +1,10 @@ -@using System.Net.Http -@using Microsoft.AspNetCore.Authorization -@using Microsoft.AspNetCore.Components.Authorization -@using Microsoft.AspNetCore.Components.Forms -@using Microsoft.AspNetCore.Components.Routing -@using Microsoft.AspNetCore.Components.Web -@using Microsoft.AspNetCore.Components.Web.Virtualization -@using Microsoft.JSInterop -@using BlazorServerWebApp -@using BlazorServerWebApp.Shared +@using System.Net.Http +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using BlazorServerWebApp +@using BlazorServerWebApp.Shared diff --git a/web-app-blazor-server/app-signed-in.png b/1-web-apps/web-app-blazor-server/app-signed-in.png similarity index 100% rename from web-app-blazor-server/app-signed-in.png rename to 1-web-apps/web-app-blazor-server/app-signed-in.png diff --git a/web-app-blazor-server/app-signed-out.png b/1-web-apps/web-app-blazor-server/app-signed-out.png similarity index 100% rename from web-app-blazor-server/app-signed-out.png rename to 1-web-apps/web-app-blazor-server/app-signed-out.png diff --git a/web-app-blazor-server/appsettings.json b/1-web-apps/web-app-blazor-server/appsettings.json similarity index 97% rename from web-app-blazor-server/appsettings.json rename to 1-web-apps/web-app-blazor-server/appsettings.json index 2d1e1e4..baf864a 100644 --- a/web-app-blazor-server/appsettings.json +++ b/1-web-apps/web-app-blazor-server/appsettings.json @@ -1,26 +1,26 @@ -{ - -/* -The following identity settings need to be configured -before the project can be successfully executed. -For more info see https://aka.ms/dotnet-template-ms-identity-platform -*/ - "AzureAd": { - "Instance": "https://login.microsoftonline.com/", - "TenantId": "[Enter 'common', or 'organizations' or the Tenant ID (Obtained from the Microsoft Entra admin center. Select 'Endpoints' from the 'App registrations' blade and use the GUID in any of the URLs), e.g. da41245a5-11b3-996c-00a8-4d99re19f292]", - "ClientId": "[Enter the Client Id (Application ID obtained from the Microsoft Entra admin center), e.g. ba74781c2-53c2-442a-97c2-3d60re42f403]", - "ClientSecret": "[Copy the client secret added to the app from the Microsoft Entra admin center]", - "CallbackPath": "/signin-oidc" - }, - "GraphApi": { - "BaseUrl": "https://graph.microsoft.com/v1.0/me", - "Scopes": ["user.read"] - }, - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*" -} +{ + +/* +The following identity settings need to be configured +before the project can be successfully executed. +For more info see https://aka.ms/dotnet-template-ms-identity-platform +*/ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "[Enter 'common', or 'organizations' or the Tenant ID (Obtained from the Microsoft Entra admin center. Select 'Endpoints' from the 'App registrations' blade and use the GUID in any of the URLs), e.g. da41245a5-11b3-996c-00a8-4d99re19f292]", + "ClientId": "[Enter the Client Id (Application ID obtained from the Microsoft Entra admin center), e.g. ba74781c2-53c2-442a-97c2-3d60re42f403]", + "ClientSecret": "[Copy the client secret added to the app from the Microsoft Entra admin center]", + "CallbackPath": "/signin-oidc" + }, + "GraphApi": { + "BaseUrl": "https://graph.microsoft.com/v1.0/me", + "Scopes": ["user.read"] + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css b/1-web-apps/web-app-blazor-server/wwwroot/css/bootstrap/bootstrap.min.css similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css rename to 1-web-apps/web-app-blazor-server/wwwroot/css/bootstrap/bootstrap.min.css diff --git a/web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map b/1-web-apps/web-app-blazor-server/wwwroot/css/bootstrap/bootstrap.min.css.map similarity index 100% rename from web-app-aspnet/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map rename to 1-web-apps/web-app-blazor-server/wwwroot/css/bootstrap/bootstrap.min.css.map diff --git a/web-app-blazor-server/wwwroot/css/open-iconic/FONT-LICENSE b/1-web-apps/web-app-blazor-server/wwwroot/css/open-iconic/FONT-LICENSE similarity index 97% rename from web-app-blazor-server/wwwroot/css/open-iconic/FONT-LICENSE rename to 1-web-apps/web-app-blazor-server/wwwroot/css/open-iconic/FONT-LICENSE index 8ae8650..a1dc03f 100644 --- a/web-app-blazor-server/wwwroot/css/open-iconic/FONT-LICENSE +++ b/1-web-apps/web-app-blazor-server/wwwroot/css/open-iconic/FONT-LICENSE @@ -1,86 +1,86 @@ -SIL OPEN FONT LICENSE Version 1.1 - -Copyright (c) 2014 Waybury - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +SIL OPEN FONT LICENSE Version 1.1 + +Copyright (c) 2014 Waybury + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/web-app-blazor-server/wwwroot/css/open-iconic/ICON-LICENSE b/1-web-apps/web-app-blazor-server/wwwroot/css/open-iconic/ICON-LICENSE similarity index 98% rename from web-app-blazor-server/wwwroot/css/open-iconic/ICON-LICENSE rename to 1-web-apps/web-app-blazor-server/wwwroot/css/open-iconic/ICON-LICENSE index af73356..2199f4a 100644 --- a/web-app-blazor-server/wwwroot/css/open-iconic/ICON-LICENSE +++ b/1-web-apps/web-app-blazor-server/wwwroot/css/open-iconic/ICON-LICENSE @@ -1,21 +1,21 @@ -The MIT License (MIT) - -Copyright (c) 2014 Waybury - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +The MIT License (MIT) + +Copyright (c) 2014 Waybury + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/web-app-blazor-server/wwwroot/css/open-iconic/README.md b/1-web-apps/web-app-blazor-server/wwwroot/css/open-iconic/README.md similarity index 96% rename from web-app-blazor-server/wwwroot/css/open-iconic/README.md rename to 1-web-apps/web-app-blazor-server/wwwroot/css/open-iconic/README.md index e2e831e..6b810e4 100644 --- a/web-app-blazor-server/wwwroot/css/open-iconic/README.md +++ b/1-web-apps/web-app-blazor-server/wwwroot/css/open-iconic/README.md @@ -1,114 +1,114 @@ -[Open Iconic v1.1.1](http://useiconic.com/open) -=========== - -### Open Iconic is the open source sibling of [Iconic](http://useiconic.com). It is a hyper-legible collection of 223 icons with a tiny footprint—ready to use with Bootstrap and Foundation. [View the collection](http://useiconic.com/open#icons) - - - -## What's in Open Iconic? - -* 223 icons designed to be legible down to 8 pixels -* Super-light SVG files - 61.8 for the entire set -* SVG sprite—the modern replacement for icon fonts -* Webfont (EOT, OTF, SVG, TTF, WOFF), PNG and WebP formats -* Webfont stylesheets (including versions for Bootstrap and Foundation) in CSS, LESS, SCSS and Stylus formats -* PNG and WebP raster images in 8px, 16px, 24px, 32px, 48px and 64px. - - -## Getting Started - -#### For code samples and everything else you need to get started with Open Iconic, check out our [Icons](http://useiconic.com/open#icons) and [Reference](http://useiconic.com/open#reference) sections. - -### General Usage - -#### Using Open Iconic's SVGs - -We like SVGs and we think they're the way to display icons on the web. Since Open Iconic are just basic SVGs, we suggest you display them like you would any other image (don't forget the `alt` attribute). - -``` -icon name -``` - -#### Using Open Iconic's SVG Sprite - -Open Iconic also comes in a SVG sprite which allows you to display all the icons in the set with a single request. It's like an icon font, without being a hack. - -Adding an icon from an SVG sprite is a little different than what you're used to, but it's still a piece of cake. *Tip: To make your icons easily style able, we suggest adding a general class to the* `` *tag and a unique class name for each different icon in the* `` *tag.* - -``` - - - -``` - -Sizing icons only needs basic CSS. All the icons are in a square format, so just set the `` tag with equal width and height dimensions. - -``` -.icon { - width: 16px; - height: 16px; -} -``` - -Coloring icons is even easier. All you need to do is set the `fill` rule on the `` tag. - -``` -.icon-account-login { - fill: #f00; -} -``` - -To learn more about SVG Sprites, read [Chris Coyier's guide](http://css-tricks.com/svg-sprites-use-better-icon-fonts/). - -#### Using Open Iconic's Icon Font... - - -##### …with Bootstrap - -You can find our Bootstrap stylesheets in `font/css/open-iconic-bootstrap.{css, less, scss, styl}` - - -``` - -``` - - -``` - -``` - -##### …with Foundation - -You can find our Foundation stylesheets in `font/css/open-iconic-foundation.{css, less, scss, styl}` - -``` - -``` - - -``` - -``` - -##### …on its own - -You can find our default stylesheets in `font/css/open-iconic.{css, less, scss, styl}` - -``` - -``` - -``` - -``` - - -## License - -### Icons - -All code (including SVG markup) is under the [MIT License](http://opensource.org/licenses/MIT). - -### Fonts - -All fonts are under the [SIL Licensed](http://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web). +[Open Iconic v1.1.1](http://useiconic.com/open) +=========== + +### Open Iconic is the open source sibling of [Iconic](http://useiconic.com). It is a hyper-legible collection of 223 icons with a tiny footprint—ready to use with Bootstrap and Foundation. [View the collection](http://useiconic.com/open#icons) + + + +## What's in Open Iconic? + +* 223 icons designed to be legible down to 8 pixels +* Super-light SVG files - 61.8 for the entire set +* SVG sprite—the modern replacement for icon fonts +* Webfont (EOT, OTF, SVG, TTF, WOFF), PNG and WebP formats +* Webfont stylesheets (including versions for Bootstrap and Foundation) in CSS, LESS, SCSS and Stylus formats +* PNG and WebP raster images in 8px, 16px, 24px, 32px, 48px and 64px. + + +## Getting Started + +#### For code samples and everything else you need to get started with Open Iconic, check out our [Icons](http://useiconic.com/open#icons) and [Reference](http://useiconic.com/open#reference) sections. + +### General Usage + +#### Using Open Iconic's SVGs + +We like SVGs and we think they're the way to display icons on the web. Since Open Iconic are just basic SVGs, we suggest you display them like you would any other image (don't forget the `alt` attribute). + +``` +icon name +``` + +#### Using Open Iconic's SVG Sprite + +Open Iconic also comes in a SVG sprite which allows you to display all the icons in the set with a single request. It's like an icon font, without being a hack. + +Adding an icon from an SVG sprite is a little different than what you're used to, but it's still a piece of cake. *Tip: To make your icons easily style able, we suggest adding a general class to the* `` *tag and a unique class name for each different icon in the* `` *tag.* + +``` + + + +``` + +Sizing icons only needs basic CSS. All the icons are in a square format, so just set the `` tag with equal width and height dimensions. + +``` +.icon { + width: 16px; + height: 16px; +} +``` + +Coloring icons is even easier. All you need to do is set the `fill` rule on the `` tag. + +``` +.icon-account-login { + fill: #f00; +} +``` + +To learn more about SVG Sprites, read [Chris Coyier's guide](http://css-tricks.com/svg-sprites-use-better-icon-fonts/). + +#### Using Open Iconic's Icon Font... + + +##### …with Bootstrap + +You can find our Bootstrap stylesheets in `font/css/open-iconic-bootstrap.{css, less, scss, styl}` + + +``` + +``` + + +``` + +``` + +##### …with Foundation + +You can find our Foundation stylesheets in `font/css/open-iconic-foundation.{css, less, scss, styl}` + +``` + +``` + + +``` + +``` + +##### …on its own + +You can find our default stylesheets in `font/css/open-iconic.{css, less, scss, styl}` + +``` + +``` + +``` + +``` + + +## License + +### Icons + +All code (including SVG markup) is under the [MIT License](http://opensource.org/licenses/MIT). + +### Fonts + +All fonts are under the [SIL Licensed](http://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web). diff --git a/spa-blazor-wasm/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css b/1-web-apps/web-app-blazor-server/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css similarity index 100% rename from spa-blazor-wasm/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css rename to 1-web-apps/web-app-blazor-server/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css diff --git a/spa-blazor-wasm/wwwroot/css/open-iconic/font/fonts/open-iconic.eot b/1-web-apps/web-app-blazor-server/wwwroot/css/open-iconic/font/fonts/open-iconic.eot similarity index 100% rename from spa-blazor-wasm/wwwroot/css/open-iconic/font/fonts/open-iconic.eot rename to 1-web-apps/web-app-blazor-server/wwwroot/css/open-iconic/font/fonts/open-iconic.eot diff --git a/spa-blazor-wasm/wwwroot/css/open-iconic/font/fonts/open-iconic.otf b/1-web-apps/web-app-blazor-server/wwwroot/css/open-iconic/font/fonts/open-iconic.otf similarity index 100% rename from spa-blazor-wasm/wwwroot/css/open-iconic/font/fonts/open-iconic.otf rename to 1-web-apps/web-app-blazor-server/wwwroot/css/open-iconic/font/fonts/open-iconic.otf diff --git a/spa-blazor-wasm/wwwroot/css/open-iconic/font/fonts/open-iconic.svg b/1-web-apps/web-app-blazor-server/wwwroot/css/open-iconic/font/fonts/open-iconic.svg similarity index 99% rename from spa-blazor-wasm/wwwroot/css/open-iconic/font/fonts/open-iconic.svg rename to 1-web-apps/web-app-blazor-server/wwwroot/css/open-iconic/font/fonts/open-iconic.svg index cf42942..32b2c4e 100644 --- a/spa-blazor-wasm/wwwroot/css/open-iconic/font/fonts/open-iconic.svg +++ b/1-web-apps/web-app-blazor-server/wwwroot/css/open-iconic/font/fonts/open-iconic.svg @@ -1,543 +1,543 @@ - - - - - -Created by FontForge 20120731 at Tue Jul 1 20:39:22 2014 - By P.J. Onori -Created by P.J. Onori with FontForge 2.0 (http://fontforge.sf.net) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + +Created by FontForge 20120731 at Tue Jul 1 20:39:22 2014 + By P.J. Onori +Created by P.J. Onori with FontForge 2.0 (http://fontforge.sf.net) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spa-blazor-wasm/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf b/1-web-apps/web-app-blazor-server/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf similarity index 100% rename from spa-blazor-wasm/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf rename to 1-web-apps/web-app-blazor-server/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf diff --git a/spa-blazor-wasm/wwwroot/css/open-iconic/font/fonts/open-iconic.woff b/1-web-apps/web-app-blazor-server/wwwroot/css/open-iconic/font/fonts/open-iconic.woff similarity index 100% rename from spa-blazor-wasm/wwwroot/css/open-iconic/font/fonts/open-iconic.woff rename to 1-web-apps/web-app-blazor-server/wwwroot/css/open-iconic/font/fonts/open-iconic.woff diff --git a/web-app-blazor-server/wwwroot/css/site.css b/1-web-apps/web-app-blazor-server/wwwroot/css/site.css similarity index 97% rename from web-app-blazor-server/wwwroot/css/site.css rename to 1-web-apps/web-app-blazor-server/wwwroot/css/site.css index dc4c68b..1f4b8cf 100644 --- a/web-app-blazor-server/wwwroot/css/site.css +++ b/1-web-apps/web-app-blazor-server/wwwroot/css/site.css @@ -1,64 +1,64 @@ -@import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); - -html, body { - font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; -} - -h1:focus { - outline: none; -} - -a, .btn-link { - color: #0071c1; -} - -.btn-primary { - color: #fff; - background-color: #1b6ec2; - border-color: #1861ac; -} - -.content { - padding-top: 1.1rem; -} - -.valid.modified:not([type=checkbox]) { - outline: 1px solid #26b050; -} - -.invalid { - outline: 1px solid red; -} - -.validation-message { - color: red; -} - -#blazor-error-ui { - background: lightyellow; - bottom: 0; - box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); - display: none; - left: 0; - padding: 0.6rem 1.25rem 0.7rem 1.25rem; - position: fixed; - width: 100%; - z-index: 1000; -} - - #blazor-error-ui .dismiss { - cursor: pointer; - position: absolute; - right: 0.75rem; - top: 0.5rem; - } - -.blazor-error-boundary { - background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; - padding: 1rem 1rem 1rem 3.7rem; - color: white; -} - - .blazor-error-boundary::after { - content: "An error has occurred." - } +@import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); + +html, body { + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; +} + +h1:focus { + outline: none; +} + +a, .btn-link { + color: #0071c1; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.content { + padding-top: 1.1rem; +} + +.valid.modified:not([type=checkbox]) { + outline: 1px solid #26b050; +} + +.invalid { + outline: 1px solid red; +} + +.validation-message { + color: red; +} + +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } + +.blazor-error-boundary { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; + padding: 1rem 1rem 1rem 3.7rem; + color: white; +} + + .blazor-error-boundary::after { + content: "An error has occurred." + } diff --git a/web-app-aspnet/wwwroot/favicon.ico b/1-web-apps/web-app-blazor-server/wwwroot/favicon.ico similarity index 100% rename from web-app-aspnet/wwwroot/favicon.ico rename to 1-web-apps/web-app-blazor-server/wwwroot/favicon.ico diff --git a/2-web-apis/README.md b/2-web-apis/README.md new file mode 100644 index 0000000..3698986 --- /dev/null +++ b/2-web-apis/README.md @@ -0,0 +1,51 @@ +# 🛡️ Web API Samples + +This folder contains a collection of .NET Web API samples demonstrating authentication and authorization scenarios using the Microsoft identity platform. Each sample showcases a different API type or authentication flow, using up-to-date libraries and best practices. + +## 📋 Samples Overview + +| 📁 Folder Name | 🔑 Authentication Libraries Used | 🏷️ .NET Version | +|-----------------------------------------------------|--------------------------------------------------------------|----------------------| +| [web-api](./web-api) | Microsoft.Identity.Web, Microsoft.Identity.Client (MSAL.NET) | .NET 8.0 | +| [web-api-azure-function](./web-api-azure-function) | Microsoft.Identity.Web, Microsoft.Identity.Client (MSAL.NET) | .NET 8.0 | +| [web-api-obo-client](./web-api-obo-client) | Microsoft.Identity.Client (MSAL.NET) | .NET 8.0 | +| [web-api-obo-user](./web-api-obo-user) | Microsoft.Identity.Web, Microsoft.Identity.Client (MSAL.NET) | .NET 8.0 | + +> [!NOTE] +> All samples use the latest supported versions of the Microsoft identity libraries and are configured for secure, modern authentication scenarios. + +--- + +## 🚀 Getting Started + +Follow these steps to set up your environment and run any of the Web API samples in this folder. + +### ☑️ Prerequisites + +You will need the following to run any of these samples + + - [.NET 8.0 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) + - A Microsoft Entra tenant and app registration (see each sample's README for details) + - An editor or IDE such as [Visual Studio](https://visualstudio.microsoft.com/) or [Visual Studio Code](https://code.visualstudio.com/) + +### 📥 Clone the Repository + +1. Navigate to where you want to have the sample located, and enter the following + + ```sh + git clone https://github.com/your-org/ms-identity-docs-code-dotnet.git + ``` +2. Navigate to the web API folder in the sample you have downloaded by using the following command; + + ```sh + cd ms-identity-docs-code-dotnet/2-web-apis + ``` +--- + +## 📚 Resources + +- [Microsoft Identity Platform Documentation](https://learn.microsoft.com/entra/identity-platform/) +- [Microsoft.Identity.Web Library](https://learn.microsoft.com/entra/msal/dotnet/microsoft-identity-web/) +- [MSAL.NET Library](https://learn.microsoft.com/entra/identity-platform/msal-overview) +- [Microsoft Entra App Registration](https://learn.microsoft.com/entra/identity-platform/quickstart-register-app) +- [Securing ASP.NET Core with Microsoft Identity](https://learn.microsoft.com/aspnet/core/security/authentication/identity) \ No newline at end of file diff --git a/web-api-azure-function/.gitignore b/2-web-apis/web-api-azure-function/.gitignore similarity index 100% rename from web-api-azure-function/.gitignore rename to 2-web-apis/web-api-azure-function/.gitignore diff --git a/web-api-azure-function/Api.csproj b/2-web-apis/web-api-azure-function/Api.csproj similarity index 97% rename from web-api-azure-function/Api.csproj rename to 2-web-apis/web-api-azure-function/Api.csproj index f6cccce..3d8ae29 100644 --- a/web-api-azure-function/Api.csproj +++ b/2-web-apis/web-api-azure-function/Api.csproj @@ -1,18 +1,18 @@ - - - net8.0 - v4 - - - - - - - PreserveNewest - - - PreserveNewest - Never - - - + + + net8.0 + v4 + + + + + + + PreserveNewest + + + PreserveNewest + Never + + + diff --git a/web-api-azure-function/README.md b/2-web-apis/web-api-azure-function/README.md similarity index 84% rename from web-api-azure-function/README.md rename to 2-web-apis/web-api-azure-function/README.md index 5212ce9..7f077d7 100644 --- a/web-api-azure-function/README.md +++ b/2-web-apis/web-api-azure-function/README.md @@ -1,11 +1,11 @@ --- -# Metadata required by https://docs.microsoft.com/samples/browse/ -# Metadata properties: https://review.docs.microsoft.com/help/contribute/samples/process/onboarding?branch=main#add-metadata-to-readme +# Metadata required by https://learn.microsoft.com/samples/browse/ +# Metadata properties: https://review.learn.microsoft.com/help/contribute/samples/process/onboarding?branch=main#add-metadata-to-readme languages: - csharp page_type: sample name: C# Azure Function that protects an HTTP trigger function with Easy Auth and access token scope validation. -description: This C# Azure Function protects its own HTTP Trigger function with Easy Auth and access token scope validation. The code in this sample is used by one or more articles on docs.microsoft.com. +description: This C# Azure Function protects its own HTTP Trigger function with Easy Auth and access token scope validation. The code in this sample is used by one or more articles on learn.microsoft.com. products: - azure - entra-id @@ -26,20 +26,20 @@ $ curl https://.azurewebsites.net/api/greeting -H "Authorization: Hello, world. You were able to access this because you provided a valid access token with the Greeting.Read scope as a claim. ``` -> :page_with_curl: This sample application backs one or more technical articles on docs.microsoft.com. +> :page_with_curl: This sample application backs one or more technical articles on learn.microsoft.com. ## Prerequisites - Microsoft Entra tenant and the permissions or role required for managing app registrations in the tenant. - [.NET 8.0 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) -- An empty [C# Azure function (v4)](https://docs.microsoft.com/azure/azure-functions/create-first-function-cli-csharp) deployed to Azure, and the permissions or role required to modify its settings. -- [Azure Functions Core Tools](https://docs.microsoft.com/azure/azure-functions/functions-run-local) +- An empty [C# Azure function (v4)](https://learn.microsoft.com/azure/azure-functions/create-first-function-cli-csharp) deployed to Azure, and the permissions or role required to modify its settings. +- [Azure Functions Core Tools](https://learn.microsoft.com/azure/azure-functions/functions-run-local) ## Setup ### 1. Register the app -First, complete the steps in [Register an API application with the Microsoft identity platform](https://docs.microsoft.com/azure/active-directory/develop/quickstart-configure-app-expose-web-apis#register-the-web-api) to register the sample app. +First, complete the steps in [Register an API application with the Microsoft identity platform](https://learn.microsoft.com/entra/identity-platform/quickstart-configure-app-expose-web-apis#register-the-web-api) to register the sample app. Use these settings in your app registration. @@ -54,7 +54,7 @@ Use these settings in your app registration. ### 2. Enable Function app authentication -Next, complete the steps in [Enable Microsoft Entra ID in your App Service app](https://docs.microsoft.com/azure/app-service/configure-authentication-provider-aad?toc=/azure/azure-functions/toc.json#-enable-azure-active-directory-in-your-app-service-app) to add Microsoft Entra ID as an identity provider for your API. +Next, complete the steps in [Enable Microsoft Entra ID in your App Service app](https://learn.microsoft.com/azure/app-service/configure-authentication-provider-aad?toc=/azure/azure-functions/toc.json#-enable-azure-active-directory-in-your-app-service-app) to add Microsoft Entra ID as an identity provider for your API. Use these settings in your identity provider configuration. @@ -93,7 +93,7 @@ Hello, world. You were able to access this because you provided a valid access t ## About the code -This Azure Function is an anonymous HTTP trigger written in csharp and uses the built-in [Authentication and authorization in Azure Functions](https://docs.microsoft.com/azure/app-service/overview-authentication-authorization) feature to offload fundamental JWT access token validation. Requests that make it through the built-in authentication feature of Azure Functions are then routed to the csharp code, which applies additional access token validation checking for a specific scope. +This Azure Function is an anonymous HTTP trigger written in csharp and uses the built-in [Authentication and authorization in Azure Functions](https://learn.microsoft.com/azure/app-service/overview-authentication-authorization) feature to offload fundamental JWT access token validation. Requests that make it through the built-in authentication feature of Azure Functions are then routed to the csharp code, which applies additional access token validation checking for a specific scope. - A missing or invalid (expired, wrong audience, etc) token will result in a `401` response. (Handled by Azure Functions authentication) - An otherwise valid token without the proper scope will result in a `403` response. @@ -116,7 +116,7 @@ If you can't get the sample working, you've checked [Stack Overflow](https://sta > :warning: WARNING: Any issue in this repository _not_ limited to running one of its sample apps will be closed without being addressed. -For all other requests, see [Support and help options for developers | Microsoft identity platform](https://docs.microsoft.com/azure/active-directory/develop/developer-support-help-options). +For all other requests, see [Support and help options for developers | Microsoft identity platform](https://learn.microsoft.com/entra/identity-platform/developer-support-help-options). ## Contributing diff --git a/web-api-azure-function/greeting.cs b/2-web-apis/web-api-azure-function/greeting.cs similarity index 97% rename from web-api-azure-function/greeting.cs rename to 2-web-apis/web-api-azure-function/greeting.cs index a5d6869..9f1ddf4 100644 --- a/web-api-azure-function/greeting.cs +++ b/2-web-apis/web-api-azure-function/greeting.cs @@ -1,49 +1,49 @@ -/* -This is an Azure Function that responds at GET /api/greeting. - -Using the built-in authentication and authorization capabilities (sometimes -referred to as "Easy Auth") of Azure Functions, offloads part of the authentication -and authorization process by ensuring that every request to this Azure Function has -an access token. That access token has had its signature, issuer (iss), expiry -dates (exp, nbf), and audience (aud) validated. This means all that is left to -perform is any per-function authorization related to your application. -*/ - -using System; -using System.IO; -using System.Security.Claims; -using System.Collections.Generic; -using System.Linq; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.Http; -using Microsoft.AspNetCore.Http; - -namespace Api -{ - public static class Greeting - { - /* - Azure Functions HTTP Triggers perform automatic input binding of the - Easy Auth-validated JWT token data into the ClaimsPincipal. - Using the claims principal allows additional access token validation specific to this function. - */ - [FunctionName("greeting")] - public static IActionResult Run( - [HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequest req, - ClaimsPrincipal principal) - { - // This API endpoint requires the "Greeting.Read" scope to be present, if it is - // not, then reject the request with a 403. - if (!principal.Claims.Any( - c => c.Type == "http://schemas.microsoft.com/identity/claims/scope" - && c.Value.Split(' ').Contains("Greeting.Read"))) - { - return new ObjectResult("Forbidden") { StatusCode = 403}; - } - - // Authentication is complete, process request. - return new OkObjectResult("Hello, world. You were able to access this because you provided a valid access token with the Greeting.Read scope as a claim."); - } - } -} +/* +This is an Azure Function that responds at GET /api/greeting. + +Using the built-in authentication and authorization capabilities (sometimes +referred to as "Easy Auth") of Azure Functions, offloads part of the authentication +and authorization process by ensuring that every request to this Azure Function has +an access token. That access token has had its signature, issuer (iss), expiry +dates (exp, nbf), and audience (aud) validated. This means all that is left to +perform is any per-function authorization related to your application. +*/ + +using System; +using System.IO; +using System.Security.Claims; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.Http; +using Microsoft.AspNetCore.Http; + +namespace Api +{ + public static class Greeting + { + /* + Azure Functions HTTP Triggers perform automatic input binding of the + Easy Auth-validated JWT token data into the ClaimsPincipal. + Using the claims principal allows additional access token validation specific to this function. + */ + [FunctionName("greeting")] + public static IActionResult Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequest req, + ClaimsPrincipal principal) + { + // This API endpoint requires the "Greeting.Read" scope to be present, if it is + // not, then reject the request with a 403. + if (!principal.Claims.Any( + c => c.Type == "http://schemas.microsoft.com/identity/claims/scope" + && c.Value.Split(' ').Contains("Greeting.Read"))) + { + return new ObjectResult("Forbidden") { StatusCode = 403}; + } + + // Authentication is complete, process request. + return new OkObjectResult("Hello, world. You were able to access this because you provided a valid access token with the Greeting.Read scope as a claim."); + } + } +} diff --git a/web-api-azure-function/host.json b/2-web-apis/web-api-azure-function/host.json similarity index 95% rename from web-api-azure-function/host.json rename to 2-web-apis/web-api-azure-function/host.json index 809a3f2..beb2e40 100644 --- a/web-api-azure-function/host.json +++ b/2-web-apis/web-api-azure-function/host.json @@ -1,11 +1,11 @@ -{ - "version": "2.0", - "logging": { - "applicationInsights": { - "samplingSettings": { - "isEnabled": true, - "excludedTypes": "Request" - } - } - } +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + } + } } \ No newline at end of file diff --git a/web-api-obo-user/Api.csproj b/2-web-apis/web-api-obo-client/Api.csproj similarity index 97% rename from web-api-obo-user/Api.csproj rename to 2-web-apis/web-api-obo-client/Api.csproj index 4aa0e60..05bf39b 100644 --- a/web-api-obo-user/Api.csproj +++ b/2-web-apis/web-api-obo-client/Api.csproj @@ -1,11 +1,11 @@ - - - net8.0 - enable - enable - - - - - + + + net8.0 + enable + enable + + + + + \ No newline at end of file diff --git a/web-api-obo-client/Program.cs b/2-web-apis/web-api-obo-client/Program.cs similarity index 97% rename from web-api-obo-client/Program.cs rename to 2-web-apis/web-api-obo-client/Program.cs index 17652d5..248b52f 100644 --- a/web-api-obo-client/Program.cs +++ b/2-web-apis/web-api-obo-client/Program.cs @@ -1,41 +1,41 @@ -using System.Text.Json; -// -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.Identity.Web; -using Microsoft.Identity.Abstractions; -// - -// -WebApplicationBuilder builder = WebApplication.CreateBuilder(args); -// Acquire an access token from Microsoft Entra ID for this client to access Microsoft Graph based -// on the permissions granted this application in its Microsoft Entra App registration. -// The client credential flow will automatically attempt to use or renew any cached -// tokens, without the need to call acquireTokenSilently first. -builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) - .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")) - .EnableTokenAcquisitionToCallDownstreamApi() - .AddInMemoryTokenCaches() - .AddDownstreamApi("GraphApi", builder.Configuration.GetSection("GraphApi")); -// - -WebApplication app = builder.Build(); - -// -app.MapGet("api/application", async (IDownstreamApi downstreamApi) => - { - using var response = await downstreamApi.CallApiForAppAsync("GraphApi").ConfigureAwait(false); - - if (response.StatusCode == System.Net.HttpStatusCode.OK) - { - var graphApiResponse = await response.Content.ReadFromJsonAsync().ConfigureAwait(false); - return JsonSerializer.Serialize(graphApiResponse, new JsonSerializerOptions { WriteIndented = true }); - } - else - { - var error = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - throw new HttpRequestException($"Invalid status code in the HttpResponseMessage: {response.StatusCode}: {error}"); - } - }); -// - -app.Run(); +using System.Text.Json; +// +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Identity.Web; +using Microsoft.Identity.Abstractions; +// + +// +WebApplicationBuilder builder = WebApplication.CreateBuilder(args); +// Acquire an access token from Microsoft Entra ID for this client to access Microsoft Graph based +// on the permissions granted this application in its Microsoft Entra App registration. +// The client credential flow will automatically attempt to use or renew any cached +// tokens, without the need to call acquireTokenSilently first. +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")) + .EnableTokenAcquisitionToCallDownstreamApi() + .AddInMemoryTokenCaches() + .AddDownstreamApi("GraphApi", builder.Configuration.GetSection("GraphApi")); +// + +WebApplication app = builder.Build(); + +// +app.MapGet("api/application", async (IDownstreamApi downstreamApi) => + { + using var response = await downstreamApi.CallApiForAppAsync("GraphApi").ConfigureAwait(false); + + if (response.StatusCode == System.Net.HttpStatusCode.OK) + { + var graphApiResponse = await response.Content.ReadFromJsonAsync().ConfigureAwait(false); + return JsonSerializer.Serialize(graphApiResponse, new JsonSerializerOptions { WriteIndented = true }); + } + else + { + var error = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new HttpRequestException($"Invalid status code in the HttpResponseMessage: {response.StatusCode}: {error}"); + } + }); +// + +app.Run(); diff --git a/web-api-obo-client/README.md b/2-web-apis/web-api-obo-client/README.md similarity index 92% rename from web-api-obo-client/README.md rename to 2-web-apis/web-api-obo-client/README.md index 8c236bc..bdc1c40 100644 --- a/web-api-obo-client/README.md +++ b/2-web-apis/web-api-obo-client/README.md @@ -1,6 +1,6 @@ --- -# Metadata required by https://docs.microsoft.com/samples/browse/ -# Metadata properties: https://review.docs.microsoft.com/help/contribute/samples/process/onboarding?branch=main#add-metadata-to-readme +# Metadata required by https://learn.microsoft.com/samples/browse/ +# Metadata properties: https://review.learn.microsoft.com/help/contribute/samples/process/onboarding?branch=main#add-metadata-to-readme languages: - csharp page_type: sample @@ -59,7 +59,7 @@ $ curl https://localhost:5001/api/application ### 1. Register the app -First, complete the steps in [Quickstart: Register an application with the Microsoft identity platform](https://docs.microsoft.com/azure/active-directory/develop/quickstart-register-app) to register the web API. +First, complete the steps in [Quickstart: Register an application with the Microsoft identity platform](https://learn.microsoft.com/entra/identity-platform/quickstart-register-app) to register the web API. Use these settings in your app registration. @@ -74,7 +74,7 @@ Use these settings in your app registration. ### 2. Configure the web API -Open the _~/msal-client-credentials-flow/appsettings.json_ file in your code editor and modify the following values values with those from your [app's registration in the Microsoft Entra admin center](https://docs.microsoft.com/azure/active-directory/develop/quickstart-register-app#register-an-application): +Open the _~/msal-client-credentials-flow/appsettings.json_ file in your code editor and modify the following values values with those from your [app's registration in the Microsoft Entra admin center](https://learn.microsoft.com/entra/identity-platform/quickstart-register-app#register-an-application): ```json "ClientId": "Enter_the_Application_Id_here", @@ -155,7 +155,7 @@ If you can't get the sample working, you've checked [Stack Overflow](http://stac > :warning: WARNING: Any issue in this repository _not_ limited to running one of its sample apps will be closed without being addressed. -For all other requests, see [Support and help options for developers | Microsoft identity platform](https://docs.microsoft.com/azure/active-directory/develop/developer-support-help-options). +For all other requests, see [Support and help options for developers | Microsoft identity platform](https://learn.microsoft.com/entra/identity-platform/developer-support-help-options). ## Contributing diff --git a/web-api-obo-client/appsettings.json b/2-web-apis/web-api-obo-client/appsettings.json similarity index 96% rename from web-api-obo-client/appsettings.json rename to 2-web-apis/web-api-obo-client/appsettings.json index 3f0331f..73acc4f 100644 --- a/web-api-obo-client/appsettings.json +++ b/2-web-apis/web-api-obo-client/appsettings.json @@ -1,20 +1,20 @@ -{ - "AzureAd": { - "Instance": "https://login.microsoftonline.com/", - "ClientId": "Enter_the_Application_Id_here", - "TenantId": "Enter_the_Tenant_Info_Here", - "ClientSecret": "Enter_the_Application_Client_Secret_here" - }, - "GraphApi": { - "BaseUrl": "https://graph.microsoft.com/v1.0/applications", - "RelativePath": "Enter_the_Application_Object_Id_here", - "Scopes": ["https://graph.microsoft.com/.default"] - }, - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*" -} +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "ClientId": "Enter_the_Application_Id_here", + "TenantId": "Enter_the_Tenant_Info_Here", + "ClientSecret": "Enter_the_Application_Client_Secret_here" + }, + "GraphApi": { + "BaseUrl": "https://graph.microsoft.com/v1.0/applications", + "RelativePath": "Enter_the_Application_Object_Id_here", + "Scopes": ["https://graph.microsoft.com/.default"] + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/web-api-obo-client/Api.csproj b/2-web-apis/web-api-obo-user/Api.csproj similarity index 97% rename from web-api-obo-client/Api.csproj rename to 2-web-apis/web-api-obo-user/Api.csproj index 4aa0e60..05bf39b 100644 --- a/web-api-obo-client/Api.csproj +++ b/2-web-apis/web-api-obo-user/Api.csproj @@ -1,11 +1,11 @@ - - - net8.0 - enable - enable - - - - - + + + net8.0 + enable + enable + + + + + \ No newline at end of file diff --git a/web-api-obo-user/Program.cs b/2-web-apis/web-api-obo-user/Program.cs similarity index 97% rename from web-api-obo-user/Program.cs rename to 2-web-apis/web-api-obo-user/Program.cs index a3419f0..eaadbee 100644 --- a/web-api-obo-user/Program.cs +++ b/2-web-apis/web-api-obo-user/Program.cs @@ -1,39 +1,39 @@ -using System.Text.Json; -// -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Authorization; -using Microsoft.Identity.Abstractions; -using Microsoft.Identity.Web; - -// - -// -WebApplicationBuilder builder = WebApplication.CreateBuilder(args); -builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) - .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")) - .EnableTokenAcquisitionToCallDownstreamApi() -// The access token came from the in-memory token cache, which maintains -// the on-behalf-of access token, per user-assertion, based on the provided access -// token to this API. - .AddInMemoryTokenCaches() - .AddDownstreamApi("GraphApi", builder.Configuration.GetSection("GraphApi")); -builder.Services.AddAuthorization(); -// - -// -WebApplication app = builder.Build(); -app.UseAuthentication(); -app.UseAuthorization(); -// - -// -app.MapGet("/api/me", [Authorize()] async (IDownstreamApi downstreamWebApi) => -{ - var response = await downstreamWebApi.CallApiForUserAsync("GraphApi").ConfigureAwait(false); - - var graphApiResponse = await response.Content.ReadFromJsonAsync().ConfigureAwait(false); - return JsonSerializer.Serialize(graphApiResponse, new JsonSerializerOptions { WriteIndented = true }); -}); -// - -app.Run(); +using System.Text.Json; +// +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Web; + +// + +// +WebApplicationBuilder builder = WebApplication.CreateBuilder(args); +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")) + .EnableTokenAcquisitionToCallDownstreamApi() +// The access token came from the in-memory token cache, which maintains +// the on-behalf-of access token, per user-assertion, based on the provided access +// token to this API. + .AddInMemoryTokenCaches() + .AddDownstreamApi("GraphApi", builder.Configuration.GetSection("GraphApi")); +builder.Services.AddAuthorization(); +// + +// +WebApplication app = builder.Build(); +app.UseAuthentication(); +app.UseAuthorization(); +// + +// +app.MapGet("/api/me", [Authorize()] async (IDownstreamApi downstreamWebApi) => +{ + var response = await downstreamWebApi.CallApiForUserAsync("GraphApi").ConfigureAwait(false); + + var graphApiResponse = await response.Content.ReadFromJsonAsync().ConfigureAwait(false); + return JsonSerializer.Serialize(graphApiResponse, new JsonSerializerOptions { WriteIndented = true }); +}); +// + +app.Run(); diff --git a/web-api-obo-user/README.md b/2-web-apis/web-api-obo-user/README.md similarity index 91% rename from web-api-obo-user/README.md rename to 2-web-apis/web-api-obo-user/README.md index 8c2c75a..032e16d 100644 --- a/web-api-obo-user/README.md +++ b/2-web-apis/web-api-obo-user/README.md @@ -1,11 +1,11 @@ --- -# Metadata required by https://docs.microsoft.com/samples/browse/ -# Metadata properties: https://review.docs.microsoft.com/help/contribute/samples/process/onboarding?branch=main#add-metadata-to-readme +# Metadata required by https://learn.microsoft.com/samples/browse/ +# Metadata properties: https://review.learn.microsoft.com/help/contribute/samples/process/onboarding?branch=main#add-metadata-to-readme languages: - csharp page_type: sample name: ASP.NET Core minimal web API that both protects its own endpoints and accesses Microsoft Graph. -description: This ASP.NET Core minimal web API protects an API endpoint that access on behalf of the user another protected API. The code in this sample is used by one or more articles on docs.microsoft.com. +description: This ASP.NET Core minimal web API protects an API endpoint that access on behalf of the user another protected API. The code in this sample is used by one or more articles on learn.microsoft.com. products: - azure - entra-id @@ -19,7 +19,7 @@ urlFragment: ms-identity-docs-code-obo-user-csharp ![Build passing.](https://img.shields.io/badge/build-passing-brightgreen.svg) ![Code coverage.](https://img.shields.io/badge/coverage-100%25-brightgreen.svg) ![License.](https://img.shields.io/badge/license-MIT-green.svg) --> -This ASP.NET Core minimal web API uses the Microsoft identity platform to protect an endpoint (require authorized access), and also accesses Microsoft Graph on behalf of the user. The API uses [ASP.NET Core Identity](https://docs.microsoft.com/aspnet/core/security/authentication/identity?view=aspnetcore-8.0) interacting with the [Microsoft Authentication Library (MSAL)](https://docs.microsoft.com/azure/active-directory/develop/msal-overview) to protect its endpoint. +This ASP.NET Core minimal web API uses the Microsoft identity platform to protect an endpoint (require authorized access), and also accesses Microsoft Graph on behalf of the user. The API uses [ASP.NET Core Identity](https://learn.microsoft.com/aspnet/core/security/authentication/identity?view=aspnetcore-8.0) interacting with the [Microsoft Authentication Library (MSAL)](https://learn.microsoft.com/entra/identity-platform/msal-overview) to protect its endpoint. ```console $ curl https://localhost:5001/api/me -H "Authorization: Bearer {valid-access-token}" @@ -38,7 +38,7 @@ $ curl https://localhost:5001/api/me -H "Authorization: Bearer {valid-access-tok } ``` -> :page_with_curl: This sample application backs one or more technical articles on docs.microsoft.com. +> :page_with_curl: This sample application backs one or more technical articles on learn.microsoft.com. ## Prerequisites @@ -49,7 +49,7 @@ $ curl https://localhost:5001/api/me -H "Authorization: Bearer {valid-access-tok ### 1. Register the web API application in Microsoft Entra ID -First, complete the steps in [Configure an application to expose a web API](https://docs.microsoft.com/azure/active-directory/develop/quickstart-register-app) to register the sample API and expose a scope. +First, complete the steps in [Configure an application to expose a web API](https://learn.microsoft.com/entra/identity-platform/quickstart-register-app) to register the sample API and expose a scope. Use the following settings for your app registration: @@ -68,7 +68,7 @@ Use the following settings for your app registration: ### 2. Register a client application in Microsoft Entra ID -Second, complete the steps in [Register an application with the Microsoft identity platform](https://docs.microsoft.com/azure/active-directory/develop/quickstart-register-app) to register the client sample app. +Second, complete the steps in [Register an application with the Microsoft identity platform](https://learn.microsoft.com/entra/identity-platform/quickstart-register-app) to register the client sample app. Use the following settings for your app registration: @@ -166,7 +166,7 @@ If you can't get the sample working, you've checked [Stack Overflow](http://stac > :warning: WARNING: Any issue _not_ limited to running this or another sample app will be closed without being addressed. -For all other requests, see [Support and help options for developers | Microsoft identity platform](https://docs.microsoft.com/azure/active-directory/develop/developer-support-help-options). +For all other requests, see [Support and help options for developers | Microsoft identity platform](https://learn.microsoft.com/entra/identity-platform/developer-support-help-options). ## Contributing diff --git a/web-api-obo-user/appsettings.json b/2-web-apis/web-api-obo-user/appsettings.json similarity index 96% rename from web-api-obo-user/appsettings.json rename to 2-web-apis/web-api-obo-user/appsettings.json index 8074aec..f7f2761 100644 --- a/web-api-obo-user/appsettings.json +++ b/2-web-apis/web-api-obo-user/appsettings.json @@ -1,19 +1,19 @@ -{ - "AzureAd": { - "Instance": "https://login.microsoftonline.com/", - "ClientId": "Enter_the_Application_Id_here", - "TenantId": "Enter_the_Tenant_Info_here", - "ClientSecret": "Enter_the_Client_Secret_here" - }, - "GraphApi": { - "BaseUrl": "https://graph.microsoft.com/v1.0/me", - "Scopes": ["user.read"] - }, - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*" -} +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "ClientId": "Enter_the_Application_Id_here", + "TenantId": "Enter_the_Tenant_Info_here", + "ClientSecret": "Enter_the_Client_Secret_here" + }, + "GraphApi": { + "BaseUrl": "https://graph.microsoft.com/v1.0/me", + "Scopes": ["user.read"] + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/web-api/Api.csproj b/2-web-apis/web-api/Api.csproj similarity index 97% rename from web-api/Api.csproj rename to 2-web-apis/web-api/Api.csproj index 5d1a161..ad9e088 100644 --- a/web-api/Api.csproj +++ b/2-web-apis/web-api/Api.csproj @@ -1,17 +1,17 @@ - - - - net9.0 - enable - enable - dotnet_web_api - - - - - - - - - + + + + net9.0 + enable + enable + dotnet_web_api + + + + + + + + + \ No newline at end of file diff --git a/web-api/Program.cs b/2-web-apis/web-api/Program.cs similarity index 96% rename from web-api/Program.cs rename to 2-web-apis/web-api/Program.cs index 075e2af..737d680 100644 --- a/web-api/Program.cs +++ b/2-web-apis/web-api/Program.cs @@ -1,52 +1,52 @@ -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.Identity.Web; - -var builder = WebApplication.CreateBuilder(args); - -// Configure authentication -builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) - .AddMicrosoftIdentityWebApi(options => - { - builder.Configuration.Bind("AzureAd", options); - options.TokenValidationParameters.NameClaimType = "name"; - }, options => { builder.Configuration.Bind("AzureAd", options); }); - -// Configure authorization -builder.Services.AddAuthorization(config => -{ -config.AddPolicy("AuthZPolicy", policy => - policy.RequireRole("Forecast.Read")); -}); - -var app = builder.Build(); - -app.UseHttpsRedirection(); -app.UseAuthentication(); -app.UseAuthorization(); - -var summaries = new[] -{ - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" -}; - -app.MapGet("/weatherforecast", () => -{ - var forecast = Enumerable.Range(1, 5).Select(index => - new WeatherForecast - ( - DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - Random.Shared.Next(-20, 55), - summaries[Random.Shared.Next(summaries.Length)] - )) - .ToArray(); - return forecast; -}) -.WithName("weatherForecast") -.RequireAuthorization("AuthZPolicy"); // Protect this endpoint with the AuthZPolicy - -app.Run(); - -record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) -{ - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Identity.Web; + +var builder = WebApplication.CreateBuilder(args); + +// Configure authentication +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApi(options => + { + builder.Configuration.Bind("AzureAd", options); + options.TokenValidationParameters.NameClaimType = "name"; + }, options => { builder.Configuration.Bind("AzureAd", options); }); + +// Configure authorization +builder.Services.AddAuthorization(config => +{ +config.AddPolicy("AuthZPolicy", policy => + policy.RequireRole("Forecast.Read")); +}); + +var app = builder.Build(); + +app.UseHttpsRedirection(); +app.UseAuthentication(); +app.UseAuthorization(); + +var summaries = new[] +{ + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" +}; + +app.MapGet("/weatherforecast", () => +{ + var forecast = Enumerable.Range(1, 5).Select(index => + new WeatherForecast + ( + DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + Random.Shared.Next(-20, 55), + summaries[Random.Shared.Next(summaries.Length)] + )) + .ToArray(); + return forecast; +}) +.WithName("weatherForecast") +.RequireAuthorization("AuthZPolicy"); // Protect this endpoint with the AuthZPolicy + +app.Run(); + +record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) +{ + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); } \ No newline at end of file diff --git a/web-api/Properties/launchSettings.json b/2-web-apis/web-api/Properties/launchSettings.json similarity index 100% rename from web-api/Properties/launchSettings.json rename to 2-web-apis/web-api/Properties/launchSettings.json diff --git a/web-api/README.md b/2-web-apis/web-api/README.md similarity index 86% rename from web-api/README.md rename to 2-web-apis/web-api/README.md index b949847..0a6d67e 100644 --- a/web-api/README.md +++ b/2-web-apis/web-api/README.md @@ -1,11 +1,11 @@ --- -# Metadata required by https://docs.microsoft.com/samples/browse/ -# Metadata properties: https://review.docs.microsoft.com/help/contribute/samples/process/onboarding?branch=main#add-metadata-to-readme +# Metadata required by https://learn.microsoft.com/samples/browse/ +# Metadata properties: https://review.learn.microsoft.com/help/contribute/samples/process/onboarding?branch=main#add-metadata-to-readme languages: - csharp page_type: sample name: ASP.NET Core minimal web API that protects API -description: This ASP.NET Core minimal web API protects an API endpoint. The code in this sample is used by one or more articles on docs.microsoft.com. +description: This ASP.NET Core minimal web API protects an API endpoint. The code in this sample is used by one or more articles on learn.microsoft.com. products: - azure - entra-id @@ -15,9 +15,9 @@ urlFragment: ms-identity-docs-code-web-apicsharp # ASP.NET Core minimal web API | web API | access control (protected routes) | Microsoft identity platform -The sample code provided here has been created using minimal web API in ASP.NET Core 6.0, and slightly modified to be protected for a single organization using [ASP.NET Core Identity](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/identity?view=aspnetcore-6.0) that interacts with [Microsoft Authentication Library (MSAL)](https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-overview). In other words, a very minimalist web api is secured by adding an authorization layer before user requests can reach protected resources. At this point it is expected that the user sign-in had already happened, so api calls can be made in the name of the signed-in user. For that to be possible a token containing user's information is being sent in the request headers and used in the authorization process. +The sample code provided here has been created using minimal web API in ASP.NET Core 6.0, and slightly modified to be protected for a single organization using [ASP.NET Core Identity](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/identity?view=aspnetcore-6.0) that interacts with [Microsoft Authentication Library (MSAL)](https://learn.microsoft.com/en-us/entra/identity-platform/msal-overview). In other words, a very minimalist web api is secured by adding an authorization layer before user requests can reach protected resources. At this point it is expected that the user sign-in had already happened, so api calls can be made in the name of the signed-in user. For that to be possible a token containing user's information is being sent in the request headers and used in the authorization process. -> :page_with_curl: This sample application backs one or more technical articles on docs.microsoft.com. +> :page_with_curl: This sample application backs one or more technical articles on learn.microsoft.com. ## Prerequisites @@ -28,7 +28,7 @@ The sample code provided here has been created using minimal web API in ASP.NET ### 1. Register the web API -First, complete the steps in [Quickstart: Configure an application to expose a web API](https://learn.microsoft.com/azure/active-directory/develop/quickstart-configure-app-expose-web-apis) to register the web API with the identity platform and configure its scopes. +First, complete the steps in [Quickstart: Configure an application to expose a web API](https://learn.microsoft.com/entra/identity-platform/quickstart-configure-app-expose-web-apis) to register the web API with the identity platform and configure its scopes. Use the following settings for your web API's app registration: @@ -141,7 +141,7 @@ If you can't get the sample working, you've checked [Stack Overflow](http://stac > :warning: WARNING: Any issue _not_ limited to running this or another sample app will be closed without being addressed. -For all other requests, see [Support and help options for developers | Microsoft identity platform](https://docs.microsoft.com/azure/active-directory/develop/developer-support-help-options). +For all other requests, see [Support and help options for developers | Microsoft identity platform](https://learn.microsoft.com/entra/identity-platform/developer-support-help-options). ## Contributing diff --git a/web-api/appsettings.json b/2-web-apis/web-api/appsettings.json similarity index 97% rename from web-api/appsettings.json rename to 2-web-apis/web-api/appsettings.json index ad9ec2b..4f19334 100644 --- a/web-api/appsettings.json +++ b/2-web-apis/web-api/appsettings.json @@ -1,15 +1,15 @@ -{ - "AzureAd": { - "Instance": "https://login.microsoftonline.com/", //For external tenants, use instance in the form of "https://Enter_the_Tenant_Subdomain_Here.ciamlogin.com/" - "TenantId": "Enter the tenant ID obtained from the Microsoft Entra admin center", - "ClientId": "Enter the client ID obtained from the Microsoft Entra admin center", - "Scopes": "Forecast.Read" - }, - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*" +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", //For external tenants, use instance in the form of "https://Enter_the_Tenant_Subdomain_Here.ciamlogin.com/" + "TenantId": "Enter the tenant ID obtained from the Microsoft Entra admin center", + "ClientId": "Enter the client ID obtained from the Microsoft Entra admin center", + "Scopes": "Forecast.Read" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" } \ No newline at end of file diff --git a/3-client-spa/README.md b/3-client-spa/README.md new file mode 100644 index 0000000..a626ca5 --- /dev/null +++ b/3-client-spa/README.md @@ -0,0 +1,48 @@ +# 🖥️ Client-Side Samples + +This folder contains client-side .NET samples demonstrating authentication and authorization scenarios using the Microsoft identity platform. These samples focus on single-page applications (SPA) and client-side Blazor WebAssembly apps. + +## 📋 Samples Overview + +| 📁 Folder Name | 🔑 Authentication Libraries Used | 🏷️ .NET Version | +|----------------------------------------------------|---------------------------------------------------------------|-------------------| +| [spa-blazor-wasm](./spa-blazor-wasm) | Microsoft.Authentication.WebAssembly.Msal, Microsoft.Identity.Client (MSAL.NET) | .NET 8.0 | + +> [!NOTE] +> All samples use the latest supported versions of the Microsoft identity libraries and are configured for secure, modern authentication scenarios. + +--- + +## 🚀 Getting Started + +Follow these steps to set up your environment and run any of the SPA samples in this folder. + +### ☑️ Prerequisites + +You will need the following to run any of these samples + + - [.NET 8.0 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) + - A Microsoft Entra tenant and app registration (see each sample's README for details) + - An editor or IDE such as [Visual Studio](https://visualstudio.microsoft.com/) or [Visual Studio Code](https://code.visualstudio.com/) + +### 📥 Clone the Repository + +1. Navigate to where you want to have the sample located, and enter the following + + ```sh + git clone https://github.com/MicrosoftDocs/ms-identity-docs-code-dotnet.git + ``` +2. Navigate to the SPA folder in the sample you have downloaded by using the following command; + + ```sh + cd ms-identity-docs-code-dotnet/3-client-spa/ + ``` +--- + +## 📚 Resources + +- [Microsoft Identity Platform Documentation](https://learn.microsoft.com/entra/identity-platform/) +- [Microsoft.Identity.Web Library](https://learn.microsoft.com/entra/msal/dotnet/microsoft-identity-web/) +- [MSAL.NET Library](https://learn.microsoft.com/entra/identity-platform/msal-overview) +- [Microsoft Entra App Registration](https://learn.microsoft.com/entra/identity-platform/quickstart-register-app) +- [Securing ASP.NET Core with Microsoft Identity](https://learn.microsoft.com/aspnet/core/security/authentication/identity) \ No newline at end of file diff --git a/spa-blazor-wasm/App.razor b/3-client-spa/spa-blazor-wasm/App.razor similarity index 97% rename from spa-blazor-wasm/App.razor rename to 3-client-spa/spa-blazor-wasm/App.razor index 3a51d03..d07039a 100644 --- a/spa-blazor-wasm/App.razor +++ b/3-client-spa/spa-blazor-wasm/App.razor @@ -1,25 +1,25 @@ - - - - - - @if (context.User.Identity?.IsAuthenticated != true) - { - - } - else - { -

You are not authorized to access this resource.

- } -
-
- -
- - Not found - -

Sorry, there's nothing at this address.

-
-
-
-
+ + + + + + @if (context.User.Identity?.IsAuthenticated != true) + { + + } + else + { +

You are not authorized to access this resource.

+ } +
+
+ +
+ + Not found + +

Sorry, there's nothing at this address.

+
+
+
+
diff --git a/spa-blazor-wasm/BlazorWasm.csproj b/3-client-spa/spa-blazor-wasm/BlazorWasm.csproj similarity index 97% rename from spa-blazor-wasm/BlazorWasm.csproj rename to 3-client-spa/spa-blazor-wasm/BlazorWasm.csproj index de92765..28daa0a 100644 --- a/spa-blazor-wasm/BlazorWasm.csproj +++ b/3-client-spa/spa-blazor-wasm/BlazorWasm.csproj @@ -1,21 +1,21 @@ - - - - net8.0 - enable - enable - service-worker-assets.js - - - - - - - - - - - - - - + + + + net8.0 + enable + enable + service-worker-assets.js + + + + + + + + + + + + + + diff --git a/spa-blazor-wasm/Pages/Authentication.razor b/3-client-spa/spa-blazor-wasm/Pages/Authentication.razor similarity index 96% rename from spa-blazor-wasm/Pages/Authentication.razor rename to 3-client-spa/spa-blazor-wasm/Pages/Authentication.razor index b53dee2..6c74356 100644 --- a/spa-blazor-wasm/Pages/Authentication.razor +++ b/3-client-spa/spa-blazor-wasm/Pages/Authentication.razor @@ -1,7 +1,7 @@ -@page "/authentication/{action}" -@using Microsoft.AspNetCore.Components.WebAssembly.Authentication - - -@code{ - [Parameter] public string? Action { get; set; } -} +@page "/authentication/{action}" +@using Microsoft.AspNetCore.Components.WebAssembly.Authentication + + +@code{ + [Parameter] public string? Action { get; set; } +} diff --git a/spa-blazor-wasm/Pages/Index.razor b/3-client-spa/spa-blazor-wasm/Pages/Index.razor similarity index 97% rename from spa-blazor-wasm/Pages/Index.razor rename to 3-client-spa/spa-blazor-wasm/Pages/Index.razor index a6d2de6..15420e6 100644 --- a/spa-blazor-wasm/Pages/Index.razor +++ b/3-client-spa/spa-blazor-wasm/Pages/Index.razor @@ -1,48 +1,48 @@ -@using Microsoft.AspNetCore.Components.Authorization -@using Microsoft.AspNetCore.Components.WebAssembly.Authentication -@using System.Text.Json -@inject HttpClient Http - -@page "/" - -Index - -

Welcome to User Sign In ASP.NET Core Blazor WebAssembly

- - - -
This page can be accessed by all users, authenticated or not.
-

Click Log in to sign into the application. Navigating to it while not logged in will automatically initiate the login process. Please note that it doesn't require that your user has any specific application roles assigned, and will access Microsoft Graph on your behalf.

-
- - @if (graphApiResponse != null) - { -

Before rendering the page, the controller was able to make a call to - Microsoft Graph's /me API for your user and received the - following:

- -

@JsonSerializer.Serialize(graphApiResponse, new JsonSerializerOptions { WriteIndented = true })

- -

Refreshing this page will continue to use the cached access token acquired for Microsoft Graph, which is valid for future page views will attempt to refresh this token as it nears its expiration.

- } - - @code { - private JsonDocument? graphApiResponse = null; - - protected override async Task OnInitializedAsync() - { - try - { - using var response = await Http.GetAsync("https://graph.microsoft.com/v1.0/me"); - response.EnsureSuccessStatusCode(); - graphApiResponse = await response.Content.ReadFromJsonAsync().ConfigureAwait(false); - } - catch (AccessTokenNotAvailableException exception) - { - exception.Redirect(); - } - } - - } -
-
+@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.WebAssembly.Authentication +@using System.Text.Json +@inject HttpClient Http + +@page "/" + +Index + +

Welcome to User Sign In ASP.NET Core Blazor WebAssembly

+ + + +
This page can be accessed by all users, authenticated or not.
+

Click Log in to sign into the application. Navigating to it while not logged in will automatically initiate the login process. Please note that it doesn't require that your user has any specific application roles assigned, and will access Microsoft Graph on your behalf.

+
+ + @if (graphApiResponse != null) + { +

Before rendering the page, the controller was able to make a call to + Microsoft Graph's /me API for your user and received the + following:

+ +

@JsonSerializer.Serialize(graphApiResponse, new JsonSerializerOptions { WriteIndented = true })

+ +

Refreshing this page will continue to use the cached access token acquired for Microsoft Graph, which is valid for future page views will attempt to refresh this token as it nears its expiration.

+ } + + @code { + private JsonDocument? graphApiResponse = null; + + protected override async Task OnInitializedAsync() + { + try + { + using var response = await Http.GetAsync("https://graph.microsoft.com/v1.0/me"); + response.EnsureSuccessStatusCode(); + graphApiResponse = await response.Content.ReadFromJsonAsync().ConfigureAwait(false); + } + catch (AccessTokenNotAvailableException exception) + { + exception.Redirect(); + } + } + + } +
+
diff --git a/spa-blazor-wasm/Program.cs b/3-client-spa/spa-blazor-wasm/Program.cs similarity index 97% rename from spa-blazor-wasm/Program.cs rename to 3-client-spa/spa-blazor-wasm/Program.cs index d4fe526..166ccd1 100644 --- a/spa-blazor-wasm/Program.cs +++ b/3-client-spa/spa-blazor-wasm/Program.cs @@ -1,29 +1,29 @@ -using Microsoft.AspNetCore.Components.Web; -using Microsoft.AspNetCore.Components.WebAssembly.Authentication; -using Microsoft.AspNetCore.Components.WebAssembly.Hosting; -using BlazorWasm; - -var builder = WebAssemblyHostBuilder.CreateDefault(args); -builder.RootComponents.Add("#app"); -builder.RootComponents.Add("head::after"); - -builder.Services.AddMsalAuthentication(options => -{ - builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication); - options.ProviderOptions.DefaultAccessTokenScopes - .Add("https://graph.microsoft.com/User.Read"); -}); - -builder.Services.AddScoped(sp => -{ - var authorizationMessageHandler = - sp.GetRequiredService(); - authorizationMessageHandler.InnerHandler = new HttpClientHandler(); - authorizationMessageHandler.ConfigureHandler( - authorizedUrls: new[] { "https://graph.microsoft.com/v1.0" }, - scopes: new[] { "User.Read" }); - - return new HttpClient(authorizationMessageHandler); -}); - -await builder.Build().RunAsync(); +using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.Components.WebAssembly.Authentication; +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using BlazorWasm; + +var builder = WebAssemblyHostBuilder.CreateDefault(args); +builder.RootComponents.Add("#app"); +builder.RootComponents.Add("head::after"); + +builder.Services.AddMsalAuthentication(options => +{ + builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication); + options.ProviderOptions.DefaultAccessTokenScopes + .Add("https://graph.microsoft.com/User.Read"); +}); + +builder.Services.AddScoped(sp => +{ + var authorizationMessageHandler = + sp.GetRequiredService(); + authorizationMessageHandler.InnerHandler = new HttpClientHandler(); + authorizationMessageHandler.ConfigureHandler( + authorizedUrls: new[] { "https://graph.microsoft.com/v1.0" }, + scopes: new[] { "User.Read" }); + + return new HttpClient(authorizationMessageHandler); +}); + +await builder.Build().RunAsync(); diff --git a/spa-blazor-wasm/README.md b/3-client-spa/spa-blazor-wasm/README.md similarity index 93% rename from spa-blazor-wasm/README.md rename to 3-client-spa/spa-blazor-wasm/README.md index b3b83bd..4b8a9c1 100644 --- a/spa-blazor-wasm/README.md +++ b/3-client-spa/spa-blazor-wasm/README.md @@ -1,11 +1,11 @@ --- -# Metadata required by https://docs.microsoft.com/samples/browse/ -# Metadata properties: https://review.docs.microsoft.com/help/contribute/samples/process/onboarding?branch=main#add-metadata-to-readme +# Metadata required by https://learn.microsoft.com/samples/browse/ +# Metadata properties: https://review.learn.microsoft.com/help/contribute/samples/process/onboarding?branch=main#add-metadata-to-readme languages: - csharp page_type: sample name: ASP.NET Core 8.0 Blazor WebAssembly that accesses Microsoft Graph -description: This ASP.NET Core 8.0 Blazor WebAssembly that signs in and contacts Microsoft Graph on behalf of the user. The code in this sample is used by one or more articles on docs.microsoft.com. +description: This ASP.NET Core 8.0 Blazor WebAssembly that signs in and contacts Microsoft Graph on behalf of the user. The code in this sample is used by one or more articles on learn.microsoft.com. products: - azure - entra-id @@ -32,7 +32,7 @@ The standalone app in this scenario is created using the ASP.NET Core 8.0 Blazor ### 1. Register the web API application in your Microsoft Entra ID -First, complete the steps in [Register an application with the Microsoft identity platform](https://docs.microsoft.com/azure/active-directory/develop/quickstart-register-app) to register the sample app. +First, complete the steps in [Register an application with the Microsoft identity platform](https://learn.microsoft.com/entra/identity-platform/quickstart-register-app) to register the sample app. Use the following settings for your app registration: @@ -104,7 +104,7 @@ If you can't get the sample working, you've checked [Stack Overflow](http://stac > :warning: WARNING: Any issue _not_ limited to running this or another sample app will be closed without being addressed. -For all other requests, see [Support and help options for developers | Microsoft identity platform](https://learn.microsoft.com/azure/active-directory/develop/developer-support-help-options). +For all other requests, see [Support and help options for developers | Microsoft identity platform](https://learn.microsoft.com/entra/identity-platform/developer-support-help-options). ## Contributing diff --git a/spa-blazor-wasm/Shared/LoginDisplay.razor b/3-client-spa/spa-blazor-wasm/Shared/LoginDisplay.razor similarity index 96% rename from spa-blazor-wasm/Shared/LoginDisplay.razor rename to 3-client-spa/spa-blazor-wasm/Shared/LoginDisplay.razor index 761d489..fdb8b6b 100644 --- a/spa-blazor-wasm/Shared/LoginDisplay.razor +++ b/3-client-spa/spa-blazor-wasm/Shared/LoginDisplay.razor @@ -1,21 +1,21 @@ -@using Microsoft.AspNetCore.Components.Authorization -@using Microsoft.AspNetCore.Components.WebAssembly.Authentication - -@inject NavigationManager Navigation - - - - Hello, @context.User.Identity?.Name! - - - - Log in - - - -@code{ - private void BeginLogout() - { - Navigation.NavigateToLogout("authentication/logout"); - } -} +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.WebAssembly.Authentication + +@inject NavigationManager Navigation + + + + Hello, @context.User.Identity?.Name! + + + + Log in + + + +@code{ + private void BeginLogout() + { + Navigation.NavigateToLogout("authentication/logout"); + } +} diff --git a/spa-blazor-wasm/Shared/MainLayout.razor b/3-client-spa/spa-blazor-wasm/Shared/MainLayout.razor similarity index 94% rename from spa-blazor-wasm/Shared/MainLayout.razor rename to 3-client-spa/spa-blazor-wasm/Shared/MainLayout.razor index bb69aa7..dae256d 100644 --- a/spa-blazor-wasm/Shared/MainLayout.razor +++ b/3-client-spa/spa-blazor-wasm/Shared/MainLayout.razor @@ -1,17 +1,17 @@ -@inherits LayoutComponentBase - -
- - -
-
- -
- -
- @Body -
-
-
+@inherits LayoutComponentBase + +
+ + +
+
+ +
+ +
+ @Body +
+
+
diff --git a/spa-blazor-wasm/Shared/MainLayout.razor.css b/3-client-spa/spa-blazor-wasm/Shared/MainLayout.razor.css similarity index 94% rename from spa-blazor-wasm/Shared/MainLayout.razor.css rename to 3-client-spa/spa-blazor-wasm/Shared/MainLayout.razor.css index c7ac763..c865427 100644 --- a/spa-blazor-wasm/Shared/MainLayout.razor.css +++ b/3-client-spa/spa-blazor-wasm/Shared/MainLayout.razor.css @@ -1,81 +1,81 @@ -.page { - position: relative; - display: flex; - flex-direction: column; -} - -main { - flex: 1; -} - -.sidebar { - background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); -} - -.top-row { - background-color: #f7f7f7; - border-bottom: 1px solid #d6d5d5; - justify-content: flex-end; - height: 3.5rem; - display: flex; - align-items: center; -} - - .top-row ::deep a, .top-row ::deep .btn-link { - white-space: nowrap; - margin-left: 1.5rem; - text-decoration: none; - } - - .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { - text-decoration: underline; - } - - .top-row ::deep a:first-child { - overflow: hidden; - text-overflow: ellipsis; - } - -@media (max-width: 640.98px) { - .top-row:not(.auth) { - display: none; - } - - .top-row.auth { - justify-content: space-between; - } - - .top-row ::deep a, .top-row ::deep .btn-link { - margin-left: 0; - } -} - -@media (min-width: 641px) { - .page { - flex-direction: row; - } - - .sidebar { - width: 250px; - height: 100vh; - position: sticky; - top: 0; - } - - .top-row { - position: sticky; - top: 0; - z-index: 1; - } - - .top-row.auth ::deep a:first-child { - flex: 1; - text-align: right; - width: 0; - } - - .top-row, article { - padding-left: 2rem !important; - padding-right: 1.5rem !important; - } -} +.page { + position: relative; + display: flex; + flex-direction: column; +} + +main { + flex: 1; +} + +.sidebar { + background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); +} + +.top-row { + background-color: #f7f7f7; + border-bottom: 1px solid #d6d5d5; + justify-content: flex-end; + height: 3.5rem; + display: flex; + align-items: center; +} + + .top-row ::deep a, .top-row ::deep .btn-link { + white-space: nowrap; + margin-left: 1.5rem; + text-decoration: none; + } + + .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { + text-decoration: underline; + } + + .top-row ::deep a:first-child { + overflow: hidden; + text-overflow: ellipsis; + } + +@media (max-width: 640.98px) { + .top-row:not(.auth) { + display: none; + } + + .top-row.auth { + justify-content: space-between; + } + + .top-row ::deep a, .top-row ::deep .btn-link { + margin-left: 0; + } +} + +@media (min-width: 641px) { + .page { + flex-direction: row; + } + + .sidebar { + width: 250px; + height: 100vh; + position: sticky; + top: 0; + } + + .top-row { + position: sticky; + top: 0; + z-index: 1; + } + + .top-row.auth ::deep a:first-child { + flex: 1; + text-align: right; + width: 0; + } + + .top-row, article { + padding-left: 2rem !important; + padding-right: 1.5rem !important; + } +} diff --git a/spa-blazor-wasm/Shared/NavMenu.razor b/3-client-spa/spa-blazor-wasm/Shared/NavMenu.razor similarity index 96% rename from spa-blazor-wasm/Shared/NavMenu.razor rename to 3-client-spa/spa-blazor-wasm/Shared/NavMenu.razor index 190717b..a347262 100644 --- a/spa-blazor-wasm/Shared/NavMenu.razor +++ b/3-client-spa/spa-blazor-wasm/Shared/NavMenu.razor @@ -1,29 +1,29 @@ - - -
- -
- -@code { - private bool collapseNavMenu = true; - - private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null; - - private void ToggleNavMenu() - { - collapseNavMenu = !collapseNavMenu; - } -} + + +
+ +
+ +@code { + private bool collapseNavMenu = true; + + private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null; + + private void ToggleNavMenu() + { + collapseNavMenu = !collapseNavMenu; + } +} diff --git a/web-app-blazor-server/Shared/NavMenu.razor.css b/3-client-spa/spa-blazor-wasm/Shared/NavMenu.razor.css similarity index 94% rename from web-app-blazor-server/Shared/NavMenu.razor.css rename to 3-client-spa/spa-blazor-wasm/Shared/NavMenu.razor.css index e681f23..acc5f9f 100644 --- a/web-app-blazor-server/Shared/NavMenu.razor.css +++ b/3-client-spa/spa-blazor-wasm/Shared/NavMenu.razor.css @@ -1,62 +1,62 @@ -.navbar-toggler { - background-color: rgba(255, 255, 255, 0.1); -} - -.top-row { - height: 3.5rem; - background-color: rgba(0,0,0,0.4); -} - -.navbar-brand { - font-size: 1.1rem; -} - -.oi { - width: 2rem; - font-size: 1.1rem; - vertical-align: text-top; - top: -2px; -} - -.nav-item { - font-size: 0.9rem; - padding-bottom: 0.5rem; -} - - .nav-item:first-of-type { - padding-top: 1rem; - } - - .nav-item:last-of-type { - padding-bottom: 1rem; - } - - .nav-item ::deep a { - color: #d7d7d7; - border-radius: 4px; - height: 3rem; - display: flex; - align-items: center; - line-height: 3rem; - } - -.nav-item ::deep a.active { - background-color: rgba(255,255,255,0.25); - color: white; -} - -.nav-item ::deep a:hover { - background-color: rgba(255,255,255,0.1); - color: white; -} - -@media (min-width: 641px) { - .navbar-toggler { - display: none; - } - - .collapse { - /* Never collapse the sidebar for wide screens */ - display: block; - } -} +.navbar-toggler { + background-color: rgba(255, 255, 255, 0.1); +} + +.top-row { + height: 3.5rem; + background-color: rgba(0,0,0,0.4); +} + +.navbar-brand { + font-size: 1.1rem; +} + +.oi { + width: 2rem; + font-size: 1.1rem; + vertical-align: text-top; + top: -2px; +} + +.nav-item { + font-size: 0.9rem; + padding-bottom: 0.5rem; +} + + .nav-item:first-of-type { + padding-top: 1rem; + } + + .nav-item:last-of-type { + padding-bottom: 1rem; + } + + .nav-item ::deep a { + color: #d7d7d7; + border-radius: 4px; + height: 3rem; + display: flex; + align-items: center; + line-height: 3rem; + } + +.nav-item ::deep a.active { + background-color: rgba(255,255,255,0.25); + color: white; +} + +.nav-item ::deep a:hover { + background-color: rgba(255,255,255,0.1); + color: white; +} + +@media (min-width: 641px) { + .navbar-toggler { + display: none; + } + + .collapse { + /* Never collapse the sidebar for wide screens */ + display: block; + } +} diff --git a/spa-blazor-wasm/Shared/RedirectToLogin.razor b/3-client-spa/spa-blazor-wasm/Shared/RedirectToLogin.razor similarity index 96% rename from spa-blazor-wasm/Shared/RedirectToLogin.razor rename to 3-client-spa/spa-blazor-wasm/Shared/RedirectToLogin.razor index 601cd56..2db61cf 100644 --- a/spa-blazor-wasm/Shared/RedirectToLogin.razor +++ b/3-client-spa/spa-blazor-wasm/Shared/RedirectToLogin.razor @@ -1,8 +1,8 @@ -@inject NavigationManager Navigation - -@code { - protected override void OnInitialized() - { - Navigation.NavigateTo($"authentication/login?returnUrl={Uri.EscapeDataString(Navigation.Uri)}"); - } -} +@inject NavigationManager Navigation + +@code { + protected override void OnInitialized() + { + Navigation.NavigateTo($"authentication/login?returnUrl={Uri.EscapeDataString(Navigation.Uri)}"); + } +} diff --git a/spa-blazor-wasm/_Imports.razor b/3-client-spa/spa-blazor-wasm/_Imports.razor similarity index 97% rename from spa-blazor-wasm/_Imports.razor rename to 3-client-spa/spa-blazor-wasm/_Imports.razor index a81a5b4..bc3ef81 100644 --- a/spa-blazor-wasm/_Imports.razor +++ b/3-client-spa/spa-blazor-wasm/_Imports.razor @@ -1,11 +1,11 @@ -@using System.Net.Http -@using System.Net.Http.Json -@using Microsoft.AspNetCore.Components.Authorization -@using Microsoft.AspNetCore.Components.Forms -@using Microsoft.AspNetCore.Components.Routing -@using Microsoft.AspNetCore.Components.Web -@using Microsoft.AspNetCore.Components.Web.Virtualization -@using Microsoft.AspNetCore.Components.WebAssembly.Http -@using Microsoft.JSInterop -@using BlazorWasm -@using BlazorWasm.Shared +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.AspNetCore.Components.WebAssembly.Http +@using Microsoft.JSInterop +@using BlazorWasm +@using BlazorWasm.Shared diff --git a/spa-blazor-wasm/media/app-signedin.png b/3-client-spa/spa-blazor-wasm/media/app-signedin.png similarity index 100% rename from spa-blazor-wasm/media/app-signedin.png rename to 3-client-spa/spa-blazor-wasm/media/app-signedin.png diff --git a/spa-blazor-wasm/media/app-signedout.png b/3-client-spa/spa-blazor-wasm/media/app-signedout.png similarity index 100% rename from spa-blazor-wasm/media/app-signedout.png rename to 3-client-spa/spa-blazor-wasm/media/app-signedout.png diff --git a/web-app-blazor-server/wwwroot/favicon.ico b/3-client-spa/spa-blazor-wasm/media/favicon.ico similarity index 100% rename from web-app-blazor-server/wwwroot/favicon.ico rename to 3-client-spa/spa-blazor-wasm/media/favicon.ico diff --git a/spa-blazor-wasm/wwwroot/appsettings.json b/3-client-spa/spa-blazor-wasm/wwwroot/appsettings.json similarity index 97% rename from spa-blazor-wasm/wwwroot/appsettings.json rename to 3-client-spa/spa-blazor-wasm/wwwroot/appsettings.json index 809c09c..97a7f9e 100644 --- a/spa-blazor-wasm/wwwroot/appsettings.json +++ b/3-client-spa/spa-blazor-wasm/wwwroot/appsettings.json @@ -1,7 +1,7 @@ -{ - "AzureAd": { - "Authority": "https://login.microsoftonline.com/", - "ClientId": "Enter the client ID obtained from the Microsoft Entra admin center", - "ValidateAuthority": true - } -} +{ + "AzureAd": { + "Authority": "https://login.microsoftonline.com/", + "ClientId": "Enter the client ID obtained from the Microsoft Entra admin center", + "ValidateAuthority": true + } +} diff --git a/spa-blazor-wasm/wwwroot/css/app.css b/3-client-spa/spa-blazor-wasm/wwwroot/css/app.css similarity index 97% rename from spa-blazor-wasm/wwwroot/css/app.css rename to 3-client-spa/spa-blazor-wasm/wwwroot/css/app.css index 7157ee8..9cd148f 100644 --- a/spa-blazor-wasm/wwwroot/css/app.css +++ b/3-client-spa/spa-blazor-wasm/wwwroot/css/app.css @@ -1,64 +1,64 @@ -@import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); - -html, body { - font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; -} - -h1:focus { - outline: none; -} - -a, .btn-link { - color: #0071c1; -} - -.btn-primary { - color: #fff; - background-color: #1b6ec2; - border-color: #1861ac; -} - -.content { - padding-top: 1.1rem; -} - -.valid.modified:not([type=checkbox]) { - outline: 1px solid #26b050; -} - -.invalid { - outline: 1px solid red; -} - -.validation-message { - color: red; -} - -#blazor-error-ui { - background: lightyellow; - bottom: 0; - box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); - display: none; - left: 0; - padding: 0.6rem 1.25rem 0.7rem 1.25rem; - position: fixed; - width: 100%; - z-index: 1000; -} - - #blazor-error-ui .dismiss { - cursor: pointer; - position: absolute; - right: 0.75rem; - top: 0.5rem; - } - -.blazor-error-boundary { - background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; - padding: 1rem 1rem 1rem 3.7rem; - color: white; -} - - .blazor-error-boundary::after { - content: "An error has occurred." - } +@import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); + +html, body { + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; +} + +h1:focus { + outline: none; +} + +a, .btn-link { + color: #0071c1; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.content { + padding-top: 1.1rem; +} + +.valid.modified:not([type=checkbox]) { + outline: 1px solid #26b050; +} + +.invalid { + outline: 1px solid red; +} + +.validation-message { + color: red; +} + +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } + +.blazor-error-boundary { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; + padding: 1rem 1rem 1rem 3.7rem; + color: white; +} + + .blazor-error-boundary::after { + content: "An error has occurred." + } diff --git a/web-app-blazor-server/wwwroot/css/bootstrap/bootstrap.min.css b/3-client-spa/spa-blazor-wasm/wwwroot/css/bootstrap/bootstrap.min.css similarity index 100% rename from web-app-blazor-server/wwwroot/css/bootstrap/bootstrap.min.css rename to 3-client-spa/spa-blazor-wasm/wwwroot/css/bootstrap/bootstrap.min.css diff --git a/web-app-blazor-server/wwwroot/css/bootstrap/bootstrap.min.css.map b/3-client-spa/spa-blazor-wasm/wwwroot/css/bootstrap/bootstrap.min.css.map similarity index 100% rename from web-app-blazor-server/wwwroot/css/bootstrap/bootstrap.min.css.map rename to 3-client-spa/spa-blazor-wasm/wwwroot/css/bootstrap/bootstrap.min.css.map diff --git a/spa-blazor-wasm/wwwroot/css/open-iconic/FONT-LICENSE b/3-client-spa/spa-blazor-wasm/wwwroot/css/open-iconic/FONT-LICENSE similarity index 97% rename from spa-blazor-wasm/wwwroot/css/open-iconic/FONT-LICENSE rename to 3-client-spa/spa-blazor-wasm/wwwroot/css/open-iconic/FONT-LICENSE index 8ae8650..a1dc03f 100644 --- a/spa-blazor-wasm/wwwroot/css/open-iconic/FONT-LICENSE +++ b/3-client-spa/spa-blazor-wasm/wwwroot/css/open-iconic/FONT-LICENSE @@ -1,86 +1,86 @@ -SIL OPEN FONT LICENSE Version 1.1 - -Copyright (c) 2014 Waybury - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +SIL OPEN FONT LICENSE Version 1.1 + +Copyright (c) 2014 Waybury + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/spa-blazor-wasm/wwwroot/css/open-iconic/ICON-LICENSE b/3-client-spa/spa-blazor-wasm/wwwroot/css/open-iconic/ICON-LICENSE similarity index 98% rename from spa-blazor-wasm/wwwroot/css/open-iconic/ICON-LICENSE rename to 3-client-spa/spa-blazor-wasm/wwwroot/css/open-iconic/ICON-LICENSE index af73356..2199f4a 100644 --- a/spa-blazor-wasm/wwwroot/css/open-iconic/ICON-LICENSE +++ b/3-client-spa/spa-blazor-wasm/wwwroot/css/open-iconic/ICON-LICENSE @@ -1,21 +1,21 @@ -The MIT License (MIT) - -Copyright (c) 2014 Waybury - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +The MIT License (MIT) + +Copyright (c) 2014 Waybury + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/spa-blazor-wasm/wwwroot/css/open-iconic/README.md b/3-client-spa/spa-blazor-wasm/wwwroot/css/open-iconic/README.md similarity index 96% rename from spa-blazor-wasm/wwwroot/css/open-iconic/README.md rename to 3-client-spa/spa-blazor-wasm/wwwroot/css/open-iconic/README.md index e2e831e..6b810e4 100644 --- a/spa-blazor-wasm/wwwroot/css/open-iconic/README.md +++ b/3-client-spa/spa-blazor-wasm/wwwroot/css/open-iconic/README.md @@ -1,114 +1,114 @@ -[Open Iconic v1.1.1](http://useiconic.com/open) -=========== - -### Open Iconic is the open source sibling of [Iconic](http://useiconic.com). It is a hyper-legible collection of 223 icons with a tiny footprint—ready to use with Bootstrap and Foundation. [View the collection](http://useiconic.com/open#icons) - - - -## What's in Open Iconic? - -* 223 icons designed to be legible down to 8 pixels -* Super-light SVG files - 61.8 for the entire set -* SVG sprite—the modern replacement for icon fonts -* Webfont (EOT, OTF, SVG, TTF, WOFF), PNG and WebP formats -* Webfont stylesheets (including versions for Bootstrap and Foundation) in CSS, LESS, SCSS and Stylus formats -* PNG and WebP raster images in 8px, 16px, 24px, 32px, 48px and 64px. - - -## Getting Started - -#### For code samples and everything else you need to get started with Open Iconic, check out our [Icons](http://useiconic.com/open#icons) and [Reference](http://useiconic.com/open#reference) sections. - -### General Usage - -#### Using Open Iconic's SVGs - -We like SVGs and we think they're the way to display icons on the web. Since Open Iconic are just basic SVGs, we suggest you display them like you would any other image (don't forget the `alt` attribute). - -``` -icon name -``` - -#### Using Open Iconic's SVG Sprite - -Open Iconic also comes in a SVG sprite which allows you to display all the icons in the set with a single request. It's like an icon font, without being a hack. - -Adding an icon from an SVG sprite is a little different than what you're used to, but it's still a piece of cake. *Tip: To make your icons easily style able, we suggest adding a general class to the* `` *tag and a unique class name for each different icon in the* `` *tag.* - -``` - - - -``` - -Sizing icons only needs basic CSS. All the icons are in a square format, so just set the `` tag with equal width and height dimensions. - -``` -.icon { - width: 16px; - height: 16px; -} -``` - -Coloring icons is even easier. All you need to do is set the `fill` rule on the `` tag. - -``` -.icon-account-login { - fill: #f00; -} -``` - -To learn more about SVG Sprites, read [Chris Coyier's guide](http://css-tricks.com/svg-sprites-use-better-icon-fonts/). - -#### Using Open Iconic's Icon Font... - - -##### …with Bootstrap - -You can find our Bootstrap stylesheets in `font/css/open-iconic-bootstrap.{css, less, scss, styl}` - - -``` - -``` - - -``` - -``` - -##### …with Foundation - -You can find our Foundation stylesheets in `font/css/open-iconic-foundation.{css, less, scss, styl}` - -``` - -``` - - -``` - -``` - -##### …on its own - -You can find our default stylesheets in `font/css/open-iconic.{css, less, scss, styl}` - -``` - -``` - -``` - -``` - - -## License - -### Icons - -All code (including SVG markup) is under the [MIT License](http://opensource.org/licenses/MIT). - -### Fonts - -All fonts are under the [SIL Licensed](http://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web). +[Open Iconic v1.1.1](http://useiconic.com/open) +=========== + +### Open Iconic is the open source sibling of [Iconic](http://useiconic.com). It is a hyper-legible collection of 223 icons with a tiny footprint—ready to use with Bootstrap and Foundation. [View the collection](http://useiconic.com/open#icons) + + + +## What's in Open Iconic? + +* 223 icons designed to be legible down to 8 pixels +* Super-light SVG files - 61.8 for the entire set +* SVG sprite—the modern replacement for icon fonts +* Webfont (EOT, OTF, SVG, TTF, WOFF), PNG and WebP formats +* Webfont stylesheets (including versions for Bootstrap and Foundation) in CSS, LESS, SCSS and Stylus formats +* PNG and WebP raster images in 8px, 16px, 24px, 32px, 48px and 64px. + + +## Getting Started + +#### For code samples and everything else you need to get started with Open Iconic, check out our [Icons](http://useiconic.com/open#icons) and [Reference](http://useiconic.com/open#reference) sections. + +### General Usage + +#### Using Open Iconic's SVGs + +We like SVGs and we think they're the way to display icons on the web. Since Open Iconic are just basic SVGs, we suggest you display them like you would any other image (don't forget the `alt` attribute). + +``` +icon name +``` + +#### Using Open Iconic's SVG Sprite + +Open Iconic also comes in a SVG sprite which allows you to display all the icons in the set with a single request. It's like an icon font, without being a hack. + +Adding an icon from an SVG sprite is a little different than what you're used to, but it's still a piece of cake. *Tip: To make your icons easily style able, we suggest adding a general class to the* `` *tag and a unique class name for each different icon in the* `` *tag.* + +``` + + + +``` + +Sizing icons only needs basic CSS. All the icons are in a square format, so just set the `` tag with equal width and height dimensions. + +``` +.icon { + width: 16px; + height: 16px; +} +``` + +Coloring icons is even easier. All you need to do is set the `fill` rule on the `` tag. + +``` +.icon-account-login { + fill: #f00; +} +``` + +To learn more about SVG Sprites, read [Chris Coyier's guide](http://css-tricks.com/svg-sprites-use-better-icon-fonts/). + +#### Using Open Iconic's Icon Font... + + +##### …with Bootstrap + +You can find our Bootstrap stylesheets in `font/css/open-iconic-bootstrap.{css, less, scss, styl}` + + +``` + +``` + + +``` + +``` + +##### …with Foundation + +You can find our Foundation stylesheets in `font/css/open-iconic-foundation.{css, less, scss, styl}` + +``` + +``` + + +``` + +``` + +##### …on its own + +You can find our default stylesheets in `font/css/open-iconic.{css, less, scss, styl}` + +``` + +``` + +``` + +``` + + +## License + +### Icons + +All code (including SVG markup) is under the [MIT License](http://opensource.org/licenses/MIT). + +### Fonts + +All fonts are under the [SIL Licensed](http://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web). diff --git a/web-app-blazor-server/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css b/3-client-spa/spa-blazor-wasm/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css similarity index 100% rename from web-app-blazor-server/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css rename to 3-client-spa/spa-blazor-wasm/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css diff --git a/web-app-blazor-server/wwwroot/css/open-iconic/font/fonts/open-iconic.eot b/3-client-spa/spa-blazor-wasm/wwwroot/css/open-iconic/font/fonts/open-iconic.eot similarity index 100% rename from web-app-blazor-server/wwwroot/css/open-iconic/font/fonts/open-iconic.eot rename to 3-client-spa/spa-blazor-wasm/wwwroot/css/open-iconic/font/fonts/open-iconic.eot diff --git a/web-app-blazor-server/wwwroot/css/open-iconic/font/fonts/open-iconic.otf b/3-client-spa/spa-blazor-wasm/wwwroot/css/open-iconic/font/fonts/open-iconic.otf similarity index 100% rename from web-app-blazor-server/wwwroot/css/open-iconic/font/fonts/open-iconic.otf rename to 3-client-spa/spa-blazor-wasm/wwwroot/css/open-iconic/font/fonts/open-iconic.otf diff --git a/web-app-blazor-server/wwwroot/css/open-iconic/font/fonts/open-iconic.svg b/3-client-spa/spa-blazor-wasm/wwwroot/css/open-iconic/font/fonts/open-iconic.svg similarity index 99% rename from web-app-blazor-server/wwwroot/css/open-iconic/font/fonts/open-iconic.svg rename to 3-client-spa/spa-blazor-wasm/wwwroot/css/open-iconic/font/fonts/open-iconic.svg index cf42942..32b2c4e 100644 --- a/web-app-blazor-server/wwwroot/css/open-iconic/font/fonts/open-iconic.svg +++ b/3-client-spa/spa-blazor-wasm/wwwroot/css/open-iconic/font/fonts/open-iconic.svg @@ -1,543 +1,543 @@ - - - - - -Created by FontForge 20120731 at Tue Jul 1 20:39:22 2014 - By P.J. Onori -Created by P.J. Onori with FontForge 2.0 (http://fontforge.sf.net) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + +Created by FontForge 20120731 at Tue Jul 1 20:39:22 2014 + By P.J. Onori +Created by P.J. Onori with FontForge 2.0 (http://fontforge.sf.net) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web-app-blazor-server/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf b/3-client-spa/spa-blazor-wasm/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf similarity index 100% rename from web-app-blazor-server/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf rename to 3-client-spa/spa-blazor-wasm/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf diff --git a/web-app-blazor-server/wwwroot/css/open-iconic/font/fonts/open-iconic.woff b/3-client-spa/spa-blazor-wasm/wwwroot/css/open-iconic/font/fonts/open-iconic.woff similarity index 100% rename from web-app-blazor-server/wwwroot/css/open-iconic/font/fonts/open-iconic.woff rename to 3-client-spa/spa-blazor-wasm/wwwroot/css/open-iconic/font/fonts/open-iconic.woff diff --git a/spa-blazor-wasm/wwwroot/icon-192.png b/3-client-spa/spa-blazor-wasm/wwwroot/icon-192.png similarity index 100% rename from spa-blazor-wasm/wwwroot/icon-192.png rename to 3-client-spa/spa-blazor-wasm/wwwroot/icon-192.png diff --git a/spa-blazor-wasm/wwwroot/icon-512.png b/3-client-spa/spa-blazor-wasm/wwwroot/icon-512.png similarity index 100% rename from spa-blazor-wasm/wwwroot/icon-512.png rename to 3-client-spa/spa-blazor-wasm/wwwroot/icon-512.png diff --git a/spa-blazor-wasm/wwwroot/index.html b/3-client-spa/spa-blazor-wasm/wwwroot/index.html similarity index 97% rename from spa-blazor-wasm/wwwroot/index.html rename to 3-client-spa/spa-blazor-wasm/wwwroot/index.html index 1af715c..326d3fa 100644 --- a/spa-blazor-wasm/wwwroot/index.html +++ b/3-client-spa/spa-blazor-wasm/wwwroot/index.html @@ -1,30 +1,30 @@ - - - - - - - BlazorWasm - - - - - - - - - - -
Loading...
- -
- An unhandled error has occurred. - Reload - 🗙 -
- - - - - - + + + + + + + BlazorWasm + + + + + + + + + + +
Loading...
+ +
+ An unhandled error has occurred. + Reload + 🗙 +
+ + + + + + diff --git a/spa-blazor-wasm/wwwroot/manifest.json b/3-client-spa/spa-blazor-wasm/wwwroot/manifest.json similarity index 95% rename from spa-blazor-wasm/wwwroot/manifest.json rename to 3-client-spa/spa-blazor-wasm/wwwroot/manifest.json index 1d73474..bdeccbb 100644 --- a/spa-blazor-wasm/wwwroot/manifest.json +++ b/3-client-spa/spa-blazor-wasm/wwwroot/manifest.json @@ -1,21 +1,21 @@ -{ - "name": "blazorwasm", - "short_name": "blazorwasm", - "start_url": "./", - "display": "standalone", - "background_color": "#ffffff", - "theme_color": "#03173d", - "prefer_related_applications": false, - "icons": [ - { - "src": "icon-512.png", - "type": "image/png", - "sizes": "512x512" - }, - { - "src": "icon-192.png", - "type": "image/png", - "sizes": "192x192" - } - ] -} +{ + "name": "blazorwasm", + "short_name": "blazorwasm", + "start_url": "./", + "display": "standalone", + "background_color": "#ffffff", + "theme_color": "#03173d", + "prefer_related_applications": false, + "icons": [ + { + "src": "icon-512.png", + "type": "image/png", + "sizes": "512x512" + }, + { + "src": "icon-192.png", + "type": "image/png", + "sizes": "192x192" + } + ] +} diff --git a/spa-blazor-wasm/wwwroot/service-worker.js b/3-client-spa/spa-blazor-wasm/wwwroot/service-worker.js similarity index 98% rename from spa-blazor-wasm/wwwroot/service-worker.js rename to 3-client-spa/spa-blazor-wasm/wwwroot/service-worker.js index c6d0085..fe614da 100644 --- a/spa-blazor-wasm/wwwroot/service-worker.js +++ b/3-client-spa/spa-blazor-wasm/wwwroot/service-worker.js @@ -1,4 +1,4 @@ -// In development, always fetch from the network and do not enable offline support. -// This is because caching would make development more difficult (changes would not -// be reflected on the first load after each change). -self.addEventListener('fetch', () => { }); +// In development, always fetch from the network and do not enable offline support. +// This is because caching would make development more difficult (changes would not +// be reflected on the first load after each change). +self.addEventListener('fetch', () => { }); diff --git a/spa-blazor-wasm/wwwroot/service-worker.published.js b/3-client-spa/spa-blazor-wasm/wwwroot/service-worker.published.js similarity index 97% rename from spa-blazor-wasm/wwwroot/service-worker.published.js rename to 3-client-spa/spa-blazor-wasm/wwwroot/service-worker.published.js index 105b713..0d9986f 100644 --- a/spa-blazor-wasm/wwwroot/service-worker.published.js +++ b/3-client-spa/spa-blazor-wasm/wwwroot/service-worker.published.js @@ -1,48 +1,48 @@ -// Caution! Be sure you understand the caveats before publishing an application with -// offline support. See https://aka.ms/blazor-offline-considerations - -self.importScripts('./service-worker-assets.js'); -self.addEventListener('install', event => event.waitUntil(onInstall(event))); -self.addEventListener('activate', event => event.waitUntil(onActivate(event))); -self.addEventListener('fetch', event => event.respondWith(onFetch(event))); - -const cacheNamePrefix = 'offline-cache-'; -const cacheName = `${cacheNamePrefix}${self.assetsManifest.version}`; -const offlineAssetsInclude = [ /\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/, /\.blat$/, /\.dat$/ ]; -const offlineAssetsExclude = [ /^service-worker\.js$/ ]; - -async function onInstall(event) { - console.info('Service worker: Install'); - - // Fetch and cache all matching items from the assets manifest - const assetsRequests = self.assetsManifest.assets - .filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url))) - .filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url))) - .map(asset => new Request(asset.url, { integrity: asset.hash, cache: 'no-cache' })); - await caches.open(cacheName).then(cache => cache.addAll(assetsRequests)); -} - -async function onActivate(event) { - console.info('Service worker: Activate'); - - // Delete unused caches - const cacheKeys = await caches.keys(); - await Promise.all(cacheKeys - .filter(key => key.startsWith(cacheNamePrefix) && key !== cacheName) - .map(key => caches.delete(key))); -} - -async function onFetch(event) { - let cachedResponse = null; - if (event.request.method === 'GET') { - // For all navigation requests, try to serve index.html from cache - // If you need some URLs to be server-rendered, edit the following check to exclude those URLs - const shouldServeIndexHtml = event.request.mode === 'navigate'; - - const request = shouldServeIndexHtml ? 'index.html' : event.request; - const cache = await caches.open(cacheName); - cachedResponse = await cache.match(request); - } - - return cachedResponse || fetch(event.request); -} +// Caution! Be sure you understand the caveats before publishing an application with +// offline support. See https://aka.ms/blazor-offline-considerations + +self.importScripts('./service-worker-assets.js'); +self.addEventListener('install', event => event.waitUntil(onInstall(event))); +self.addEventListener('activate', event => event.waitUntil(onActivate(event))); +self.addEventListener('fetch', event => event.respondWith(onFetch(event))); + +const cacheNamePrefix = 'offline-cache-'; +const cacheName = `${cacheNamePrefix}${self.assetsManifest.version}`; +const offlineAssetsInclude = [ /\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/, /\.blat$/, /\.dat$/ ]; +const offlineAssetsExclude = [ /^service-worker\.js$/ ]; + +async function onInstall(event) { + console.info('Service worker: Install'); + + // Fetch and cache all matching items from the assets manifest + const assetsRequests = self.assetsManifest.assets + .filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url))) + .filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url))) + .map(asset => new Request(asset.url, { integrity: asset.hash, cache: 'no-cache' })); + await caches.open(cacheName).then(cache => cache.addAll(assetsRequests)); +} + +async function onActivate(event) { + console.info('Service worker: Activate'); + + // Delete unused caches + const cacheKeys = await caches.keys(); + await Promise.all(cacheKeys + .filter(key => key.startsWith(cacheNamePrefix) && key !== cacheName) + .map(key => caches.delete(key))); +} + +async function onFetch(event) { + let cachedResponse = null; + if (event.request.method === 'GET') { + // For all navigation requests, try to serve index.html from cache + // If you need some URLs to be server-rendered, edit the following check to exclude those URLs + const shouldServeIndexHtml = event.request.mode === 'navigate'; + + const request = shouldServeIndexHtml ? 'index.html' : event.request; + const cache = await caches.open(cacheName); + cachedResponse = await cache.match(request); + } + + return cachedResponse || fetch(event.request); +} diff --git a/4-desktop-apps/README.md b/4-desktop-apps/README.md new file mode 100644 index 0000000..8b8c411 --- /dev/null +++ b/4-desktop-apps/README.md @@ -0,0 +1,55 @@ +# 🖥️ Desktop Samples + +This folder contains .NET desktop samples demonstrating authentication and authorization scenarios using the Microsoft identity platform. These samples cover a variety of desktop technologies, including Console, Windows Forms, WPF, WinUI, and cross-platform MAUI. + +## 📋 Samples Overview + +| Folder Name | Authentication Libraries Used | .NET Version | +|---------------------------------------------------------------------------------|----------------------------------------------|-----------------------| +| [console-cli](./console-cli) | Microsoft.Identity.Client (MSAL.NET) 4.64.0 | .NET 8.0 | +| [console-daemon](./console-daemon) | Microsoft.Identity.Web 3.1 | .NET 8.0 | +| [console-device-code-flow](./console-device-code-flow) | Microsoft.Identity.Client (MSAL.NET) 4.13.0 | netcoreapp3.1; net472 | +| [console-html-browser](./console-html-browser) | Microsoft.Identity.Client (MSAL.NET) 4.9.0 | netcoreapp3.1; net472 | +| [console-interactive-multitarget-graph](./console-interactive-multitarget-graph)| Microsoft.Identity.Client (MSAL.NET) | netcoreapp3.1; net472 | +| [console-web-browser](./console-web-browser/) | Microsoft.Identity.Client (MSAL.NET) 4.9.0 | netcoreapp3.1 | +| [desktop-winforms](./desktop-winforms) | Microsoft.Identity.Client (MSAL.NET) 4.x | .NET 8.0 | +| [desktop-wpf](./desktop-wpf) | Microsoft.Identity.Client (MSAL.NET) 4.x | .NET 8.0 | +| [desktop-winui](./desktop-winui) | Microsoft.Identity.Client (MSAL.NET) 4.x | .NET 8.0 | +| [xplat-maui](./xplat-maui) | Microsoft.Identity.Client (MSAL.NET) 4.38.0 | .NET 6.0 | + +> [!NOTE] +> All samples use the latest supported versions of the Microsoft identity libraries and are configured for secure, modern authentication scenarios. + +--- + +### ☑️ Prerequisites + +You will need the following to run any of these samples + + - [.NET](https://dotnet.microsoft.com/download/dotnet/8.0) + - A Microsoft Entra tenant and app registration (see each sample's README for details) + - An editor or IDE such as [Visual Studio](https://visualstudio.microsoft.com/) or [Visual Studio Code](https://code.visualstudio.com/) + +### 📥 Clone the Repository + +1. Navigate to where you want to have the sample located, and enter the following + + ```sh + git clone https://github.com/MicrosoftDocs/ms-identity-docs-code-dotnet.git + ``` +2. Navigate to the desktop app folder in the sample you have downloaded by using the following command; + + ```sh + cd ms-identity-docs-code-dotnet/4-desktop-apps/ + ``` +--- + +## 📚 Resources + +- [Microsoft Identity Platform Documentation](https://learn.microsoft.com/entra/identity-platform/) +- [Microsoft.Identity.Client (MSAL.NET) Library](https://learn.microsoft.com/entra/identity-platform/msal-overview) +- [Microsoft Entra App Registration](https://learn.microsoft.com/entra/identity-platform/quickstart-register-app) +- [Securing Desktop Apps with Microsoft Identity](https://learn.microsoft.com/entra/identity-platform/tutorial-v2-windows-desktop) +- [Microsoft .NET Desktop Documentation](https://learn.microsoft.com/dotnet/desktop/) + +--- \ No newline at end of file diff --git a/console-cli/Program.cs b/4-desktop-apps/console-cli/Program.cs similarity index 97% rename from console-cli/Program.cs rename to 4-desktop-apps/console-cli/Program.cs index b776a2d..42dde12 100644 --- a/console-cli/Program.cs +++ b/4-desktop-apps/console-cli/Program.cs @@ -1,76 +1,76 @@ -using Microsoft.Identity.Client; -using System.Net.Http.Headers; -using System.Text.Json; - -var config = new PublicClientApplicationOptions -{ - // 'Directory (tenant) ID' of the app registration in the Microsoft Entra admin center - TenantId = "Enter the client ID obtained from the Microsoft Entra admin center", - - // 'Application (client) ID' of the app registration in the Microsoft Entra admin center - ClientId = "Enter the tenant ID obtained from the Microsoft Entra admin center" -}; - -// In order to take advantage of token caching, your MSAL client singleton must -// have a lifecycle that at least matches the lifecycle of the user's session in -// the console application. -IPublicClientApplication publicMsalClient = PublicClientApplicationBuilder.CreateWithApplicationOptions(config) - .Build(); - -AuthenticationResult? msalAuthenticationResult = null; - -// Attempt to use a cached access token if one is available. This will renew existing, but -// expired access tokens if possible. In this specific sample, this will always result in -// a cache miss, but this pattern would be what you'd use on subsequent calls that require -// the usage of the same access token. -IEnumerable accounts = (await publicMsalClient.GetAccountsAsync()).ToList(); - -if (accounts.Any()) -{ - try - { - msalAuthenticationResult = await publicMsalClient.AcquireTokenSilent( - new[] { "https://graph.microsoft.com/User.Read" }, - accounts.First()).ExecuteAsync(); - } - catch (MsalUiRequiredException) - { - // No usable cached token was found for this scope + account or Entra ID insists in - // an interactive user flow. - } -} - -if (msalAuthenticationResult == null) -{ - // Initiate the device code flow. - msalAuthenticationResult = await publicMsalClient.AcquireTokenWithDeviceCode( - new[] { "https://graph.microsoft.com/User.Read" }, deviceCodeResultCallback => - { - // This will print the message on the console which tells the user where to go sign-in using - // a separate browser and the code to enter once they sign in. - // The AcquireTokenWithDeviceCode() method will poll the server after firing this - // device code callback to look for the successful login of the user via that browser. - Console.WriteLine(deviceCodeResultCallback.Message); - return Task.CompletedTask; - }).ExecuteAsync(); -} - -// At this point we now have a valid access token for Microsoft Graph, with only the specific scopes -// necessary to complete the following call. Build the Microsoft Graph HTTP request, using the obtained -// access token. -using var graphHttpRequest = new HttpRequestMessage(HttpMethod.Get, "https://graph.microsoft.com/v1.0/me"); -graphHttpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", msalAuthenticationResult.AccessToken); - -// Make the API call to Microsoft Graph -var httpClient = new HttpClient(); -HttpResponseMessage graphHttpResponse = await httpClient.SendAsync(graphHttpRequest); -graphHttpResponse.EnsureSuccessStatusCode(); - -// Present the results to the user (formatting the JSON for readability) -var graphResponseBody = JsonDocument.Parse(await graphHttpResponse.Content.ReadAsStringAsync()); -Console.WriteLine(JsonSerializer.Serialize(graphResponseBody, - new JsonSerializerOptions() - { - WriteIndented = true, - Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping +using Microsoft.Identity.Client; +using System.Net.Http.Headers; +using System.Text.Json; + +var config = new PublicClientApplicationOptions +{ + // 'Directory (tenant) ID' of the app registration in the Microsoft Entra admin center + TenantId = "Enter the client ID obtained from the Microsoft Entra admin center", + + // 'Application (client) ID' of the app registration in the Microsoft Entra admin center + ClientId = "Enter the tenant ID obtained from the Microsoft Entra admin center" +}; + +// In order to take advantage of token caching, your MSAL client singleton must +// have a lifecycle that at least matches the lifecycle of the user's session in +// the console application. +IPublicClientApplication publicMsalClient = PublicClientApplicationBuilder.CreateWithApplicationOptions(config) + .Build(); + +AuthenticationResult? msalAuthenticationResult = null; + +// Attempt to use a cached access token if one is available. This will renew existing, but +// expired access tokens if possible. In this specific sample, this will always result in +// a cache miss, but this pattern would be what you'd use on subsequent calls that require +// the usage of the same access token. +IEnumerable accounts = (await publicMsalClient.GetAccountsAsync()).ToList(); + +if (accounts.Any()) +{ + try + { + msalAuthenticationResult = await publicMsalClient.AcquireTokenSilent( + new[] { "https://graph.microsoft.com/User.Read" }, + accounts.First()).ExecuteAsync(); + } + catch (MsalUiRequiredException) + { + // No usable cached token was found for this scope + account or Entra ID insists in + // an interactive user flow. + } +} + +if (msalAuthenticationResult == null) +{ + // Initiate the device code flow. + msalAuthenticationResult = await publicMsalClient.AcquireTokenWithDeviceCode( + new[] { "https://graph.microsoft.com/User.Read" }, deviceCodeResultCallback => + { + // This will print the message on the console which tells the user where to go sign-in using + // a separate browser and the code to enter once they sign in. + // The AcquireTokenWithDeviceCode() method will poll the server after firing this + // device code callback to look for the successful login of the user via that browser. + Console.WriteLine(deviceCodeResultCallback.Message); + return Task.CompletedTask; + }).ExecuteAsync(); +} + +// At this point we now have a valid access token for Microsoft Graph, with only the specific scopes +// necessary to complete the following call. Build the Microsoft Graph HTTP request, using the obtained +// access token. +using var graphHttpRequest = new HttpRequestMessage(HttpMethod.Get, "https://graph.microsoft.com/v1.0/me"); +graphHttpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", msalAuthenticationResult.AccessToken); + +// Make the API call to Microsoft Graph +var httpClient = new HttpClient(); +HttpResponseMessage graphHttpResponse = await httpClient.SendAsync(graphHttpRequest); +graphHttpResponse.EnsureSuccessStatusCode(); + +// Present the results to the user (formatting the JSON for readability) +var graphResponseBody = JsonDocument.Parse(await graphHttpResponse.Content.ReadAsStringAsync()); +Console.WriteLine(JsonSerializer.Serialize(graphResponseBody, + new JsonSerializerOptions() + { + WriteIndented = true, + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping })); \ No newline at end of file diff --git a/console-cli/README.md b/4-desktop-apps/console-cli/README.md similarity index 90% rename from console-cli/README.md rename to 4-desktop-apps/console-cli/README.md index b8fd68c..1b44f5c 100644 --- a/console-cli/README.md +++ b/4-desktop-apps/console-cli/README.md @@ -1,6 +1,6 @@ --- -# Metadata required by https://docs.microsoft.com/samples/browse/ -# Metadata properties: https://review.docs.microsoft.com/help/contribute/samples/process/onboarding?branch=main#add-metadata-to-readme +# Metadata required by https://learn.microsoft.com/samples/browse/ +# Metadata properties: https://review.learn.microsoft.com/help/contribute/samples/process/onboarding?branch=main#add-metadata-to-readme languages: - csharp page_type: sample @@ -45,7 +45,7 @@ To sign in, use a web browser to open the page https://microsoft.com/devicelogin ### 1. Register the app -First, complete the steps in [Register an application with the Microsoft identity platform](https://docs.microsoft.com/azure/active-directory/develop/quickstart-register-app) to register the application. +First, complete the steps in [Register an application with the Microsoft identity platform](https://learn.microsoft.com/entra/identity-platform/quickstart-register-app) to register the application. Use these settings in your app registration. @@ -95,9 +95,9 @@ Follow the device code flow instructions that are presented. If everything worke ## About the code -This .NET 6 (C#) console application prompts the user to sign in via their device using a code provided by Microsoft Authentication Library (MSAL). The user completes this flow in their chosen web browser. Upon successful authentication, an HTTP GET request to the Microsoft Graph /me endpoint is issued with the user's access token in the HTTP header. The response from the GET request is then displayed in the console. +This .NET 8 (C#) console application prompts the user to sign in via their device using a code provided by Microsoft Authentication Library (MSAL). The user completes this flow in their chosen web browser. Upon successful authentication, an HTTP GET request to the Microsoft Graph /me endpoint is issued with the user's access token in the HTTP header. The response from the GET request is then displayed in the console. -This sample does not demonstrate the usage of cached access tokens. Access token caching should be used in situations where the console application will need to access the same protected API using the same access token multiple times across the life of the user's session in the application. This sample, as written, does not perform multiple calls and wouldn't result in a token cache hit. For additional information, see [Get a token from the token cache using MSAL.NET](https://docs.microsoft.com/azure/active-directory/develop/msal-net-acquire-token-silently). +This sample does not demonstrate the usage of cached access tokens. Access token caching should be used in situations where the console application will need to access the same protected API using the same access token multiple times across the life of the user's session in the application. This sample, as written, does not perform multiple calls and wouldn't result in a token cache hit. For additional information, see [Get a token from the token cache using MSAL.NET](https://learn.microsoft.com/entra/identity-platform/msal-net-acquire-token-silently). ## Reporting problems @@ -112,7 +112,7 @@ If you can't get the sample working, you've checked [Stack Overflow](http://stac > :warning: WARNING: Any issue in this repository _not_ limited to running one of its sample apps will be closed without being addressed. -For all other requests, see [Support and help options for developers | Microsoft identity platform](https://docs.microsoft.com/azure/active-directory/develop/developer-support-help-options). +For all other requests, see [Support and help options for developers | Microsoft identity platform](https://learn.microsoft.com/entra/identity-platform/developer-support-help-options). ## Contributing diff --git a/console-cli/cli.csproj b/4-desktop-apps/console-cli/cli.csproj similarity index 96% rename from console-cli/cli.csproj rename to 4-desktop-apps/console-cli/cli.csproj index b67d71a..e760ff3 100644 --- a/console-cli/cli.csproj +++ b/4-desktop-apps/console-cli/cli.csproj @@ -1,12 +1,12 @@ - - - Exe - net8.0 - App - enable - enable - - - - - + + + Exe + net8.0 + App + enable + enable + + + + + diff --git a/console-daemon/Cli.csproj b/4-desktop-apps/console-daemon/Cli.csproj similarity index 97% rename from console-daemon/Cli.csproj rename to 4-desktop-apps/console-daemon/Cli.csproj index ead809c..cc02d8a 100644 --- a/console-daemon/Cli.csproj +++ b/4-desktop-apps/console-daemon/Cli.csproj @@ -1,11 +1,11 @@ - - - Exe - net8.0 - enable - enable - - - - + + + Exe + net8.0 + enable + enable + + + + \ No newline at end of file diff --git a/console-daemon/Program.cs b/4-desktop-apps/console-daemon/Program.cs similarity index 98% rename from console-daemon/Program.cs rename to 4-desktop-apps/console-daemon/Program.cs index d92b14f..1aea233 100644 --- a/console-daemon/Program.cs +++ b/4-desktop-apps/console-daemon/Program.cs @@ -1,37 +1,37 @@ -using Microsoft.Identity.Client; -using Microsoft.Identity.Web; -using System.Net.Http.Headers; -using System.Text.Encodings.Web; -using System.Text.Json; - -var config = new { - // Full directory URL, in the form of https://login.microsoftonline.com/ - Authority = " https://login.microsoftonline.com/Enter the tenant ID obtained from the Microsoft Entra admin center", - // Enter the client ID obtained from the Microsoft Entra admin center - ClientId = "Enter the client ID obtained from the Microsoft Entra admin center", - // Client secret 'Value' (not its ID) from 'Client secrets' in the Microsoft Entra admin center - ClientSecret = "Enter the client secret value obtained from the Microsoft Entra admin center", - // Client 'Object ID' of app registration in Microsoft Entra admin center - this value is a GUID - ClientObjectId = "Enter the client Object ID obtained from the Microsoft Entra admin center" -}; - -// This app instance should be a long-lived instance because -// it maintains the in-memory token cache. -IConfidentialClientApplication msalClient = ConfidentialClientApplicationBuilder.Create(config.ClientId) - .WithClientSecret(config.ClientSecret) - .WithAuthority(new Uri(config.Authority)) - .Build(); - -msalClient.AddInMemoryTokenCache(); - -AuthenticationResult msalAuthenticationResult = await msalClient.AcquireTokenForClient(new string[] { "https://graph.microsoft.com/.default" }).ExecuteAsync(); - -// Get *this* application's application object from Microsoft Graph -var httpClient = new HttpClient(); -using var graphRequest = new HttpRequestMessage(HttpMethod.Get, $"https://graph.microsoft.com/v1.0/applications/{config.ClientObjectId}"); -graphRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", msalAuthenticationResult.AccessToken); -var graphResponseMessage = await httpClient.SendAsync(graphRequest); -graphResponseMessage.EnsureSuccessStatusCode(); - -using var graphResponseJson = JsonDocument.Parse(await graphResponseMessage.Content.ReadAsStreamAsync()); -Console.WriteLine(JsonSerializer.Serialize(graphResponseJson, new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping })); +using Microsoft.Identity.Client; +using Microsoft.Identity.Web; +using System.Net.Http.Headers; +using System.Text.Encodings.Web; +using System.Text.Json; + +var config = new { + // Full directory URL, in the form of https://login.microsoftonline.com/ + Authority = " https://login.microsoftonline.com/Enter the tenant ID obtained from the Microsoft Entra admin center", + // Enter the client ID obtained from the Microsoft Entra admin center + ClientId = "Enter the client ID obtained from the Microsoft Entra admin center", + // Client secret 'Value' (not its ID) from 'Client secrets' in the Microsoft Entra admin center + ClientSecret = "Enter the client secret value obtained from the Microsoft Entra admin center", + // Client 'Object ID' of app registration in Microsoft Entra admin center - this value is a GUID + ClientObjectId = "Enter the client Object ID obtained from the Microsoft Entra admin center" +}; + +// This app instance should be a long-lived instance because +// it maintains the in-memory token cache. +IConfidentialClientApplication msalClient = ConfidentialClientApplicationBuilder.Create(config.ClientId) + .WithClientSecret(config.ClientSecret) + .WithAuthority(new Uri(config.Authority)) + .Build(); + +msalClient.AddInMemoryTokenCache(); + +AuthenticationResult msalAuthenticationResult = await msalClient.AcquireTokenForClient(new string[] { "https://graph.microsoft.com/.default" }).ExecuteAsync(); + +// Get *this* application's application object from Microsoft Graph +var httpClient = new HttpClient(); +using var graphRequest = new HttpRequestMessage(HttpMethod.Get, $"https://graph.microsoft.com/v1.0/applications/{config.ClientObjectId}"); +graphRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", msalAuthenticationResult.AccessToken); +var graphResponseMessage = await httpClient.SendAsync(graphRequest); +graphResponseMessage.EnsureSuccessStatusCode(); + +using var graphResponseJson = JsonDocument.Parse(await graphResponseMessage.Content.ReadAsStreamAsync()); +Console.WriteLine(JsonSerializer.Serialize(graphResponseJson, new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping })); diff --git a/console-daemon/README.md b/4-desktop-apps/console-daemon/README.md similarity index 94% rename from console-daemon/README.md rename to 4-desktop-apps/console-daemon/README.md index eb31dbe..03340c3 100644 --- a/console-daemon/README.md +++ b/4-desktop-apps/console-daemon/README.md @@ -1,14 +1,14 @@ --- -# Metadata required by https://docs.microsoft.com/samples/browse/ -# Metadata properties: https://review.docs.microsoft.com/help/contribute/samples/process/onboarding?branch=main#add-metadata-to-readme +# Metadata required by https://learn.microsoft.com/samples/browse/ +# Metadata properties: https://review.learn.microsoft.com/help/contribute/samples/process/onboarding?branch=main#add-metadata-to-readme languages: - csharp page_type: sample name: .NET console application that accesses a protected API -description: This a .NET console application that accesses a protected API. The code in this sample is used by one or more articles on docs.microsoft.com. +description: This a .NET console application that accesses a protected API. The code in this sample is used by one or more articles on learn.microsoft.com. products: - azure -- azure-active-directory +- entra-id - ms-graph urlFragment: ms-identity-docs-code-dotnet-console --- @@ -46,7 +46,7 @@ Could not find a cached token, so fetching a new one. ### 1. Register the app with the Microsoft identity platform -First, complete the steps in [Register an application with the Microsoft identity platform](https://docs.microsoft.com/azure/active-directory/develop/quickstart-register-app) to register the application. +First, complete the steps in [Register an application with the Microsoft identity platform](https://learn.microsoft.com/entra/identity-platform/quickstart-register-app) to register the application. Use these settings in your app registration. diff --git a/4-desktop-apps/console-device-code-flow/AppCreationScripts/AppCreationScripts.md b/4-desktop-apps/console-device-code-flow/AppCreationScripts/AppCreationScripts.md new file mode 100644 index 0000000..ae8be6a --- /dev/null +++ b/4-desktop-apps/console-device-code-flow/AppCreationScripts/AppCreationScripts.md @@ -0,0 +1,166 @@ +# Registering the sample apps with Microsoft identity platform and updating the configuration files using PowerShell scripts + +## Overview + +### Quick summary + +1. On Windows run PowerShell and navigate to the root of the cloned directory +1. In PowerShell run: + ```PowerShell + Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force + ``` +1. Run the script to create your Microsoft Entra application and configure the code of the sample application accordingly. (Other ways of running the scripts are described below) + ```PowerShell + cd .\AppCreationScripts\ + .\Configure.ps1 + ``` +1. Open the Visual Studio solution and click start + +### More details + +The following paragraphs: + +- [Present the scripts](#presentation-of-the-scripts) and explain their [usage patterns](#usage-pattern-for-tests-and-devops-scenarios) for test and DevOps scenarios. +- Explain the [pre-requisites](#pre-requisites) +- Explain [four ways of running the scripts](#four-ways-to-run-the-script): + - [Interactively](#option-1-interactive) to create the app in your home tenant + - [Passing credentials](#option-2-non-interactive) to create the app in your home tenant + - [Interactively in a specific tenant](#option-3-interactive-but-create-apps-in-a-specified-tenant) + - [Passing credentials in a specific tenant](#option-4-non-interactive-and-create-apps-in-a-specified-tenant) + - [Passing environment name, for Sovereign clouds](#running-the-script-on-azure-sovereign-clouds) + +## Goal of the scripts + +### Presentation of the scripts + +This sample comes with two PowerShell scripts, which automate the creation of the Microsoft Entra applications, and the configuration of the code for this sample. Once you run them, you will only need to build the solution and you are good to test. + +These scripts are: + +- `Configure.ps1` which: + - creates Microsoft Entra applications and their related objects (permissions, dependencies, secrets), + - changes the configuration files in the C# and JavaScript projects. + - creates a summary file named `createdApps.html` in the folder from which you ran the script, and containing, for each Microsoft Entra application it created: + - the identifier of the application + - the AppId of the application + - the url of its registration in the [Microsoft Entra admin center](https://entra.microsoft.com). + +- `Cleanup.ps1` which cleans-up the Microsoft Entra objects created by `Configure.ps1`. Note that this script does not revert the changes done in the configuration files, though. You will need to undo the change from source control (from Visual Studio, or from the command line using, for instance, git reset). + +### Usage pattern for tests and DevOps scenarios + +The `Configure.ps1` will stop if it tries to create a Microsoft Entra application which already exists in the tenant. For this, if you are using the script to try/test the sample, or in DevOps scenarios, you might want to run `Cleanup.ps1` just before `Configure.ps1`. This is what is shown in the steps below. + +## How to use the app creation scripts? + +### Pre-requisites + +1. Open PowerShell (On Windows, press `Windows-R` and type `PowerShell` in the search window) +2. Navigate to the root directory of the project. +3. Until you change it, the default [Execution Policy](https:/go.microsoft.com/fwlink/?LinkID=135170) for scripts is usually `Restricted`. In order to run the PowerShell script you need to set the Execution Policy to `RemoteSigned`. You can set this just for the current PowerShell process by running the command: + ```PowerShell + Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process + ``` +### (Optionally) install AzureAD PowerShell modules +The scripts install the required PowerShell module (AzureAD) for the current user if needed. However, if you want to install if for all users on the machine, you can follow the following steps: + +4. If you have never done it already, in the PowerShell window, install the AzureAD PowerShell modules. For this: + + 1. Open PowerShell as admin (On Windows, Search Powershell in the search bar, right click on it and select Run as administrator). + 2. Type: + ```PowerShell + Install-Module AzureAD + ``` + + or if you cannot be administrator on your machine, run: + ```PowerShell + Install-Module AzureAD -Scope CurrentUser + ``` + +### Run the script and start running + +5. Go to the `AppCreationScripts` sub-folder. From the folder where you cloned the repo, + ```PowerShell + cd AppCreationScripts + ``` +6. Run the scripts. See below for the [four options](#four-ways-to-run-the-script) to do that. +7. Open the Visual Studio solution, and in the solution's context menu, choose **Set Startup Projects**. +8. select **Start** for the projects + +You're done. this just works! + +### Four ways to run the script + +We advise four ways of running the script: + +- Interactive: you will be prompted for credentials, and the scripts decide in which tenant to create the objects, +- non-interactive: you will provide credentials, and the scripts decide in which tenant to create the objects, +- Interactive in specific tenant: you will provide the tenant in which you want to create the objects and then you will be prompted for credentials, and the scripts will create the objects, +- non-interactive in specific tenant: you will provide tenant in which you want to create the objects and credentials, and the scripts will create the objects. + +Here are the details on how to do this. + +#### Option 1 (interactive) + +- Just run ``. .\Configure.ps1``, and you will be prompted to sign-in (email address, password, and if needed MFA). +- The script will be run as the signed-in user and will use the tenant in which the user is defined. + +Note that the script will choose the tenant in which to create the applications, based on the user. Also to run the `Cleanup.ps1` script, you will need to re-sign-in. + +#### Option 2 (non-interactive) + +When you know the identity and credentials of the user in the name of whom you want to create the applications, you can use the non-interactive approach. It's more adapted to DevOps. Here is an example of script you'd want to run in a PowerShell Window + +```PowerShell +$secpasswd = ConvertTo-SecureString "[Password here]" -AsPlainText -Force +$mycreds = New-Object System.Management.Automation.PSCredential ("[login@tenantName here]", $secpasswd) +. .\Cleanup.ps1 -Credential $mycreds +. .\Configure.ps1 -Credential $mycreds +``` + +Of course, in real life, you might already get the password as a `SecureString`. You might also want to get the password from KeyVault. + +#### Option 3 (Interactive, but create apps in a specified tenant) + + if you want to create the apps in a particular tenant, you can use the following option: +- open the [Microsoft Entra admin center](https://entra.microsoft.com) +- Select the Microsoft Entra ID you are interested in (in the combo-box below your name on the top right of the browser window) +- Find the "Active Directory" object in this tenant +- Go to **Properties** and copy the content of the **Directory Id** property +- Then use the full syntax to run the scripts: + +```PowerShell +$tenantId = "yourTenantIdGuid" +. .\Cleanup.ps1 -TenantId $tenantId +. .\Configure.ps1 -TenantId $tenantId +``` + +#### Option 4 (non-interactive, and create apps in a specified tenant) + +This option combines option 2 and option 3: it creates the application in a specific tenant. See option 3 for the way to get the tenant Id. Then run: + +```PowerShell +$secpasswd = ConvertTo-SecureString "[Password here]" -AsPlainText -Force +$mycreds = New-Object System.Management.Automation.PSCredential ("[login@tenantName here]", $secpasswd) +$tenantId = "yourTenantIdGuid" +. .\Cleanup.ps1 -Credential $mycreds -TenantId $tenantId +. .\Configure.ps1 -Credential $mycreds -TenantId $tenantId +``` + +### Running the script on Azure Sovereign clouds + +All the four options listed above, can be used on any Azure Sovereign clouds. By default, the script targets `AzureCloud`, but it can be changed using the parameter `-AzureEnvironmentName`. + +The acceptable values for this parameter are: + +- AzureCloud +- AzureChinaCloud +- AzureUSGovernment +- AzureGermanyCloud + +Example: + + ```PowerShell + . .\Cleanup.ps1 -AzureEnvironmentName "AzureGermanyCloud" + . .\Configure.ps1 -AzureEnvironmentName "AzureGermanyCloud" + ``` diff --git a/4-desktop-apps/console-device-code-flow/AppCreationScripts/Cleanup.ps1 b/4-desktop-apps/console-device-code-flow/AppCreationScripts/Cleanup.ps1 new file mode 100644 index 0000000..1078703 --- /dev/null +++ b/4-desktop-apps/console-device-code-flow/AppCreationScripts/Cleanup.ps1 @@ -0,0 +1,80 @@ +[CmdletBinding()] +param( + [PSCredential] $Credential, + [Parameter(Mandatory=$False, HelpMessage='Tenant ID (This is a GUID which represents the "Directory ID" of the AzureAD tenant into which you want to create the apps')] + [string] $tenantId, + [Parameter(Mandatory=$False, HelpMessage='Azure environment to use while running the script (it defaults to AzureCloud)')] + [string] $azureEnvironmentName +) + +#Requires -Modules AzureAD + + +if ($null -eq (Get-Module -ListAvailable -Name "AzureAD")) { + Install-Module "AzureAD" -Scope CurrentUser +} +Import-Module AzureAD +$ErrorActionPreference = "Stop" + +Function Cleanup +{ + if (!$azureEnvironmentName) + { + $azureEnvironmentName = "AzureCloud" + } + + <# + .Description + This function removes the Microsoft Entra applications for the sample. These applications were created by the Configure.ps1 script + #> + + # $tenantId is the Active Directory Tenant. This is a GUID which represents the "Directory ID" of the AzureAD tenant + # into which you want to create the apps. Look it up in the Azure portal in the "Properties" of Microsoft Entra. + + # Login to Azure PowerShell (interactive if credentials are not already provided: + # you'll need to sign-in with creds enabling your to create apps in the tenant) + if (!$Credential -and $TenantId) + { + $creds = Connect-AzureAD -TenantId $tenantId -AzureEnvironmentName $azureEnvironmentName + } + else + { + if (!$TenantId) + { + $creds = Connect-AzureAD -Credential $Credential -AzureEnvironmentName $azureEnvironmentName + } + else + { + $creds = Connect-AzureAD -TenantId $tenantId -Credential $Credential -AzureEnvironmentName $azureEnvironmentName + } + } + + if (!$tenantId) + { + $tenantId = $creds.Tenant.Id + } + $tenant = Get-AzureADTenantDetail + $tenantName = ($tenant.VerifiedDomains | Where-Object { $_._Default -eq $True }).Name + + # Removes the applications + Write-Host "Cleaning-up applications from tenant '$tenantName'" + + Write-Host "Removing 'client' (Console-DeviceCodeFlow-MultiTarget-v2) if needed" + Get-AzureADApplication -Filter "DisplayName eq 'Console-DeviceCodeFlow-MultiTarget-v2'" | ForEach-Object {Remove-AzureADApplication -ObjectId $_.ObjectId } + $apps = Get-AzureADApplication -Filter "DisplayName eq 'Console-DeviceCodeFlow-MultiTarget-v2'" + if ($apps) + { + Remove-AzureADApplication -ObjectId $apps.ObjectId + } + + foreach ($app in $apps) + { + Remove-AzureADApplication -ObjectId $app.ObjectId + Write-Host "Removed Console-DeviceCodeFlow-MultiTarget-v2.." + } + # also remove service principals of this app + Get-AzureADServicePrincipal -filter "DisplayName eq 'Console-DeviceCodeFlow-MultiTarget-v2'" | ForEach-Object {Remove-AzureADServicePrincipal -ObjectId $_.Id -Confirm:$false} + +} + +Cleanup -Credential $Credential -tenantId $TenantId \ No newline at end of file diff --git a/4-desktop-apps/console-device-code-flow/AppCreationScripts/Configure.ps1 b/4-desktop-apps/console-device-code-flow/AppCreationScripts/Configure.ps1 new file mode 100644 index 0000000..f72400a --- /dev/null +++ b/4-desktop-apps/console-device-code-flow/AppCreationScripts/Configure.ps1 @@ -0,0 +1,225 @@ +[CmdletBinding()] +param( + [PSCredential] $Credential, + [Parameter(Mandatory=$False, HelpMessage='Tenant ID (This is a GUID which represents the "Directory ID" of the AzureAD tenant into which you want to create the apps')] + [string] $tenantId, + [Parameter(Mandatory=$False, HelpMessage='Azure environment to use while running the script (it defaults to AzureCloud)')] + [string] $azureEnvironmentName +) + +#Requires -Modules AzureAD + +<# + This script creates the Microsoft Entra applications needed for this sample and updates the configuration files + for the visual Studio projects from the data in the Microsoft Entra applications. + + Before running this script you need to install the AzureAD cmdlets as an administrator. + For this: + 1) Run Powershell as an administrator + 2) in the PowerShell window, type: Install-Module AzureAD + + There are four ways to run this script. For more information, read the AppCreationScripts.md file in the same folder as this script. +#> + +# Adds the requiredAccesses (expressed as a pipe separated string) to the requiredAccess structure +# The exposed permissions are in the $exposedPermissions collection, and the type of permission (Scope | Role) is +# described in $permissionType +Function AddResourcePermission($requiredAccess, ` + $exposedPermissions, [string]$requiredAccesses, [string]$permissionType) +{ + foreach($permission in $requiredAccesses.Trim().Split("|")) + { + foreach($exposedPermission in $exposedPermissions) + { + if ($exposedPermission.Value -eq $permission) + { + $resourceAccess = New-Object Microsoft.Open.AzureAD.Model.ResourceAccess + $resourceAccess.Type = $permissionType # Scope = Delegated permissions | Role = Application permissions + $resourceAccess.Id = $exposedPermission.Id # Read directory data + $requiredAccess.ResourceAccess.Add($resourceAccess) + } + } + } +} + +# +# Example: GetRequiredPermissions "Microsoft Graph" "Graph.Read|User.Read" +# See also: http://stackoverflow.com/questions/42164581/how-to-configure-a-new-azure-ad-application-through-powershell +Function GetRequiredPermissions([string] $applicationDisplayName, [string] $requiredDelegatedPermissions, [string]$requiredApplicationPermissions, $servicePrincipal) +{ + # If we are passed the service principal we use it directly, otherwise we find it from the display name (which might not be unique) + if ($servicePrincipal) + { + $sp = $servicePrincipal + } + else + { + $sp = Get-AzureADServicePrincipal -Filter "DisplayName eq '$applicationDisplayName'" + } + $appid = $sp.AppId + $requiredAccess = New-Object Microsoft.Open.AzureAD.Model.RequiredResourceAccess + $requiredAccess.ResourceAppId = $appid + $requiredAccess.ResourceAccess = New-Object System.Collections.Generic.List[Microsoft.Open.AzureAD.Model.ResourceAccess] + + # $sp.Oauth2Permissions | Select Id,AdminConsentDisplayName,Value: To see the list of all the Delegated permissions for the application: + if ($requiredDelegatedPermissions) + { + AddResourcePermission $requiredAccess -exposedPermissions $sp.Oauth2Permissions -requiredAccesses $requiredDelegatedPermissions -permissionType "Scope" + } + + # $sp.AppRoles | Select Id,AdminConsentDisplayName,Value: To see the list of all the Application permissions for the application + if ($requiredApplicationPermissions) + { + AddResourcePermission $requiredAccess -exposedPermissions $sp.AppRoles -requiredAccesses $requiredApplicationPermissions -permissionType "Role" + } + return $requiredAccess +} + + +Function UpdateLine([string] $line, [string] $value) +{ + $index = $line.IndexOf('=') + $delimiter = ';' + if ($index -eq -1) + { + $index = $line.IndexOf(':') + $delimiter = ',' + } + if ($index -ige 0) + { + $line = $line.Substring(0, $index+1) + " "+'"'+$value+'"'+$delimiter + } + return $line +} + +Function UpdateTextFile([string] $configFilePath, [System.Collections.HashTable] $dictionary) +{ + $lines = Get-Content $configFilePath + $index = 0 + while($index -lt $lines.Length) + { + $line = $lines[$index] + foreach($key in $dictionary.Keys) + { + if ($line.Contains($key)) + { + $lines[$index] = UpdateLine $line $dictionary[$key] + } + } + $index++ + } + + Set-Content -Path $configFilePath -Value $lines -Force +} + +Set-Content -Value "" -Path createdApps.html +Add-Content -Value "" -Path createdApps.html + +$ErrorActionPreference = "Stop" + +Function ConfigureApplications +{ +<#.Description + This function creates the Microsoft Entra applications for the sample in the provided Microsoft Entra tenant and updates the + configuration files in the client and service project of the visual studio solution (App.Config and Web.Config) + so that they are consistent with the Applications parameters +#> + $commonendpoint = "common" + + if (!$azureEnvironmentName) + { + $azureEnvironmentName = "AzureCloud" + } + + # $tenantId is the Active Directory Tenant. This is a GUID which represents the "Directory ID" of the AzureAD tenant + # into which you want to create the apps. Look it up in the Azure portal in the "Properties" of Microsoft Entra. + + # Login to Azure PowerShell (interactive if credentials are not already provided: + # you'll need to sign-in with creds enabling your to create apps in the tenant) + if (!$Credential -and $TenantId) + { + $creds = Connect-AzureAD -TenantId $tenantId -AzureEnvironmentName $azureEnvironmentName + } + else + { + if (!$TenantId) + { + $creds = Connect-AzureAD -Credential $Credential -AzureEnvironmentName $azureEnvironmentName + } + else + { + $creds = Connect-AzureAD -TenantId $tenantId -Credential $Credential -AzureEnvironmentName $azureEnvironmentName + } + } + + if (!$tenantId) + { + $tenantId = $creds.Tenant.Id + } + + + + $tenant = Get-AzureADTenantDetail + $tenantName = ($tenant.VerifiedDomains | Where { $_._Default -eq $True }).Name + + # Get the user running the script to add the user as the app owner + $user = Get-AzureADUser -ObjectId $creds.Account.Id + + # Create the client AAD application + Write-Host "Creating the AAD application (Console-DeviceCodeFlow-MultiTarget-v2)" + # create the application + $clientAadApplication = New-AzureADApplication -DisplayName "Console-DeviceCodeFlow-MultiTarget-v2" ` + -ReplyUrls "https://login.microsoftonline.com/common/oauth2/nativeclient" ` + -PublicClient $True + + # create the service principal of the newly created application + $currentAppId = $clientAadApplication.AppId + $clientServicePrincipal = New-AzureADServicePrincipal -AppId $currentAppId -Tags {WindowsAzureActiveDirectoryIntegratedApp} + + # add the user running the script as an app owner if needed + $owner = Get-AzureADApplicationOwner -ObjectId $clientAadApplication.ObjectId + if ($owner -eq $null) + { + Add-AzureADApplicationOwner -ObjectId $clientAadApplication.ObjectId -RefObjectId $user.ObjectId + Write-Host "'$($user.UserPrincipalName)' added as an application owner to app '$($clientServicePrincipal.DisplayName)'" + } + + + Write-Host "Done creating the client application (Console-DeviceCodeFlow-MultiTarget-v2)" + + # URL of the AAD application in the Azure portal + # Future? $clientPortalUrl = "https://portal.azure.com/#@"+$tenantName+"/blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/Overview/appId/"+$clientAadApplication.AppId+"/objectId/"+$clientAadApplication.ObjectId+"/isMSAApp/" + $clientPortalUrl = "https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/CallAnAPI/appId/"+$clientAadApplication.AppId+"/objectId/"+$clientAadApplication.ObjectId+"/isMSAApp/" + Add-Content -Value "" -Path createdApps.html + + $requiredResourcesAccess = New-Object System.Collections.Generic.List[Microsoft.Open.AzureAD.Model.RequiredResourceAccess] + + # Add Required Resources Access (from 'client' to 'Microsoft Graph') + Write-Host "Getting access from 'client' to 'Microsoft Graph'" + $requiredPermissions = GetRequiredPermissions -applicationDisplayName "Microsoft Graph" ` + -requiredDelegatedPermissions "User.Read" ` + + $requiredResourcesAccess.Add($requiredPermissions) + + + Set-AzureADApplication -ObjectId $clientAadApplication.ObjectId -RequiredResourceAccess $requiredResourcesAccess + Write-Host "Granted permissions." + + # Update config file for 'client' + $configFile = $pwd.Path + "\..\Console-DeviceCodeFlow-v2\appsettings.json" + Write-Host "Updating the sample code ($configFile)" + $dictionary = @{ "ClientId" = $clientAadApplication.AppId;"TenantId" = $tenantId }; + UpdateTextFile -configFilePath $configFile -dictionary $dictionary + + Add-Content -Value "
ApplicationAppIdUrl in the Azure portal
client$currentAppIdConsole-DeviceCodeFlow-MultiTarget-v2
" -Path createdApps.html +} + +# Pre-requisites +if ((Get-Module -ListAvailable -Name "AzureAD") -eq $null) { + Install-Module "AzureAD" -Scope CurrentUser +} + +Import-Module AzureAD + +# Run interactively (will ask you for the tenant ID) +ConfigureApplications -Credential $Credential -tenantId $TenantId \ No newline at end of file diff --git a/4-desktop-apps/console-device-code-flow/AppCreationScripts/sample.json b/4-desktop-apps/console-device-code-flow/AppCreationScripts/sample.json new file mode 100644 index 0000000..74dd77f --- /dev/null +++ b/4-desktop-apps/console-device-code-flow/AppCreationScripts/sample.json @@ -0,0 +1,53 @@ +{ + "Sample": { + "Title": "Sign-in a user with the Microsoft identity platform using the device code flow and call Microsoft Graph on the user's behalf.", + "Level": 100, + "Client": ".NET Desktop (Console)", + "Service": "Microsoft Graph", + "RepositoryUrl": "ms-identity-dotnet-desktop-tutorial", + "Endpoint": "AAD v2.0" + }, + + /* + This section describes the Microsoft Entra applications to configure, and their dependencies + */ + "AADApps": [ + { + "Id": "client", + "Name": "Console-DeviceCodeFlow-MultiTarget-v2", + "Kind": "Desktop", + "UsesROPCOrIWA": true, + "Audience": "AzureADMyOrg", + "RequiredResourcesAccess": [ + { + "Resource": "Microsoft Graph", + "DelegatedPermissions": [ "User.Read" ] + } + ] + } + ], + + /* + This section describes how to update the code in configuration files from the apps coordinates, once the apps + are created in Microsoft Entra. + Each section describes a configuration file, for one of the apps, it's type (XML, JSon, plain text), its location + with respect to the root of the sample, and the mappping (which string in the config file is mapped to which value + */ + "CodeConfiguration": [ + { + "App": "client", + "SettingKind": "JSon", + "SettingFile": "\\..\\Console-DeviceCodeFlow-v2\\appsettings.json", + "Mappings": [ + { + "key": "ClientId", + "value": ".AppId" + }, + { + "key": "TenantId", + "value": "$tenantId" + } + ] + } + ] +} diff --git a/4-desktop-apps/console-device-code-flow/Console-DeviceCodeFlow-v2.sln b/4-desktop-apps/console-device-code-flow/Console-DeviceCodeFlow-v2.sln new file mode 100644 index 0000000..a853acc --- /dev/null +++ b/4-desktop-apps/console-device-code-flow/Console-DeviceCodeFlow-v2.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30011.22 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Console-DeviceCodeFlow-v2", "Console-DeviceCodeFlow-v2\Console-DeviceCodeFlow-v2.csproj", "{23239B0D-FF39-43E7-998D-BCC885D918C2}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {23239B0D-FF39-43E7-998D-BCC885D918C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {23239B0D-FF39-43E7-998D-BCC885D918C2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {23239B0D-FF39-43E7-998D-BCC885D918C2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {23239B0D-FF39-43E7-998D-BCC885D918C2}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {04F72127-4CDB-4189-A854-4B2C7FD7D6C3} + EndGlobalSection +EndGlobal diff --git a/4-desktop-apps/console-device-code-flow/Console-DeviceCodeFlow-v2/Console-DeviceCodeFlow-v2.csproj b/4-desktop-apps/console-device-code-flow/Console-DeviceCodeFlow-v2/Console-DeviceCodeFlow-v2.csproj new file mode 100644 index 0000000..674a37a --- /dev/null +++ b/4-desktop-apps/console-device-code-flow/Console-DeviceCodeFlow-v2/Console-DeviceCodeFlow-v2.csproj @@ -0,0 +1,22 @@ + + + + Exe + netcoreapp3.1; net472 + Console_DeviceCodeFlow_MultiTarget + + + + + + + + + + + + + PreserveNewest + + + diff --git a/4-desktop-apps/console-device-code-flow/Console-DeviceCodeFlow-v2/Program.cs b/4-desktop-apps/console-device-code-flow/Console-DeviceCodeFlow-v2/Program.cs new file mode 100644 index 0000000..140d522 --- /dev/null +++ b/4-desktop-apps/console-device-code-flow/Console-DeviceCodeFlow-v2/Program.cs @@ -0,0 +1,123 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Graph; +using Microsoft.Identity.Client; +using System; +using System.Linq; +using System.Net.Http.Headers; +using System.Threading.Tasks; + +namespace Console_DeviceCodeFlow_MultiTarget +{ + internal class Program + { + private static PublicClientApplicationOptions appConfiguration = null; + private static IConfiguration configuration; + private static string MSGraphURL; + + // The MSAL Public client app + private static IPublicClientApplication application; + + private static async Task Main(string[] args) + { + // Using appsettings.json to load the configuration settings + var builder = new ConfigurationBuilder() + .SetBasePath(System.IO.Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json"); + + configuration = builder.Build(); + + appConfiguration = configuration.Get(); + + // We intend to obtain a token for Graph for the following scopes (permissions) + string[] scopes = new[] { "user.read" }; + + MSGraphURL = configuration.GetValue("GraphApiUrl"); + + // Sign-in user using MSAL and obtain an access token for MS Graph + GraphServiceClient graphClient = await SignInAndInitializeGraphServiceClient(appConfiguration, scopes); + + // Call the /me endpoint of MS Graph + await CallMSGraph(graphClient); + } + + /// + /// Signs in the user using the device code flow and obtains an Access token for MS Graph + /// + /// + /// + /// + private static async Task SignInUserAndGetTokenUsingMSAL(PublicClientApplicationOptions configuration, string[] scopes) + { + // build the AAd authority Url + string authority = string.Concat(configuration.Instance, configuration.TenantId); + + // Initialize the MSAL library by building a public client application + application = PublicClientApplicationBuilder.Create(configuration.ClientId) + .WithAuthority(authority) + .WithDefaultRedirectUri() + .Build(); + + + AuthenticationResult result; + + try + { + var accounts = await application.GetAccountsAsync(); + // Try to acquire an access token from the cache. If device code is required, Exception will be thrown. + result = await application.AcquireTokenSilent(scopes, accounts.FirstOrDefault()).ExecuteAsync(); + } + catch (MsalUiRequiredException) + { + result = await application.AcquireTokenWithDeviceCode(scopes, deviceCodeResult => + { + // This will print the message on the console which tells the user where to go sign-in using + // a separate browser and the code to enter once they sign in. + // The AcquireTokenWithDeviceCode() method will poll the server after firing this + // device code callback to look for the successful login of the user via that browser. + // This background polling (whose interval and timeout data is also provided as fields in the + // deviceCodeCallback class) will occur until: + // * The user has successfully logged in via browser and entered the proper code + // * The timeout specified by the server for the lifetime of this code (typically ~15 minutes) has been reached + // * The developing application calls the Cancel() method on a CancellationToken sent into the method. + // If this occurs, an OperationCanceledException will be thrown (see catch below for more details). + Console.WriteLine(deviceCodeResult.Message); + return Task.FromResult(0); + }).ExecuteAsync(); + } + return result.AccessToken; + } + + /// + /// Sign in user using MSAL and obtain a token for MS Graph + /// + /// + private async static Task SignInAndInitializeGraphServiceClient(PublicClientApplicationOptions configuration, string[] scopes) + { + GraphServiceClient graphClient = new GraphServiceClient(MSGraphURL, + new DelegateAuthenticationProvider(async (requestMessage) => + { + requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", await SignInUserAndGetTokenUsingMSAL(configuration, scopes)); + })); + + return await Task.FromResult(graphClient); + } + + /// + /// Call MS Graph and print results + /// + /// + /// + private static async Task CallMSGraph(GraphServiceClient graphClient) + { + var me = await graphClient.Me.Request().GetAsync(); + + // Printing the results + Console.Write(Environment.NewLine); + Console.WriteLine("-------- Data from call to MS Graph --------"); + Console.Write(Environment.NewLine); + Console.WriteLine($"Id: {me.Id}"); + Console.WriteLine($"Display Name: {me.DisplayName}"); + Console.WriteLine($"Email: {me.Mail}"); + } + } +} \ No newline at end of file diff --git a/4-desktop-apps/console-device-code-flow/Console-DeviceCodeFlow-v2/appsettings.json b/4-desktop-apps/console-device-code-flow/Console-DeviceCodeFlow-v2/appsettings.json new file mode 100644 index 0000000..8a39179 --- /dev/null +++ b/4-desktop-apps/console-device-code-flow/Console-DeviceCodeFlow-v2/appsettings.json @@ -0,0 +1,6 @@ +{ + "Instance": "https://login.microsoftonline.com/", + "ClientId": "[Enter the Client Id (Application ID obtained from the Azure portal), e.g. ba74781c2-53c2-442a-97c2-3d60re42f403]", + "TenantId": "[Enter 'common', or 'organizations' or the Tenant Id (Obtained from the Azure portal. Select 'Endpoints' from the 'App registrations' blade and use the GUID in any of the URLs), e.g. da41245a5-11b3-996c-00a8-4d99re19f292]", + "GraphApiUrl": "https://graph.microsoft.com/v1.0/" +} diff --git a/4-desktop-apps/console-device-code-flow/Readme.md b/4-desktop-apps/console-device-code-flow/Readme.md new file mode 100644 index 0000000..5bef8bf --- /dev/null +++ b/4-desktop-apps/console-device-code-flow/Readme.md @@ -0,0 +1,254 @@ +--- +services: active-directory +platforms: dotnet +author: Shama-K +level: 100 +client: .NET Desktop (Console) +service: Microsoft Graph +endpoint: Microsoft identity platform +page_type: sample +languages: + - csharp +products: + - azure + - microsoft-entra-id + - dotnet + - office-ms-graph +description: "This sample demonstrates a .NET Desktop (Console) application authenticating a user with the device code flow" +--- +# Sign-in a user with the Microsoft identity platform using the device code flow and call Microsoft Graph. + +![.NET Core](https://github.com/Azure-Samples/ms-identity-dotnet-desktop-tutorial/workflows/.NET%20Core/badge.svg) + +## About this sample + +### Overview + +This sample demonstrates a .NET Desktop (Console) application authenticating a user with the [device code flow](https://learn.microsoft.com/entra/identity-platform/v2-oauth2-device-code) and calling Microsoft Graph on behalf of the user. This flow allows users to sign in to input-constrained devices such as a smart TV, IoT device, or printer. To enable this flow, the device has the user visit a webpage in their browser on another device to sign in. Once the user signs in, the device is able to get access tokens and refresh tokens as needed. + +1. The .NET Desktop (Console) application uses the [Microsoft Authentication Library (MSAL)](https://learn.microsoft.com/entra/identity-platform/msal-overview) to sign-in a user and obtains an [access token](https://learn.microsoft.com/entra/identity-platform/access-tokens) for Microsoft Graph from Microsoft Entra ID. The user is using the [device code flow](https://learn.microsoft.com/entra/identity-platform/v2-oauth2-device-code). +2. The access token is used as a bearer token to authenticate the user when calling Microsoft Graph. + +> Looking for previous versions of this code sample? Check out the tags on the [releases](../../releases) GitHub page. + +![Overview](./ReadmeFiles/topology.png) + +### Scenario + +This console application displays a code and a URL which the user will open in a browser on a device with an available input device (keyboard). The user will open the URL in the browser and enter the code to start the authentication process. +Once the authentication process completes, the console application will resume and call Microsoft Graph on behalf of the user to fetch some information and prints it on the screen. + +## How to run this sample + +To run this sample, you'll need: + +- [Visual Studio 2019](https://aka.ms/vsdownload) +- An Internet connection +- a Microsoft Entra tenant. For more information on how to get a Microsoft Entra tenant, see [How to get a Microsoft Entra tenant](https://azure.microsoft.com/documentation/articles/active-directory-howto-tenant/) +- A user account in your Microsoft Entra tenant. This sample will not work with a Microsoft account (formerly Windows Live account). Therefore, if you signed in to the [Microsoft Entra admin center](https://entra.microsoft.com) with a Microsoft account and have never created a user account in your directory before, you need to do that now. + +### Step 1: Clone or download this repository + +From your shell or command line: + +```Shell +git clone https://github.com/Azure-Samples/ms-identity-dotnet-desktop-tutorial.git +cd "4-DeviceCodeFlow" +``` + +or download and extract the repository .zip file. + +> Given that the name of the sample is quiet long, and so are the names of the referenced NuGet packages, you might want to clone it in a folder close to the root of your hard drive, to avoid the 256 character path length limitation on Windows. + +### Step 2: Register the sample application with your Microsoft Entra tenant + +There is one project in this sample. To register it, you can: + +- either follow the steps [Step 2: Register the sample with your Microsoft Entra tenant](#step-2-register-the-sample-with-your-azure-active-directory-tenant) and [Step 3: Configure the sample to use your Microsoft Entra tenant](#choose-the-azure-ad-tenant-where-you-want-to-create-your-applications) +- or use PowerShell scripts that: + - **automatically** creates the Microsoft Entra applications and related objects (passwords, permissions, dependencies) for you. Note that this works for Visual Studio only. + - modify the Visual Studio projects' configuration files. + +
+ Expand this section if you want to use this automation: + +1. On Windows, run PowerShell and navigate to the root of the cloned directory +1. In PowerShell run: + + ```PowerShell + Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force + ``` + +1. Run the script to create your Microsoft Entra application and configure the code of the sample application accordingly. +1. In PowerShell run: + + ```PowerShell + cd .\AppCreationScripts\ + .\Configure.ps1 + ``` + + > Other ways of running the scripts are described in [App Creation Scripts](./AppCreationScripts/AppCreationScripts.md) + > The scripts also provide a guide to automated application registration, configuration and removal which can help in your CI/CD scenarios. + +1. Open the Visual Studio solution and click start to run the code. + +
+ +Follow the steps below to manually walk through the steps to register and configure the applications in the Microsoft Entra admin center. + +#### Choose the Microsoft Entra tenant where you want to create your applications + +As a first step you'll need to: + +1. Sign in to the [Microsoft Entra admin center](https://entra.microsoft.com) using either a work or school account or a personal Microsoft account. +1. If your account is present in more than one Microsoft Entra tenant, select your profile at the top right corner in the menu on top of the page. Then select **switch directory** to change your portal session to the desired Microsoft Entra tenant. + +#### Register the client app (Console-DeviceCodeFlow-MultiTarget-v2) + +1. Navigate to the Microsoft identity platform for developers [App registrations](https://go.microsoft.com/fwlink/?linkid=2083908) page. +1. Select **New registration**. +1. In the **Register an application page** that appears, enter your application's registration information: + - In the **Name** section, enter a meaningful application name that will be displayed to users of the app, for example `Console-DeviceCodeFlow-MultiTarget-v2`. + - Under **Supported account types**, select **Accounts in this organizational directory only**. +1. Select **Register** to create the application. +1. In the app's registration screen, find and note the **Application (client) ID**. You use this value in your app's configuration file(s) later in your code. + - In the **Advanced settings** | **Default client type** section, flip the switch for `Treat application as a public client` to **Yes**. +1. Select **Save** to save your changes. +1. In the app's registration screen, click on the **API permissions** blade in the left to open the page where we add access to the Apis that your application needs. + - Click the **Add a permission** button and then, + - Ensure that the **Microsoft APIs** tab is selected. + - In the *Commonly used Microsoft APIs* section, click on **Microsoft Graph** + - In the **Delegated permissions** section, select the **User.Read** in the list. Use the search box if necessary. + - Click on the **Add permissions** button at the bottom. + +##### Configure the client app (Console-DeviceCodeFlow-MultiTarget-v2) to use your app registration + +Open the project in your IDE (like Visual Studio) to configure the code. +>In the steps below, "ClientID" is the same as "Application ID" or "AppId". + +1. Open the `Console-DeviceCodeFlow-v2\appsettings.json` file +1. Find the app key `ClientId` and replace the existing value with the application ID (clientId) of the `Console-DeviceCodeFlow-MultiTarget-v2` application copied from the Microsoft Entra admin center. +1. Find the app key `TenantId` and replace the existing value with your Microsoft Entra tenant ID. + +### Step 4: Run the sample + +Clean the solution, rebuild the solution, and run it. + +Use a web browser to open the Url (https://microsoft.com/devicelogin) that is displayed in console app. Input the code presented in the console , sign-in and check the result of the operation back in the console. + +> [Consider taking a moment to share your experience with us.](https://forms.office.com/Pages/ResponsePage.aspx?id=v4j5cvGGr0GRqy180BHbR73pcsbpbxNJuZCMKN0lURpUREhEVDBOTFBMUVRPUElBUE5WMjdPQ1RaMiQlQCN0PWcu) + +## About the code + +The relevant code for this sample is in the `Program.cs` file, in the Main() method. The steps are: + +1- We use the `appsettings.json` as our configuration file and build the **PublicClientApplicationOptions** object with the app registration settings + +```csharp +var builder = new ConfigurationBuilder() + .SetBasePath(System.IO.Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json"); + +configuration = builder.Build(); + +appConfiguration = configuration.Get(); +``` + +2- The method **SignInUserAndGetTokenUsingMSAL** contains the code to initialize MSAL and get an access token for MS Graph. +Try to acquire an access token for Microsoft Graph silently, but if it fails, do it using `AcquireTokenWithDeviceCode()`. +This method will give you code, which will have the lifetime of 15 minutes, and URL for authentication. + +```csharp +private static async Task SignInUserAndGetTokenUsingMSAL(PublicClientApplicationOptions configuration, string[] scopes) +{ + // build the Microsoft Entra authority Url + string authority = string.Concat(configuration.Instance, configuration.TenantId); + + // Initialize the MSAL library by building a public client application + application = PublicClientApplicationBuilder.Create(configuration.ClientId) + .WithAuthority(authority) + .WithDefaultRedirectUri() + .Build(); + + AuthenticationResult result; + + try + { + var accounts = await application.GetAccountsAsync(); + result = await application.AcquireTokenSilent(scopes, accounts.FirstOrDefault()).ExecuteAsync(); + } + catch (MsalUiRequiredException) + { + result = await application.AcquireTokenWithDeviceCode(scopes, deviceCodeResult => + { + Console.WriteLine(deviceCodeResult.Message); + return Task.FromResult(0); + }).ExecuteAsync(); + } + return result.AccessToken; +} +``` + +3- The method **SignInAndInitializeGraphServiceClient** initializes the Graph SDK with the access token we obtained earlier in **SignInUserAndGetTokenUsingMSAL**. + +```csharp +private async static Task SignInAndInitializeGraphServiceClient(PublicClientApplicationOptions configuration, string[] scopes) +{ + GraphServiceClient graphClient = new GraphServiceClient(MSGraphURL, + new DelegateAuthenticationProvider(async (requestMessage) => + { + requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", await SignInUserAndGetTokenUsingMSAL(configuration, scopes)); + })); + + return await Task.FromResult(graphClient); +} +``` + +4- The method **CallMSGraph** uses the initialized Graph SDK to make a call to Graph and fetch data from it. +```csharp +private static async Task CallMSGraph(GraphServiceClient graphClient) +{ + var me = await graphClient.Me.Request().GetAsync(); + + // Printing the results + Console.Write(Environment.NewLine); + Console.WriteLine("-------- Data from call to MS Graph --------"); + Console.Write(Environment.NewLine); + Console.WriteLine($"Id: {me.Id}"); + Console.WriteLine($"Display Name: {me.DisplayName}"); + Console.WriteLine($"Email: {me.Mail}"); +} +``` + +## Community Help and Support + +Use [Stack Overflow](http://stackoverflow.com/questions/tagged/msal) to get support from the community. +Ask your questions on Stack Overflow first and browse existing issues to see if someone has asked your question before. +Make sure that your questions or comments are tagged with [`azure-active-directory` `msal` `dotnet`]. + +If you find a bug in the sample, please raise the issue on [GitHub Issues](../../issues). + +To provide a recommendation, visit the following [User Voice page](https://feedback.azure.com/forums/169401-azure-active-directory). + +## Contributing + +If you'd like to contribute to this sample, see [CONTRIBUTING.MD](/CONTRIBUTING.md). + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information, see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +## More information + +For more information, see MSAL.NET's conceptual documentation: + +- [MSAL.NET's conceptual documentation](https://aka.ms/msal-net) +- [Device code flow](https://learn.microsoft.com/entra/identity-platform/v2-oauth2-device-code) +- [Microsoft identity platform (Microsoft Entra ID for developers)](https://learn.microsoft.com/entra/identity-platform/) +- [Quickstart: Register an application with the Microsoft identity platform](https://learn.microsoft.com/entra/identity-platform/quickstart-register-app) +- [Quickstart: Configure a client application to access web APIs](https://learn.microsoft.com/entra/identity-platform/quickstart-configure-app-access-web-apis) +- [Understanding Microsoft Entra application consent experiences](https://learn.microsoft.com/entra/identity-platform/application-consent-experience) +- [Understand user and admin consent](https://learn.microsoft.com/entra/identity-platform/howto-convert-app-to-be-multi-tenant#understand-user-and-admin-consent) +- [Application and service principal objects in Microsoft Entra ID](https://learn.microsoft.com/entra/identity-platform/app-objects-and-service-principals) +- [Acquiring Tokens](https://aka.ms/msal-net-acquiring-tokens) +- [National Clouds](https://learn.microsoft.com/entra/identity-platform/authentication-national-cloud#app-registration-endpoints) + +For more information about how OAuth 2.0 protocols work in this scenario and other scenarios, see [Authentication Scenarios for Microsoft Entra ID](http://go.microsoft.com/fwlink/?LinkId=394414). diff --git a/4-desktop-apps/console-device-code-flow/ReadmeFiles/topology.png b/4-desktop-apps/console-device-code-flow/ReadmeFiles/topology.png new file mode 100644 index 0000000..2d3f8d7 Binary files /dev/null and b/4-desktop-apps/console-device-code-flow/ReadmeFiles/topology.png differ diff --git a/4-desktop-apps/console-html-browser/AppCreationScripts/AppCreationScripts.md b/4-desktop-apps/console-html-browser/AppCreationScripts/AppCreationScripts.md new file mode 100644 index 0000000..ae8be6a --- /dev/null +++ b/4-desktop-apps/console-html-browser/AppCreationScripts/AppCreationScripts.md @@ -0,0 +1,166 @@ +# Registering the sample apps with Microsoft identity platform and updating the configuration files using PowerShell scripts + +## Overview + +### Quick summary + +1. On Windows run PowerShell and navigate to the root of the cloned directory +1. In PowerShell run: + ```PowerShell + Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force + ``` +1. Run the script to create your Microsoft Entra application and configure the code of the sample application accordingly. (Other ways of running the scripts are described below) + ```PowerShell + cd .\AppCreationScripts\ + .\Configure.ps1 + ``` +1. Open the Visual Studio solution and click start + +### More details + +The following paragraphs: + +- [Present the scripts](#presentation-of-the-scripts) and explain their [usage patterns](#usage-pattern-for-tests-and-devops-scenarios) for test and DevOps scenarios. +- Explain the [pre-requisites](#pre-requisites) +- Explain [four ways of running the scripts](#four-ways-to-run-the-script): + - [Interactively](#option-1-interactive) to create the app in your home tenant + - [Passing credentials](#option-2-non-interactive) to create the app in your home tenant + - [Interactively in a specific tenant](#option-3-interactive-but-create-apps-in-a-specified-tenant) + - [Passing credentials in a specific tenant](#option-4-non-interactive-and-create-apps-in-a-specified-tenant) + - [Passing environment name, for Sovereign clouds](#running-the-script-on-azure-sovereign-clouds) + +## Goal of the scripts + +### Presentation of the scripts + +This sample comes with two PowerShell scripts, which automate the creation of the Microsoft Entra applications, and the configuration of the code for this sample. Once you run them, you will only need to build the solution and you are good to test. + +These scripts are: + +- `Configure.ps1` which: + - creates Microsoft Entra applications and their related objects (permissions, dependencies, secrets), + - changes the configuration files in the C# and JavaScript projects. + - creates a summary file named `createdApps.html` in the folder from which you ran the script, and containing, for each Microsoft Entra application it created: + - the identifier of the application + - the AppId of the application + - the url of its registration in the [Microsoft Entra admin center](https://entra.microsoft.com). + +- `Cleanup.ps1` which cleans-up the Microsoft Entra objects created by `Configure.ps1`. Note that this script does not revert the changes done in the configuration files, though. You will need to undo the change from source control (from Visual Studio, or from the command line using, for instance, git reset). + +### Usage pattern for tests and DevOps scenarios + +The `Configure.ps1` will stop if it tries to create a Microsoft Entra application which already exists in the tenant. For this, if you are using the script to try/test the sample, or in DevOps scenarios, you might want to run `Cleanup.ps1` just before `Configure.ps1`. This is what is shown in the steps below. + +## How to use the app creation scripts? + +### Pre-requisites + +1. Open PowerShell (On Windows, press `Windows-R` and type `PowerShell` in the search window) +2. Navigate to the root directory of the project. +3. Until you change it, the default [Execution Policy](https:/go.microsoft.com/fwlink/?LinkID=135170) for scripts is usually `Restricted`. In order to run the PowerShell script you need to set the Execution Policy to `RemoteSigned`. You can set this just for the current PowerShell process by running the command: + ```PowerShell + Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process + ``` +### (Optionally) install AzureAD PowerShell modules +The scripts install the required PowerShell module (AzureAD) for the current user if needed. However, if you want to install if for all users on the machine, you can follow the following steps: + +4. If you have never done it already, in the PowerShell window, install the AzureAD PowerShell modules. For this: + + 1. Open PowerShell as admin (On Windows, Search Powershell in the search bar, right click on it and select Run as administrator). + 2. Type: + ```PowerShell + Install-Module AzureAD + ``` + + or if you cannot be administrator on your machine, run: + ```PowerShell + Install-Module AzureAD -Scope CurrentUser + ``` + +### Run the script and start running + +5. Go to the `AppCreationScripts` sub-folder. From the folder where you cloned the repo, + ```PowerShell + cd AppCreationScripts + ``` +6. Run the scripts. See below for the [four options](#four-ways-to-run-the-script) to do that. +7. Open the Visual Studio solution, and in the solution's context menu, choose **Set Startup Projects**. +8. select **Start** for the projects + +You're done. this just works! + +### Four ways to run the script + +We advise four ways of running the script: + +- Interactive: you will be prompted for credentials, and the scripts decide in which tenant to create the objects, +- non-interactive: you will provide credentials, and the scripts decide in which tenant to create the objects, +- Interactive in specific tenant: you will provide the tenant in which you want to create the objects and then you will be prompted for credentials, and the scripts will create the objects, +- non-interactive in specific tenant: you will provide tenant in which you want to create the objects and credentials, and the scripts will create the objects. + +Here are the details on how to do this. + +#### Option 1 (interactive) + +- Just run ``. .\Configure.ps1``, and you will be prompted to sign-in (email address, password, and if needed MFA). +- The script will be run as the signed-in user and will use the tenant in which the user is defined. + +Note that the script will choose the tenant in which to create the applications, based on the user. Also to run the `Cleanup.ps1` script, you will need to re-sign-in. + +#### Option 2 (non-interactive) + +When you know the identity and credentials of the user in the name of whom you want to create the applications, you can use the non-interactive approach. It's more adapted to DevOps. Here is an example of script you'd want to run in a PowerShell Window + +```PowerShell +$secpasswd = ConvertTo-SecureString "[Password here]" -AsPlainText -Force +$mycreds = New-Object System.Management.Automation.PSCredential ("[login@tenantName here]", $secpasswd) +. .\Cleanup.ps1 -Credential $mycreds +. .\Configure.ps1 -Credential $mycreds +``` + +Of course, in real life, you might already get the password as a `SecureString`. You might also want to get the password from KeyVault. + +#### Option 3 (Interactive, but create apps in a specified tenant) + + if you want to create the apps in a particular tenant, you can use the following option: +- open the [Microsoft Entra admin center](https://entra.microsoft.com) +- Select the Microsoft Entra ID you are interested in (in the combo-box below your name on the top right of the browser window) +- Find the "Active Directory" object in this tenant +- Go to **Properties** and copy the content of the **Directory Id** property +- Then use the full syntax to run the scripts: + +```PowerShell +$tenantId = "yourTenantIdGuid" +. .\Cleanup.ps1 -TenantId $tenantId +. .\Configure.ps1 -TenantId $tenantId +``` + +#### Option 4 (non-interactive, and create apps in a specified tenant) + +This option combines option 2 and option 3: it creates the application in a specific tenant. See option 3 for the way to get the tenant Id. Then run: + +```PowerShell +$secpasswd = ConvertTo-SecureString "[Password here]" -AsPlainText -Force +$mycreds = New-Object System.Management.Automation.PSCredential ("[login@tenantName here]", $secpasswd) +$tenantId = "yourTenantIdGuid" +. .\Cleanup.ps1 -Credential $mycreds -TenantId $tenantId +. .\Configure.ps1 -Credential $mycreds -TenantId $tenantId +``` + +### Running the script on Azure Sovereign clouds + +All the four options listed above, can be used on any Azure Sovereign clouds. By default, the script targets `AzureCloud`, but it can be changed using the parameter `-AzureEnvironmentName`. + +The acceptable values for this parameter are: + +- AzureCloud +- AzureChinaCloud +- AzureUSGovernment +- AzureGermanyCloud + +Example: + + ```PowerShell + . .\Cleanup.ps1 -AzureEnvironmentName "AzureGermanyCloud" + . .\Configure.ps1 -AzureEnvironmentName "AzureGermanyCloud" + ``` diff --git a/4-desktop-apps/console-html-browser/AppCreationScripts/Cleanup.ps1 b/4-desktop-apps/console-html-browser/AppCreationScripts/Cleanup.ps1 new file mode 100644 index 0000000..cca3c75 --- /dev/null +++ b/4-desktop-apps/console-html-browser/AppCreationScripts/Cleanup.ps1 @@ -0,0 +1,77 @@ +[CmdletBinding()] +param( + [PSCredential] $Credential, + [Parameter(Mandatory=$False, HelpMessage='Tenant ID (This is a GUID which represents the "Directory ID" of the AzureAD tenant into which you want to create the apps')] + [string] $tenantId, + [Parameter(Mandatory=$False, HelpMessage='Azure environment to use while running the script (it defaults to AzureCloud)')] + [string] $azureEnvironmentName +) + +if ($null -eq (Get-Module -ListAvailable -Name "AzureAD")) { + Install-Module "AzureAD" -Scope CurrentUser +} +Import-Module AzureAD +$ErrorActionPreference = "Stop" + +Function Cleanup +{ + if (!$azureEnvironmentName) + { + $azureEnvironmentName = "AzureCloud" + } + + <# + .Description + This function removes the Microsoft Entra applications for the sample. These applications were created by the Configure.ps1 script + #> + + # $tenantId is the Active Directory Tenant. This is a GUID which represents the "Directory ID" of the AzureAD tenant + # into which you want to create the apps. Look it up in the Azure portal in the "Properties" of Microsoft Entra. + + # Login to Azure PowerShell (interactive if credentials are not already provided: + # you'll need to sign-in with creds enabling your to create apps in the tenant) + if (!$Credential -and $TenantId) + { + $creds = Connect-AzureAD -TenantId $tenantId -AzureEnvironmentName $azureEnvironmentName + } + else + { + if (!$TenantId) + { + $creds = Connect-AzureAD -Credential $Credential -AzureEnvironmentName $azureEnvironmentName + } + else + { + $creds = Connect-AzureAD -TenantId $tenantId -Credential $Credential -AzureEnvironmentName $azureEnvironmentName + } + } + + if (!$tenantId) + { + $tenantId = $creds.Tenant.Id + } + $tenant = Get-AzureADTenantDetail + $tenantName = ($tenant.VerifiedDomains | Where-Object { $_._Default -eq $True }).Name + + # Removes the applications + Write-Host "Cleaning-up applications from tenant '$tenantName'" + + Write-Host "Removing 'client' (Console-Interactive-MultiTarget-v2) if needed" + Get-AzureADApplication -Filter "DisplayName eq 'Console-Interactive-MultiTarget-v2'" | ForEach-Object {Remove-AzureADApplication -ObjectId $_.ObjectId } + $apps = Get-AzureADApplication -Filter "DisplayName eq 'Console-Interactive-MultiTarget-v2'" + if ($apps) + { + Remove-AzureADApplication -ObjectId $apps.ObjectId + } + + foreach ($app in $apps) + { + Remove-AzureADApplication -ObjectId $app.ObjectId + Write-Host "Removed Console-Interactive-MultiTarget-v2.." + } + # also remove service principals of this app + Get-AzureADServicePrincipal -filter "DisplayName eq 'Console-Interactive-MultiTarget-v2'" | ForEach-Object {Remove-AzureADServicePrincipal -ObjectId $_.Id -Confirm:$false} + +} + +Cleanup -Credential $Credential -tenantId $TenantId \ No newline at end of file diff --git a/4-desktop-apps/console-html-browser/AppCreationScripts/Configure.ps1 b/4-desktop-apps/console-html-browser/AppCreationScripts/Configure.ps1 new file mode 100644 index 0000000..6454176 --- /dev/null +++ b/4-desktop-apps/console-html-browser/AppCreationScripts/Configure.ps1 @@ -0,0 +1,224 @@ +[CmdletBinding()] +param( + [PSCredential] $Credential, + [Parameter(Mandatory=$False, HelpMessage='Tenant ID (This is a GUID which represents the "Directory ID" of the AzureAD tenant into which you want to create the apps')] + [string] $tenantId, + [Parameter(Mandatory=$False, HelpMessage='Azure environment to use while running the script (it defaults to AzureCloud)')] + [string] $azureEnvironmentName +) + +<# + This script creates the Microsoft Entra applications needed for this sample and updates the configuration files + for the visual Studio projects from the data in the Microsoft Entra applications. + + Before running this script you need to install the AzureAD cmdlets as an administrator. + For this: + 1) Run Powershell as an administrator + 2) in the PowerShell window, type: Install-Module AzureAD + + There are four ways to run this script. For more information, read the AppCreationScripts.md file in the same folder as this script. +#> + +# Adds the requiredAccesses (expressed as a pipe separated string) to the requiredAccess structure +# The exposed permissions are in the $exposedPermissions collection, and the type of permission (Scope | Role) is +# described in $permissionType +Function AddResourcePermission($requiredAccess, ` + $exposedPermissions, [string]$requiredAccesses, [string]$permissionType) +{ + foreach($permission in $requiredAccesses.Trim().Split("|")) + { + foreach($exposedPermission in $exposedPermissions) + { + if ($exposedPermission.Value -eq $permission) + { + $resourceAccess = New-Object Microsoft.Open.AzureAD.Model.ResourceAccess + $resourceAccess.Type = $permissionType # Scope = Delegated permissions | Role = Application permissions + $resourceAccess.Id = $exposedPermission.Id # Read directory data + $requiredAccess.ResourceAccess.Add($resourceAccess) + } + } + } +} + +# +# Example: GetRequiredPermissions "Microsoft Graph" "Graph.Read|User.Read" +# See also: http://stackoverflow.com/questions/42164581/how-to-configure-a-new-azure-ad-application-through-powershell +Function GetRequiredPermissions([string] $applicationDisplayName, [string] $requiredDelegatedPermissions, [string]$requiredApplicationPermissions, $servicePrincipal) +{ + # If we are passed the service principal we use it directly, otherwise we find it from the display name (which might not be unique) + if ($servicePrincipal) + { + $sp = $servicePrincipal + } + else + { + $sp = Get-AzureADServicePrincipal -Filter "DisplayName eq '$applicationDisplayName'" + } + $appid = $sp.AppId + $requiredAccess = New-Object Microsoft.Open.AzureAD.Model.RequiredResourceAccess + $requiredAccess.ResourceAppId = $appid + $requiredAccess.ResourceAccess = New-Object System.Collections.Generic.List[Microsoft.Open.AzureAD.Model.ResourceAccess] + + # $sp.Oauth2Permissions | Select Id,AdminConsentDisplayName,Value: To see the list of all the Delegated permissions for the application: + if ($requiredDelegatedPermissions) + { + AddResourcePermission $requiredAccess -exposedPermissions $sp.Oauth2Permissions -requiredAccesses $requiredDelegatedPermissions -permissionType "Scope" + } + + # $sp.AppRoles | Select Id,AdminConsentDisplayName,Value: To see the list of all the Application permissions for the application + if ($requiredApplicationPermissions) + { + AddResourcePermission $requiredAccess -exposedPermissions $sp.AppRoles -requiredAccesses $requiredApplicationPermissions -permissionType "Role" + } + return $requiredAccess +} + + +Function UpdateLine([string] $line, [string] $value) +{ + $index = $line.IndexOf('=') + $delimiter = ';' + if ($index -eq -1) + { + $index = $line.IndexOf(':') + $delimiter = ',' + } + if ($index -ige 0) + { + $line = $line.Substring(0, $index+1) + " "+'"'+$value+'"'+$delimiter + } + return $line +} + +Function UpdateTextFile([string] $configFilePath, [System.Collections.HashTable] $dictionary) +{ + $lines = Get-Content $configFilePath + $index = 0 + while($index -lt $lines.Length) + { + $line = $lines[$index] + foreach($key in $dictionary.Keys) + { + if ($line.Contains($key)) + { + $lines[$index] = UpdateLine $line $dictionary[$key] + } + } + $index++ + } + + Set-Content -Path $configFilePath -Value $lines -Force +} + +Set-Content -Value "" -Path createdApps.html +Add-Content -Value "" -Path createdApps.html + +$ErrorActionPreference = "Stop" + +Function ConfigureApplications +{ +<#.Description + This function creates the Microsoft Entra applications for the sample in the provided Microsoft Entra tenant and updates the + configuration files in the client and service project of the visual studio solution (App.Config and Web.Config) + so that they are consistent with the Applications parameters +#> + $commonendpoint = "common" + + if (!$azureEnvironmentName) + { + $azureEnvironmentName = "AzureCloud" + } + + # $tenantId is the Active Directory Tenant. This is a GUID which represents the "Directory ID" of the AzureAD tenant + # into which you want to create the apps. Look it up in the Azure portal in the "Properties" of Microsoft Entra. + + # Login to Azure PowerShell (interactive if credentials are not already provided: + # you'll need to sign-in with creds enabling your to create apps in the tenant) + if (!$Credential -and $TenantId) + { + $creds = Connect-AzureAD -TenantId $tenantId -AzureEnvironmentName $azureEnvironmentName + } + else + { + if (!$TenantId) + { + $creds = Connect-AzureAD -Credential $Credential -AzureEnvironmentName $azureEnvironmentName + } + else + { + $creds = Connect-AzureAD -TenantId $tenantId -Credential $Credential -AzureEnvironmentName $azureEnvironmentName + } + } + + if (!$tenantId) + { + $tenantId = $creds.Tenant.Id + } + + + + $tenant = Get-AzureADTenantDetail + $tenantName = ($tenant.VerifiedDomains | Where { $_._Default -eq $True }).Name + + # Get the user running the script to add the user as the app owner + $user = Get-AzureADUser -ObjectId $creds.Account.Id + + # Create the client AAD application + Write-Host "Creating the AAD application (Console-Interactive-MultiTarget-v2)" + # create the application + $clientAadApplication = New-AzureADApplication -DisplayName "Console-Interactive-MultiTarget-v2" ` + -ReplyUrls "https://login.microsoftonline.com/common/oauth2/nativeclient", "http://localhost" ` + -AvailableToOtherTenants $True ` + -PublicClient $True + + # create the service principal of the newly created application + $currentAppId = $clientAadApplication.AppId + $clientServicePrincipal = New-AzureADServicePrincipal -AppId $currentAppId -Tags {WindowsAzureActiveDirectoryIntegratedApp} + + # add the user running the script as an app owner if needed + $owner = Get-AzureADApplicationOwner -ObjectId $clientAadApplication.ObjectId + if ($owner -eq $null) + { + Add-AzureADApplicationOwner -ObjectId $clientAadApplication.ObjectId -RefObjectId $user.ObjectId + Write-Host "'$($user.UserPrincipalName)' added as an application owner to app '$($clientServicePrincipal.DisplayName)'" + } + + + Write-Host "Done creating the client application (Console-Interactive-MultiTarget-v2)" + + # URL of the AAD application in the Azure portal + # Future? $clientPortalUrl = "https://portal.azure.com/#@"+$tenantName+"/blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/Overview/appId/"+$clientAadApplication.AppId+"/objectId/"+$clientAadApplication.ObjectId+"/isMSAApp/" + $clientPortalUrl = "https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/CallAnAPI/appId/"+$clientAadApplication.AppId+"/objectId/"+$clientAadApplication.ObjectId+"/isMSAApp/" + Add-Content -Value "" -Path createdApps.html + + $requiredResourcesAccess = New-Object System.Collections.Generic.List[Microsoft.Open.AzureAD.Model.RequiredResourceAccess] + + # Add Required Resources Access (from 'client' to 'Microsoft Graph') + Write-Host "Getting access from 'client' to 'Microsoft Graph'" + $requiredPermissions = GetRequiredPermissions -applicationDisplayName "Microsoft Graph" ` + -requiredDelegatedPermissions "User.Read" ` + + $requiredResourcesAccess.Add($requiredPermissions) + + + Set-AzureADApplication -ObjectId $clientAadApplication.ObjectId -RequiredResourceAccess $requiredResourcesAccess + Write-Host "Granted permissions." + + # Update config file for 'client' + $configFile = $pwd.Path + "\..\Console-Interactive-CustomWebUI\appsettings.json" + Write-Host "Updating the sample code ($configFile)" + $dictionary = @{ "ClientId" = $clientAadApplication.AppId;"TenantId" = $tenantId }; + UpdateTextFile -configFilePath $configFile -dictionary $dictionary + + Add-Content -Value "
ApplicationAppIdUrl in the Azure portal
client$currentAppIdConsole-Interactive-MultiTarget-v2
" -Path createdApps.html +} + +# Pre-requisites +if ((Get-Module -ListAvailable -Name "AzureAD") -eq $null) { + Install-Module "AzureAD" -Scope CurrentUser +} + +Import-Module AzureAD + +# Run interactively (will ask you for the tenant ID) +ConfigureApplications -Credential $Credential -tenantId $TenantId \ No newline at end of file diff --git a/4-desktop-apps/console-html-browser/AppCreationScripts/sample.json b/4-desktop-apps/console-html-browser/AppCreationScripts/sample.json new file mode 100644 index 0000000..17c3e2b --- /dev/null +++ b/4-desktop-apps/console-html-browser/AppCreationScripts/sample.json @@ -0,0 +1,52 @@ +{ + "Sample": { + "Title": "Using the Microsoft identity platform to call Microsoft Graph API with custom web ui.", + "Level": 300, + "Client": ".NET Desktop (Console)", + "Service": "Microsoft Graph", + "RepositoryUrl": "ms-identity-dotnet-desktop-tutorial", + "Endpoint": "AAD v2.0" + }, + + /* + This section describes the Microsoft Entra applications to configure, and their dependencies + */ + "AADApps": [ + { + "Id": "client", + "Name": "Console-Interactive-MultiTarget-v2", + "Kind": "Desktop", + "ReplyUrls": "https://login.microsoftonline.com/common/oauth2/nativeclient, http://localhost", + "RequiredResourcesAccess": [ + { + "Resource": "Microsoft Graph", + "DelegatedPermissions": [ "User.Read" ] + } + ] + } + ], + + /* + This section describes how to update the code in configuration files from the apps coordinates, once the apps + are created in Microsoft Entra. + Each section describes a configuration file, for one of the apps, it's type (XML, JSon, plain text), its location + with respect to the root of the sample, and the mappping (which string in the config file is mapped to which value + */ + "CodeConfiguration": [ + { + "App": "client", + "SettingKind": "JSon", + "SettingFile": "\\..\\Console-Interactive-CustomWebUI\\appsettings.json", + "Mappings": [ + { + "key": "ClientId", + "value": ".AppId" + }, + { + "key": "TenantId", + "value": "$tenantId" + } + ] + } + ] +} diff --git a/4-desktop-apps/console-html-browser/Console-Interactive-CustomWebUI/Console-Interactive-CustomWebUI.csproj b/4-desktop-apps/console-html-browser/Console-Interactive-CustomWebUI/Console-Interactive-CustomWebUI.csproj new file mode 100644 index 0000000..57fc6d2 --- /dev/null +++ b/4-desktop-apps/console-html-browser/Console-Interactive-CustomWebUI/Console-Interactive-CustomWebUI.csproj @@ -0,0 +1,25 @@ + + + + Exe + netcoreapp3.1; net472 + Console_Interactive_CustomWebUI + + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/4-desktop-apps/console-html-browser/Console-Interactive-CustomWebUI/Program.cs b/4-desktop-apps/console-html-browser/Console-Interactive-CustomWebUI/Program.cs new file mode 100644 index 0000000..e605666 --- /dev/null +++ b/4-desktop-apps/console-html-browser/Console-Interactive-CustomWebUI/Program.cs @@ -0,0 +1,161 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Graph; +using Microsoft.Identity.Client; +using System; +using System.Linq; +using System.Net.Http.Headers; +using System.Threading.Tasks; + +namespace Console_Interactive_CustomWebUI +{ + class Program + { + private static PublicClientApplicationOptions appConfiguration = null; + private static IConfiguration configuration; + private static string graphURL; + + // The MSAL Public client app + private static IPublicClientApplication application; + + // Object with the custom HTML + private static SystemWebViewOptions _customWebView = GetCustomHTML(); + + static async Task Main(string[] args) + { + // Using appsettings.json as our configuration settings + var builder = new ConfigurationBuilder() + .SetBasePath(System.IO.Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json"); + + configuration = builder.Build(); + + appConfiguration = configuration + .Get(); + + // We intend to obtain a token for Graph for the following scopes (permissions) + string[] scopes = new[] { "user.read" }; + + graphURL = configuration.GetValue("GraphApiUrl"); + + // Sign-in user using MSAL and obtain an access token for MS Graph + GraphServiceClient graphClient = await SignInAndInitializeGraphServiceClient(appConfiguration, scopes); + + // Call the /me endpoint of MS Graph + await CallMSGraph(graphClient); + + Console.ReadKey(); + } + + /// + /// Sign in user using MSAL and obtain a token for MS Graph + /// + /// + private async static Task SignInAndInitializeGraphServiceClient(PublicClientApplicationOptions configuration, string[] scopes) + { + GraphServiceClient graphClient = new GraphServiceClient(graphURL, + new DelegateAuthenticationProvider(async (requestMessage) => + { + requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", await SignInUserAndGetTokenUsingMSAL(configuration, scopes)); + })); + + return await Task.FromResult(graphClient); + } + + /// + /// Signs in the user using the device code flow and obtains an Access token for MS Graph + /// + /// + /// + /// + private static async Task SignInUserAndGetTokenUsingMSAL(PublicClientApplicationOptions configuration, string[] scopes) + { + // build the AAd authority Url + string authority = string.Concat(configuration.Instance, configuration.TenantId); + + // Initialize the MSAL library by building a public client application + application = PublicClientApplicationBuilder.Create(configuration.ClientId) + .WithAuthority(authority) + .WithRedirectUri(configuration.RedirectUri) + .Build(); + + + AuthenticationResult result; + + try + { + var accounts = await application.GetAccountsAsync(); + + // Try to acquire an access token from the cache, if UI interaction is required, MsalUiRequiredException will be thrown. + result = await application.AcquireTokenSilent(scopes, accounts.FirstOrDefault()).ExecuteAsync(); + } + catch (MsalUiRequiredException) + { + // Acquiring an access token interactively using the custom html. + result = await application.AcquireTokenInteractive(scopes) + .WithSystemWebViewOptions(_customWebView) // Using the custom html + .ExecuteAsync(); + } + + return result.AccessToken; + } + + /// + /// Call MS Graph and print results + /// + /// + /// + private static async Task CallMSGraph(GraphServiceClient graphClient) + { + var me = await graphClient.Me.Request().GetAsync(); + + // Printing the results + Console.Write(Environment.NewLine); + Console.WriteLine("-------- Data from call to MS Graph --------"); + Console.Write(Environment.NewLine); + Console.WriteLine($"Id: {me.Id}"); + Console.WriteLine($"Display Name: {me.DisplayName}"); + Console.WriteLine($"Email: {me.Mail}"); + } + + /// + /// Returns a custom HTML for the authorization success or failure, and redirect url. + /// For more available options, please inspect the SystemWebViewOptions class. + /// + /// + private static SystemWebViewOptions GetCustomHTML() + { + return new SystemWebViewOptions + { + HtmlMessageSuccess = @" + Authentication Complete + +
+

Custom Web UI

+
+
+

Authentication complete

+
You can return to the application. Feel free to close this browser tab.
+
+ + + ", + + HtmlMessageError = @" + Authentication Failed + +
+

Custom Web UI

+
+
+

Authentication failed

+
Error details: error {0} error_description: {1}
+
+
You can return to the application. Feel free to close this browser tab.
+
+ + + " + }; + } + } +} diff --git a/4-desktop-apps/console-html-browser/Console-Interactive-CustomWebUI/appsettings.json b/4-desktop-apps/console-html-browser/Console-Interactive-CustomWebUI/appsettings.json new file mode 100644 index 0000000..070f49e --- /dev/null +++ b/4-desktop-apps/console-html-browser/Console-Interactive-CustomWebUI/appsettings.json @@ -0,0 +1,7 @@ +{ + "Instance": "https://login.microsoftonline.com/", + "ClientId": "[Enter the Client Id (Application ID obtained from the Azure portal), e.g. ba74781c2-53c2-442a-97c2-3d60re42f403]", + "TenantId": "[Enter 'common', or 'organizations' or the Tenant Id (Obtained from the Azure portal. Select 'Endpoints' from the 'App registrations' blade and use the GUID in any of the URLs), e.g. da41245a5-11b3-996c-00a8-4d99re19f292]", + "RedirectUri": "http://localhost", + "GraphApiUrl": "https://graph.microsoft.com/beta/" +} diff --git a/4-desktop-apps/console-html-browser/Console-Interactive.sln b/4-desktop-apps/console-html-browser/Console-Interactive.sln new file mode 100644 index 0000000..bf66cc9 --- /dev/null +++ b/4-desktop-apps/console-html-browser/Console-Interactive.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29709.97 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Console-Interactive-CustomWebUI", "Console-Interactive-CustomWebUI\Console-Interactive-CustomWebUI.csproj", "{509D5688-3BD1-4650-8A43-3CEF5B64198E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {509D5688-3BD1-4650-8A43-3CEF5B64198E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {509D5688-3BD1-4650-8A43-3CEF5B64198E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {509D5688-3BD1-4650-8A43-3CEF5B64198E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {509D5688-3BD1-4650-8A43-3CEF5B64198E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {42EE218E-9522-4DBA-9DFE-2EC0FB0CD3D5} + EndGlobalSection +EndGlobal diff --git a/4-desktop-apps/console-html-browser/README.md b/4-desktop-apps/console-html-browser/README.md new file mode 100644 index 0000000..133303f --- /dev/null +++ b/4-desktop-apps/console-html-browser/README.md @@ -0,0 +1,253 @@ +--- +services: active-directory +platforms: dotnet +author: TiagoBrenck +level: 200 +client: .NET Desktop (Console) +service: Microsoft Graph +endpoint: Microsoft identity platform +page_type: sample +languages: + - csharp +products: + - azure + - microsoft-entra-id + - dotnet + - office-ms-graph +description: "This sample demonstrates a .NET Desktop (Console) application calling Microsoft Graph using custom web UI HTML" +--- + +# Using the Microsoft identity platform to call Microsoft Graph API with custom web UI HTML. + +## About this sample + +### Overview + +This sample demonstrates how to use a custom web UI HTML on MSAL.NET. + +1. The .NET Desktop (Console) application uses the Microsoft Authentication Library (MSAL) to obtain a JWT access token from Microsoft Entra ID. +1. Once the authorization request is finished (successfully or not), the customized HTML will be shown and the browser will return the response to MSAL. + +### Scenario + +The console application: + +- gets an access token from Microsoft Entra ID interactively using a custom web UI HTML +- and then calls the Microsoft Graph `/me` endpoint to get the user information, which it then displays in the console. + +![Overview](./ReadmeFiles/topology.png) + +## How to run this sample + +To run this sample, you'll need: + +- [Visual Studio 2019](https://aka.ms/vsdownload) +- An Internet connection +- a Microsoft Entra tenant. For more information on how to get a Microsoft Entra tenant, see [How to get a Microsoft Entra tenant](https://azure.microsoft.com/documentation/articles/active-directory-howto-tenant/) +- A user account in your Microsoft Entra tenant. This sample will not work with a Microsoft account (formerly Windows Live account). Therefore, if you signed in to the [Microsoft Entra admin center](https://entra.microsoft.com) with a Microsoft account and have never created a user account in your directory before, you need to do that now. + +### Step 1: Clone or download this repository + +From your shell or command line: + +```Shell +git clone https://github.com/Azure-Samples/ms-identity-dotnet-desktop-tutorial.git +``` + +or download and extract the repository .zip file. + +> Given that the name of the sample is quiet long, and so are the names of the referenced NuGet packages, you might want to clone it in a folder close to the root of your hard drive, to avoid file size limitations on Windows. + +### Step 2: Register the sample application with your Microsoft Entra tenant + +There is one project in this sample. To register it, you can: + +- either follow the steps [Step 2: Register the sample with your Microsoft Entra tenant](#step-2-register-the-sample-with-your-azure-active-directory-tenant) and [Step 3: Configure the sample to use your Microsoft Entra tenant](#choose-the-azure-ad-tenant-where-you-want-to-create-your-applications) +- or use PowerShell scripts that: + - **automatically** creates the Microsoft Entra applications and related objects (passwords, permissions, dependencies) for you. Note that this works for Visual Studio only. + - modify the Visual Studio projects' configuration files. + +
+ Expand this section if you want to use this automation: + +1. On Windows, run PowerShell and navigate to the root of the cloned directory +1. In PowerShell run: + + ```PowerShell + Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force + ``` + +1. Run the script to create your Microsoft Entra application and configure the code of the sample application accordingly. +1. In PowerShell run: + + ```PowerShell + cd .\AppCreationScripts\ + .\Configure.ps1 + ``` + + > Other ways of running the scripts are described in [App Creation Scripts](./AppCreationScripts/AppCreationScripts.md) + > The scripts also provide a guide to automated application registration, configuration and removal which can help in your CI/CD scenarios. + +1. Open the Visual Studio solution and click start to run the code. + +
+ +Follow the steps below to manually walk through the steps to register and configure the applications. + +#### Choose the Microsoft Entra tenant where you want to create your applications + +As a first step you'll need to: + +1. Sign in to the [Microsoft Entra admin center](https://entra.microsoft.com) using either a work or school account or a personal Microsoft account. +1. If your account is present in more than one Microsoft Entra tenant, select your profile at the top right corner in the menu on top of the page, and then **switch directory**. + Change your portal session to the desired Microsoft Entra tenant. + +#### Register the client app (Console-Interactive-MultiTarget-v2) + +1. Navigate to the Microsoft identity platform for developers [App registrations](https://go.microsoft.com/fwlink/?linkid=2083908) page. +1. Click **New registration** on top. +1. In the **Register an application page** that appears, enter your application's registration information: + - In the **Name** section, enter a meaningful application name that will be displayed to users of the app, for example `Console-Interactive-MultiTarget-v2`. + - Change **Supported account types** to **Accounts in any organizational directory and personal Microsoft accounts (e.g. Skype, Xbox, Outlook.com)**. +1. Click on the **Register** button in bottom to create the application. +1. In the app's registration screen, find the **Application (client) ID** value and record it for use later. You'll need it to configure the configuration file(s) later in your code. +1. In the app's registration screen, click on the **Authentication** blade in the left. + - If you don't have a platform added yet, click on **Add a platform** and select the **Public client (mobile & desktop)** option. + - In the **Redirect URIs** section, enter the following redirect URIs. + - `http://localhost` + - In the **Redirect URIs** | **Suggested Redirect URIs for public clients (mobile, desktop)** section, select **https://login.microsoftonline.com/common/oauth2/nativeclient** + +1. Click the **Save** button on top to save the changes. +1. In the app's registration screen, click on the **API permissions** blade in the left to open the page where we add access to the Apis that your application needs. + - Click the **Add a permission** button and then, + - Ensure that the **Microsoft APIs** tab is selected. + - In the *Commonly used Microsoft APIs* section, click on **Microsoft Graph** + - In the **Delegated permissions** section, select the **User.Read** in the list. Use the search box if necessary. + - Click on the **Add permissions** button at the bottom. + +##### Configure the client app (Console-Interactive-MultiTarget-v2) to use your app registration + +Open the project in your IDE (like Visual Studio) to configure the code. +>In the steps below, "ClientID" is the same as "Application ID" or "AppId". + +1. Open the `Console-Interactive-MultiTarget\appsettings.json` file +1. Find the app key `ClientId` and replace the existing value with the application ID (clientId) of the `Console-Interactive-MultiTarget-v2` application copied from the Microsoft Entra admin center. +1. Find the app key `TenantId` and replace the existing value with your Microsoft Entra tenant ID. + +### Step 4: Run the sample + +Clean the solution, rebuild the solution, and run it. + +Start the application, sign-in and check the result in the console. + +> [Consider taking a moment to share your experience with us.](https://forms.office.com/Pages/ResponsePage.aspx?id=v4j5cvGGr0GRqy180BHbR73pcsbpbxNJuZCMKN0lURpUREhEVDBOTFBMUVRPUElBUE5WMjdPQ1RaMiQlQCN0PWcu) + +## About the code + +MSAL has the class [SystemWebViewOptions.cs](https://learn.microsoft.com/dotnet/api/microsoft.identity.client.systemwebviewoptions?view=azure-dotnet) which allows you to set properties to customize the UI after the authentication request. + +In this sample, we customized the HTML for successful and failure authorization requests, but more options are available. Please refer to the [SystemWebViewOptions.cs documentation](https://learn.microsoft.com/dotnet/api/microsoft.identity.client.systemwebviewoptions?view=azure-dotnet) for more options. + +1- Customizing the success or failure HTML message: + +```csharp +private static SystemWebViewOptions GetCustomHTML() +{ + return new SystemWebViewOptions + { + HtmlMessageSuccess = @" + Authentication Complete + +
+

Custom Web UI

+
+
+

Authentication complete

+
You can return to the application. Feel free to close this browser tab.
+
+ + + ", + + HtmlMessageError = @" + Authentication Failed + +
+

Custom Web UI

+
+
+

Authentication failed

+
Error details: error {0} error_description: {1}
+
+
You can return to the application. Feel free to close this browser tab.
+
+ + + " + }; +} +``` + +2- Using `.WithSystemWebViewOptions()` on the `AcquireTokenInteractive` request, passing the customized SystemWebViewOptions object so MSAL can use it to display the response. + +```csharp +var app = PublicClientApplicationBuilder.Create(appConfiguration.ClientId) + .WithAuthority(_authority) + .WithRedirectUri(appConfiguration.RedirectUri) + .Build(); + +string[] scopes = new[] { "user.read" }; +AuthenticationResult result; + +try +{ + var accounts = await app.GetAccountsAsync(); + result = await app.AcquireTokenSilent(scopes, accounts.FirstOrDefault()) + .ExecuteAsync(); +} +catch (MsalUiRequiredException) +{ + result = await app.AcquireTokenInteractive(scopes) + .WithSystemWebViewOptions(_customWebView) // Using the custom html + .ExecuteAsync(); +} +``` + +3- Once the authorization response gets back, the opened tab will display the custom successful or failure message. + +Success custom message: +![Overview](./ReadmeFiles/successMessage.png) + +Failure custom message: +![Overview](./ReadmeFiles/failureMessage.png) + +## Community Help and Support + +Use [Stack Overflow](http://stackoverflow.com/questions/tagged/msal) to get support from the community. +Ask your questions on Stack Overflow first and browse existing issues to see if someone has asked your question before. +Make sure that your questions or comments are tagged with [`azure-active-directory` `msal` `dotnet`]. + +If you find a bug in the sample, please raise the issue on [GitHub Issues](../../../../issues). + +To provide a recommendation, visit the following [User Voice page](https://feedback.azure.com/forums/169401-azure-active-directory). + +## Contributing + +If you'd like to contribute to this sample, see [CONTRIBUTING.MD](/CONTRIBUTING.md). + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information, see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +## More information + +For more information, see MSAL.NET's conceptual documentation: + +- [MSAL.NET's conceptual documentation](https://aka.ms/msal-net) +- [Microsoft identity platform (Microsoft Entra ID for developers)](https://learn.microsoft.com/entra/identity-platform/) +- [Quickstart: Register an application with the Microsoft identity platform](https://learn.microsoft.com/entra/identity-platform/quickstart-register-app) +- [Quickstart: Configure a client application to access web APIs](https://learn.microsoft.com/entra/identity-platform/quickstart-configure-app-access-web-apis) + +- [Understanding Microsoft Entra application consent experiences](https://learn.microsoft.com/entra/identity-platform/application-consent-experience) +- [Understand user and admin consent](https://learn.microsoft.com/entra/identity-platform/howto-convert-app-to-be-multi-tenant#understand-user-and-admin-consent) +- [Application and service principal objects in Microsoft Entra ID](https://learn.microsoft.com/entra/identity-platform/app-objects-and-service-principals) + +For more information about how OAuth 2.0 protocols work in this scenario and other scenarios, see [Authentication Scenarios for Microsoft Entra ID](http://go.microsoft.com/fwlink/?LinkId=394414). diff --git a/4-desktop-apps/console-html-browser/ReadmeFiles/failureMessage.png b/4-desktop-apps/console-html-browser/ReadmeFiles/failureMessage.png new file mode 100644 index 0000000..8d682a4 Binary files /dev/null and b/4-desktop-apps/console-html-browser/ReadmeFiles/failureMessage.png differ diff --git a/4-desktop-apps/console-html-browser/ReadmeFiles/successMessage.png b/4-desktop-apps/console-html-browser/ReadmeFiles/successMessage.png new file mode 100644 index 0000000..96fc552 Binary files /dev/null and b/4-desktop-apps/console-html-browser/ReadmeFiles/successMessage.png differ diff --git a/4-desktop-apps/console-html-browser/ReadmeFiles/topology.png b/4-desktop-apps/console-html-browser/ReadmeFiles/topology.png new file mode 100644 index 0000000..185b93d Binary files /dev/null and b/4-desktop-apps/console-html-browser/ReadmeFiles/topology.png differ diff --git a/4-desktop-apps/console-interactive-multitarget-graph/AppCreationScripts/AppCreationScripts.md b/4-desktop-apps/console-interactive-multitarget-graph/AppCreationScripts/AppCreationScripts.md new file mode 100644 index 0000000..336e31c --- /dev/null +++ b/4-desktop-apps/console-interactive-multitarget-graph/AppCreationScripts/AppCreationScripts.md @@ -0,0 +1,166 @@ +# Registering the sample apps with Microsoft identity platform and updating the configuration files using PowerShell scripts + +## Overview + +### Quick summary + +1. On Windows run PowerShell and navigate to the root of the cloned directory +1. In PowerShell run: + ```PowerShell + Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force + ``` +1. Run the script to create your Microsoft Entra application and configure the code of the sample application accordingly. (Other ways of running the scripts are described below) + ```PowerShell + cd .\AppCreationScripts\ + .\Configure.ps1 + ``` +1. Open the Visual Studio solution and click start + +### More details + +The following paragraphs: + +- [Present the scripts](#presentation-of-the-scripts) and explain their [usage patterns](#usage-pattern-for-tests-and-devops-scenarios) for test and DevOps scenarios. +- Explain the [pre-requisites](#pre-requisites) +- Explain [four ways of running the scripts](#four-ways-to-run-the-script): + - [Interactively](#option-1-interactive) to create the app in your home tenant + - [Passing credentials](#option-2-non-interactive) to create the app in your home tenant + - [Interactively in a specific tenant](#option-3-interactive-but-create-apps-in-a-specified-tenant) + - [Passing credentials in a specific tenant](#option-4-non-interactive-and-create-apps-in-a-specified-tenant) + - [Passing environment name, for Sovereign clouds](#running-the-script-on-azure-sovereign-clouds) + +## Goal of the scripts + +### Presentation of the scripts + +This sample comes with two PowerShell scripts, which automate the creation of the Microsoft Entra applications, and the configuration of the code for this sample. Once you run them, you will only need to build the solution and you are good to test. + +These scripts are: + +- `Configure.ps1` which: + - creates Microsoft Entra applications and their related objects (permissions, dependencies, secrets), + - changes the configuration files in the C# and JavaScript projects. + - creates a summary file named `createdApps.html` in the folder from which you ran the script, and containing, for each Microsoft Entra application it created: + - the identifier of the application + - the AppId of the application + - the url of its registration in the [Microsoft Entra admin center](https://entra.microsoft.com). + +- `Cleanup.ps1` which cleans-up the Microsoft Entra objects created by `Configure.ps1`. Note that this script does not revert the changes done in the configuration files, though. You will need to undo the change from source control (from Visual Studio, or from the command line using, for instance, git reset). + +### Usage pattern for tests and DevOps scenarios + +The `Configure.ps1` will stop if it tries to create a Microsoft Entra application which already exists in the tenant. For this, if you are using the script to try/test the sample, or in DevOps scenarios, you might want to run `Cleanup.ps1` just before `Configure.ps1`. This is what is shown in the steps below. + +## How to use the app creation scripts? + +### Pre-requisites + +1. Open PowerShell (On Windows, press `Windows-R` and type `PowerShell` in the search window) +2. Navigate to the root directory of the project. +3. Until you change it, the default [Execution Policy](https:/go.microsoft.com/fwlink/?LinkID=135170) for scripts is usually `Restricted`. In order to run the PowerShell script you need to set the Execution Policy to `RemoteSigned`. You can set this just for the current PowerShell process by running the command: + ```PowerShell + Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process + ``` +### (Optionally) install AzureAD PowerShell modules +The scripts install the required PowerShell module (AzureAD) for the current user if needed. However, if you want to install if for all users on the machine, you can follow the following steps: + +4. If you have never done it already, in the PowerShell window, install the AzureAD PowerShell modules. For this: + + 1. Open PowerShell as admin (On Windows, Search Powershell in the search bar, right click on it and select Run as administrator). + 2. Type: + ```PowerShell + Install-Module AzureAD + ``` + + or if you cannot be administrator on your machine, run: + ```PowerShell + Install-Module AzureAD -Scope CurrentUser + ``` + +### Run the script and start running + +5. Go to the `AppCreationScripts` sub-folder. From the folder where you cloned the repo, + ```PowerShell + cd AppCreationScripts + ``` +6. Run the scripts. See below for the [four options](#four-ways-to-run-the-script) to do that. +7. Open the Visual Studio solution, and in the solution's context menu, choose **Set Startup Projects**. +8. select **Start** for the projects + +You're done. this just works! + +### Four ways to run the script + +We advise four ways of running the script: + +- Interactive: you will be prompted for credentials, and the scripts decide in which tenant to create the objects, +- non-interactive: you will provide credentials, and the scripts decide in which tenant to create the objects, +- Interactive in specific tenant: you will provide the tenant in which you want to create the objects and then you will be prompted for credentials, and the scripts will create the objects, +- non-interactive in specific tenant: you will provide tenant in which you want to create the objects and credentials, and the scripts will create the objects. + +Here are the details on how to do this. + +#### Option 1 (interactive) + +- Just run ``. .\Configure.ps1``, and you will be prompted to sign-in (email address, password, and if needed MFA). +- The script will be run as the signed-in user and will use the tenant in which the user is defined. + +Note that the script will choose the tenant in which to create the applications, based on the user. Also to run the `Cleanup.ps1` script, you will need to re-sign-in. + +#### Option 2 (non-interactive) + +When you know the identity and credentials of the user in the name of whom you want to create the applications, you can use the non-interactive approach. It's more adapted to DevOps. Here is an example of script you'd want to run in a PowerShell Window + +```PowerShell +$secpasswd = ConvertTo-SecureString "[Password here]" -AsPlainText -Force +$mycreds = New-Object System.Management.Automation.PSCredential ("[login@tenantName here]", $secpasswd) +. .\Cleanup.ps1 -Credential $mycreds +. .\Configure.ps1 -Credential $mycreds +``` + +Of course, in real life, you might already get the password as a `SecureString`. You might also want to get the password from KeyVault. + +#### Option 3 (Interactive, but create apps in a specified tenant) + + if you want to create the apps in a particular tenant, you can use the following option: +- open the [Microsoft Entra admin center](https://entra.microsoft.com) +- Select the Microsoft Entra tenant you are interested in (in the combo-box below your name on the top right of the browser window) +- Find the "Active Directory" object in this tenant +- Go to **Properties** and copy the content of the **Directory Id** property +- Then use the full syntax to run the scripts: + +```PowerShell +$tenantId = "yourTenantIdGuid" +. .\Cleanup.ps1 -TenantId $tenantId +. .\Configure.ps1 -TenantId $tenantId +``` + +#### Option 4 (non-interactive, and create apps in a specified tenant) + +This option combines option 2 and option 3: it creates the application in a specific tenant. See option 3 for the way to get the tenant Id. Then run: + +```PowerShell +$secpasswd = ConvertTo-SecureString "[Password here]" -AsPlainText -Force +$mycreds = New-Object System.Management.Automation.PSCredential ("[login@tenantName here]", $secpasswd) +$tenantId = "yourTenantIdGuid" +. .\Cleanup.ps1 -Credential $mycreds -TenantId $tenantId +. .\Configure.ps1 -Credential $mycreds -TenantId $tenantId +``` + +### Running the script on Azure Sovereign clouds + +All the four options listed above, can be used on any Azure Sovereign clouds. By default, the script targets `AzureCloud`, but it can be changed using the parameter `-AzureEnvironmentName`. + +The acceptable values for this parameter are: + +- AzureCloud +- AzureChinaCloud +- AzureUSGovernment +- AzureGermanyCloud + +Example: + + ```PowerShell + . .\Cleanup.ps1 -AzureEnvironmentName "AzureGermanyCloud" + . .\Configure.ps1 -AzureEnvironmentName "AzureGermanyCloud" + ``` diff --git a/4-desktop-apps/console-interactive-multitarget-graph/AppCreationScripts/Cleanup.ps1 b/4-desktop-apps/console-interactive-multitarget-graph/AppCreationScripts/Cleanup.ps1 new file mode 100644 index 0000000..cca3c75 --- /dev/null +++ b/4-desktop-apps/console-interactive-multitarget-graph/AppCreationScripts/Cleanup.ps1 @@ -0,0 +1,77 @@ +[CmdletBinding()] +param( + [PSCredential] $Credential, + [Parameter(Mandatory=$False, HelpMessage='Tenant ID (This is a GUID which represents the "Directory ID" of the AzureAD tenant into which you want to create the apps')] + [string] $tenantId, + [Parameter(Mandatory=$False, HelpMessage='Azure environment to use while running the script (it defaults to AzureCloud)')] + [string] $azureEnvironmentName +) + +if ($null -eq (Get-Module -ListAvailable -Name "AzureAD")) { + Install-Module "AzureAD" -Scope CurrentUser +} +Import-Module AzureAD +$ErrorActionPreference = "Stop" + +Function Cleanup +{ + if (!$azureEnvironmentName) + { + $azureEnvironmentName = "AzureCloud" + } + + <# + .Description + This function removes the Microsoft Entra applications for the sample. These applications were created by the Configure.ps1 script + #> + + # $tenantId is the Active Directory Tenant. This is a GUID which represents the "Directory ID" of the AzureAD tenant + # into which you want to create the apps. Look it up in the Azure portal in the "Properties" of Microsoft Entra. + + # Login to Azure PowerShell (interactive if credentials are not already provided: + # you'll need to sign-in with creds enabling your to create apps in the tenant) + if (!$Credential -and $TenantId) + { + $creds = Connect-AzureAD -TenantId $tenantId -AzureEnvironmentName $azureEnvironmentName + } + else + { + if (!$TenantId) + { + $creds = Connect-AzureAD -Credential $Credential -AzureEnvironmentName $azureEnvironmentName + } + else + { + $creds = Connect-AzureAD -TenantId $tenantId -Credential $Credential -AzureEnvironmentName $azureEnvironmentName + } + } + + if (!$tenantId) + { + $tenantId = $creds.Tenant.Id + } + $tenant = Get-AzureADTenantDetail + $tenantName = ($tenant.VerifiedDomains | Where-Object { $_._Default -eq $True }).Name + + # Removes the applications + Write-Host "Cleaning-up applications from tenant '$tenantName'" + + Write-Host "Removing 'client' (Console-Interactive-MultiTarget-v2) if needed" + Get-AzureADApplication -Filter "DisplayName eq 'Console-Interactive-MultiTarget-v2'" | ForEach-Object {Remove-AzureADApplication -ObjectId $_.ObjectId } + $apps = Get-AzureADApplication -Filter "DisplayName eq 'Console-Interactive-MultiTarget-v2'" + if ($apps) + { + Remove-AzureADApplication -ObjectId $apps.ObjectId + } + + foreach ($app in $apps) + { + Remove-AzureADApplication -ObjectId $app.ObjectId + Write-Host "Removed Console-Interactive-MultiTarget-v2.." + } + # also remove service principals of this app + Get-AzureADServicePrincipal -filter "DisplayName eq 'Console-Interactive-MultiTarget-v2'" | ForEach-Object {Remove-AzureADServicePrincipal -ObjectId $_.Id -Confirm:$false} + +} + +Cleanup -Credential $Credential -tenantId $TenantId \ No newline at end of file diff --git a/4-desktop-apps/console-interactive-multitarget-graph/AppCreationScripts/Configure.ps1 b/4-desktop-apps/console-interactive-multitarget-graph/AppCreationScripts/Configure.ps1 new file mode 100644 index 0000000..7367ac5 --- /dev/null +++ b/4-desktop-apps/console-interactive-multitarget-graph/AppCreationScripts/Configure.ps1 @@ -0,0 +1,224 @@ +[CmdletBinding()] +param( + [PSCredential] $Credential, + [Parameter(Mandatory=$False, HelpMessage='Tenant ID (This is a GUID which represents the "Directory ID" of the AzureAD tenant into which you want to create the apps')] + [string] $tenantId, + [Parameter(Mandatory=$False, HelpMessage='Azure environment to use while running the script (it defaults to AzureCloud)')] + [string] $azureEnvironmentName +) + +<# + This script creates the Microsoft Entra applications needed for this sample and updates the configuration files + for the visual Studio projects from the data in the Microsoft Entra applications. + + Before running this script you need to install the AzureAD cmdlets as an administrator. + For this: + 1) Run Powershell as an administrator + 2) in the PowerShell window, type: Install-Module AzureAD + + There are four ways to run this script. For more information, read the AppCreationScripts.md file in the same folder as this script. +#> + +# Adds the requiredAccesses (expressed as a pipe separated string) to the requiredAccess structure +# The exposed permissions are in the $exposedPermissions collection, and the type of permission (Scope | Role) is +# described in $permissionType +Function AddResourcePermission($requiredAccess, ` + $exposedPermissions, [string]$requiredAccesses, [string]$permissionType) +{ + foreach($permission in $requiredAccesses.Trim().Split("|")) + { + foreach($exposedPermission in $exposedPermissions) + { + if ($exposedPermission.Value -eq $permission) + { + $resourceAccess = New-Object Microsoft.Open.AzureAD.Model.ResourceAccess + $resourceAccess.Type = $permissionType # Scope = Delegated permissions | Role = Application permissions + $resourceAccess.Id = $exposedPermission.Id # Read directory data + $requiredAccess.ResourceAccess.Add($resourceAccess) + } + } + } +} + +# +# Example: GetRequiredPermissions "Microsoft Graph" "Graph.Read|User.Read" +# See also: http://stackoverflow.com/questions/42164581/how-to-configure-a-new-azure-ad-application-through-powershell +Function GetRequiredPermissions([string] $applicationDisplayName, [string] $requiredDelegatedPermissions, [string]$requiredApplicationPermissions, $servicePrincipal) +{ + # If we are passed the service principal we use it directly, otherwise we find it from the display name (which might not be unique) + if ($servicePrincipal) + { + $sp = $servicePrincipal + } + else + { + $sp = Get-AzureADServicePrincipal -Filter "DisplayName eq '$applicationDisplayName'" + } + $appid = $sp.AppId + $requiredAccess = New-Object Microsoft.Open.AzureAD.Model.RequiredResourceAccess + $requiredAccess.ResourceAppId = $appid + $requiredAccess.ResourceAccess = New-Object System.Collections.Generic.List[Microsoft.Open.AzureAD.Model.ResourceAccess] + + # $sp.Oauth2Permissions | Select Id,AdminConsentDisplayName,Value: To see the list of all the Delegated permissions for the application: + if ($requiredDelegatedPermissions) + { + AddResourcePermission $requiredAccess -exposedPermissions $sp.Oauth2Permissions -requiredAccesses $requiredDelegatedPermissions -permissionType "Scope" + } + + # $sp.AppRoles | Select Id,AdminConsentDisplayName,Value: To see the list of all the Application permissions for the application + if ($requiredApplicationPermissions) + { + AddResourcePermission $requiredAccess -exposedPermissions $sp.AppRoles -requiredAccesses $requiredApplicationPermissions -permissionType "Role" + } + return $requiredAccess +} + + +Function UpdateLine([string] $line, [string] $value) +{ + $index = $line.IndexOf('=') + $delimiter = ';' + if ($index -eq -1) + { + $index = $line.IndexOf(':') + $delimiter = ',' + } + if ($index -ige 0) + { + $line = $line.Substring(0, $index+1) + " "+'"'+$value+'"'+$delimiter + } + return $line +} + +Function UpdateTextFile([string] $configFilePath, [System.Collections.HashTable] $dictionary) +{ + $lines = Get-Content $configFilePath + $index = 0 + while($index -lt $lines.Length) + { + $line = $lines[$index] + foreach($key in $dictionary.Keys) + { + if ($line.Contains($key)) + { + $lines[$index] = UpdateLine $line $dictionary[$key] + } + } + $index++ + } + + Set-Content -Path $configFilePath -Value $lines -Force +} + +Set-Content -Value "" -Path createdApps.html +Add-Content -Value "" -Path createdApps.html + +$ErrorActionPreference = "Stop" + +Function ConfigureApplications +{ +<#.Description + This function creates the Microsoft Entra applications for the sample in the provided Microsoft Entra tenant and updates the + configuration files in the client and service project of the visual studio solution (App.Config and Web.Config) + so that they are consistent with the Applications parameters +#> + $commonendpoint = "common" + + if (!$azureEnvironmentName) + { + $azureEnvironmentName = "AzureCloud" + } + + # $tenantId is the Active Directory Tenant. This is a GUID which represents the "Directory ID" of the AzureAD tenant + # into which you want to create the apps. Look it up in the Azure portal in the "Properties" of Microsoft Entra. + + # Login to Azure PowerShell (interactive if credentials are not already provided: + # you'll need to sign-in with creds enabling your to create apps in the tenant) + if (!$Credential -and $TenantId) + { + $creds = Connect-AzureAD -TenantId $tenantId -AzureEnvironmentName $azureEnvironmentName + } + else + { + if (!$TenantId) + { + $creds = Connect-AzureAD -Credential $Credential -AzureEnvironmentName $azureEnvironmentName + } + else + { + $creds = Connect-AzureAD -TenantId $tenantId -Credential $Credential -AzureEnvironmentName $azureEnvironmentName + } + } + + if (!$tenantId) + { + $tenantId = $creds.Tenant.Id + } + + + + $tenant = Get-AzureADTenantDetail + $tenantName = ($tenant.VerifiedDomains | Where { $_._Default -eq $True }).Name + + # Get the user running the script to add the user as the app owner + $user = Get-AzureADUser -ObjectId $creds.Account.Id + + # Create the client AAD application + Write-Host "Creating the AAD application (Console-Interactive-MultiTarget-v2)" + # create the application + $clientAadApplication = New-AzureADApplication -DisplayName "Console-Interactive-MultiTarget-v2" ` + -ReplyUrls "https://login.microsoftonline.com/common/oauth2/nativeclient", "http://localhost" ` + -AvailableToOtherTenants $True ` + -PublicClient $True + + # create the service principal of the newly created application + $currentAppId = $clientAadApplication.AppId + $clientServicePrincipal = New-AzureADServicePrincipal -AppId $currentAppId -Tags {WindowsAzureActiveDirectoryIntegratedApp} + + # add the user running the script as an app owner if needed + $owner = Get-AzureADApplicationOwner -ObjectId $clientAadApplication.ObjectId + if ($owner -eq $null) + { + Add-AzureADApplicationOwner -ObjectId $clientAadApplication.ObjectId -RefObjectId $user.ObjectId + Write-Host "'$($user.UserPrincipalName)' added as an application owner to app '$($clientServicePrincipal.DisplayName)'" + } + + + Write-Host "Done creating the client application (Console-Interactive-MultiTarget-v2)" + + # URL of the AAD application in the Azure portal + # Future? $clientPortalUrl = "https://portal.azure.com/#@"+$tenantName+"/blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/Overview/appId/"+$clientAadApplication.AppId+"/objectId/"+$clientAadApplication.ObjectId+"/isMSAApp/" + $clientPortalUrl = "https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/CallAnAPI/appId/"+$clientAadApplication.AppId+"/objectId/"+$clientAadApplication.ObjectId+"/isMSAApp/" + Add-Content -Value "" -Path createdApps.html + + $requiredResourcesAccess = New-Object System.Collections.Generic.List[Microsoft.Open.AzureAD.Model.RequiredResourceAccess] + + # Add Required Resources Access (from 'client' to 'Microsoft Graph') + Write-Host "Getting access from 'client' to 'Microsoft Graph'" + $requiredPermissions = GetRequiredPermissions -applicationDisplayName "Microsoft Graph" ` + -requiredDelegatedPermissions "User.Read" ` + + $requiredResourcesAccess.Add($requiredPermissions) + + + Set-AzureADApplication -ObjectId $clientAadApplication.ObjectId -RequiredResourceAccess $requiredResourcesAccess + Write-Host "Granted permissions." + + # Update config file for 'client' + $configFile = $pwd.Path + "\..\Console-TokenCache\appsettings.json" + Write-Host "Updating the sample code ($configFile)" + $dictionary = @{ "ClientId" = $clientAadApplication.AppId;"TenantId" = $tenantId }; + UpdateTextFile -configFilePath $configFile -dictionary $dictionary + + Add-Content -Value "
ApplicationAppIdUrl in the Azure portal
client$currentAppIdConsole-Interactive-MultiTarget-v2
" -Path createdApps.html +} + +# Pre-requisites +if ((Get-Module -ListAvailable -Name "AzureAD") -eq $null) { + Install-Module "AzureAD" -Scope CurrentUser +} + +Import-Module AzureAD + +# Run interactively (will ask you for the tenant ID) +ConfigureApplications -Credential $Credential -tenantId $TenantId \ No newline at end of file diff --git a/4-desktop-apps/console-interactive-multitarget-graph/AppCreationScripts/sample.json b/4-desktop-apps/console-interactive-multitarget-graph/AppCreationScripts/sample.json new file mode 100644 index 0000000..216c7d1 --- /dev/null +++ b/4-desktop-apps/console-interactive-multitarget-graph/AppCreationScripts/sample.json @@ -0,0 +1,52 @@ +{ + "Sample": { + "Title": "Using the Microsoft identity platform to call Microsoft Graph API from a multi-target console application, with token cache", + "Level": 200, + "Client": ".NET Desktop (Console)", + "Service": "Microsoft Graph", + "RepositoryUrl": "ms-identity-dotnet-desktop-tutorial", + "Endpoint": "AAD v2.0" + }, + + /* + This section describes the Microsoft Entra applications to configure, and their dependencies + */ + "AADApps": [ + { + "Id": "client", + "Name": "Console-Interactive-MultiTarget-v2", + "Kind": "Desktop", + "ReplyUrls": "https://login.microsoftonline.com/common/oauth2/nativeclient, http://localhost", + "RequiredResourcesAccess": [ + { + "Resource": "Microsoft Graph", + "DelegatedPermissions": [ "User.Read" ] + } + ] + } + ], + + /* + This section describes how to update the code in configuration files from the apps coordinates, once the apps + are created in Microsoft Entra. + Each section describes a configuration file, for one of the apps, it's type (XML, JSon, plain text), its location + with respect to the root of the sample, and the mappping (which string in the config file is mapped to which value + */ + "CodeConfiguration": [ + { + "App": "client", + "SettingKind": "JSon", + "SettingFile": "\\..\\Console-TokenCache\\appsettings.json", + "Mappings": [ + { + "key": "ClientId", + "value": ".AppId" + }, + { + "key": "TenantId", + "value": "$tenantId" + } + ] + } + ] +} diff --git a/4-desktop-apps/console-interactive-multitarget-graph/Console-TokenCache.sln b/4-desktop-apps/console-interactive-multitarget-graph/Console-TokenCache.sln new file mode 100644 index 0000000..66a5d9a --- /dev/null +++ b/4-desktop-apps/console-interactive-multitarget-graph/Console-TokenCache.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29424.173 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Console-TokenCache", "Console-TokenCache\Console-TokenCache.csproj", "{9286F81F-F57A-4B43-99AD-A7789771EC4C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9286F81F-F57A-4B43-99AD-A7789771EC4C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9286F81F-F57A-4B43-99AD-A7789771EC4C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9286F81F-F57A-4B43-99AD-A7789771EC4C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9286F81F-F57A-4B43-99AD-A7789771EC4C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {305EF8E8-BB35-44FD-A26D-546A38F197EC} + EndGlobalSection +EndGlobal diff --git a/4-desktop-apps/console-interactive-multitarget-graph/Console-TokenCache/CacheSettings.cs b/4-desktop-apps/console-interactive-multitarget-graph/Console-TokenCache/CacheSettings.cs new file mode 100644 index 0000000..62ad4e6 --- /dev/null +++ b/4-desktop-apps/console-interactive-multitarget-graph/Console-TokenCache/CacheSettings.cs @@ -0,0 +1,26 @@ +using Microsoft.Identity.Client.Extensions.Msal; +using System.Collections.Generic; +using System.IO; + +namespace Console_TokenCache +{ + public static class CacheSettings + { + // computing the root directory is not very simple on Linux and Mac, so a helper is provided + private static readonly string s_cacheFilePath = + Path.Combine(MsalCacheHelper.UserRootDirectory, "msal.contoso.cache"); + + public static readonly string CacheFileName = Path.GetFileName(s_cacheFilePath); + public static readonly string CacheDir = Path.GetDirectoryName(s_cacheFilePath); + + + public static readonly string KeyChainServiceName = "Contoso.MyProduct"; + public static readonly string KeyChainAccountName = "MSALCache"; + + public static readonly string LinuxKeyRingSchema = "com.contoso.devtools.tokencache"; + public static readonly string LinuxKeyRingCollection = MsalCacheHelper.LinuxKeyRingDefaultCollection; + public static readonly string LinuxKeyRingLabel = "MSAL token cache for all Contoso dev tool apps."; + public static readonly KeyValuePair LinuxKeyRingAttr1 = new KeyValuePair("Version", "1"); + public static readonly KeyValuePair LinuxKeyRingAttr2 = new KeyValuePair("ProductGroup", "MyApps"); + } +} diff --git a/4-desktop-apps/console-interactive-multitarget-graph/Console-TokenCache/Console-TokenCache.csproj b/4-desktop-apps/console-interactive-multitarget-graph/Console-TokenCache/Console-TokenCache.csproj new file mode 100644 index 0000000..7769a09 --- /dev/null +++ b/4-desktop-apps/console-interactive-multitarget-graph/Console-TokenCache/Console-TokenCache.csproj @@ -0,0 +1,24 @@ + + + + Exe + netcoreapp3.1; net472 + Console_TokenCache + + + + + + + + + + + + + + + PreserveNewest + + + diff --git a/4-desktop-apps/console-interactive-multitarget-graph/Console-TokenCache/Program.cs b/4-desktop-apps/console-interactive-multitarget-graph/Console-TokenCache/Program.cs new file mode 100644 index 0000000..afc098f --- /dev/null +++ b/4-desktop-apps/console-interactive-multitarget-graph/Console-TokenCache/Program.cs @@ -0,0 +1,209 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Graph; +using Microsoft.Identity.Client; +using Microsoft.Identity.Client.Extensions.Msal; +using System; +using System.Linq; +using System.Net.Http.Headers; +using System.Threading.Tasks; + +namespace Console_TokenCache +{ + class Program + { + private static PublicClientApplicationOptions appConfiguration = null; + private static IConfiguration configuration; + private static string _authority; + + static async Task Main(string[] _) + { + // Using appsettings.json as our configuration settings + var builder = new ConfigurationBuilder() + .SetBasePath(System.IO.Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json"); + + configuration = builder.Build(); + + // Loading PublicClientApplicationOptions from the values set on appsettings.json + appConfiguration = configuration + .Get(); + + // Building the AAD authority, https://login.microsoftonline.com/ + _authority = string.Concat(appConfiguration.Instance, appConfiguration.TenantId); + + // Building a public client application + var app = PublicClientApplicationBuilder.Create(appConfiguration.ClientId) + .WithAuthority(_authority) + .WithRedirectUri(appConfiguration.RedirectUri) + .Build(); + + // Building StorageCreationProperties + var storageProperties = + new StorageCreationPropertiesBuilder(CacheSettings.CacheFileName, CacheSettings.CacheDir, appConfiguration.ClientId) + .WithLinuxKeyring( + CacheSettings.LinuxKeyRingSchema, + CacheSettings.LinuxKeyRingCollection, + CacheSettings.LinuxKeyRingLabel, + CacheSettings.LinuxKeyRingAttr1, + CacheSettings.LinuxKeyRingAttr2) + .WithMacKeyChain( + CacheSettings.KeyChainServiceName, + CacheSettings.KeyChainAccountName) + .Build(); + + // This hooks up the cross-platform cache into MSAL + var cacheHelper = await MsalCacheHelper.CreateAsync(storageProperties); + cacheHelper.RegisterCache(app.UserTokenCache); + + // Scope for Microsoft Graph + string[] scopes = new[] { "user.read" }; + string graphApiUrl = configuration.GetValue("GraphApiUrl"); + + AuthenticationResult result; + GraphServiceClient graphClient; + User me; + + while (true) + { + // Display menu + Console.WriteLine("------------ MENU ------------"); + Console.WriteLine("1. Acquire Token Silent / Interactive (not using embedded view)"); + Console.WriteLine("2. Acquire Token Silent / Interactive (using embedded view, currently not supported on .NET Core)"); + Console.WriteLine("3. Display Accounts (reads the cache)"); + Console.WriteLine("4. Clear cache"); + Console.WriteLine("x. Exit app"); + Console.Write("Enter your Selection:"); + char.TryParse(Console.ReadLine(), out var selection); + + try + { + switch (selection) + { + case '1': // Silent / Interactive + Console.Clear(); + Console.WriteLine("Acquiring token from the cache (silently), if it fails do it interactively"); + + result = await AcquireToken(app, scopes, false); + + graphClient = GetGraphServiceClient(result.AccessToken, graphApiUrl); + me = await graphClient.Me.Request().GetAsync(); + + DisplayGraphResult(result, me); + break; + + case '2': // Silent / Interactive with embedded view + Console.Clear(); + Console.WriteLine("Acquiring token from the cache (silently), if it fails do it interactively using embedded view"); + + result = await AcquireToken(app, scopes, true); + + graphClient = GetGraphServiceClient(result.AccessToken, graphApiUrl); + me = await graphClient.Me.Request().GetAsync(); + + DisplayGraphResult(result, me); + break; + + case '3': // Display Accounts + Console.Clear(); + var accounts2 = await app.GetAccountsAsync().ConfigureAwait(false); + if (!accounts2.Any()) + { + Console.WriteLine("No accounts were found in the cache."); + Console.Write(Environment.NewLine); + } + + foreach (var acc in accounts2) + { + Console.WriteLine($"Account for {acc.Username}"); + Console.Write(Environment.NewLine); + } + break; + + case '4': // Clear cache + Console.Clear(); + var accounts3 = await app.GetAccountsAsync().ConfigureAwait(false); + foreach (var acc in accounts3) + { + Console.WriteLine($"Removing account for {acc.Username}"); + Console.Write(Environment.NewLine); + await app.RemoveAsync(acc).ConfigureAwait(false); + } + break; + + case 'x': + return; + } + + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("Exception : " + ex); + Console.ResetColor(); + Console.WriteLine("Hit Enter to continue"); + Console.Read(); + Console.Clear(); + } + } + } + + private static async Task AcquireToken(IPublicClientApplication app, string[] scopes, bool useEmbaddedView) + { + AuthenticationResult result; + try + { + var accounts = await app.GetAccountsAsync(); + + // Try to acquire an access token from the cache. If an interaction is required, + // MsalUiRequiredException will be thrown. + result = await app.AcquireTokenSilent(scopes, accounts.FirstOrDefault()) + .ExecuteAsync(); + } + catch (MsalUiRequiredException) + { + // Acquiring an access token interactively. MSAL will cache it so we can use AcquireTokenSilent + // on future calls. + result = await app.AcquireTokenInteractive(scopes) + .WithUseEmbeddedWebView(useEmbaddedView) + .ExecuteAsync(); + } + + return result; + } + + private static GraphServiceClient GetGraphServiceClient(string accessToken, string graphApiUrl) + { + GraphServiceClient graphServiceClient = new GraphServiceClient(graphApiUrl, + new DelegateAuthenticationProvider( + async (requestMessage) => + { + await Task.Run(() => + { + requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken); + }); + })); + + return graphServiceClient; + } + + private static void DisplayGraphResult(AuthenticationResult result, User me) + { + Console.ForegroundColor = ConsoleColor.Green; + + Console.Write(Environment.NewLine); + Console.WriteLine($"Hello {result.Account.Username}"); + Console.Write(Environment.NewLine); + Console.WriteLine("-------- GRAPH RESULT --------"); + Console.Write(Environment.NewLine); + Console.WriteLine($"Id: {me.Id}"); + Console.WriteLine($"Display Name: {me.DisplayName}"); + Console.WriteLine($"Email: {me.Mail}"); + Console.Write(Environment.NewLine); + Console.WriteLine("------------------------------"); + Console.Write(Environment.NewLine); + Console.Write(Environment.NewLine); + Console.ResetColor(); + + } + } +} diff --git a/4-desktop-apps/console-interactive-multitarget-graph/Console-TokenCache/appsettings.json b/4-desktop-apps/console-interactive-multitarget-graph/Console-TokenCache/appsettings.json new file mode 100644 index 0000000..4137cb8 --- /dev/null +++ b/4-desktop-apps/console-interactive-multitarget-graph/Console-TokenCache/appsettings.json @@ -0,0 +1,7 @@ +{ + "Instance": "https://login.microsoftonline.com/", + "ClientId": "[Enter the Client Id (Application ID obtained from the Azure portal), e.g. ba74781c2-53c2-442a-97c2-3d60re42f403]", + "TenantId": "[Enter 'common', or 'organizations' or the Tenant Id (Obtained from the Azure portal. Select 'Endpoints' from the 'App registrations' blade and use the GUID in any of the URLs), e.g. da41245a5-11b3-996c-00a8-4d99re19f292]", + "RedirectUri": "http://localhost", + "GraphApiUrl": "https://graph.microsoft.com/beta/" +} diff --git a/4-desktop-apps/console-interactive-multitarget-graph/README-national-clouds.md b/4-desktop-apps/console-interactive-multitarget-graph/README-national-clouds.md new file mode 100644 index 0000000..61df1d0 --- /dev/null +++ b/4-desktop-apps/console-interactive-multitarget-graph/README-national-clouds.md @@ -0,0 +1,96 @@ +--- +services: active-directory +platforms: dotnet +author: TiagoBrenck +level: 100 +client: .NET Desktop (Console) +service: Microsoft Graph +endpoint: Microsoft identity platform +page_type: sample +languages: + - csharp +products: + - azure + - microsoft-entra-id + - dotnet + - office-ms-graph +description: "This sample demonstrates a .NET Desktop (Console) application calling Microsoft Graph on National clouds" +--- + +# Using the Microsoft identity platform to call Microsoft Graph API from a multi-target console application on National clouds. + +## About this sample + +### Overview + +This sample demonstrates a .NET Desktop (Console) application calling Microsoft Graph on National clouds. + +1. The .NET Desktop (Console) application uses the Microsoft Authentication Library (MSAL) to obtain a JWT access token from a National cloud Microsoft Entra ID +2. The access token is used as a bearer token to authenticate the user when calling Microsoft Graph. + +![Overview](./ReadmeFiles/topology.png) + +National clouds (aka Sovereign clouds) are physically isolated instances of Azure. These regions of Azure are designed to make sure that data residency, sovereignty, and compliance requirements are honored within geographical boundaries. + +In addition to the public cloud​, Azure Active Directory is deployed in the following National clouds:   + +- Azure US Government +- Azure China 21Vianet +- Azure Germany + +Note that enabling your application for National clouds requires you to: + +- register your application in a specific portal, depending on the cloud +- use a specific authority, depending on the cloud in the config file for your application +- in case you want to call the graph, this requires a specific Graph endpoint URL, depending on the cloud. + +More details in [Authentication in National Clouds](https://learn.microsoft.com/en-us/entra/identity-platform/authentication-national-cloud) + +## How to run this sample + +To run this sample, you can follow the same steps on [the first sample in this tutorial](https://github.com/Azure-Samples/ms-identity-dotnet-desktop-tutorial/tree/master/1-Calling-MSGraph/1-1-AzureAD) but using the desired National cloud portal to create the application. + +To use the PowerShell script that **automatically** creates the Microsoft Entra application for this sample in your National cloud, please use the parameter `-AzureEnvironmentName` [as described in this document](https://github.com/Azure-Samples/ms-identity-dotnet-desktop-tutorial/blob/master/1-Calling-MSGraph/1-1-AzureAD/AppCreationScripts/AppCreationScripts.md#running-the-script-on-azure-sovereign-clouds). + +### Configure the client app (Console-Interactive-MultiTarget-v2) to use your app registration + +Open the project in your IDE (like Visual Studio) to configure the code. + +1. Open the `Console-Interactive-MultiTarget\appsettings.json` file +1. Find the app key `Instance` and replace the existing value with the [correspondent endpoint for your National clouds](https://learn.microsoft.com/en-us/entra/identity-platform/authentication-national-cloud#azure-ad-authentication-endpoints). +1. Find the app key `ClientId` and replace the existing value with the application ID (clientId) of the `Console-Interactive-MultiTarget-v2` application copied from the Microsoft Entra admin center. +1. Find the app key `TenantId` and replace the existing value with your Microsoft Entra tenant ID. +1. Find the app key `GraphApiUrl` and replace the existing value with the Microsoft Graph endpoint for your National clouds. [See this reference for more info on which graph endpoint to use](https://learn.microsoft.com/graph/deployments#microsoft-graph-and-graph-explorer-service-root-endpoints). + +## Community Help and Support + +Use [Stack Overflow](http://stackoverflow.com/questions/tagged/msal) to get support from the community. +Ask your questions on Stack Overflow first and browse existing issues to see if someone has asked your question before. +Make sure that your questions or comments are tagged with [`azure-active-directory` `msal` `dotnet`]. + +If you find a bug in the sample, please raise the issue on [GitHub Issues](../../../../issues). + +To provide a recommendation, visit the following [User Voice page](https://feedback.azure.com/forums/169401-azure-active-directory). + +> [Consider taking a moment to share your experience with us.](https://forms.office.com/Pages/ResponsePage.aspx?id=v4j5cvGGr0GRqy180BHbR73pcsbpbxNJuZCMKN0lURpUREhEVDBOTFBMUVRPUElBUE5WMjdPQ1RaMiQlQCN0PWcu) + +## Contributing + +If you'd like to contribute to this sample, see [CONTRIBUTING.MD](/CONTRIBUTING.md). + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information, see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +## More information + +For more information, see MSAL.NET's conceptual documentation: + +- [MSAL.NET's conceptual documentation](https://aka.ms/msal-net) +- [Microsoft identity platform (Microsoft Entra for developers)](https://learn.microsoft.com/entra/identity-platform/) +- [Quickstart: Register an application with the Microsoft identity platform](https://learn.microsoft.com/entra/identity-platform/quickstart-register-app) +- [Quickstart: Configure a client application to access web APIs](https://learn.microsoft.com/entra/identity-platform/quickstart-configure-app-access-web-apis) + +- [Understanding Microsoft Entra application consent experiences](https://learn.microsoft.com/entra/identity-platform/application-consent-experience) +- [Understand user and admin consent](https://learn.microsoft.com/entra/identity-platform/howto-convert-app-to-be-multi-tenant#understand-user-and-admin-consent) +- [Application and service principal objects in Microsoft Entra ID](https://learn.microsoft.com/entra/identity-platform/app-objects-and-service-principals) + +For more information about how OAuth 2.0 protocols work in this scenario and other scenarios, see [Authentication Scenarios for Microsoft Entra ID](http://go.microsoft.com/fwlink/?LinkId=394414). diff --git a/4-desktop-apps/console-interactive-multitarget-graph/README.md b/4-desktop-apps/console-interactive-multitarget-graph/README.md new file mode 100644 index 0000000..8eb02c4 --- /dev/null +++ b/4-desktop-apps/console-interactive-multitarget-graph/README.md @@ -0,0 +1,220 @@ +--- +services: active-directory +platforms: dotnet +author: TiagoBrenck +level: 200 +client: .NET Desktop (Console) +service: Microsoft Graph +endpoint: Microsoft identity platform +page_type: sample +languages: + - csharp +products: + - azure + - microsoft-entra-id + - dotnet + - office-ms-graph +description: "This sample demonstrates a .NET Desktop (Console) application calling Microsoft Graph leveraging a cross platfrom token cache" +--- + +# Using the Microsoft identity platform to call Microsoft Graph API from a multi-target console application leveraging a cross platform token cache. + +![Build badge](https://identitydivision.visualstudio.com/_apis/public/build/definitions/a7934fdd-dcde-4492-a406-7fad6ac00e17//badge) + +## About this sample + +### Overview + +This sample demonstrates a .NET Desktop (Console) application leveraging token cache operations. + +1. The .NET Desktop (Console) application uses the Microsoft Authentication Library (MSAL) to obtain a JWT access token from Microsoft Entra ID and cache the access token. +2. The application displays a menu allowing the user to execute the following cache operations: + - Get an access token from the cache + - List all items in the cache + - Clear the cache + +## How to run this sample + +To run this sample, you'll need: + +- [Visual Studio 2019](https://aka.ms/vsdownload) +- An Internet connection +- a Microsoft Entra tenant. For more information on how to get a Microsoft Entra tenant, see [How to get a Microsoft Entra tenant](https://azure.microsoft.com/documentation/articles/active-directory-howto-tenant/) +- A user account in your Microsoft Entra tenant. This sample will not work with a Microsoft account (formerly Windows Live account). Therefore, if you signed in to the [Microsoft Entra admin center](https://entra.microsoft.com) with a Microsoft account and have never created a user account in your directory before, you need to do that now. + +### Step 1: Clone or download this repository + +From your shell or command line: + +```Shell +git clone https://github.com/Azure-Samples/ms-identity-dotnet-desktop-tutorial.git +``` + +or download and extract the repository .zip file. + +> Given that the name of the sample is quiet long, and so are the names of the referenced NuGet packages, you might want to clone it in a folder close to the root of your hard drive, to avoid file size limitations on Windows. + +### Step 2: Register the sample application with your Microsoft Entra tenant + +There is one project in this sample. To register it, you can: + +- either follow the steps [Step 2: Register the sample with your Microsoft Entra tenant](#step-2-register-the-sample-with-your-azure-active-directory-tenant) and [Step 3: Configure the sample to use your Microsoft Entra tenant](#choose-the-azure-ad-tenant-where-you-want-to-create-your-applications) +- or use PowerShell scripts that: + - **automatically** creates the Microsoft Entra applications and related objects (passwords, permissions, dependencies) for you. Note that this works for Visual Studio only. + - modify the Visual Studio projects' configuration files. + +
+ Expand this section if you want to use this automation: + +1. On Windows, run PowerShell and navigate to the root of the cloned directory +1. In PowerShell run: + + ```PowerShell + Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force + ``` + +1. Run the script to create your Microsoft Entra application and configure the code of the sample application accordingly. +1. In PowerShell run: + + ```PowerShell + cd .\AppCreationScripts\ + .\Configure.ps1 + ``` + + > Other ways of running the scripts are described in [App Creation Scripts](./AppCreationScripts/AppCreationScripts.md) + > The scripts also provide a guide to automated application registration, configuration and removal which can help in your CI/CD scenarios. + +1. Open the Visual Studio solution and click start to run the code. + +
+ +Follow the steps below to manually walk through the steps to register and configure the applications. + +#### Choose the Microsoft Entra tenant where you want to create your applications + +As a first step you'll need to: + +1. Sign in to the [Microsoft Entra admin center](https://entra.microsoft.com) using either a work or school account or a personal Microsoft account. +1. If your account is present in more than one Microsoft Entra tenant, select your profile at the top right corner in the menu on top of the page, and then **switch directory**. + Change your portal session to the desired Microsoft Entra tenant. + +#### Register the client app (Console-Interactive-MultiTarget-v2) + +1. Navigate to the Microsoft identity platform for developers [App registrations](https://go.microsoft.com/fwlink/?linkid=2083908) page. +1. Click **New registration** on top. +1. In the **Register an application page** that appears, enter your application's registration information: + - In the **Name** section, enter a meaningful application name that will be displayed to users of the app, for example `Console-Interactive-MultiTarget-v2`. + - Change **Supported account types** to **Accounts in any organizational directory and personal Microsoft accounts (e.g. Skype, Xbox, Outlook.com)**. +1. Click on the **Register** button in bottom to create the application. +1. In the app's registration screen, find the **Application (client) ID** value and record it for use later. You'll need it to configure the configuration file(s) later in your code. +1. In the app's registration screen, click on the **Authentication** blade in the left. + - If you don't have a platform added yet, click on **Add a platform** and select the **Public client (mobile & desktop)** option. + - In the **Redirect URIs** section, enter the following redirect URIs. + - `http://localhost` + - In the **Redirect URIs** | **Suggested Redirect URIs for public clients (mobile, desktop)** section, select **https://login.microsoftonline.com/common/oauth2/nativeclient** + +1. Click the **Save** button on top to save the changes. +1. In the app's registration screen, click on the **API permissions** blade in the left to open the page where we add access to the Apis that your application needs. + - Click the **Add a permission** button and then, + - Ensure that the **Microsoft APIs** tab is selected. + - In the *Commonly used Microsoft APIs* section, click on **Microsoft Graph** + - In the **Delegated permissions** section, select the **User.Read** in the list. Use the search box if necessary. + - Click on the **Add permissions** button at the bottom. + +##### Configure the client app (Console-Interactive-MultiTarget-v2) to use your app registration + +Open the project in your IDE (like Visual Studio) to configure the code. +>In the steps below, "ClientID" is the same as "Application ID" or "AppId". + +1. Open the `Console-Interactive-MultiTarget\appsettings.json` file +1. Find the app key `ClientId` and replace the existing value with the application ID (clientId) of the `Console-Interactive-MultiTarget-v2` application copied from the Microsoft Entra admin center. +1. Find the app key `TenantId` and replace the existing value with your Microsoft Entra tenant ID. + +### Step 4: Run the sample + +Clean the solution, rebuild the solution, and run it. + +Start the application, sign-in and check the result in the console. + +> [Consider taking a moment to share your experience with us.](https://forms.office.com/Pages/ResponsePage.aspx?id=v4j5cvGGr0GRqy180BHbR73pcsbpbxNJuZCMKN0lURpUREhEVDBOTFBMUVRPUElBUE5WMjdPQ1RaMiQlQCN0PWcu) + +## About the code + +The relevant code for this sample is the cache setup and its operations,located in the `Program.cs` file: + +1. Import `Microsoft.Identity.Client.Extensions.Msal` NuGet package for the [cross platform token cache](https://github.com/AzureAD/microsoft-authentication-extensions-for-dotnet/wiki/Cross-platform-Token-Cache). + +2. Build a `StorageCreationProperties` object, with all the settings for the cross platform cache. We created the static class `CacheSettings` just to store the settings constants. + +```c# +// Building StorageCreationProperties +var storageProperties = + new StorageCreationPropertiesBuilder(CacheSettings.CacheFileName, CacheSettings.CacheDir, appConfiguration.ClientId) + .WithLinuxKeyring( + CacheSettings.LinuxKeyRingSchema, + CacheSettings.LinuxKeyRingCollection, + CacheSettings.LinuxKeyRingLabel, + CacheSettings.LinuxKeyRingAttr1, + CacheSettings.LinuxKeyRingAttr2) + .WithMacKeyChain( + CacheSettings.KeyChainServiceName, + CacheSettings.KeyChainAccountName) + .Build(); +``` + +3. Create a `MsalCacheHelper` object and register it in the `PublicClientApplication`. + +```c# +var cacheHelper = await MsalCacheHelper.CreateAsync(storageProperties); +cacheHelper.RegisterCache(app.UserTokenCache); +``` + +4. Since the cache is registered in the `PublicClientApplication` object, the cache may be accessed many times by MSAL. Some examples are: + + - `AcquireTokenInteractive` will add an account into the cache in case it is not there yet + - `AcquireTokenSilent` will use the cached token for the account sent as parameter + - `GetAccountsAsync` will return all account in the cache + - `RemoveAsync` will remove the account from the cache + +### Using Embedded View - supported on **.NET Framework only** + +This sample is a multi-target framework (.NET Core and .NET Framework) project, and MSAL has the option to use embedded view when acquiring a token interactively. You can test this functionality by running the project on .NET Framework. + +```c# +app.AcquireTokenInteractive(scopes) + .WithUseEmbeddedWebView(true) // currently only supported on .NET framework + .ExecuteAsync(); +``` + +## Community Help and Support + +Use [Stack Overflow](http://stackoverflow.com/questions/tagged/msal) to get support from the community. +Ask your questions on Stack Overflow first and browse existing issues to see if someone has asked your question before. +Make sure that your questions or comments are tagged with [`azure-active-directory` `msal` `dotnet`]. + +If you find a bug in the sample, please raise the issue on [GitHub Issues](../../issues). + +To provide a recommendation, visit the following [User Voice page](https://feedback.azure.com/forums/169401-azure-active-directory). + +## Contributing + +If you'd like to contribute to this sample, see [CONTRIBUTING.MD](/CONTRIBUTING.md). + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information, see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +## More information + +For more information, see MSAL.NET's conceptual documentation: + +- [MSAL.NET's conceptual documentation](https://aka.ms/msal-net) +- [MSAL.NET Extensions](https://github.com/AzureAD/microsoft-authentication-extensions-for-dotnet) +- [Cross platform token cache](https://github.com/AzureAD/microsoft-authentication-extensions-for-dotnet/wiki/Cross-platform-Token-Cache) +- [Microsoft identity platform (Microsoft Entra ID for developers)](https://learn.microsoft.com/entra/identity-platform/) +- [Quickstart: Register an application with the Microsoft identity platform](https://learn.microsoft.com/entra/identity-platform/quickstart-register-app) +- [Quickstart: Configure a client application to access web APIs](https://learn.microsoft.com/entra/identity-platform/quickstart-configure-app-access-web-apis) + +- [Understanding Microsoft Entra application consent experiences](https://learn.microsoft.com/entra/identity-platform/application-consent-experience) +- [Understand user and admin consent](https://learn.microsoft.com/entra/identity-platform/howto-convert-app-to-be-multi-tenant#understand-user-and-admin-consent) +- [Application and service principal objects in Microsoft Entra ID](https://learn.microsoft.com/entra/identity-platform/app-objects-and-service-principals) + +For more information about how OAuth 2.0 protocols work in this scenario and other scenarios, see [Authentication Scenarios for Microsoft Entra ID](http://go.microsoft.com/fwlink/?LinkId=394414). diff --git a/4-desktop-apps/console-web-browser/AppCreationScripts/AppCreationScripts.md b/4-desktop-apps/console-web-browser/AppCreationScripts/AppCreationScripts.md new file mode 100644 index 0000000..ae8be6a --- /dev/null +++ b/4-desktop-apps/console-web-browser/AppCreationScripts/AppCreationScripts.md @@ -0,0 +1,166 @@ +# Registering the sample apps with Microsoft identity platform and updating the configuration files using PowerShell scripts + +## Overview + +### Quick summary + +1. On Windows run PowerShell and navigate to the root of the cloned directory +1. In PowerShell run: + ```PowerShell + Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force + ``` +1. Run the script to create your Microsoft Entra application and configure the code of the sample application accordingly. (Other ways of running the scripts are described below) + ```PowerShell + cd .\AppCreationScripts\ + .\Configure.ps1 + ``` +1. Open the Visual Studio solution and click start + +### More details + +The following paragraphs: + +- [Present the scripts](#presentation-of-the-scripts) and explain their [usage patterns](#usage-pattern-for-tests-and-devops-scenarios) for test and DevOps scenarios. +- Explain the [pre-requisites](#pre-requisites) +- Explain [four ways of running the scripts](#four-ways-to-run-the-script): + - [Interactively](#option-1-interactive) to create the app in your home tenant + - [Passing credentials](#option-2-non-interactive) to create the app in your home tenant + - [Interactively in a specific tenant](#option-3-interactive-but-create-apps-in-a-specified-tenant) + - [Passing credentials in a specific tenant](#option-4-non-interactive-and-create-apps-in-a-specified-tenant) + - [Passing environment name, for Sovereign clouds](#running-the-script-on-azure-sovereign-clouds) + +## Goal of the scripts + +### Presentation of the scripts + +This sample comes with two PowerShell scripts, which automate the creation of the Microsoft Entra applications, and the configuration of the code for this sample. Once you run them, you will only need to build the solution and you are good to test. + +These scripts are: + +- `Configure.ps1` which: + - creates Microsoft Entra applications and their related objects (permissions, dependencies, secrets), + - changes the configuration files in the C# and JavaScript projects. + - creates a summary file named `createdApps.html` in the folder from which you ran the script, and containing, for each Microsoft Entra application it created: + - the identifier of the application + - the AppId of the application + - the url of its registration in the [Microsoft Entra admin center](https://entra.microsoft.com). + +- `Cleanup.ps1` which cleans-up the Microsoft Entra objects created by `Configure.ps1`. Note that this script does not revert the changes done in the configuration files, though. You will need to undo the change from source control (from Visual Studio, or from the command line using, for instance, git reset). + +### Usage pattern for tests and DevOps scenarios + +The `Configure.ps1` will stop if it tries to create a Microsoft Entra application which already exists in the tenant. For this, if you are using the script to try/test the sample, or in DevOps scenarios, you might want to run `Cleanup.ps1` just before `Configure.ps1`. This is what is shown in the steps below. + +## How to use the app creation scripts? + +### Pre-requisites + +1. Open PowerShell (On Windows, press `Windows-R` and type `PowerShell` in the search window) +2. Navigate to the root directory of the project. +3. Until you change it, the default [Execution Policy](https:/go.microsoft.com/fwlink/?LinkID=135170) for scripts is usually `Restricted`. In order to run the PowerShell script you need to set the Execution Policy to `RemoteSigned`. You can set this just for the current PowerShell process by running the command: + ```PowerShell + Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process + ``` +### (Optionally) install AzureAD PowerShell modules +The scripts install the required PowerShell module (AzureAD) for the current user if needed. However, if you want to install if for all users on the machine, you can follow the following steps: + +4. If you have never done it already, in the PowerShell window, install the AzureAD PowerShell modules. For this: + + 1. Open PowerShell as admin (On Windows, Search Powershell in the search bar, right click on it and select Run as administrator). + 2. Type: + ```PowerShell + Install-Module AzureAD + ``` + + or if you cannot be administrator on your machine, run: + ```PowerShell + Install-Module AzureAD -Scope CurrentUser + ``` + +### Run the script and start running + +5. Go to the `AppCreationScripts` sub-folder. From the folder where you cloned the repo, + ```PowerShell + cd AppCreationScripts + ``` +6. Run the scripts. See below for the [four options](#four-ways-to-run-the-script) to do that. +7. Open the Visual Studio solution, and in the solution's context menu, choose **Set Startup Projects**. +8. select **Start** for the projects + +You're done. this just works! + +### Four ways to run the script + +We advise four ways of running the script: + +- Interactive: you will be prompted for credentials, and the scripts decide in which tenant to create the objects, +- non-interactive: you will provide credentials, and the scripts decide in which tenant to create the objects, +- Interactive in specific tenant: you will provide the tenant in which you want to create the objects and then you will be prompted for credentials, and the scripts will create the objects, +- non-interactive in specific tenant: you will provide tenant in which you want to create the objects and credentials, and the scripts will create the objects. + +Here are the details on how to do this. + +#### Option 1 (interactive) + +- Just run ``. .\Configure.ps1``, and you will be prompted to sign-in (email address, password, and if needed MFA). +- The script will be run as the signed-in user and will use the tenant in which the user is defined. + +Note that the script will choose the tenant in which to create the applications, based on the user. Also to run the `Cleanup.ps1` script, you will need to re-sign-in. + +#### Option 2 (non-interactive) + +When you know the identity and credentials of the user in the name of whom you want to create the applications, you can use the non-interactive approach. It's more adapted to DevOps. Here is an example of script you'd want to run in a PowerShell Window + +```PowerShell +$secpasswd = ConvertTo-SecureString "[Password here]" -AsPlainText -Force +$mycreds = New-Object System.Management.Automation.PSCredential ("[login@tenantName here]", $secpasswd) +. .\Cleanup.ps1 -Credential $mycreds +. .\Configure.ps1 -Credential $mycreds +``` + +Of course, in real life, you might already get the password as a `SecureString`. You might also want to get the password from KeyVault. + +#### Option 3 (Interactive, but create apps in a specified tenant) + + if you want to create the apps in a particular tenant, you can use the following option: +- open the [Microsoft Entra admin center](https://entra.microsoft.com) +- Select the Microsoft Entra ID you are interested in (in the combo-box below your name on the top right of the browser window) +- Find the "Active Directory" object in this tenant +- Go to **Properties** and copy the content of the **Directory Id** property +- Then use the full syntax to run the scripts: + +```PowerShell +$tenantId = "yourTenantIdGuid" +. .\Cleanup.ps1 -TenantId $tenantId +. .\Configure.ps1 -TenantId $tenantId +``` + +#### Option 4 (non-interactive, and create apps in a specified tenant) + +This option combines option 2 and option 3: it creates the application in a specific tenant. See option 3 for the way to get the tenant Id. Then run: + +```PowerShell +$secpasswd = ConvertTo-SecureString "[Password here]" -AsPlainText -Force +$mycreds = New-Object System.Management.Automation.PSCredential ("[login@tenantName here]", $secpasswd) +$tenantId = "yourTenantIdGuid" +. .\Cleanup.ps1 -Credential $mycreds -TenantId $tenantId +. .\Configure.ps1 -Credential $mycreds -TenantId $tenantId +``` + +### Running the script on Azure Sovereign clouds + +All the four options listed above, can be used on any Azure Sovereign clouds. By default, the script targets `AzureCloud`, but it can be changed using the parameter `-AzureEnvironmentName`. + +The acceptable values for this parameter are: + +- AzureCloud +- AzureChinaCloud +- AzureUSGovernment +- AzureGermanyCloud + +Example: + + ```PowerShell + . .\Cleanup.ps1 -AzureEnvironmentName "AzureGermanyCloud" + . .\Configure.ps1 -AzureEnvironmentName "AzureGermanyCloud" + ``` diff --git a/4-desktop-apps/console-web-browser/AppCreationScripts/Cleanup.ps1 b/4-desktop-apps/console-web-browser/AppCreationScripts/Cleanup.ps1 new file mode 100644 index 0000000..cca3c75 --- /dev/null +++ b/4-desktop-apps/console-web-browser/AppCreationScripts/Cleanup.ps1 @@ -0,0 +1,77 @@ +[CmdletBinding()] +param( + [PSCredential] $Credential, + [Parameter(Mandatory=$False, HelpMessage='Tenant ID (This is a GUID which represents the "Directory ID" of the AzureAD tenant into which you want to create the apps')] + [string] $tenantId, + [Parameter(Mandatory=$False, HelpMessage='Azure environment to use while running the script (it defaults to AzureCloud)')] + [string] $azureEnvironmentName +) + +if ($null -eq (Get-Module -ListAvailable -Name "AzureAD")) { + Install-Module "AzureAD" -Scope CurrentUser +} +Import-Module AzureAD +$ErrorActionPreference = "Stop" + +Function Cleanup +{ + if (!$azureEnvironmentName) + { + $azureEnvironmentName = "AzureCloud" + } + + <# + .Description + This function removes the Microsoft Entra applications for the sample. These applications were created by the Configure.ps1 script + #> + + # $tenantId is the Active Directory Tenant. This is a GUID which represents the "Directory ID" of the AzureAD tenant + # into which you want to create the apps. Look it up in the Azure portal in the "Properties" of Microsoft Entra. + + # Login to Azure PowerShell (interactive if credentials are not already provided: + # you'll need to sign-in with creds enabling your to create apps in the tenant) + if (!$Credential -and $TenantId) + { + $creds = Connect-AzureAD -TenantId $tenantId -AzureEnvironmentName $azureEnvironmentName + } + else + { + if (!$TenantId) + { + $creds = Connect-AzureAD -Credential $Credential -AzureEnvironmentName $azureEnvironmentName + } + else + { + $creds = Connect-AzureAD -TenantId $tenantId -Credential $Credential -AzureEnvironmentName $azureEnvironmentName + } + } + + if (!$tenantId) + { + $tenantId = $creds.Tenant.Id + } + $tenant = Get-AzureADTenantDetail + $tenantName = ($tenant.VerifiedDomains | Where-Object { $_._Default -eq $True }).Name + + # Removes the applications + Write-Host "Cleaning-up applications from tenant '$tenantName'" + + Write-Host "Removing 'client' (Console-Interactive-MultiTarget-v2) if needed" + Get-AzureADApplication -Filter "DisplayName eq 'Console-Interactive-MultiTarget-v2'" | ForEach-Object {Remove-AzureADApplication -ObjectId $_.ObjectId } + $apps = Get-AzureADApplication -Filter "DisplayName eq 'Console-Interactive-MultiTarget-v2'" + if ($apps) + { + Remove-AzureADApplication -ObjectId $apps.ObjectId + } + + foreach ($app in $apps) + { + Remove-AzureADApplication -ObjectId $app.ObjectId + Write-Host "Removed Console-Interactive-MultiTarget-v2.." + } + # also remove service principals of this app + Get-AzureADServicePrincipal -filter "DisplayName eq 'Console-Interactive-MultiTarget-v2'" | ForEach-Object {Remove-AzureADServicePrincipal -ObjectId $_.Id -Confirm:$false} + +} + +Cleanup -Credential $Credential -tenantId $TenantId \ No newline at end of file diff --git a/4-desktop-apps/console-web-browser/AppCreationScripts/Configure.ps1 b/4-desktop-apps/console-web-browser/AppCreationScripts/Configure.ps1 new file mode 100644 index 0000000..6454176 --- /dev/null +++ b/4-desktop-apps/console-web-browser/AppCreationScripts/Configure.ps1 @@ -0,0 +1,224 @@ +[CmdletBinding()] +param( + [PSCredential] $Credential, + [Parameter(Mandatory=$False, HelpMessage='Tenant ID (This is a GUID which represents the "Directory ID" of the AzureAD tenant into which you want to create the apps')] + [string] $tenantId, + [Parameter(Mandatory=$False, HelpMessage='Azure environment to use while running the script (it defaults to AzureCloud)')] + [string] $azureEnvironmentName +) + +<# + This script creates the Microsoft Entra applications needed for this sample and updates the configuration files + for the visual Studio projects from the data in the Microsoft Entra applications. + + Before running this script you need to install the AzureAD cmdlets as an administrator. + For this: + 1) Run Powershell as an administrator + 2) in the PowerShell window, type: Install-Module AzureAD + + There are four ways to run this script. For more information, read the AppCreationScripts.md file in the same folder as this script. +#> + +# Adds the requiredAccesses (expressed as a pipe separated string) to the requiredAccess structure +# The exposed permissions are in the $exposedPermissions collection, and the type of permission (Scope | Role) is +# described in $permissionType +Function AddResourcePermission($requiredAccess, ` + $exposedPermissions, [string]$requiredAccesses, [string]$permissionType) +{ + foreach($permission in $requiredAccesses.Trim().Split("|")) + { + foreach($exposedPermission in $exposedPermissions) + { + if ($exposedPermission.Value -eq $permission) + { + $resourceAccess = New-Object Microsoft.Open.AzureAD.Model.ResourceAccess + $resourceAccess.Type = $permissionType # Scope = Delegated permissions | Role = Application permissions + $resourceAccess.Id = $exposedPermission.Id # Read directory data + $requiredAccess.ResourceAccess.Add($resourceAccess) + } + } + } +} + +# +# Example: GetRequiredPermissions "Microsoft Graph" "Graph.Read|User.Read" +# See also: http://stackoverflow.com/questions/42164581/how-to-configure-a-new-azure-ad-application-through-powershell +Function GetRequiredPermissions([string] $applicationDisplayName, [string] $requiredDelegatedPermissions, [string]$requiredApplicationPermissions, $servicePrincipal) +{ + # If we are passed the service principal we use it directly, otherwise we find it from the display name (which might not be unique) + if ($servicePrincipal) + { + $sp = $servicePrincipal + } + else + { + $sp = Get-AzureADServicePrincipal -Filter "DisplayName eq '$applicationDisplayName'" + } + $appid = $sp.AppId + $requiredAccess = New-Object Microsoft.Open.AzureAD.Model.RequiredResourceAccess + $requiredAccess.ResourceAppId = $appid + $requiredAccess.ResourceAccess = New-Object System.Collections.Generic.List[Microsoft.Open.AzureAD.Model.ResourceAccess] + + # $sp.Oauth2Permissions | Select Id,AdminConsentDisplayName,Value: To see the list of all the Delegated permissions for the application: + if ($requiredDelegatedPermissions) + { + AddResourcePermission $requiredAccess -exposedPermissions $sp.Oauth2Permissions -requiredAccesses $requiredDelegatedPermissions -permissionType "Scope" + } + + # $sp.AppRoles | Select Id,AdminConsentDisplayName,Value: To see the list of all the Application permissions for the application + if ($requiredApplicationPermissions) + { + AddResourcePermission $requiredAccess -exposedPermissions $sp.AppRoles -requiredAccesses $requiredApplicationPermissions -permissionType "Role" + } + return $requiredAccess +} + + +Function UpdateLine([string] $line, [string] $value) +{ + $index = $line.IndexOf('=') + $delimiter = ';' + if ($index -eq -1) + { + $index = $line.IndexOf(':') + $delimiter = ',' + } + if ($index -ige 0) + { + $line = $line.Substring(0, $index+1) + " "+'"'+$value+'"'+$delimiter + } + return $line +} + +Function UpdateTextFile([string] $configFilePath, [System.Collections.HashTable] $dictionary) +{ + $lines = Get-Content $configFilePath + $index = 0 + while($index -lt $lines.Length) + { + $line = $lines[$index] + foreach($key in $dictionary.Keys) + { + if ($line.Contains($key)) + { + $lines[$index] = UpdateLine $line $dictionary[$key] + } + } + $index++ + } + + Set-Content -Path $configFilePath -Value $lines -Force +} + +Set-Content -Value "" -Path createdApps.html +Add-Content -Value "" -Path createdApps.html + +$ErrorActionPreference = "Stop" + +Function ConfigureApplications +{ +<#.Description + This function creates the Microsoft Entra applications for the sample in the provided Microsoft Entra tenant and updates the + configuration files in the client and service project of the visual studio solution (App.Config and Web.Config) + so that they are consistent with the Applications parameters +#> + $commonendpoint = "common" + + if (!$azureEnvironmentName) + { + $azureEnvironmentName = "AzureCloud" + } + + # $tenantId is the Active Directory Tenant. This is a GUID which represents the "Directory ID" of the AzureAD tenant + # into which you want to create the apps. Look it up in the Azure portal in the "Properties" of Microsoft Entra. + + # Login to Azure PowerShell (interactive if credentials are not already provided: + # you'll need to sign-in with creds enabling your to create apps in the tenant) + if (!$Credential -and $TenantId) + { + $creds = Connect-AzureAD -TenantId $tenantId -AzureEnvironmentName $azureEnvironmentName + } + else + { + if (!$TenantId) + { + $creds = Connect-AzureAD -Credential $Credential -AzureEnvironmentName $azureEnvironmentName + } + else + { + $creds = Connect-AzureAD -TenantId $tenantId -Credential $Credential -AzureEnvironmentName $azureEnvironmentName + } + } + + if (!$tenantId) + { + $tenantId = $creds.Tenant.Id + } + + + + $tenant = Get-AzureADTenantDetail + $tenantName = ($tenant.VerifiedDomains | Where { $_._Default -eq $True }).Name + + # Get the user running the script to add the user as the app owner + $user = Get-AzureADUser -ObjectId $creds.Account.Id + + # Create the client AAD application + Write-Host "Creating the AAD application (Console-Interactive-MultiTarget-v2)" + # create the application + $clientAadApplication = New-AzureADApplication -DisplayName "Console-Interactive-MultiTarget-v2" ` + -ReplyUrls "https://login.microsoftonline.com/common/oauth2/nativeclient", "http://localhost" ` + -AvailableToOtherTenants $True ` + -PublicClient $True + + # create the service principal of the newly created application + $currentAppId = $clientAadApplication.AppId + $clientServicePrincipal = New-AzureADServicePrincipal -AppId $currentAppId -Tags {WindowsAzureActiveDirectoryIntegratedApp} + + # add the user running the script as an app owner if needed + $owner = Get-AzureADApplicationOwner -ObjectId $clientAadApplication.ObjectId + if ($owner -eq $null) + { + Add-AzureADApplicationOwner -ObjectId $clientAadApplication.ObjectId -RefObjectId $user.ObjectId + Write-Host "'$($user.UserPrincipalName)' added as an application owner to app '$($clientServicePrincipal.DisplayName)'" + } + + + Write-Host "Done creating the client application (Console-Interactive-MultiTarget-v2)" + + # URL of the AAD application in the Azure portal + # Future? $clientPortalUrl = "https://portal.azure.com/#@"+$tenantName+"/blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/Overview/appId/"+$clientAadApplication.AppId+"/objectId/"+$clientAadApplication.ObjectId+"/isMSAApp/" + $clientPortalUrl = "https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/CallAnAPI/appId/"+$clientAadApplication.AppId+"/objectId/"+$clientAadApplication.ObjectId+"/isMSAApp/" + Add-Content -Value "" -Path createdApps.html + + $requiredResourcesAccess = New-Object System.Collections.Generic.List[Microsoft.Open.AzureAD.Model.RequiredResourceAccess] + + # Add Required Resources Access (from 'client' to 'Microsoft Graph') + Write-Host "Getting access from 'client' to 'Microsoft Graph'" + $requiredPermissions = GetRequiredPermissions -applicationDisplayName "Microsoft Graph" ` + -requiredDelegatedPermissions "User.Read" ` + + $requiredResourcesAccess.Add($requiredPermissions) + + + Set-AzureADApplication -ObjectId $clientAadApplication.ObjectId -RequiredResourceAccess $requiredResourcesAccess + Write-Host "Granted permissions." + + # Update config file for 'client' + $configFile = $pwd.Path + "\..\Console-Interactive-CustomWebUI\appsettings.json" + Write-Host "Updating the sample code ($configFile)" + $dictionary = @{ "ClientId" = $clientAadApplication.AppId;"TenantId" = $tenantId }; + UpdateTextFile -configFilePath $configFile -dictionary $dictionary + + Add-Content -Value "
ApplicationAppIdUrl in the Azure portal
client$currentAppIdConsole-Interactive-MultiTarget-v2
" -Path createdApps.html +} + +# Pre-requisites +if ((Get-Module -ListAvailable -Name "AzureAD") -eq $null) { + Install-Module "AzureAD" -Scope CurrentUser +} + +Import-Module AzureAD + +# Run interactively (will ask you for the tenant ID) +ConfigureApplications -Credential $Credential -tenantId $TenantId \ No newline at end of file diff --git a/4-desktop-apps/console-web-browser/AppCreationScripts/sample.json b/4-desktop-apps/console-web-browser/AppCreationScripts/sample.json new file mode 100644 index 0000000..17c3e2b --- /dev/null +++ b/4-desktop-apps/console-web-browser/AppCreationScripts/sample.json @@ -0,0 +1,52 @@ +{ + "Sample": { + "Title": "Using the Microsoft identity platform to call Microsoft Graph API with custom web ui.", + "Level": 300, + "Client": ".NET Desktop (Console)", + "Service": "Microsoft Graph", + "RepositoryUrl": "ms-identity-dotnet-desktop-tutorial", + "Endpoint": "AAD v2.0" + }, + + /* + This section describes the Microsoft Entra applications to configure, and their dependencies + */ + "AADApps": [ + { + "Id": "client", + "Name": "Console-Interactive-MultiTarget-v2", + "Kind": "Desktop", + "ReplyUrls": "https://login.microsoftonline.com/common/oauth2/nativeclient, http://localhost", + "RequiredResourcesAccess": [ + { + "Resource": "Microsoft Graph", + "DelegatedPermissions": [ "User.Read" ] + } + ] + } + ], + + /* + This section describes how to update the code in configuration files from the apps coordinates, once the apps + are created in Microsoft Entra. + Each section describes a configuration file, for one of the apps, it's type (XML, JSon, plain text), its location + with respect to the root of the sample, and the mappping (which string in the config file is mapped to which value + */ + "CodeConfiguration": [ + { + "App": "client", + "SettingKind": "JSon", + "SettingFile": "\\..\\Console-Interactive-CustomWebUI\\appsettings.json", + "Mappings": [ + { + "key": "ClientId", + "value": ".AppId" + }, + { + "key": "TenantId", + "value": "$tenantId" + } + ] + } + ] +} diff --git a/4-desktop-apps/console-web-browser/Console-Interactive-Core.sln b/4-desktop-apps/console-web-browser/Console-Interactive-Core.sln new file mode 100644 index 0000000..bf66cc9 --- /dev/null +++ b/4-desktop-apps/console-web-browser/Console-Interactive-Core.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29709.97 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Console-Interactive-CustomWebUI", "Console-Interactive-CustomWebUI\Console-Interactive-CustomWebUI.csproj", "{509D5688-3BD1-4650-8A43-3CEF5B64198E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {509D5688-3BD1-4650-8A43-3CEF5B64198E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {509D5688-3BD1-4650-8A43-3CEF5B64198E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {509D5688-3BD1-4650-8A43-3CEF5B64198E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {509D5688-3BD1-4650-8A43-3CEF5B64198E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {42EE218E-9522-4DBA-9DFE-2EC0FB0CD3D5} + EndGlobalSection +EndGlobal diff --git a/4-desktop-apps/console-web-browser/Console-Interactive-CustomWebUI/Console-Interactive-CustomWebUI.csproj b/4-desktop-apps/console-web-browser/Console-Interactive-CustomWebUI/Console-Interactive-CustomWebUI.csproj new file mode 100644 index 0000000..8755c19 --- /dev/null +++ b/4-desktop-apps/console-web-browser/Console-Interactive-CustomWebUI/Console-Interactive-CustomWebUI.csproj @@ -0,0 +1,25 @@ + + + + Exe + netcoreapp3.1 + Console_Interactive_CustomWebUI + + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/4-desktop-apps/console-web-browser/Console-Interactive-CustomWebUI/CustomWebBrowser/CustomBrowserWebUi.cs b/4-desktop-apps/console-web-browser/Console-Interactive-CustomWebUI/CustomWebBrowser/CustomBrowserWebUi.cs new file mode 100644 index 0000000..347c09b --- /dev/null +++ b/4-desktop-apps/console-web-browser/Console-Interactive-CustomWebUI/CustomWebBrowser/CustomBrowserWebUi.cs @@ -0,0 +1,166 @@ +using Microsoft.Identity.Client.Extensibility; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Net; +using System.Net.Sockets; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Web; + +namespace Console_Interactive_CustomWebUI.CustomWebBrowser +{ + internal class CustomBrowserWebUi : ICustomWebUi + { + //Success authentication html block + private const string CloseWindowSuccessHtml = @" + Authentication Complete + +
+

Custom Web UI

+
+
+

Authentication complete

+
You can return to the application. Feel free to close this browser tab.
+
+ + +"; + + //Failure authentication html block + private const string CloseWindowFailureHtml = @" + Authentication Failed + +
+

Custom Web UI

+
+
+

Authentication failed

+
Error details: error {0} error_description: {1}
+
+
You can return to the application. Feel free to close this browser tab.
+
+ + +"; + + public async Task AcquireAuthorizationCodeAsync( + Uri authorizationUri, + Uri redirectUri, + CancellationToken cancellationToken) + { + if (!redirectUri.IsLoopback) + { + throw new ArgumentException("Only loopback redirect uri is supported with this WebUI. Configure http://localhost or http://localhost:port during app registration. "); + } + + Uri result = await InterceptAuthorizationUriAsync(authorizationUri,redirectUri,cancellationToken) + .ConfigureAwait(true); + + return result; + } + + public static string FindFreeLocalhostRedirectUri() + { + TcpListener listner = new TcpListener(IPAddress.Loopback, 0); + try + { + listner.Start(); + int port = ((IPEndPoint)listner.LocalEndpoint).Port; + return "http://localhost:" + port; + } + finally + { + listner?.Stop(); + } + } + + private static void OpenBrowser(string url) + { + try + { + ProcessStartInfo psi = new ProcessStartInfo + { + FileName = url, + UseShellExecute = true + }; + Process.Start(psi); + } + catch + { + // hack because of this: https://github.com/dotnet/corefx/issues/10361 + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + url = url.Replace("&", "^&"); + Process.Start(new ProcessStartInfo("cmd", $"/c start {url}") { CreateNoWindow = true }); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Process.Start("xdg-open", url); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + Process.Start("open", url); + } + else + { + throw new PlatformNotSupportedException(RuntimeInformation.OSDescription); + } + } + } + + /// + /// Opens a new tab on the OS default browser and navigates to the authorization URI, while listening to its response. + /// Then, displays an HTML block based on the authorization response. + /// + private async Task InterceptAuthorizationUriAsync( + Uri authorizationUri, + Uri redirectUri, + CancellationToken cancellationToken) + { + // Opens a browser sending the authorization request + OpenBrowser(authorizationUri.ToString()); + + // Listens to the localhost socket that opened the request + using (var listener = new SingleMessageTcpListener(redirectUri.Port)) + { + Uri authCodeUri = null; + await listener.ListenToSingleRequestAndRespondAsync( + (uri) => + { + Trace.WriteLine("Intercepted an auth code url: " + uri.ToString()); + authCodeUri = uri; + + // Displays the success or failure HTML block based on the authorization response + return GetMessageToShowInBroswerAfterAuth(uri); + }, + cancellationToken) + .ConfigureAwait(false); + + return authCodeUri; + } + } + + // Parses the authorization response and displays the success or failure HTML block accordingly + private static string GetMessageToShowInBroswerAfterAuth(Uri uri) + { + // Parse the uri to understand if an error was returned. This is done just to show the user a nice error message in the browser. + var authCodeQueryKeyValue = HttpUtility.ParseQueryString(uri.Query); + + string errorString = authCodeQueryKeyValue.Get("error"); + if (!string.IsNullOrEmpty(errorString)) + { + return string.Format( + CultureInfo.InvariantCulture, + CloseWindowFailureHtml, + errorString, + authCodeQueryKeyValue.Get("error_description")); + } + + return CloseWindowSuccessHtml; + } + } +} diff --git a/4-desktop-apps/console-web-browser/Console-Interactive-CustomWebUI/CustomWebBrowser/SingleMessageTcpListener.cs b/4-desktop-apps/console-web-browser/Console-Interactive-CustomWebUI/CustomWebBrowser/SingleMessageTcpListener.cs new file mode 100644 index 0000000..752689e --- /dev/null +++ b/4-desktop-apps/console-web-browser/Console-Interactive-CustomWebUI/CustomWebBrowser/SingleMessageTcpListener.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; + +namespace Console_Interactive_CustomWebUI.CustomWebBrowser +{ + internal class SingleMessageTcpListener : IDisposable + { + private readonly int _port; + private readonly TcpListener _tcpListener; + + public SingleMessageTcpListener(int port) + { + if (port < 1 || port == 80) + { + throw new ArgumentOutOfRangeException("Expected a valid port number, > 0, not 80"); + } + + _port = port; + _tcpListener = new TcpListener(IPAddress.Loopback, _port); + + + } + + public async Task ListenToSingleRequestAndRespondAsync(Func responseProducer, CancellationToken cancellationToken) + { + cancellationToken.Register(() => _tcpListener.Stop()); + _tcpListener.Start(); + + TcpClient tcpClient = null; + try + { + tcpClient = await AcceptTcpClientAsync(cancellationToken) + .ConfigureAwait(false); + + await ExtractUriAndRespondAsync(tcpClient, responseProducer, cancellationToken).ConfigureAwait(false); + + } + finally + { + tcpClient?.Close(); + } + } + + /// + /// AcceptTcpClientAsync does not natively support cancellation, so use this wrapper. Make sure + /// the cancellation token is registered to stop the listener. + /// + /// See https://stackoverflow.com/questions/19220957/tcplistener-how-to-stop-listening-while-awaiting-accepttcpclientasync + private async Task AcceptTcpClientAsync(CancellationToken token) + { + try + { + return await _tcpListener.AcceptTcpClientAsync().ConfigureAwait(false); + } + catch (Exception ex) when (token.IsCancellationRequested) + { + throw new OperationCanceledException("Cancellation was requested while awaiting TCP client connection.", ex); + } + } + + private async Task ExtractUriAndRespondAsync( + TcpClient tcpClient, + Func responseProducer, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + string httpRequest = await GetTcpResponseAsync(tcpClient, cancellationToken).ConfigureAwait(false); + Uri uri = ExtractUriFromHttpRequest(httpRequest); + + // write an "OK, please close the browser message" + await WriteResponseAsync(responseProducer(uri), tcpClient.GetStream(), cancellationToken) + .ConfigureAwait(false); + } + +#pragma warning disable CS1570 // XML comment has badly formed XML + /// + /// Example TCP response: + /// + /// {GET /?code=OAQABAAIAAAC5una0EUFgTIF8ElaxtWjTl5wse5YHycjcaO_qJukUUexKz660btJtJSiQKz1h4b5DalmXspKis-bS6Inu8lNs4CpoE4FITrLv00Mr3MEYEQzgrn6JiNoIwDFSl4HBzHG8Kjd4Ho65QGUMVNyTjhWyQDf_12E8Gw9sll_sbOU51FIreZlVuvsqIWBMIJ8mfmExZBSckofV6LbcKJTeEZKaqjC09x3k1dpsCNJAtYTQIus5g1DyhAW8viDpWDpQJlT55_0W4rrNKY3CSD5AhKd3Ng4_ePPd7iC6qObfmMBlCcldX688vR2IghV0GoA0qNalzwqP7lov-yf38uVZ3ir6VlDNpbzCoV-drw0zhlMKgSq6LXT7QQYmuA4RVy_7TE9gjQpW-P0_ZXUHirpgdsblaa3JUq4cXpbMU8YCLQm7I2L0oCkBTupYXKLoM2gHSYPJ5HChhj1x0pWXRzXdqbx_TPTujBLsAo4Skr_XiLQ4QPJZpkscmXezpPa5Z87gDenUBRBI9ppROhOksekMbvPataF0qBaM38QzcnzeOCFyih1OjIKsq3GeryChrEtfY9CL9lBZ6alIIQB4thD__Tc24OUmr04hX34PjMyt1Z9Qvr76Pw0r7A52JvqQLWupx8bqok6AyCwqUGfLCPjwylSLA7NYD7vScAbfkOOszfoCC3ff14Dqm3IAB1tUJfCZoab61c6Mozls74c2Ujr3roHw4NdPuo-re5fbpSw5RVu8MffWYwXrO3GdmgcvIMkli2uperucLldNVIp6Pc3MatMYSBeAikuhtaZiZAhhl3uQxzoMhU-MO9WXuG2oIkqSvKjghxi1NUhfTK4-du7I5h1r0lFh9b3h8kvE1WBhAIxLdSAA&state=b380f309-7d24-4793-b938-e4a512b2c7f6&session_state=a442c3cd-a25e-4b88-8b33-36d194ba11b2 HTTP/1.1 + /// Host: localhost:9001 + /// Accept-Language: en-GB,en;q=0.9,en-US;q=0.8,ro;q=0.7,fr;q=0.6 + /// Connection: keep-alive + /// Upgrade-Insecure-Requests: 1 + /// User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 + /// Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 + /// Accept-Encoding: gzip, deflate, br + /// + /// http://localhost:9001/?code=foo&session_state=bar + private Uri ExtractUriFromHttpRequest(string httpRequest) +#pragma warning restore CS1570 // XML comment has badly formed XML + { + string regexp = @"GET \/\?(.*) HTTP"; + string getQuery = null; + Regex r1 = new Regex(regexp); + Match match = r1.Match(httpRequest); + if (!match.Success) + { + throw new InvalidOperationException("Not a GET query"); + } + + getQuery = match.Groups[1].Value; + UriBuilder uriBuilder = new UriBuilder(); + uriBuilder.Query = getQuery; + uriBuilder.Port = _port; + + return uriBuilder.Uri; + } + + private static async Task GetTcpResponseAsync(TcpClient client, CancellationToken cancellationToken) + { + NetworkStream networkStream = client.GetStream(); + + byte[] readBuffer = new byte[1024]; + StringBuilder stringBuilder = new StringBuilder(); + int numberOfBytesRead = 0; + + // Incoming message may be larger than the buffer size. + do + { + numberOfBytesRead = await networkStream.ReadAsync(readBuffer, 0, readBuffer.Length, cancellationToken) + .ConfigureAwait(false); + + string s = Encoding.ASCII.GetString(readBuffer, 0, numberOfBytesRead); + stringBuilder.Append(s); + + } + while (networkStream.DataAvailable); + + return stringBuilder.ToString(); + } + + private async Task WriteResponseAsync(string message, NetworkStream stream, CancellationToken cancellationToken) + { + string fullResponse = $"HTTP/1.1 200 OK\r\n\r\n{message}"; + var response = Encoding.ASCII.GetBytes(fullResponse); + await stream.WriteAsync(response, 0, response.Length, cancellationToken).ConfigureAwait(false); + await stream.FlushAsync(cancellationToken).ConfigureAwait(false); + } + + public void Dispose() + { + _tcpListener?.Stop(); + } + } +} diff --git a/4-desktop-apps/console-web-browser/Console-Interactive-CustomWebUI/Program.cs b/4-desktop-apps/console-web-browser/Console-Interactive-CustomWebUI/Program.cs new file mode 100644 index 0000000..8c77414 --- /dev/null +++ b/4-desktop-apps/console-web-browser/Console-Interactive-CustomWebUI/Program.cs @@ -0,0 +1,122 @@ +using Console_Interactive_CustomWebUI.CustomWebBrowser; +using Microsoft.Extensions.Configuration; +using Microsoft.Graph; +using Microsoft.Identity.Client; +using Microsoft.Identity.Client.Extensibility; +using System; +using System.Linq; +using System.Net.Http.Headers; +using System.Threading.Tasks; + +namespace Console_Interactive_CustomWebUI +{ + class Program + { + private static PublicClientApplicationOptions appConfiguration = null; + private static IConfiguration configuration; + private static string graphURL; + + // The MSAL Public client app + private static IPublicClientApplication application; + + // Since the browser is started via Process.Start, there is no control over it, + // So it is recommended to configure a timeout + private const int TimeoutWaitingForBrowserMs = 30 * 1000; //30 seconds + + static async Task Main(string[] args) + { + // Using appsettings.json as our configuration settings + var builder = new ConfigurationBuilder() + .SetBasePath(System.IO.Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json"); + + configuration = builder.Build(); + + appConfiguration = configuration + .Get(); + + string[] scopes = new[] { "user.read" }; + + graphURL = configuration.GetValue("GraphApiUrl"); + + // Sign-in user using MSAL and obtain an access token for MS Graph + GraphServiceClient graphClient = await SignInAndInitializeGraphServiceClient(appConfiguration, scopes); + + // Call the /me endpoint of MS Graph + await CallMSGraph(graphClient); + + Console.ReadKey(); + } + + /// + /// Sign in user using MSAL and obtain a token for MS Graph + /// + /// + private async static Task SignInAndInitializeGraphServiceClient(PublicClientApplicationOptions configuration, string[] scopes) + { + GraphServiceClient graphClient = new GraphServiceClient(graphURL, + new DelegateAuthenticationProvider(async (requestMessage) => + { + requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", await SignInUserAndGetTokenUsingMSAL(configuration, scopes)); + })); + + return await Task.FromResult(graphClient); + } + + /// + /// Signs in the user using the device code flow and obtains an Access token for MS Graph + /// + /// + /// + /// + private static async Task SignInUserAndGetTokenUsingMSAL(PublicClientApplicationOptions configuration, string[] scopes) + { + // build the AAd authority Url + string authority = string.Concat(configuration.Instance, configuration.TenantId); + + // Initialize the MSAL library by building a public client application + application = PublicClientApplicationBuilder.Create(configuration.ClientId) + .WithAuthority(authority) + .WithRedirectUri(CustomBrowserWebUi.FindFreeLocalhostRedirectUri()) // required for CustomBrowserWebUi + .Build(); + + + AuthenticationResult result; + + try + { + var accounts = await application.GetAccountsAsync(); + + // Try to acquire an access token from the cache, if UI interaction is required, MsalUiRequiredException will be thrown. + result = await application.AcquireTokenSilent(scopes, accounts.FirstOrDefault()).ExecuteAsync(); + } + catch (MsalUiRequiredException) + { + // Acquiring an access token interactively using custom web UI. + result = await application.AcquireTokenInteractive(scopes) + .WithCustomWebUi(new CustomBrowserWebUi()) //Using our custom web ui + .ExecuteAsync(); + } + + return result.AccessToken; + } + + /// + /// Call MS Graph and print results + /// + /// + /// + private static async Task CallMSGraph(GraphServiceClient graphClient) + { + var me = await graphClient.Me.Request().GetAsync(); + + // Printing the results + Console.Write(Environment.NewLine); + Console.WriteLine("-------- Data from call to MS Graph --------"); + Console.Write(Environment.NewLine); + Console.WriteLine($"Id: {me.Id}"); + Console.WriteLine($"Display Name: {me.DisplayName}"); + Console.WriteLine($"Email: {me.Mail}"); + } + } +} diff --git a/4-desktop-apps/console-web-browser/Console-Interactive-CustomWebUI/appsettings.json b/4-desktop-apps/console-web-browser/Console-Interactive-CustomWebUI/appsettings.json new file mode 100644 index 0000000..070f49e --- /dev/null +++ b/4-desktop-apps/console-web-browser/Console-Interactive-CustomWebUI/appsettings.json @@ -0,0 +1,7 @@ +{ + "Instance": "https://login.microsoftonline.com/", + "ClientId": "[Enter the Client Id (Application ID obtained from the Azure portal), e.g. ba74781c2-53c2-442a-97c2-3d60re42f403]", + "TenantId": "[Enter 'common', or 'organizations' or the Tenant Id (Obtained from the Azure portal. Select 'Endpoints' from the 'App registrations' blade and use the GUID in any of the URLs), e.g. da41245a5-11b3-996c-00a8-4d99re19f292]", + "RedirectUri": "http://localhost", + "GraphApiUrl": "https://graph.microsoft.com/beta/" +} diff --git a/4-desktop-apps/console-web-browser/README.md b/4-desktop-apps/console-web-browser/README.md new file mode 100644 index 0000000..3c45106 --- /dev/null +++ b/4-desktop-apps/console-web-browser/README.md @@ -0,0 +1,255 @@ +--- +services: active-directory +platforms: dotnet +author: TiagoBrenck +level: 400 +client: .NET Desktop (Console) +service: Microsoft Graph +endpoint: Microsoft identity platform +page_type: sample +languages: + - csharp +products: + - azure + - microsoft-entra-id + - dotnet + - office-ms-graph +description: "This sample demonstrates a .NET Desktop (Console) application calling Microsoft Graph using custom web browser" +--- + +# Using the Microsoft identity platform to call Microsoft Graph API with custom web browser. + +![Build badge](https://identitydivision.visualstudio.com/_apis/public/build/definitions/a7934fdd-dcde-4492-a406-7fad6ac00e17//badge) + +## About this sample + +### Overview + +This sample demonstrates how to use a custom web browser on MSAL.NET. + +>Note: **Custom web browser is only available on .NET Core applications.** + +1. The .NET Desktop (Console) application uses the Microsoft Authentication Library (MSAL) to obtain a JWT access token from Microsoft Entra ID. +2. The custom web browser will intercept the authorization code request triggered by MSAL and execute it on a new browser tab that is being listened. +3. Once the authorization request is finished (successfully or not), the custom web browser will print a custom HTML block and return the response to MSAL. + +### Scenario + +The console application: + +- gets an access token from Microsoft Entra ID interactively using a custom web browser (restricted to **.net core only**) +- and then calls the Microsoft Graph `/me` endpoint to get the user information, which it then displays in the console. + +![Overview](./ReadmeFiles/topology.png) + +## How to run this sample + +To run this sample, you'll need: + +- [Visual Studio 2019](https://aka.ms/vsdownload) +- An Internet connection +- a Microsoft Entra tenant. For more information on how to get a Microsoft Entra tenant, see [How to get a Microsoft Entra tenant](https://azure.microsoft.com/documentation/articles/active-directory-howto-tenant/) +- A user account in your Microsoft Entra tenant. This sample will not work with a Microsoft account (formerly Windows Live account). Therefore, if you signed in to the [Microsoft Entra admin center](https://entra.microsoft.com) with a Microsoft account and have never created a user account in your directory before, you need to do that now. + +### Step 1: Clone or download this repository + +From your shell or command line: + +```Shell +git clone https://github.com/Azure-Samples/ms-identity-dotnet-desktop-tutorial.git +``` + +or download and extract the repository .zip file. + +> Given that the name of the sample is quiet long, and so are the names of the referenced NuGet packages, you might want to clone it in a folder close to the root of your hard drive, to avoid file size limitations on Windows. + +### Step 2: Register the sample application with your Microsoft Entra tenant + +There is one project in this sample. To register it, you can: + +- either follow the steps [Step 2: Register the sample with your Microsoft Entra tenant](#step-2-register-the-sample-with-your-azure-active-directory-tenant) and [Step 3: Configure the sample to use your Microsoft Entra tenant](#choose-the-azure-ad-tenant-where-you-want-to-create-your-applications) +- or use PowerShell scripts that: + - **automatically** creates the Microsoft Entra applications and related objects (passwords, permissions, dependencies) for you. Note that this works for Visual Studio only. + - modify the Visual Studio projects' configuration files. + +
+ Expand this section if you want to use this automation: + +1. On Windows, run PowerShell and navigate to the root of the cloned directory +1. In PowerShell run: + + ```PowerShell + Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force + ``` + +1. Run the script to create your Microsoft Entra application and configure the code of the sample application accordingly. +1. In PowerShell run: + + ```PowerShell + cd .\AppCreationScripts\ + .\Configure.ps1 + ``` + + > Other ways of running the scripts are described in [App Creation Scripts](./AppCreationScripts/AppCreationScripts.md) + > The scripts also provide a guide to automated application registration, configuration and removal which can help in your CI/CD scenarios. + +1. Open the Visual Studio solution and click start to run the code. + +
+ +Follow the steps below to manually walk through the steps to register and configure the applications. + +#### Choose the Microsoft Entra tenant where you want to create your applications + +As a first step you'll need to: + +1. Sign in to the [Microsoft Entra admin center](https://entra.microsoft.com) using either a work or school account or a personal Microsoft account. +1. If your account is present in more than one Microsoft Entra tenant, select your profile at the top right corner in the menu on top of the page, and then **switch directory**. + Change your portal session to the desired Microsoft Entra tenant. + +#### Register the client app (Console-Interactive-MultiTarget-v2) + +1. Navigate to the Microsoft identity platform for developers [App registrations](https://go.microsoft.com/fwlink/?linkid=2083908) page. +1. Click **New registration** on top. +1. In the **Register an application page** that appears, enter your application's registration information: + - In the **Name** section, enter a meaningful application name that will be displayed to users of the app, for example `Console-Interactive-MultiTarget-v2`. + - Change **Supported account types** to **Accounts in any organizational directory and personal Microsoft accounts (e.g. Skype, Xbox, Outlook.com)**. +1. Click on the **Register** button in bottom to create the application. +1. In the app's registration screen, find the **Application (client) ID** value and record it for use later. You'll need it to configure the configuration file(s) later in your code. +1. In the app's registration screen, click on the **Authentication** blade in the left. + - If you don't have a platform added yet, click on **Add a platform** and select the **Public client (mobile & desktop)** option. + - In the **Redirect URIs** section, enter the following redirect URIs. + - `http://localhost` + - In the **Redirect URIs** | **Suggested Redirect URIs for public clients (mobile, desktop)** section, select **https://login.microsoftonline.com/common/oauth2/nativeclient** + +1. Click the **Save** button on top to save the changes. +1. In the app's registration screen, click on the **API permissions** blade in the left to open the page where we add access to the Apis that your application needs. + - Click the **Add a permission** button and then, + - Ensure that the **Microsoft APIs** tab is selected. + - In the *Commonly used Microsoft APIs* section, click on **Microsoft Graph** + - In the **Delegated permissions** section, select the **User.Read** in the list. Use the search box if necessary. + - Click on the **Add permissions** button at the bottom. + +##### Configure the client app (Console-Interactive-MultiTarget-v2) to use your app registration + +Open the project in your IDE (like Visual Studio) to configure the code. +>In the steps below, "ClientID" is the same as "Application ID" or "AppId". + +1. Open the `Console-Interactive-MultiTarget\appsettings.json` file +1. Find the app key `ClientId` and replace the existing value with the application ID (clientId) of the `Console-Interactive-MultiTarget-v2` application copied from the Microsoft Entra admin center. +1. Find the app key `TenantId` and replace the existing value with your Microsoft Entra tenant ID. + +### Step 4: Run the sample + +Clean the solution, rebuild the solution, and run it. + +Start the application, sign-in and check the result in the console. + +> [Consider taking a moment to share your experience with us.](https://forms.office.com/Pages/ResponsePage.aspx?id=v4j5cvGGr0GRqy180BHbR73pcsbpbxNJuZCMKN0lURpUREhEVDBOTFBMUVRPUElBUE5WMjdPQ1RaMiQlQCN0PWcu) + +## About the code + +The relevant code for this sample is the `CustomBrowserWebUi.cs` class, that inherits `ICustomWebUi`. + +> Note that MSAL custom web UI is **only available on .Net Core**. + +There are a few considerations about custom web UI: + +- MSAL does not have control over the browser, e.g. MSAL cannot close the window, cannot detect if the user decides to navigate away etc. The app using MSAL can only set a cancellation token / timeout. +- On successful navigation to the redirect URI, the application can write a message back to the browser. The experience can be further enhanced by redirecting the browser to a page of your choice. +- In order to capture the result of the authentication, MSAL listens to a localhost socket. Applications must register "http:\localhost" as a redirect URI. + +1- On `CustomBrowserWebUi.cs`, we implement the interface `ICustomWebUi`, intercepting the authorization URI: + +```csharp +public async Task AcquireAuthorizationCodeAsync( + Uri authorizationUri, + Uri redirectUri, + CancellationToken cancellationToken) +{ + if (!redirectUri.IsLoopback) + { + throw new ArgumentException("Only loopback redirect uri is supported with this WebUI. Configure http://localhost or http://localhost:port during app registration. "); + } + + Uri result = await InterceptAuthorizationUriAsync(authorizationUri,redirectUri,cancellationToken) + .ConfigureAwait(true); + + return result; +} +``` + +The custom web UI can be used in the `AcquireTokenInteractive()` method: + +```c# + await application.AcquireTokenInteractive(scopes) + .WithCustomWebUi(new CustomBrowserWebUi()) //Using our custom web ui + .ExecuteAsync(); +``` + +2- The interception method, `InterceptAuthorizationUriAsync`, opens a new tab on the OS default browser and navigates to the authorization URI, while listening to its response. Then, it displays an HTML block based on the authorization response. + +```csharp +private async Task InterceptAuthorizationUriAsync( + Uri authorizationUri, + Uri redirectUri, + CancellationToken cancellationToken) +{ + OpenBrowser(authorizationUri.ToString()); + using (var listener = new SingleMessageTcpListener(redirectUri.Port)) + { + Uri authCodeUri = null; + await listener.ListenToSingleRequestAndRespondAsync( + (uri) => + { + Trace.WriteLine("Intercepted an auth code url: " + uri.ToString()); + authCodeUri = uri; + + return GetMessageToShowInBroswerAfterAuth(uri); + }, + cancellationToken) + .ConfigureAwait(false); + + return authCodeUri; + } +} +``` + +3- Once the authorization response gets back, the opened tab will display a custom successful or failure message, MSAL will process the response accordingly and the custom web UI flow is concluded. + +Success custom message: +![Overview](./ReadmeFiles/successMessage.png) + +Failure custom message: +![Overview](./ReadmeFiles/failureMessage.png) + +## Community Help and Support + +Use [Stack Overflow](http://stackoverflow.com/questions/tagged/msal) to get support from the community. +Ask your questions on Stack Overflow first and browse existing issues to see if someone has asked your question before. +Make sure that your questions or comments are tagged with [`azure-active-directory` `msal` `dotnet`]. + +If you find a bug in the sample, please raise the issue on [GitHub Issues](../../../../issues). + +To provide a recommendation, visit the following [User Voice page](https://feedback.azure.com/forums/169401-azure-active-directory). + +## Contributing + +If you'd like to contribute to this sample, see [CONTRIBUTING.MD](/CONTRIBUTING.md). + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information, see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +## More information + +For more information, see MSAL.NET's conceptual documentation: + +- [MSAL.NET's conceptual documentation](https://aka.ms/msal-net) +- [Microsoft identity platform (Microsoft Entra ID for developers)](https://learn.microsoft.com/entra/identity-platform/) +- [Quickstart: Register an application with the Microsoft identity platform](https://learn.microsoft.com/entra/identity-platform/quickstart-register-app) +- [Quickstart: Configure a client application to access web APIs](https://learn.microsoft.com/entra/identity-platform/quickstart-configure-app-access-web-apis) + +- [Understanding Microsoft Entra application consent experiences](https://learn.microsoft.com/entra/identity-platform/application-consent-experience) +- [Understand user and admin consent](https://learn.microsoft.com/entra/identity-platform/howto-convert-app-to-be-multi-tenant#understand-user-and-admin-consent) +- [Application and service principal objects in Microsoft Entra ID](https://learn.microsoft.com/entra/identity-platform/app-objects-and-service-principals) + +For more information about how OAuth 2.0 protocols work in this scenario and other scenarios, see [Authentication Scenarios for Microsoft Entra ID](http://go.microsoft.com/fwlink/?LinkId=394414). diff --git a/4-desktop-apps/console-web-browser/ReadmeFiles/failureMessage.png b/4-desktop-apps/console-web-browser/ReadmeFiles/failureMessage.png new file mode 100644 index 0000000..8d682a4 Binary files /dev/null and b/4-desktop-apps/console-web-browser/ReadmeFiles/failureMessage.png differ diff --git a/4-desktop-apps/console-web-browser/ReadmeFiles/successMessage.png b/4-desktop-apps/console-web-browser/ReadmeFiles/successMessage.png new file mode 100644 index 0000000..96fc552 Binary files /dev/null and b/4-desktop-apps/console-web-browser/ReadmeFiles/successMessage.png differ diff --git a/4-desktop-apps/console-web-browser/ReadmeFiles/topology.png b/4-desktop-apps/console-web-browser/ReadmeFiles/topology.png new file mode 100644 index 0000000..185b93d Binary files /dev/null and b/4-desktop-apps/console-web-browser/ReadmeFiles/topology.png differ diff --git a/desktop-winforms/.gitignore b/4-desktop-apps/desktop-winforms/.gitignore similarity index 100% rename from desktop-winforms/.gitignore rename to 4-desktop-apps/desktop-winforms/.gitignore diff --git a/desktop-winforms/MainWindow.Designer.cs b/4-desktop-apps/desktop-winforms/MainWindow.Designer.cs similarity index 98% rename from desktop-winforms/MainWindow.Designer.cs rename to 4-desktop-apps/desktop-winforms/MainWindow.Designer.cs index 6d539a1..42349c2 100644 --- a/desktop-winforms/MainWindow.Designer.cs +++ b/4-desktop-apps/desktop-winforms/MainWindow.Designer.cs @@ -1,205 +1,205 @@ -namespace MsalExample -{ - partial class MainWindow - { - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; - - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) - { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Windows Form Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - System.Windows.Forms.Button ExitButton; - System.Windows.Forms.TableLayoutPanel tableLayoutPanel1; - System.Windows.Forms.Label label2; - System.Windows.Forms.Button SignInButton; - System.Windows.Forms.Label label1; - System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainWindow)); - this.AccessTokenSourceLabel = new System.Windows.Forms.Label(); - this.SignOutButton = new System.Windows.Forms.Button(); - this.SignInCallToActionLabel = new System.Windows.Forms.Label(); - this.GraphResultsPanel = new System.Windows.Forms.Panel(); - this.GraphResultsTextBox = new System.Windows.Forms.TextBox(); - ExitButton = new System.Windows.Forms.Button(); - tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel(); - label2 = new System.Windows.Forms.Label(); - SignInButton = new System.Windows.Forms.Button(); - label1 = new System.Windows.Forms.Label(); - tableLayoutPanel1.SuspendLayout(); - this.GraphResultsPanel.SuspendLayout(); - this.SuspendLayout(); - // - // ExitButton - // - ExitButton.Anchor = System.Windows.Forms.AnchorStyles.Bottom; - ExitButton.Location = new System.Drawing.Point(432, 598); - ExitButton.Name = "ExitButton"; - ExitButton.Size = new System.Drawing.Size(112, 34); - ExitButton.TabIndex = 3; - ExitButton.Text = "E&xit"; - ExitButton.UseVisualStyleBackColor = true; - ExitButton.Click += new System.EventHandler(this.ExitButton_Click); - // - // tableLayoutPanel1 - // - tableLayoutPanel1.ColumnCount = 3; - tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); - tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); - tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); - tableLayoutPanel1.Controls.Add(this.AccessTokenSourceLabel, 1, 0); - tableLayoutPanel1.Controls.Add(label2, 0, 0); - tableLayoutPanel1.Controls.Add(this.SignOutButton, 2, 0); - tableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Bottom; - tableLayoutPanel1.Location = new System.Drawing.Point(0, 486); - tableLayoutPanel1.Name = "tableLayoutPanel1"; - tableLayoutPanel1.RowCount = 1; - tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50F)); - tableLayoutPanel1.Size = new System.Drawing.Size(954, 43); - tableLayoutPanel1.TabIndex = 2; - // - // AccessTokenSourceLabel - // - this.AccessTokenSourceLabel.Anchor = System.Windows.Forms.AnchorStyles.Left; - this.AccessTokenSourceLabel.AutoSize = true; - this.AccessTokenSourceLabel.Location = new System.Drawing.Point(123, 9); - this.AccessTokenSourceLabel.Margin = new System.Windows.Forms.Padding(0, 0, 3, 0); - this.AccessTokenSourceLabel.Name = "AccessTokenSourceLabel"; - this.AccessTokenSourceLabel.Size = new System.Drawing.Size(218, 25); - this.AccessTokenSourceLabel.TabIndex = 1; - this.AccessTokenSourceLabel.Text = "[Cached | Newly Acquired]"; - // - // label2 - // - label2.Anchor = System.Windows.Forms.AnchorStyles.Left; - label2.AutoSize = true; - label2.Location = new System.Drawing.Point(3, 9); - label2.Margin = new System.Windows.Forms.Padding(3, 0, 0, 0); - label2.Name = "label2"; - label2.Size = new System.Drawing.Size(120, 25); - label2.TabIndex = 0; - label2.Text = "Access Token:"; - // - // SignOutButton - // - this.SignOutButton.Anchor = System.Windows.Forms.AnchorStyles.Right; - this.SignOutButton.Location = new System.Drawing.Point(839, 4); - this.SignOutButton.Name = "SignOutButton"; - this.SignOutButton.Size = new System.Drawing.Size(112, 34); - this.SignOutButton.TabIndex = 2; - this.SignOutButton.Text = "Sign &Out"; - this.SignOutButton.UseVisualStyleBackColor = true; - this.SignOutButton.Click += new System.EventHandler(this.SignOutButton_Click); - // - // SignInButton - // - SignInButton.Anchor = System.Windows.Forms.AnchorStyles.Top; - SignInButton.Location = new System.Drawing.Point(329, 17); - SignInButton.Name = "SignInButton"; - SignInButton.Size = new System.Drawing.Size(315, 34); - SignInButton.TabIndex = 0; - SignInButton.Text = "&Sign In (if needed) && Call Graph"; - SignInButton.UseVisualStyleBackColor = true; - SignInButton.Click += new System.EventHandler(this.SignInButton_Click); - // - // label1 - // - label1.AutoSize = true; - label1.Dock = System.Windows.Forms.DockStyle.Top; - label1.Location = new System.Drawing.Point(0, 0); - label1.Name = "label1"; - label1.Size = new System.Drawing.Size(226, 25); - label1.TabIndex = 0; - label1.Text = "Microsoft Graph Response:"; - // - // SignInCallToActionLabel - // - this.SignInCallToActionLabel.Anchor = System.Windows.Forms.AnchorStyles.Top; - this.SignInCallToActionLabel.AutoSize = true; - this.SignInCallToActionLabel.Location = new System.Drawing.Point(189, 211); - this.SignInCallToActionLabel.Name = "SignInCallToActionLabel"; - this.SignInCallToActionLabel.Size = new System.Drawing.Size(578, 75); - this.SignInCallToActionLabel.TabIndex = 2; - this.SignInCallToActionLabel.Text = "This application will access Microsoft Graph, if you authorize it to do so.\r\n\r\nCl" + - "ick the Sign In button above to get started."; - this.SignInCallToActionLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; - this.SignInCallToActionLabel.UseMnemonic = false; - // - // GraphResultsPanel - // - this.GraphResultsPanel.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) - | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); - this.GraphResultsPanel.Controls.Add(label1); - this.GraphResultsPanel.Controls.Add(this.GraphResultsTextBox); - this.GraphResultsPanel.Controls.Add(tableLayoutPanel1); - this.GraphResultsPanel.Location = new System.Drawing.Point(12, 63); - this.GraphResultsPanel.Name = "GraphResultsPanel"; - this.GraphResultsPanel.Size = new System.Drawing.Size(954, 529); - this.GraphResultsPanel.TabIndex = 1; - this.GraphResultsPanel.Visible = false; - // - // GraphResultsTextBox - // - this.GraphResultsTextBox.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) - | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); - this.GraphResultsTextBox.Location = new System.Drawing.Point(8, 28); - this.GraphResultsTextBox.Multiline = true; - this.GraphResultsTextBox.Name = "GraphResultsTextBox"; - this.GraphResultsTextBox.ReadOnly = true; - this.GraphResultsTextBox.ScrollBars = System.Windows.Forms.ScrollBars.Both; - this.GraphResultsTextBox.Size = new System.Drawing.Size(940, 452); - this.GraphResultsTextBox.TabIndex = 1; - // - // MainWindow - // - this.AutoScaleDimensions = new System.Drawing.SizeF(10F, 25F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.CancelButton = ExitButton; - this.ClientSize = new System.Drawing.Size(978, 644); - this.Controls.Add(this.GraphResultsPanel); - this.Controls.Add(SignInButton); - this.Controls.Add(ExitButton); - this.Controls.Add(this.SignInCallToActionLabel); - this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); - this.MaximizeBox = false; - this.MinimumSize = new System.Drawing.Size(800, 500); - this.Name = "MainWindow"; - this.Text = "MSAL Windows Forms Sample"; - tableLayoutPanel1.ResumeLayout(false); - tableLayoutPanel1.PerformLayout(); - this.GraphResultsPanel.ResumeLayout(false); - this.GraphResultsPanel.PerformLayout(); - this.ResumeLayout(false); - this.PerformLayout(); - - } - - #endregion - - private Label SignInCallToActionLabel; - private Panel GraphResultsPanel; - private Label AccessTokenSourceLabel; - private Button SignOutButton; - private TextBox GraphResultsTextBox; - } +namespace MsalExample +{ + partial class MainWindow + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + System.Windows.Forms.Button ExitButton; + System.Windows.Forms.TableLayoutPanel tableLayoutPanel1; + System.Windows.Forms.Label label2; + System.Windows.Forms.Button SignInButton; + System.Windows.Forms.Label label1; + System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainWindow)); + this.AccessTokenSourceLabel = new System.Windows.Forms.Label(); + this.SignOutButton = new System.Windows.Forms.Button(); + this.SignInCallToActionLabel = new System.Windows.Forms.Label(); + this.GraphResultsPanel = new System.Windows.Forms.Panel(); + this.GraphResultsTextBox = new System.Windows.Forms.TextBox(); + ExitButton = new System.Windows.Forms.Button(); + tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel(); + label2 = new System.Windows.Forms.Label(); + SignInButton = new System.Windows.Forms.Button(); + label1 = new System.Windows.Forms.Label(); + tableLayoutPanel1.SuspendLayout(); + this.GraphResultsPanel.SuspendLayout(); + this.SuspendLayout(); + // + // ExitButton + // + ExitButton.Anchor = System.Windows.Forms.AnchorStyles.Bottom; + ExitButton.Location = new System.Drawing.Point(432, 598); + ExitButton.Name = "ExitButton"; + ExitButton.Size = new System.Drawing.Size(112, 34); + ExitButton.TabIndex = 3; + ExitButton.Text = "E&xit"; + ExitButton.UseVisualStyleBackColor = true; + ExitButton.Click += new System.EventHandler(this.ExitButton_Click); + // + // tableLayoutPanel1 + // + tableLayoutPanel1.ColumnCount = 3; + tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); + tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); + tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); + tableLayoutPanel1.Controls.Add(this.AccessTokenSourceLabel, 1, 0); + tableLayoutPanel1.Controls.Add(label2, 0, 0); + tableLayoutPanel1.Controls.Add(this.SignOutButton, 2, 0); + tableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Bottom; + tableLayoutPanel1.Location = new System.Drawing.Point(0, 486); + tableLayoutPanel1.Name = "tableLayoutPanel1"; + tableLayoutPanel1.RowCount = 1; + tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50F)); + tableLayoutPanel1.Size = new System.Drawing.Size(954, 43); + tableLayoutPanel1.TabIndex = 2; + // + // AccessTokenSourceLabel + // + this.AccessTokenSourceLabel.Anchor = System.Windows.Forms.AnchorStyles.Left; + this.AccessTokenSourceLabel.AutoSize = true; + this.AccessTokenSourceLabel.Location = new System.Drawing.Point(123, 9); + this.AccessTokenSourceLabel.Margin = new System.Windows.Forms.Padding(0, 0, 3, 0); + this.AccessTokenSourceLabel.Name = "AccessTokenSourceLabel"; + this.AccessTokenSourceLabel.Size = new System.Drawing.Size(218, 25); + this.AccessTokenSourceLabel.TabIndex = 1; + this.AccessTokenSourceLabel.Text = "[Cached | Newly Acquired]"; + // + // label2 + // + label2.Anchor = System.Windows.Forms.AnchorStyles.Left; + label2.AutoSize = true; + label2.Location = new System.Drawing.Point(3, 9); + label2.Margin = new System.Windows.Forms.Padding(3, 0, 0, 0); + label2.Name = "label2"; + label2.Size = new System.Drawing.Size(120, 25); + label2.TabIndex = 0; + label2.Text = "Access Token:"; + // + // SignOutButton + // + this.SignOutButton.Anchor = System.Windows.Forms.AnchorStyles.Right; + this.SignOutButton.Location = new System.Drawing.Point(839, 4); + this.SignOutButton.Name = "SignOutButton"; + this.SignOutButton.Size = new System.Drawing.Size(112, 34); + this.SignOutButton.TabIndex = 2; + this.SignOutButton.Text = "Sign &Out"; + this.SignOutButton.UseVisualStyleBackColor = true; + this.SignOutButton.Click += new System.EventHandler(this.SignOutButton_Click); + // + // SignInButton + // + SignInButton.Anchor = System.Windows.Forms.AnchorStyles.Top; + SignInButton.Location = new System.Drawing.Point(329, 17); + SignInButton.Name = "SignInButton"; + SignInButton.Size = new System.Drawing.Size(315, 34); + SignInButton.TabIndex = 0; + SignInButton.Text = "&Sign In (if needed) && Call Graph"; + SignInButton.UseVisualStyleBackColor = true; + SignInButton.Click += new System.EventHandler(this.SignInButton_Click); + // + // label1 + // + label1.AutoSize = true; + label1.Dock = System.Windows.Forms.DockStyle.Top; + label1.Location = new System.Drawing.Point(0, 0); + label1.Name = "label1"; + label1.Size = new System.Drawing.Size(226, 25); + label1.TabIndex = 0; + label1.Text = "Microsoft Graph Response:"; + // + // SignInCallToActionLabel + // + this.SignInCallToActionLabel.Anchor = System.Windows.Forms.AnchorStyles.Top; + this.SignInCallToActionLabel.AutoSize = true; + this.SignInCallToActionLabel.Location = new System.Drawing.Point(189, 211); + this.SignInCallToActionLabel.Name = "SignInCallToActionLabel"; + this.SignInCallToActionLabel.Size = new System.Drawing.Size(578, 75); + this.SignInCallToActionLabel.TabIndex = 2; + this.SignInCallToActionLabel.Text = "This application will access Microsoft Graph, if you authorize it to do so.\r\n\r\nCl" + + "ick the Sign In button above to get started."; + this.SignInCallToActionLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; + this.SignInCallToActionLabel.UseMnemonic = false; + // + // GraphResultsPanel + // + this.GraphResultsPanel.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.GraphResultsPanel.Controls.Add(label1); + this.GraphResultsPanel.Controls.Add(this.GraphResultsTextBox); + this.GraphResultsPanel.Controls.Add(tableLayoutPanel1); + this.GraphResultsPanel.Location = new System.Drawing.Point(12, 63); + this.GraphResultsPanel.Name = "GraphResultsPanel"; + this.GraphResultsPanel.Size = new System.Drawing.Size(954, 529); + this.GraphResultsPanel.TabIndex = 1; + this.GraphResultsPanel.Visible = false; + // + // GraphResultsTextBox + // + this.GraphResultsTextBox.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.GraphResultsTextBox.Location = new System.Drawing.Point(8, 28); + this.GraphResultsTextBox.Multiline = true; + this.GraphResultsTextBox.Name = "GraphResultsTextBox"; + this.GraphResultsTextBox.ReadOnly = true; + this.GraphResultsTextBox.ScrollBars = System.Windows.Forms.ScrollBars.Both; + this.GraphResultsTextBox.Size = new System.Drawing.Size(940, 452); + this.GraphResultsTextBox.TabIndex = 1; + // + // MainWindow + // + this.AutoScaleDimensions = new System.Drawing.SizeF(10F, 25F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.CancelButton = ExitButton; + this.ClientSize = new System.Drawing.Size(978, 644); + this.Controls.Add(this.GraphResultsPanel); + this.Controls.Add(SignInButton); + this.Controls.Add(ExitButton); + this.Controls.Add(this.SignInCallToActionLabel); + this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); + this.MaximizeBox = false; + this.MinimumSize = new System.Drawing.Size(800, 500); + this.Name = "MainWindow"; + this.Text = "MSAL Windows Forms Sample"; + tableLayoutPanel1.ResumeLayout(false); + tableLayoutPanel1.PerformLayout(); + this.GraphResultsPanel.ResumeLayout(false); + this.GraphResultsPanel.PerformLayout(); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private Label SignInCallToActionLabel; + private Panel GraphResultsPanel; + private Label AccessTokenSourceLabel; + private Button SignOutButton; + private TextBox GraphResultsTextBox; + } } \ No newline at end of file diff --git a/desktop-winforms/MainWindow.cs b/4-desktop-apps/desktop-winforms/MainWindow.cs similarity index 97% rename from desktop-winforms/MainWindow.cs rename to 4-desktop-apps/desktop-winforms/MainWindow.cs index 4c5ade4..df66b1b 100644 --- a/desktop-winforms/MainWindow.cs +++ b/4-desktop-apps/desktop-winforms/MainWindow.cs @@ -1,118 +1,118 @@ -using Microsoft.Identity.Client; -using System.Net.Http.Headers; -using System.Text.Encodings.Web; -using System.Text.Json; - -namespace MsalExample -{ - public partial class MainWindow : Form - { - private readonly HttpClient _httpClient = new(); - - // In order to take advantage of token caching, your MSAL client singleton must - // have a lifecycle that at least matches the lifecycle of the user's session in - // the application. In this sample, the lifecycle of the MSAL client is tied to - // the lifecycle of this form instance, which is the whole of the application. - private readonly IPublicClientApplication msalPublicClientApp; - - public MainWindow() - { - InitializeComponent(); - - // Configure your public client application - msalPublicClientApp = PublicClientApplicationBuilder - .CreateWithApplicationOptions(new PublicClientApplicationOptions - { - // Enter the tenant ID obtained from the Microsoft Entra admin center - TenantId = "Enter the client ID obtained from the Microsoft Entra admin center", - - // Enter the client ID obtained from the Microsoft Entra admin center - ClientId = "Enter the tenant ID obtained from the Microsoft Entra admin center" - }) - .WithDefaultRedirectUri() // http://localhost - .Build(); - } - - // - // Handle the "Sign In" button click. This will acquire an access token scoped to - // Microsoft Graph, either from the cache or from an interactive session. It will - // then use that access token in an HTTP request to Microsoft Graph and display - // the results. - // - private async void SignInButton_Click(object sender, EventArgs e) - { - AuthenticationResult? msalAuthenticationResult = null; - - // Acquire a cached access token for Microsoft Graph if one is available from a prior - // execution of this authentication flow. - var accounts = await msalPublicClientApp.GetAccountsAsync(); - if (accounts.Any()) - { - try - { - // Will return a cached access token if available, refreshing if necessary. - msalAuthenticationResult = await msalPublicClientApp.AcquireTokenSilent( - new[] { "https://graph.microsoft.com/User.Read" }, - accounts.First()) - .ExecuteAsync(); - } - catch (MsalUiRequiredException) - { - // Nothing in cache for this account + scope, and interactive experience required. - } - } - - if (msalAuthenticationResult == null) - { - // This is likely the first authentication request since application start or after - // Sign Out was clicked, so calling this will launch the user's default browser and - // send them through a login flow. After the flow is complete, the rest of this method - // will continue to execute. - msalAuthenticationResult = await msalPublicClientApp.AcquireTokenInteractive( - new[] { "https://graph.microsoft.com/User.Read" }) - .ExecuteAsync(); - } - - // Call Microsoft Graph using the access token acquired above. - using var graphRequest = new HttpRequestMessage(HttpMethod.Get, "https://graph.microsoft.com/v1.0/me"); - graphRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", msalAuthenticationResult.AccessToken); - var graphResponseMessage = await _httpClient.SendAsync(graphRequest); - graphResponseMessage.EnsureSuccessStatusCode(); - - // Present the results to the user (formatting the json for readability) - using var graphResponseJson = JsonDocument.Parse(await graphResponseMessage.Content.ReadAsStreamAsync()); - GraphResultsTextBox.Text = JsonSerializer.Serialize(graphResponseJson, new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); - var tokenWasFromCache = TokenSource.Cache == msalAuthenticationResult.AuthenticationResultMetadata.TokenSource; - AccessTokenSourceLabel.Text = $"{(tokenWasFromCache ? "Cached" : "Newly Acquired")} (Expires: {msalAuthenticationResult.ExpiresOn:R})"; - - // Hide the call to action and show the results. - SignInCallToActionLabel.Hide(); - GraphResultsPanel.Show(); - } - - /// - /// Handle the "Sign Out" button click. This will remove all cached tokens from - /// the MSAL client, resulting in any future usage requiring a reauthentication - /// experience. - /// - private async void SignOutButton_Click(object sender, EventArgs e) - { - // Signing out is removing all cached tokens, meaning the next token request will - // require the user to sign in. - foreach (var account in (await msalPublicClientApp.GetAccountsAsync()).ToList()) - { - await msalPublicClientApp.RemoveAsync(account); - } - - // Show the call to action and hide the results. - GraphResultsPanel.Hide(); - GraphResultsTextBox.Clear(); - SignInCallToActionLabel.Show(); - } - - private void ExitButton_Click(object sender, EventArgs e) - { - Application.Exit(); - } - } -} +using Microsoft.Identity.Client; +using System.Net.Http.Headers; +using System.Text.Encodings.Web; +using System.Text.Json; + +namespace MsalExample +{ + public partial class MainWindow : Form + { + private readonly HttpClient _httpClient = new(); + + // In order to take advantage of token caching, your MSAL client singleton must + // have a lifecycle that at least matches the lifecycle of the user's session in + // the application. In this sample, the lifecycle of the MSAL client is tied to + // the lifecycle of this form instance, which is the whole of the application. + private readonly IPublicClientApplication msalPublicClientApp; + + public MainWindow() + { + InitializeComponent(); + + // Configure your public client application + msalPublicClientApp = PublicClientApplicationBuilder + .CreateWithApplicationOptions(new PublicClientApplicationOptions + { + // Enter the tenant ID obtained from the Microsoft Entra admin center + TenantId = "Enter the client ID obtained from the Microsoft Entra admin center", + + // Enter the client ID obtained from the Microsoft Entra admin center + ClientId = "Enter the tenant ID obtained from the Microsoft Entra admin center" + }) + .WithDefaultRedirectUri() // http://localhost + .Build(); + } + + // + // Handle the "Sign In" button click. This will acquire an access token scoped to + // Microsoft Graph, either from the cache or from an interactive session. It will + // then use that access token in an HTTP request to Microsoft Graph and display + // the results. + // + private async void SignInButton_Click(object sender, EventArgs e) + { + AuthenticationResult? msalAuthenticationResult = null; + + // Acquire a cached access token for Microsoft Graph if one is available from a prior + // execution of this authentication flow. + var accounts = await msalPublicClientApp.GetAccountsAsync(); + if (accounts.Any()) + { + try + { + // Will return a cached access token if available, refreshing if necessary. + msalAuthenticationResult = await msalPublicClientApp.AcquireTokenSilent( + new[] { "https://graph.microsoft.com/User.Read" }, + accounts.First()) + .ExecuteAsync(); + } + catch (MsalUiRequiredException) + { + // Nothing in cache for this account + scope, and interactive experience required. + } + } + + if (msalAuthenticationResult == null) + { + // This is likely the first authentication request since application start or after + // Sign Out was clicked, so calling this will launch the user's default browser and + // send them through a login flow. After the flow is complete, the rest of this method + // will continue to execute. + msalAuthenticationResult = await msalPublicClientApp.AcquireTokenInteractive( + new[] { "https://graph.microsoft.com/User.Read" }) + .ExecuteAsync(); + } + + // Call Microsoft Graph using the access token acquired above. + using var graphRequest = new HttpRequestMessage(HttpMethod.Get, "https://graph.microsoft.com/v1.0/me"); + graphRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", msalAuthenticationResult.AccessToken); + var graphResponseMessage = await _httpClient.SendAsync(graphRequest); + graphResponseMessage.EnsureSuccessStatusCode(); + + // Present the results to the user (formatting the json for readability) + using var graphResponseJson = JsonDocument.Parse(await graphResponseMessage.Content.ReadAsStreamAsync()); + GraphResultsTextBox.Text = JsonSerializer.Serialize(graphResponseJson, new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); + var tokenWasFromCache = TokenSource.Cache == msalAuthenticationResult.AuthenticationResultMetadata.TokenSource; + AccessTokenSourceLabel.Text = $"{(tokenWasFromCache ? "Cached" : "Newly Acquired")} (Expires: {msalAuthenticationResult.ExpiresOn:R})"; + + // Hide the call to action and show the results. + SignInCallToActionLabel.Hide(); + GraphResultsPanel.Show(); + } + + /// + /// Handle the "Sign Out" button click. This will remove all cached tokens from + /// the MSAL client, resulting in any future usage requiring a reauthentication + /// experience. + /// + private async void SignOutButton_Click(object sender, EventArgs e) + { + // Signing out is removing all cached tokens, meaning the next token request will + // require the user to sign in. + foreach (var account in (await msalPublicClientApp.GetAccountsAsync()).ToList()) + { + await msalPublicClientApp.RemoveAsync(account); + } + + // Show the call to action and hide the results. + GraphResultsPanel.Hide(); + GraphResultsTextBox.Clear(); + SignInCallToActionLabel.Show(); + } + + private void ExitButton_Click(object sender, EventArgs e) + { + Application.Exit(); + } + } +} diff --git a/desktop-winforms/MainWindow.resx b/4-desktop-apps/desktop-winforms/MainWindow.resx similarity index 98% rename from desktop-winforms/MainWindow.resx rename to 4-desktop-apps/desktop-winforms/MainWindow.resx index 6d79c24..1c0f849 100644 --- a/desktop-winforms/MainWindow.resx +++ b/4-desktop-apps/desktop-winforms/MainWindow.resx @@ -1,294 +1,294 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - False - - - False - - - False - - - False - - - False - - - - - AAABAAEAAAAAAAEAIAAEMgAAFgAAAIlQTkcNChoKAAAADUlIRFIAAAEAAAABAAgGAAAAXHKoZgAAAAFv - ck5UAc+id5oAADG+SURBVHja7X0HnFTVvf+ULbCFugvSBREQCyKILQgJYokNWywhkqfR6LMkMY8XzYvy - YppJXl70n2Y+iVFjisGXxERjSHZm7uzszvZeYNmlw1IElrl1+j3/87szFxYCsjM7O/fcO7/vx8sq7s7O - nHN+3/PrP5sNgUAgELmFjzUIKz7RKO2a1ySUbSDEgSuCQOQQbu8M3jCOE8m51SL5ZLMyC1cEgcghrGyS - bijlBFJEn8lV4kpcEQQih1DqlSocHoHY6VPsCYSW1MlrcFUQCItjRRcpmVcree1ugdg8iQeIYI5fiD2y - WVmBK4RAWBh3t0vVozwnhH8wCUz1iZtxhRAIq8ItLbK7AiH7aQgAHtAKijjhZdsGjAogEJbC2KrAnHy3 - MHAm4bcl/QFO+lxeL/3uno0bnbhqCIQFcFWDtLicEwTHRwj/YBIo5gRyV5vyI1w5BMLkeLwjOOeiWmlI - wj+YBEZREijxCeW4ggiEiXFJvbQ5FeE/iQQ8/PZy7tA5uIoIhAkxgeO/5HQH4vY0CECPDMyoEo/d3qnM - wNVEIEyERfXytwo8fFqCP/jJA6dgrbj9C61kHK4qAmEC3NMe+tJYD686hin8uingcGvRgdtxZREIE6DM - K4QzIfwnhQfdgXCZV/wUri4CwSjGVwyMzXcFqu0ZFP7BJDCxUiArmrBmAIFgDrc0kaLpPrHHMQLCP9gp - OMnLhx/oCF6HK45AMIJH+0nR0nrJ50ze1LYRJgG7i3/bRogdVx6BYAB5FYHL890jL/wnagZ4Mo4TNs3i - yChcfQTCQIzyCNc43LyULeHX/QEQHlxcJ3yAO4BAGATI8R9fyUezKfynhgdva5XfwJ1AILKM+7vkZdO8 - ouAwQPgH+wOc7sBBm0ucjDuCQGTz9m8UY04DhX+wJlDoFvh5fmEB7goCMdIgxFHMia8Obull9AOawMxq - 6cMbW1SsHkQgRhLza8SXCjzZ8/gPmQTcWiORgxt2hlATQCAyjRUcyVvdpPyo0BVQWRN+/QGTpNQrfgFz - BBCIDOMC/8CsCZUiYVX4dX9APn0W1IrP4I4hEBnCZL84qcgd6Hdw7Ar/YH9AqedYfGVL8GHcOQRimJj7 - gVpY5g10OTzsC/9gTaCcaiv/vT30HO4gApEmHu4QJ19cK+5yuM0j/IM1gQKXWD25TS3GnUQg0sBdbXJf - AScwbfeftXrQJzTiTiIQKWKMV1wNRTc2kwr/4HThS2uFTet2YuEQAjEkzKgSVxe6+KDdxMJ/UqYgfR7u - CNbhziIQZ8GnOkN3TazkY1YQ/sEkQLWZwGgPfwXuMAJxBnxuc2je3CohbCXhH0wCRe6Acn6tcAHuNAJx - Orj4t83o8U/FKTjZKxy9qy28GDcbgUhiSRPJn1QpvuZwm9fjn0rNwMebxegGghOIEQgNn2iU/lhoccE/ - dQx5vkf4Hu48IudxY7O4tshj/Zv/dDUDc6r59XgCEDkLaKqZ5w7055Lwnxoe/GSL9AOsHkTkHBb7lVml - bn5PLgr/4PJhqBm4qlmZhScCkTP4t161/Lxq6Ygzh4X/pA7DLqEPphrhyUBYHs/0qTOWNko7rBzuSyc8 - WO4V666sUUfjCUFYGsXuwAP5Oeb0G2p48LI6efPDe9QJeEoQlsS0SuHzee5ADIX/9E+h1lxUXIsnBWE5 - rGxW7h3D8SoK/1nGkHsC8fGV/NV4YhCWwbNbw/dP4oSIA4V8aDUDLkE8r0pegicHYX5wXJ7TLbyFN39q - JDDHL5JHNisr8AAhTIu5H/QWTvCKXrz508sRKHYJ79s2EieeJIQpsbhW2liAHv/0ewjQp4QTXsGThDAd - 7utS3svDWP/wnYIuXj2vWnwVBqPgqUKYA25+vt0dOIg3f2ZIgGoB5N528VU8WAjmMb+anz/azSt2xlXr - VP+f4c1F/3nsoO2f8lQ8YQhmcZV/YNY5lfxBB4uqNKf35qNPBU9sLvr/dBPFnXjg7+2uZH4+Y+3ItfCg - R+ydxe3E7sII9vBcb2jhZTWixJzwJ5pvkKk+kVzRIJOHNwfJ8ztC5Bf9EfLGgQj555EYeYt+/dX+CPnG - zhB5fGuQrGpRyJwqkYzmEnn6LJHAlEpx2/JGaQqeOAQ7IMRe5hO/42SouAaEZRQnkpXNCnlzf5RsV+Ik - EFUJ/YfEBj3xU/4bHjFGyIGwSt77MEru6lDIWG8iV5+F2YSwxisblb3YUgzBDKb4xKfyGLGfQVBhivC6 - zQqpPBYjkTghVKa1B4Qdvn4U9O+LD/r3XjlO1vcGyaxqibDQt1D7/f88dieePIThWFInf77QY3yBjz15 - O65okkltIErCQxD2oQJeJ5YkgnXdQc00MPLz2unvL3AHwuWV0g14AhGGYf22yPpzqG1tZ0DlH0WFYn1f - iByJqEO66dMhAXjdEFUNfnMgQqb4JEP9A7DmxfQzL6mTHsSTiMg6JtQdGZPnOrbZwcDNP4mq/D/aEybh - eOYF/0xE0MDHycW1sqHkB2t/TpUQ/WwnagKILKPYI9QY7fSD0N7MKpF4B6Ikoo688J9KBLuCKlndIhuu - CZxbLR7GE4nICm7sVcfMrhFr7G7jb/4xlADeOxI97uTLNuB3QnRhYY3x5kAJJ7xhw3RhxEhjTYvcUmiw - x19rqU2F/2d7I5q33kgACTTyMVJeKRm6JqCNXVwr/glPKGLkUBG43O4KhFhw+kFCjxQnhhMASUYIXu+P - kmIDiTGRKSiQW1vEn2GOACLjmFwlXpjnEgQWwn2LG2QiUqlTCRuA9wEOyMe3hIiTgchAyQdCOZ5YRMaw - qlm5arxXkFgQ/nFegWw6EmNG+HWAJrJNiZPz/JLh5lGeW9g9ufLYbDy5iGHjy72hhRfUCoqDibx+nqxp - V4gSo7cuawyQJIEXtoeJ0Q5SyFacVS2Kd7TL0/EEI4aFi/zyZgcjhTCj6eM6GtVsbhYBb2t3UCXTqkXD - i4iclAQW+YU9T2zBEmJEmijhhOccrgATrbzhPVzTKJNgnBBG5f94OPI/tgZJHgs9BKAS0h14AE8yImVc - 1Sz9MN/NTk083Kgv7Q4z4fU/GwlwA1FSyhlfPZioj+BjkyvFJ/FEI4aMm1qU50oZqoWHgwzvxzsQI6wD - zJO9VE2ZWy2yUR1JHyhnXt6s3IsnG3F2fNBbWMrxYeYaYVSJWo2+agINAEKCt7YrxObmmVm/Mk6IPNEb - RnMAcWaM2RSYUOAWGlnriwfv54YWRavEUwn7gAjFF3tDhkcDTtUE8lz8/2G6MOK0WNF6bNxUn7iTxSEe - 4Mx6qic0ImW+I6UF/HJfhCkCON4shQtU4hhyxEn4965DJVfUiQ1OBoVfC2m5BPIDcACaQfqTBOA+Gj3R - dJShB/ojLq6X34c2bnjyERo+0yn3ON3sTvABAvgFvVFjJiKAZiHGJAHYkzkCd7Qp7+LJR9jGcuJKZ0WA - 6T7+DkoAb+2PmEoD2KaozJkAJ+cIBA4VcoG5KAE5jMX1ylVjOD7O+gQf0AD+d3eE+RyAwQRQdYxNE+Dk - wiE+flGteAlKQi6q/ZuVqyZxgmyGyb1AAM9tDxvW+CMdAnj/wyizGsDgyMB0n3jkBpwzkHtY1iDGzDK2 - G7zX67qCmg/ALATwk70R5glAX9ur6iVsKZYz4Ehenkf4tZkGd8J7XVArET5qjkQgIKrPdAeZSQQ6OwkE - 4pQInsXIQA5gvl/8ab6HXY//mQhgolcg3VKceQIAR+VARCVXNMqmWl8oXjq/WlqPEmJRPNpE8j/eLP+i - oIKN6r5UnwL6/P5ghHn1HwhgCyUqmEXIwiixlJyCnkBsRZP4RdQELIgL/AOzxleKxIzCrx/Qx7aEtNl+ - rJsAr1L7v8BkWpbuFJxMz8hzO0IvoMRYCBNcR6YVegKHzHQjnY4AplWJWqUdy/0A5DghVzRIxGHidXZ6 - +LbxTWQsSo4FAPPkJ/mEPrMeyFMbXHxzZ/j44E4WCaDiaExLtzXzWic0Ab4VpcfkuG+LPHWBn99rhnDU - UA8mDOHYF1KZSwoC4YcoBQwntVtgraH/4sIasfa2LWopSpJJMcYduD/fhLbo2QZgvLAtzCQBvHMoqg0p - tcp6gx+jyHtsMUqSCVHCHbvbbpI4dMpjsLwiqQ3EmCIBIID3DkdJuYkdrafXBALhEo+wAiXKRDi3Wr6t - wBMI2y0m/Md9AckGITAchKUCIUgA+vGesNa92Cprb+e0bszy/BrxYpQsE+DuTuXBCVwgbkXh1wngglqJ - 1AfizDkD4b1E6Jt6ri9ECixkCsDnmOoVhQe7lWtRwhjGp9tD8+f4+ahVhd+RaGZBtsrshgJBI+GpKnBX - h2KaVOChrv2yOiH8aFN/EUoao8hzC+853Na89eG5tkkhfUrcFK3BD0dUsqrFvPkAp/cHCCTfJbyCksYY - FnaRgqk+6TcOt7U8/oPz1G9ulcneoEk6giQ1gR4pThbUWIcEYC/yXbw6p1p6AdOFGcINjcLfCzzWFH4g - tXs6ZXIkopqmIciprcEmWigyAJ9jlEckd7VJP0LJYwCrm8W1oz2CpVRN/aABqUFZ7UCUMOHtT6chCZAW - tDMrthBBw1mDcOdNzfJSlEADMZYj45yuwGGbRYX/6Z4wkWKpC12mOwepSUEGlb5PjqdERlpkgP7x3Z1h - y0UGCtyB/ROrD2OmoCF2f01wboknsN+Kan8JFZSXdoW1kFo6wg8/9/sDUU1Yh0sEIPgwkPS1fVFCbV+y - vBFSkVN7Xfhehb7GE1uDWkdeK+3VxMpAx6LWY+NQIrMa7lOnz/WLghXV/lKvSF7eE9FuTVVNXfhhetBP - 90bIGPpad3QoVINIP1kIfuzDsEoe3Rykdu+JJKS724NaElJKJEC/WaCmzM2tiuGTgzOdmn1FvbT7mR6h - DCUzC/ivnuDsy+rFvVYL99mTQyx/S2/udCYAJW5qlWzYESKjk5N5i+jzO/p6apoqfwMfI1c3yJrQ20/t - TdATTJBLiq+7m6oTS+slSzlsod5kYa30PEpnFlDq5h/Ks5jHX8s0qxLJXz6MppXdp1fire8LaYfRMeh1 - p9PX3aYMXWVXkyr/6/ujZDzVRk6nZTkS6bHk1b3hlBuUwOdrF+JkZrW1IgN57kCszIc1AyOKyX7x6Tw3 - H7OS8IOAlflE4joaS2vyj672P9ydEH77aV7/vk6FyEN0JoLK/9luhRSdhWR1X8Vv09Aw4HNuOhIj47iE - Cm0VEij2CPKlteJqlNQRwI0tyrpxZ7iRzCz88/wSqQ3E07LTwa4+RAX2Hi3t9vQCa0/aqR9Qgfuo36Em - vfyX1klaxttQSBY0Aej918innp0Iv+81CA9y1goPnucXydqu4CdQYjOIL2xWHiyrFKKWEn4qZJAl1y2n - l9qb6L5LyPXNykfeorrj7q+Hz04Af6C3eaqhOvjeefRzgJmRKonB979ksfAgmKdjObHatnGjEyU3E+BI - nt0d+JOV7EUQ/o81KVpRTzxN73wv/dnljTI5mzM0NQKIpdXM05FMVYZ24GqK/gAhqpL7O4PWqhnQsgUF - zBQcLpY09ReN5YR6SzmL6HNLm0L2h9ILz1FTnnRLMbKgVhyS/ZwNAtBuPkpEn+0OEhnCgykmCgFxXN8s - W44EZvv5t5c0kXyU5DSxtF7+Wz5nrZt/baeiOdrSDc15B2JkfgoFNtkiAO3Wo3v1rR3hlEeY6bMELrBY - 4RA4Se/rlF9HSU4Dt7bIm2AopmWqyOjzuc1BLSMunbwcCLe9+2GUlFWmJiTZIoDj6bH00P/mYETLRkzV - H1ATiGnhUCtFefIqAkHbpqMzUKJTwd/FC+1u4YglustyCQH88tYQORZN7+YHlfr3B6OkPA3hyCYB6L+v - 3CeSCqqpqGmQ3P8dilrKKehImH3bx3KYLjwkzPaJlxRzvCX6+SWdQeRr20NarD4d4QeheK0/QkZ7xLRV - 82wSgP47Z1SLWlgx1ZoBeI8v7w5bLjw4o0o4cE2TMhMl/COwvO7Y7Mk+0RI3P8TIC+nX/9kVJkoaFX0g - CKBFf48KQyk3PLU82wSg58hf3SCRo9QWSNUpCGbSkz0hYrdQujesx7Jacfe6nWQUSvpp8OKO8KJLa0XZ - Cjn+IEDjvAL54e4ICcdJWkkyoDF8Z2eIDLfXgVEEoP/uezuDJJCi6aN9fvrHmrZEjoNlNAF6tm9pVZpQ - 2k/FBuIoqxT+x2kRdW9KpUD+djia1iBPfbz2o1tCmjBmqo24EQSg33xf6k3dBAKtYU8wThZbqHAoWTMQ - LvcKd6LQD8L0KuFrTots8Bxq+/7jSHpFPSCgB8Mq+VSnclJRj5kJQMuRpybMT/emPs0Ivr9LjJPza6xF - AhAevKZeWoeST3FZnfjFQrf5+/jD+z+vWiJ1gVhaYb5EqaxKbmxVSCbNIKMJ4LhJRA/9u8lqx1QLh9wD - MVLmtU7bt+Tw0eiDm0N35LTwP7E1/CKUnFpB+C9rkEmrkH5RD6T2XnGa2nsrEMBxT7ifEiSfWtWjmiSB - tw5EhuUMZfHMzKWaTc4K/9iqwPi8CqHHbvJNhIN9KbVTN0vxtG/+Hir8SxtGZqouKwSgkwBUG25TUncK - gjP1+W1hYqVhr5o54Dn2wdxetTDnCKDUy7c6TL55EKa6tkkmOxU1LU8/PJ0ipMCKI1YXzxIB6E7Bm1qH - 3p9g8PsT6c9AvYGVIgNQG7KkQdiUM4J/eysZN6tKbDBzjFdP7b2lXU4U9aSZ4FN9LE7Orx5ZBxdrBKDX - RDy8OdFSLFUSgPkIq5pkS9UMFGjVlMHf/TwXCodubJE3m1mN0xpsQFFPVzDt1F6waf96OErO8Y18gxPW - CGBw4dA3t0dS7oIE3w7hwYup/WylbkLg3yjnpHOsLf2cuNLuMu/Ybn0qzBNbQlq2WroJPr/qj5Dxldnp - bsQiAQwujX5jfyT16kH6+I5Zq3AooVXyB+f5hQWWlP2J1fJSerhkUws/ZWlwRGmpvWm27H55d3bbYLFK - AHq6dFmloAlzqtET6IvwjyNxUuy1Vg+BGVVS4I52ebqlhH9FY3BlCWdu4R/NieT7uxNdcNNR+6HT7gs7 - QlmvdGOZAI6Hw/wS6RDjKbcYBxL+xb6Ili5tpXThS+qE/vXbQudbQvif3iIvuahGCDtMLPzj6C31q/5o - yqrqiYEYKnm6J0SMaGfOOgHo4cGPNcpaFmSq6cKgVX2hN9ER2UrNYgs9wqPWKO+tkjYbKfygZjqTT17y - cSYHZwxFeGC4xm8ORNK++cFX8GBXkBg1y8AMBKDffLe3KZpjNdXZg0CwMK0olSQqR/Is2JLh3MGP/v+M - PLfOhDnwnLlj/Zz4dYdRByrpZBpDbcRpPoHM9IlkQbVIZtGvM3yJKTzOj8i3T9hjIvnH0Wh6jTvpyTxA - b7Q1HcqQ22znMgHoodUXtqc+ExH2pz+kkisbz94tCYjGoc30E8l8ur9LqOaxqkUht1ECgefmtiBZVCeR - 2fT/jUmeEbtBF9cYT0C9tkF6yJTCf2mN+PM8d3YFXvt3+jsn+STyQGdQG1rhPxYjO+g1DCOpQMWEENIu - +kD7qbcPRclTW0Japx3bIPbXKvroa1TTn023ou9IhJBVzcbPwTMLARz3tdDn58nCoVSWHswzKBw6t/pf - oyvaf7t4zYn7QKdC3twfJc1CTCNo0DggwUhJPpCgdDSikr3Utqii+//Kngi5tE4+oSFwWTY/uUD8qb7Q - M6YS/rvag18vzaIKBRsDbH0rVSFf3x8hQuzEoR6cdafb8IP/HgCb/v7hKLmb3tYw931hrUQa+PRbdkO7 - 72uG0LIbCeAMk5LoHmw6EkurrqLiaEz7efugnA1oovrc9hDpFtXj+6+fB3jipzwx9USIV3fg/vXDCLml - TSbF3InXzpYmkOfiN9k4szQSqT5cWuIORLIVS4YDu7xJJpUDMU2Q052Eq4/BhtTc7Up6RT3wGs08dO1l - Z4KR2Qjg+Nhtaqr5A6mPS4M9eOdgonColF4Kz/WFyMFktuZwRqXD2sn0j01HouQqSu7ZTGZzaH0WhYbp - NXtGMy375/iE8kI335Et4YfBl7/sj2hOIL1t9nCgh5aG07Kbtdp1MxKAfuiX1EtkXyiNbkJ0M17ZEyae - gdhx561Khg/9dQTKSt/fFda0zmytFZiSC2qFvzM7Z+BjLUL51EqhPxuprfZkBV57Mnacic0lw7wd/kJN - CLi17AyGMc1IALonfFWzrOX/pzOAdKSgmw/vfhgjk7O450ACd7ZJXuaE/6EtaumldXKHM0s3w+3tilaB - pxos+bq28OsDUTKZ0bRUMxOAPtgUCoeCKaZej/TR0DsYw2DX82uzo/VpETWXcNRWIS1migDua5f7shHu - g8Owmt4I6UzWGYkDAOrlL/dFYCw0s5loZiYAfa5CQbK7cihOmIM+0GSyT8paDcVoFx+dXStewoTwF7sC - qx0VgaA9C+x3UZ2UcNAxsOmUg8g3d1I7kPEuNWYnAP0zwDrDcBQGOUC7CCD6VJClfA+tpZhXPHpbOz/f - UOFf1hj8eAmXnfJRsLUgLs/CzQ9x42/uGH7LbiSA1A49VP818LG0Iz0jqg1SZvri1lDWEodgPa6oF1XD - hP/e7uCqci8fzIbTD2K5394ZHlHHTirC/2/dIWYFxaoEoB/6+X4xMVadQRKAiMXCWimr4UGnS/iBjRB7 - 1glgUb0Qc2TpQ15EFxWy+IzedPj9h+j7uKzOPFNsrUQA+nm4vpUNP9DpIg8v7YwQh5vP6rj5BX7p2awJ - /sIuUlDoFt+xZSnLDW7/n+4LM8P4YIO+sT+qvS8kAKNIgCe3tSnaxCGWNAF4K9RC0S4sRxb3d5Sbjy9v - lL++gRDHiBPAfL/0VrayoPTWyQMRwgzbaxV+dJOX1JmjL50VCUAfuvrD3WHmtAC9R0FWG6vQZ1KlSL7S - F35xxAR/A735V7VQ4XfxWftgcMv+D2ObrGeDweHLN4mwWI0A9JDwy3vD7IUFSWLGwzTo95jlwqEij7Bj - TE1gwogQwLn+gVljvdktgphYKZA2IcYcy8Mmt4gxMtErZHWTkQBO3HhwFiEJh0UCEGMqWdkiE5s7++sy - uZJvWcGRvMzm+PuVWfQ2PpJNlReadiz0i+RwRGXO45so91W1dGQ0AQy4/TlwfIkJxzCDeQFgBqzvC0HW - niGNVRbVye0Pd2VIE5jFkVFlHL/HYcDBvaVN0ext1ghAT/64qzNIN5lHAsj2Z3JDJEAhEqNnA97SxgNR - YtTsi0KopuTElcOP9XcqMxbWCP3Zrmu3J3KeyTN9QSaKfc6kBXxzR9gQlkcCEMj9XUEtNZhVAoBRcUYO - v4HfXeYRVgxvik978JBRjSztFTx5cUcorY482SKA1/ujWbfzcp0A9Mvhqa1B7fOweDwS4UDV2MYqHDgF - eXl+vbw0vX5+bv4hQxmMEsAPdrFLAPC2Ko7E0Adg0OXwPL0cYoTdswGOQKNnFsBaTasUIg92h25KSfjn - 1Sr3FroDUSPj3GYgAGgAggRgLAGwqgFAk5oCjo2IyaJaIfRYmzhpSMK/rjvy2ASOj9sZ2OTv7GSYAOj7 - +t0BNAGMMQF48uz2ELP+IXhP0KWYlXUEH14RJ756VuG/vyO04FyfscI/mAC+Sjc5xrAP4H93h5EADPIB - wJjwVNuHZ5MAoBs1SxOw81y8OqdKKxw6c7pwvod3Oxjy9K6lmxyOEyZjvfCe/m0zhgGN+jzLm2XNzmY1 - CvCTvRGmCEBPn76rXf7lvwj+U729hQtqxI0OD1vtoC5vkJkr+NBvf+gnv6xRxlRgAz4L9P6HArEQoxoA - 9Aa4t4teDm62LgdYuwmV0FdQvvUkAriHEOdnu5RXCly8wooGAKOZpvtEslOJM1n2CTbeAqj6yvKwiFxu - CJLnEcmnOxWtHwCjlqF2NvYnzwaLnZXg+USj3LfhdKZAsUucXOQObHEw8mbhMP7hUJRZO28fvYLW94ZI - mU9gYgiIJXsCJs3BC2tk8tr+5IxGVqU/qR1yR2PaXAKW6kT03gHTOP7HL+4OX3hGP8CcioGxF9YIe/MY - OAiw8Z9sVZgOBcJ7a+JjWmpqIaP99MzcFXgMJ5KneoJaA5AYw4KvryH4hh7bEiROxs5AkZuPzqwSnxxS - 96CHmuWpkyvFJ51u4w8DOC5cAzFmVT69PTSMI3ujP6K1hHK42DELzDoZCMh/WaNEPjgS1RqwMi77x9ew - PhDXqlhZItEZ1SK5vll+MuVswHKfdGORa+Q7/p69+0tQG/ulmuAGOEDNgsfpDTDRK2R1hpxVhoNqY8Eq - RbK+L6h5+c0g+Pr6wRm9qyPIhPDrvTSLOaFpSRMpSrtv4CU10o3n+iXDDrOmvnDpD4s0IkEIbiwYLvrJ - NpluAk/sSABDyl0HP8rHmxVSF4hp4V+zCL8eFnZT23+sV2SD9F08Od8v1pRzpGTYVYH/uTV0Z5FL9DqN - DAnWy1pDTrOogtqNEE/Eg2E6rRMJ4CO7+0yrEsm3doZJIJq5WX7ZzvxbUm98mzhtcAgl0xualfoNGe0R - 2EUKJlVKf9DsWyPCgvT5d6paR1ViKrUQHFcQynxyS+h4CzE7EsDxW9/mErSx213S8Oc8GkEc+jDSB7uD - 2hk1uk1aEQdkKl+5fotaOiKtwa5oEP9cyhlTIgwC9N2dEWYTQD5KPQQi+Ac1Yz7WJJNRWVw/FglAJ8GZ - 1SL54Z5EQk8mhB/MBiWeXeGHhjVf7Qsl8kEMvvmn+MTAvBrhmhHtCnwP1QTO9Suz8l3igBFdgmDm+6/2 - h0lEzc4ASBBcOaZmpPAEXgMyCF/dF9UcXY4sCRtLBOBIdqy5r1PJyJg3/dbfFVTJpzqDWo2AEBv5VmH6 - zf91mBDFGew4dfPxRbXS4bvbQ9kbFTajOnDe1ErekG5BsOD/QVmXj46cT0Bv7vi93WFyLb21f05teS0t - eZhEoP88dI59qidExoyw04gVAtBDexfXSuS3B6LHnXyZuPX/eChKLkwOa9HahTXLZFtyepA6Qmdjf1gl - n+kKZm0U2JnWFMi02Ct+w5DxYFdXHy69sl7sdxhg18LCr2lVtHFMaoaZHTYYmn7CbZKfFCD4elm9rB02 - GA+WicML/gzPQIwsrpdGVPCMJgB4vfFeUSO8gUgioUfNwD4dpa/1ld4QKeZOJlH4vAtqJFJxNKaNFVcz - dC50woH5hFc1yMRhsPDDwNRrG5Uf2rIxEORM+HJPcDZ9I4/nZ3lQpj1Z5ww516/sSdzOusqebgon/Cg0 - mnz7YJQsoxtsH2TX6YIEh201JR4IS4IZMhyNQD9UQDYvU1t4SpV00u80OwEkwlFQ1CVpyVyZ8N3oawZC - uLxRPuMNnGgdLpL7OoKkIZD47PqT6rmAB84V1CA8skUhZUnzzdAwn5tvv6tVXGtjBQv9wqPj3HzUblDC - wxJ6i/7pwyg5EDqhpqtn8RCrg258SDUFJx2o+3nuM3vr9b+DycCfpjYnzIaPDNPgVJPks4PaxPfR1xzL - ZVgIs0wA+u8s84nkWWqqHUvmcmdC+MEs+zEly7FDEELd7ICb8stUU6ilezWQbDOvkiE89A+4WFqEGHmO - fo7yStHw5C6INEz08l22jaTAxhpub1cemOk3pgrKkQyBwFy2J6iq6aLqnxQ7kT9+KgHAIQjS295Hb6bH - 6PeDGq4VcAxxg3UzBNI+11JbUK9Qiw/zgIPD8Z9Ho5q5ASEyh8kIwJHUzJZTIoWEnkiG1H3ATkUlN1Pt - a1SK71EvhBnvFcgVVGv43JYg+fX+KNlJVRLI1QglHzAXIJsPYvrv0fX6cm+QXE0/Bwi+02DBh9RyILMV - TeLWy+vViTZ2QeyjPbw7z6hFgn93J57x9AYCVf6z3YrG4P+9PaSl6d7ToZBr6EGYkVS5bckbP53qLd3x - BIfk27tCCc+2OnwVF/wM0BX5XP09moAAHMmEnm/sCBMhgwk9ENr7K9Xuzq8Znol0fH/dwvHXAS1lFr20 - zk0+k6uk45qDfo6MrupLhsDlm1qCf3ylVy20sY4pf+0vurRe8jkZiI06kjd1XvJxJp9MO3B0jWAmFYDv - 74xoaq9eKJQuEcDtuZ3eeqBhFKb5nrNBAFqtPt3rW9sU0i3FM+bkg/cLwv+lrSFSOpLRiVMe1mYgjnUH - opMr5WU2s+G6Zvm9Ys4cwyYzeZiS45nIL/dFyOGwmnY4Sh1EBO8cipJl9al7nkeSAPTPO9cvkp/sTXbo - UTPndYe+euCTgdvYkSNn6NS1nV3NR1c3yXfYzAhQV2Cu4Gh39pOGWNhAiIwsrpNOhA6HIRzgW4C8h+/v - DpEpKTijRooA9HyM+6g5BR1wMtWjP65FYlTyv7vCZJzB3najK/no88YdQ23nzTIurJMWLfCLgsOdO9rA - YOGD1N+bWhXy9yMxTT0ejkYADvVWIU7WdQeH5AzLNAEc79BTK5E/HIxqKnrG4uz0jx5JJWs6gkw2V8la - 8056XqZWCa/YrIQne4KzF9XLTXZ3IG7PwU21JVuc3d8ZJPX88BJUQFAiScfY0jr5IysNM0UA9kEJPU9S - m/zDkJqxTDt4jSB9MTBzZlWJOSn4+hqPo+u7pk1+/Z6NG502K2Kcl396tCc3VTvdqQOe5890nWhumW7y - EvwcjMj+1s6QVlsAXWhP9VhnggAcyfryZQ0S4Qaix219kiFH3wBVax7ZEiQlnJCz50KrkXDze29vk2/d - YGRmXxZgX94ofr2M49VcZnpQo4sp2z+/PUT6hhE61B2FHWJc88IXe08W4OESgJbQQ8nla9tCWuptpkJ7 - +uv4jsU0P4md8e7KI30pzPSJffNHqoSXRTyyOfjkJFD33Lm56YM7H0NZ7IYdYa20lKQZOowncwf+Qs2C - eTXS8fh1ugSgaRL0NVY2yqSOz0xob3ASFvgOdM0lZ2/95Brf0iwdu6FRmmLLRUzyitV5OerwGWxb26j6 - fjG9Cd/cH9HSktOpMdB/BjonPd0TIpNAuJJEMGQCSLbmgoSeDVQ7kWIko7c+EAnkCqxpD2qebkcO7zvU - lZRw8mO2XMYlbWrxqma5M9+TuyrgqeWdl1Ai+DO9yeV4eiq3ljtA2aCdmgXXt0C7cnFIBPA2JQDI21jT - lvBPZLINt9Yrkb6nX/VHyfQcdvTp+1zO8eHL66UbbQibbUM/KVreqDxfaHAHYpY0AriJb2iVtdCh3h1X - TcPGVujPgtDVBeIf6bjT7PGBGHmtP3I8QpFJlX9vMNEtucCT27d+MnX8N490hR9AyT8F51ZJi6hNGMh1 - EhhMBFB1+ElKBBD7D6UhmHqB0lD8CnGVZHTktq7yuymxLKgxvqCGBX9PmS/wrq2J5KO0nwEfaxSuuaxO - jNpz+LD8S7FRsupwXbdCNidz7VmcmHyq8ENrrhe2hxMhSg86exfVCm+u4EgeSvlZ+w52FYxyCz/KRxL4 - l0pHaHLxn9tCWu8AVkdlRZOOvhtblYQTMseF3+Hm9z3UHXzH6vH9jGOuX3q+MMcP0JnsyDnVIvnKNmix - xVD342So8Jf7omRGtYSCD84+nxAcwwXmojSniQe6lJ9M9omoCZyhD8HSeokcjhg/MEUlifkHn+sO4q2f - rAi9oEboX9EuT0cpHk6EYANxXNcsXDDWy+9zouD/S7+95/rChg9LgZvfeyyuOfrykJip8AfiNhe/9nMo - /JnDxR2B8ef5+TanG/0C+kGDGL8QM1j46fPnQxEywSuSXHfc6sU806rEL6LEjgBu26KW3twq99grAoo9 - x4V/RpVItkhxwwemaqp/ME5mVks5n8052ctHbm5X1kG9C0rrCKKokr95jCeg5mxCCVX9P98TzMp0pKFW - Jb64M0xyUTvT7f1RLuH9L/Vick/WcE1jcOW0SkHOxVunhKqZnWKcmXHp8D52B1Uy1Zd7hT2Qur3AL1Zh - co8BeKhbudbpEn5g9/C503eQ3jaPbQkylQikZ/w90aNoWkAuqf13tMvc3XvU0SiNRoEQ+wQv//vCHCoW - evdwlLnkHyCkRj6uJSo5cmAfRrsCh0vd/HyIUqEQGgxIr7y2QflFPgM93Ee0cQTMfq8UE52EGMsCBDPg - QFjVZvLZLS78030SmV/Nz0fJY88k+HmeW9hht7D3H+bhCVGVGfv/5L5+hNzeoVg6s28s1Tbv6w4vRmlj - FCWcUDbZJx22YpMR6Mv31NZgRiv2Mo3/2h6yXJcnrZjHHYiP56T1YHKilDGO65qUmUsbxB1WOoj2JAE8 - vz3MdCXgy3siWljMSloXVDNeWid9FSXLRPj3LlIyulK+Pd/DhxxWIYAKgfx0byL1l1X8/kCUON28ZYSf - fpbtT/eENuDNb1Kcwwl3l1mg8aTeSRgEjNUyYHhbfzscJXluc4dl9eSeSZVC4/iKgbEoRSbHHW3KfTOq - hLDZc9UhA/BX/RGmCeB9kxOAFmrltCrLdmzeYSE8s0W5usgj/sms48kSJgBPfryHbRPgzf6ECWA3qcoP - zuNJVeJ161rJOJQaC2J+tfD3Is68TkDoBhRnmABe3BE2pRMQ1reME4TpNcIKlBILY91OMuoz3crf7a5A - yGy3FPgAbmhViBxTmSQB0Ewe6Apqwy7MltK7wC9EPt4iLUIJyREUcMKC0V7+oJmcg5DlOKdKJP0hBhOB - 6Ps5ElHJZfWy6Ua4j+GE176xI3o9SkWOYXpNYMLcaumwwyTOQUeyKWhVIMZkLUCHFCMToAMwZxJnX2Im - 36u2jcSJ0pCjeGgLP7/EHXjWLD3r4T1CMpDKIAFs2BEiTpOsIRDVykbxeygBCK2isNwnPjXaE4jZTXB4 - Z/olbaS2ypD6D2PK5/rZ7wTs1Cr5xL5yjpRgm27ESbisVnikrJL9jrbgDPzq9hA7HYHo8//2REg+wyHW - ZH9+Mq9a6ptdK07G0444fb5Ab+Q/HW6hnmW/ALy3WVUi6ZPZ6AkIt/+FtRLTwm938crSWmETNu9AnBWg - Hs6sEprsDN9ooMp+pTdkqAagJvsAPNsXZtb2hx4K+dpXcSWebERqJkGd5C3k2CUByFr77YFEarARRADC - v+lIjJQyOvsP3tMUHx9dVCuuxdOMSBmPNpGi0mp+vtPNSw5G8wKm+ETSLMSybgrAr9sRjJPZ0A6cYzO5 - Z5ZfPPyZDgUz+xDDwzQff8W0KlFkMcUViGlJnUy2SvGsaQHwe/YGVXJ1o8zcza9P4h1TKb6GJxeRMaxp - C82bUyVutrsDcRYP/UW1EmngYyM+JiymTf6NkWVU+B0MrkMx1UZWNEpvYyUfYkSQVxF4zulhL1SoTQuq - Fsk7h6JaUk5sBAQfiGUjff3ZfpFJ4c9zB/pXNytrMb6PGDkQYp/vF787xsOrLGoC4LT82rYQ2aYM3yRQ - B2X57aCv9/y2sKZe2xkkvzGcsGvs+4HxeEARWcGdrcr6CQx6v/UCl1lUG/jOjjARoyc89kONFsD36Y1H - FapK/GB3mJznlzThZ+qzcomkqKvqpMDiZmUWnkpEttUBe7FbaHSwmvxChWNhrUS+tytMWoSY1rdfJwL4 - qqonYvn638NziH5fmxgj39kZJhfXylp5L2vdlHRn32V10h83bBMn4VlEGII5FQNjL/SL21kdjKlrBOOh - 5r1GJvd3K+SFHSGtt+CfDkZJsxAl79Cvb+2PUtMhTD7VGdRIY3wy2cjOaCZksScQm1Yl34knEGE41h5Q - i1c1K3X5DKcPOzldK+CP9+3XyGFQohP0HLQn23k5OXbTeudUS+SGZvkxPHkIZrCh61BJqY+/YjQnSGZq - MmI30ftMaln/uLUDi3kQjOKi2uDq86qFmMMjkFwcXT5yk3kEMrta3GjD+D6CdTzWG7lyWpXQ5EQSyIyG - 4g4curVN+Rsm9yBMhYk+8dvaxGIU5LSdfSVePmRzixfiaUKYD4TYV7TIr433oiaQVmajjz+I8X2EuZ2D - hDgurw8stbmFo6gJDC25B/IObmqVyd3Nwbl4ghCWwBROKCv3ij1OFPKPtPdHUQIo8ggv4IlBWA5X1gQm - LG+UDzg5NAlOJ/zjuEDs8nr5MZzEi7AsvrpLmrKgVnq+0BVQ0SQYVMlXwbsf7pYfxxOCyAmc55duGOsO - BHNdE4DsSRjDbeN2jsJTgcgpXNsQumlBjWSqjLzMFivxZFmdVH1Jm1qMpwGRk1jYRQpGuQLv5uWY8Dsr - hIEHNwd9mNyDyHksaSL5F1TLbzrd1tcEwO8x2SeSsZx0Ke48AjEI17fIb9ld/B67ZVV+gcz3i8dWtciX - 424jEKfDP+WpRV5BdFpM+B0VAbXAI2x4sic4GzcZgfgIXNwQnDO7StjLYhvytPr1eUUyq1p+CXcWgRgi - bu4g469rlHbZXYGIw8zOPleg/77O4EvYqReBSANFHP/ZEq/5KgodWlov32arwk69CMSwsLxRWlfOCRG7 - iZJ75laLW+f2qoW4ewhEBnBvZ/B6u0t4X+/Zx2olH7y3Ne1K15rN6kTcNQQikyDEXu7luXxG7f0iDy+N - 5cTr0N5HIEYIS5pI0TWNUqXWwZdjx96f5hPJgjr5StwhBCIL+FS7/GuqCRyxGx3fdwvkohoh9GiPvAZ3 - BYHIIsoqpSnneEXBiA7EiWGcAplYKbz5TJ8yA3cDgTAAqxrFi5fUScfs7uwKfyknkkW14s9wBxAIg/Hp - XnVMgZtf7/TwMXsW7H17Bb/36R759XsIceLqIxCMYFKl+MVSbuSShsDeL68Sd0zg5Om42ggEayDEfm+H - 8sI5Xl7NtCYA/QoW14nbVnSRElxoBIJhPNitXFvs5VshaWjYtz6XuPlHu4Q7Pr9Vnoari0CYBOfXiLX5 - 3PCcfeO9gdj0KukhXE0EwmS4bYtaurZD6bK5+XAqSUP25DPVK8ifbJZuxJVEIEyMEo+wosgjyPahxvfp - U+gW38GVQyAsgvk14sXTKkXBfhbhL6TPAr/4l4Ubuwpw1RAIC+GhbuXafHfgu6fLHIT/LqZmwuom+U0s - 5kEgrAoq3OdUCa+McvHq4OSeQk+gv9gvTtqAbboRCOtjaYP0wlivQKD56IJqkZ/t4+fhqiAQuaMJ2O9s - VZrmVgmbPt0oTcEFQSByDI9vDV2Aq5Db+P9MQWf1FB5/cAAAAABJRU5ErkJggg== - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + False + + + False + + + False + + + False + + + False + + + + + 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 + + \ No newline at end of file diff --git a/desktop-winforms/MsalExample.csproj b/4-desktop-apps/desktop-winforms/MsalExample.csproj similarity index 97% rename from desktop-winforms/MsalExample.csproj rename to 4-desktop-apps/desktop-winforms/MsalExample.csproj index 444aa03..74cb5a3 100644 --- a/desktop-winforms/MsalExample.csproj +++ b/4-desktop-apps/desktop-winforms/MsalExample.csproj @@ -1,12 +1,12 @@ - - - WinExe - net8.0-windows - enable - true - enable - - - - + + + WinExe + net8.0-windows + enable + true + enable + + + + \ No newline at end of file diff --git a/desktop-winforms/Program.cs b/4-desktop-apps/desktop-winforms/Program.cs similarity index 95% rename from desktop-winforms/Program.cs rename to 4-desktop-apps/desktop-winforms/Program.cs index 349f2d4..983c437 100644 --- a/desktop-winforms/Program.cs +++ b/4-desktop-apps/desktop-winforms/Program.cs @@ -1,17 +1,17 @@ -using Microsoft.Identity.Client; - -namespace MsalExample -{ - internal static class Program - { - /// - /// The main entry point for the application. - /// - [STAThread] - static void Main() - { - ApplicationConfiguration.Initialize(); - Application.Run(new MainWindow()); - } - } +using Microsoft.Identity.Client; + +namespace MsalExample +{ + internal static class Program + { + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main() + { + ApplicationConfiguration.Initialize(); + Application.Run(new MainWindow()); + } + } } \ No newline at end of file diff --git a/desktop-winforms/README.md b/4-desktop-apps/desktop-winforms/README.md similarity index 91% rename from desktop-winforms/README.md rename to 4-desktop-apps/desktop-winforms/README.md index 81d4374..f51a9d6 100644 --- a/desktop-winforms/README.md +++ b/4-desktop-apps/desktop-winforms/README.md @@ -1,6 +1,6 @@ --- -# Metadata required by https://docs.microsoft.com/samples/browse/ -# Metadata properties: https://review.docs.microsoft.com/help/contribute/samples/process/onboarding?branch=main#add-metadata-to-readme +# Metadata required by https://learn.microsoft.com/samples/browse/ +# Metadata properties: https://review.learn.microsoft.com/help/contribute/samples/process/onboarding?branch=main#add-metadata-to-readme languages: - csharp page_type: sample @@ -22,13 +22,13 @@ This .NET Windows Forms application authenticates a user and then makes a reques ## Prerequisites - Microsoft Entra tenant and the permissions or role required for managing app registrations in the tenant. -- Visual Studio 2022, [configured for the .NET 8 desktop development workload](https://docs.microsoft.com/dotnet/desktop/winforms/get-started/create-app-visual-studio?view=netdesktop-8.0#prerequisites) +- Visual Studio 2022, [configured for the .NET 8 desktop development workload](https://learn.microsoft.com/dotnet/desktop/winforms/get-started/create-app-visual-studio?view=netdesktop-8.0#prerequisites) ## Setup ### 1. Register the app -First, complete the steps in [Register an application with the Microsoft identity platform](https://docs.microsoft.com/azure/active-directory/develop/quickstart-register-app) to register the application. +First, complete the steps in [Register an application with the Microsoft identity platform](https://learn.microsoft.com/entra/identity-platform/quickstart-register-app) to register the application. Use these settings in your app registration. diff --git a/desktop-winforms/app-launch.png b/4-desktop-apps/desktop-winforms/app-launch.png similarity index 100% rename from desktop-winforms/app-launch.png rename to 4-desktop-apps/desktop-winforms/app-launch.png diff --git a/desktop-winforms/app.png b/4-desktop-apps/desktop-winforms/app.png similarity index 100% rename from desktop-winforms/app.png rename to 4-desktop-apps/desktop-winforms/app.png diff --git a/desktop-winui/App.xaml b/4-desktop-apps/desktop-winui/App.xaml similarity index 97% rename from desktop-winui/App.xaml rename to 4-desktop-apps/desktop-winui/App.xaml index bb625fc..12d0c08 100644 --- a/desktop-winui/App.xaml +++ b/4-desktop-apps/desktop-winui/App.xaml @@ -1,15 +1,15 @@ - - - - - - - - - - - + + + + + + + + + + + diff --git a/desktop-winui/App.xaml.cs b/4-desktop-apps/desktop-winui/App.xaml.cs similarity index 97% rename from desktop-winui/App.xaml.cs rename to 4-desktop-apps/desktop-winui/App.xaml.cs index bb17a2c..4ff359e 100644 --- a/desktop-winui/App.xaml.cs +++ b/4-desktop-apps/desktop-winui/App.xaml.cs @@ -1,51 +1,51 @@ -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; -using Microsoft.UI.Xaml.Controls.Primitives; -using Microsoft.UI.Xaml.Data; -using Microsoft.UI.Xaml.Input; -using Microsoft.UI.Xaml.Media; -using Microsoft.UI.Xaml.Navigation; -using Microsoft.UI.Xaml.Shapes; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices.WindowsRuntime; -using Windows.ApplicationModel; -using Windows.ApplicationModel.Activation; -using Windows.Foundation; -using Windows.Foundation.Collections; - -// To learn more about WinUI, the WinUI project structure, -// and more about our project templates, see: http://aka.ms/winui-project-info. - -namespace WinUIApp -{ - /// - /// Provides application-specific behavior to supplement the default Application class. - /// - public partial class App : Application - { - /// - /// Initializes the singleton application object. This is the first line of authored code - /// executed, and as such is the logical equivalent of main() or WinMain(). - /// - public App() - { - this.InitializeComponent(); - } - - /// - /// Invoked when the application is launched normally by the end user. Other entry points - /// will be used such as when the application is launched to open a specific file. - /// - /// Details about the launch request and process. - protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args) - { - m_window = new MainWindow(); - m_window.Activate(); - } - - private Window m_window; - } -} +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls.Primitives; +using Microsoft.UI.Xaml.Data; +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Navigation; +using Microsoft.UI.Xaml.Shapes; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using Windows.ApplicationModel; +using Windows.ApplicationModel.Activation; +using Windows.Foundation; +using Windows.Foundation.Collections; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace WinUIApp +{ + /// + /// Provides application-specific behavior to supplement the default Application class. + /// + public partial class App : Application + { + /// + /// Initializes the singleton application object. This is the first line of authored code + /// executed, and as such is the logical equivalent of main() or WinMain(). + /// + public App() + { + this.InitializeComponent(); + } + + /// + /// Invoked when the application is launched normally by the end user. Other entry points + /// will be used such as when the application is launched to open a specific file. + /// + /// Details about the launch request and process. + protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args) + { + m_window = new MainWindow(); + m_window.Activate(); + } + + private Window m_window; + } +} diff --git a/desktop-winui/Assets/LockScreenLogo.scale-200.png b/4-desktop-apps/desktop-winui/Assets/LockScreenLogo.scale-200.png similarity index 100% rename from desktop-winui/Assets/LockScreenLogo.scale-200.png rename to 4-desktop-apps/desktop-winui/Assets/LockScreenLogo.scale-200.png diff --git a/desktop-winui/Assets/SplashScreen.scale-200.png b/4-desktop-apps/desktop-winui/Assets/SplashScreen.scale-200.png similarity index 100% rename from desktop-winui/Assets/SplashScreen.scale-200.png rename to 4-desktop-apps/desktop-winui/Assets/SplashScreen.scale-200.png diff --git a/desktop-winui/Assets/Square150x150Logo.scale-200.png b/4-desktop-apps/desktop-winui/Assets/Square150x150Logo.scale-200.png similarity index 100% rename from desktop-winui/Assets/Square150x150Logo.scale-200.png rename to 4-desktop-apps/desktop-winui/Assets/Square150x150Logo.scale-200.png diff --git a/desktop-winui/Assets/Square44x44Logo.scale-200.png b/4-desktop-apps/desktop-winui/Assets/Square44x44Logo.scale-200.png similarity index 100% rename from desktop-winui/Assets/Square44x44Logo.scale-200.png rename to 4-desktop-apps/desktop-winui/Assets/Square44x44Logo.scale-200.png diff --git a/desktop-winui/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/4-desktop-apps/desktop-winui/Assets/Square44x44Logo.targetsize-24_altform-unplated.png similarity index 100% rename from desktop-winui/Assets/Square44x44Logo.targetsize-24_altform-unplated.png rename to 4-desktop-apps/desktop-winui/Assets/Square44x44Logo.targetsize-24_altform-unplated.png diff --git a/desktop-winui/Assets/StoreLogo.png b/4-desktop-apps/desktop-winui/Assets/StoreLogo.png similarity index 100% rename from desktop-winui/Assets/StoreLogo.png rename to 4-desktop-apps/desktop-winui/Assets/StoreLogo.png diff --git a/desktop-winui/Assets/Wide310x150Logo.scale-200.png b/4-desktop-apps/desktop-winui/Assets/Wide310x150Logo.scale-200.png similarity index 100% rename from desktop-winui/Assets/Wide310x150Logo.scale-200.png rename to 4-desktop-apps/desktop-winui/Assets/Wide310x150Logo.scale-200.png diff --git a/desktop-winui/MainWindow.xaml b/4-desktop-apps/desktop-winui/MainWindow.xaml similarity index 98% rename from desktop-winui/MainWindow.xaml rename to 4-desktop-apps/desktop-winui/MainWindow.xaml index 6f79dd7..3d91163 100644 --- a/desktop-winui/MainWindow.xaml +++ b/4-desktop-apps/desktop-winui/MainWindow.xaml @@ -1,23 +1,23 @@ - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + diff --git a/desktop-winui/MainWindow.xaml.cs b/4-desktop-apps/desktop-winui/MainWindow.xaml.cs similarity index 98% rename from desktop-winui/MainWindow.xaml.cs rename to 4-desktop-apps/desktop-winui/MainWindow.xaml.cs index 3481488..c066af9 100644 --- a/desktop-winui/MainWindow.xaml.cs +++ b/4-desktop-apps/desktop-winui/MainWindow.xaml.cs @@ -1,110 +1,110 @@ -using Microsoft.Identity.Client; -using Microsoft.UI.Xaml; -using System.Linq; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text.Encodings.Web; -using System.Text.Json; - -// To learn more about WinUI, the WinUI project structure, -// and more about our project templates, see: http://aka.ms/winui-project-info. - -namespace WinUIApp -{ - /// - /// An empty window that can be used on its own or navigated to within a Frame. - /// - public sealed partial class MainWindow : Window - { - private readonly HttpClient httpClient = new(); - - // Generally, your MSAL client will have a lifecycle that matches the lifecycle - // of the user's session in the application. In this sample, the lifecycle of the - // MSAL client to the lifecycle of this form. - private readonly IPublicClientApplication msalPublicClientApp; - - public MainWindow() - { - this.InitializeComponent(); - this.Title = "MSAL WinUI 3 Packaged Desktop App Sample"; - - // Configure your public client application - msalPublicClientApp = PublicClientApplicationBuilder - .CreateWithApplicationOptions(new PublicClientApplicationOptions - { - // Enter the tenant ID obtained from the Microsoft Entra admin center - TenantId = "Enter the tenant ID obtained from the Microsoft Entra admin center", - - // Enter the client ID obtained from the Microsoft Entra admin center - ClientId = "Enter the client ID obtained from the Microsoft Entra admin center" - }) - .WithDefaultRedirectUri() - .Build(); - } - - private async void SignIn_Click(object sender, RoutedEventArgs e) - { - AuthenticationResult? msalAuthenticationResult = null; - - // Acquire a cached access token for Microsoft Graph if one is available from a prior - // execution of this process. - var accounts = await msalPublicClientApp.GetAccountsAsync(); - if (accounts.Any()) - { - try - { - // Will return a cached access token if available, refreshing if necessary. - msalAuthenticationResult = await msalPublicClientApp.AcquireTokenSilent( - new[] { "https://graph.microsoft.com/User.Read" }, - accounts.First()) - .ExecuteAsync(); - } - catch (MsalUiRequiredException) - { - // Nothing in cache for this account + scope, and interactive experience required. - } - } - - if (msalAuthenticationResult == null) - { - // This is likely the first authentication request in the application, so calling - // this will launch the user's default browser and send them through a login flow. - // After the flow is complete, the rest of this method will continue to execute. - msalAuthenticationResult = await msalPublicClientApp.AcquireTokenInteractive( - new[] { "https://graph.microsoft.com/User.Read" }) - .ExecuteAsync(); - } - - // Call Microsoft Graph using the access token acquired above. - using var graphRequest = new HttpRequestMessage(HttpMethod.Get, "https://graph.microsoft.com/v1.0/me"); - graphRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", msalAuthenticationResult.AccessToken); - var graphResponseMessage = await httpClient.SendAsync(graphRequest); - graphResponseMessage.EnsureSuccessStatusCode(); - - // Present the results to the user (formatting the json for readability) - using var graphResponseJson = JsonDocument.Parse(await graphResponseMessage.Content.ReadAsStreamAsync()); - graphCallResultTextBlock.Text = JsonSerializer.Serialize(graphResponseJson, new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); - var tokenWasFromCache = TokenSource.Cache == msalAuthenticationResult.AuthenticationResultMetadata.TokenSource; - tokenAcquisitionTextBlock.Text = $"Access Token: {(tokenWasFromCache ? "Cached" : "Newly Acquired")} (Expires: {msalAuthenticationResult.ExpiresOn:R})"; - - this.signInCallToActionTextBlock.Visibility = Visibility.Collapsed; - this.authorizedPanel.Visibility = Visibility.Visible; - } - - private async void SignOut_Click(object sender, RoutedEventArgs e) - { - // All cached tokens will be removed. - // The next token request will require the user to sign in. - foreach (var account in (await msalPublicClientApp.GetAccountsAsync()).ToList()) - { - await msalPublicClientApp.RemoveAsync(account); - } - - // Show the call to action and hide the results. - graphCallResultTextBlock.Text = string.Empty; - tokenAcquisitionTextBlock.Text = ""; - this.signInCallToActionTextBlock.Visibility = Visibility.Visible; - this.authorizedPanel.Visibility = Visibility.Collapsed; - } - } +using Microsoft.Identity.Client; +using Microsoft.UI.Xaml; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text.Encodings.Web; +using System.Text.Json; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace WinUIApp +{ + /// + /// An empty window that can be used on its own or navigated to within a Frame. + /// + public sealed partial class MainWindow : Window + { + private readonly HttpClient httpClient = new(); + + // Generally, your MSAL client will have a lifecycle that matches the lifecycle + // of the user's session in the application. In this sample, the lifecycle of the + // MSAL client to the lifecycle of this form. + private readonly IPublicClientApplication msalPublicClientApp; + + public MainWindow() + { + this.InitializeComponent(); + this.Title = "MSAL WinUI 3 Packaged Desktop App Sample"; + + // Configure your public client application + msalPublicClientApp = PublicClientApplicationBuilder + .CreateWithApplicationOptions(new PublicClientApplicationOptions + { + // Enter the tenant ID obtained from the Microsoft Entra admin center + TenantId = "Enter the tenant ID obtained from the Microsoft Entra admin center", + + // Enter the client ID obtained from the Microsoft Entra admin center + ClientId = "Enter the client ID obtained from the Microsoft Entra admin center" + }) + .WithDefaultRedirectUri() + .Build(); + } + + private async void SignIn_Click(object sender, RoutedEventArgs e) + { + AuthenticationResult? msalAuthenticationResult = null; + + // Acquire a cached access token for Microsoft Graph if one is available from a prior + // execution of this process. + var accounts = await msalPublicClientApp.GetAccountsAsync(); + if (accounts.Any()) + { + try + { + // Will return a cached access token if available, refreshing if necessary. + msalAuthenticationResult = await msalPublicClientApp.AcquireTokenSilent( + new[] { "https://graph.microsoft.com/User.Read" }, + accounts.First()) + .ExecuteAsync(); + } + catch (MsalUiRequiredException) + { + // Nothing in cache for this account + scope, and interactive experience required. + } + } + + if (msalAuthenticationResult == null) + { + // This is likely the first authentication request in the application, so calling + // this will launch the user's default browser and send them through a login flow. + // After the flow is complete, the rest of this method will continue to execute. + msalAuthenticationResult = await msalPublicClientApp.AcquireTokenInteractive( + new[] { "https://graph.microsoft.com/User.Read" }) + .ExecuteAsync(); + } + + // Call Microsoft Graph using the access token acquired above. + using var graphRequest = new HttpRequestMessage(HttpMethod.Get, "https://graph.microsoft.com/v1.0/me"); + graphRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", msalAuthenticationResult.AccessToken); + var graphResponseMessage = await httpClient.SendAsync(graphRequest); + graphResponseMessage.EnsureSuccessStatusCode(); + + // Present the results to the user (formatting the json for readability) + using var graphResponseJson = JsonDocument.Parse(await graphResponseMessage.Content.ReadAsStreamAsync()); + graphCallResultTextBlock.Text = JsonSerializer.Serialize(graphResponseJson, new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); + var tokenWasFromCache = TokenSource.Cache == msalAuthenticationResult.AuthenticationResultMetadata.TokenSource; + tokenAcquisitionTextBlock.Text = $"Access Token: {(tokenWasFromCache ? "Cached" : "Newly Acquired")} (Expires: {msalAuthenticationResult.ExpiresOn:R})"; + + this.signInCallToActionTextBlock.Visibility = Visibility.Collapsed; + this.authorizedPanel.Visibility = Visibility.Visible; + } + + private async void SignOut_Click(object sender, RoutedEventArgs e) + { + // All cached tokens will be removed. + // The next token request will require the user to sign in. + foreach (var account in (await msalPublicClientApp.GetAccountsAsync()).ToList()) + { + await msalPublicClientApp.RemoveAsync(account); + } + + // Show the call to action and hide the results. + graphCallResultTextBlock.Text = string.Empty; + tokenAcquisitionTextBlock.Text = ""; + this.signInCallToActionTextBlock.Visibility = Visibility.Visible; + this.authorizedPanel.Visibility = Visibility.Collapsed; + } + } } \ No newline at end of file diff --git a/desktop-winui/Package.appxmanifest b/4-desktop-apps/desktop-winui/Package.appxmanifest similarity index 97% rename from desktop-winui/Package.appxmanifest rename to 4-desktop-apps/desktop-winui/Package.appxmanifest index e214088..f8ed379 100644 --- a/desktop-winui/Package.appxmanifest +++ b/4-desktop-apps/desktop-winui/Package.appxmanifest @@ -1,48 +1,48 @@ - - - - - - - - WinUIApp - fla - Assets\StoreLogo.png - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + WinUIApp + fla + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/desktop-winui/Properties/launchSettings.json b/4-desktop-apps/desktop-winui/Properties/launchSettings.json similarity index 94% rename from desktop-winui/Properties/launchSettings.json rename to 4-desktop-apps/desktop-winui/Properties/launchSettings.json index 5e0c8d3..6dbdc20 100644 --- a/desktop-winui/Properties/launchSettings.json +++ b/4-desktop-apps/desktop-winui/Properties/launchSettings.json @@ -1,10 +1,10 @@ -{ - "profiles": { - "WinUIApp (Package)": { - "commandName": "MsixPackage" - }, - "WinUIApp (Unpackaged)": { - "commandName": "Project" - } - } +{ + "profiles": { + "WinUIApp (Package)": { + "commandName": "MsixPackage" + }, + "WinUIApp (Unpackaged)": { + "commandName": "Project" + } + } } \ No newline at end of file diff --git a/desktop-winui/README.md b/4-desktop-apps/desktop-winui/README.md similarity index 87% rename from desktop-winui/README.md rename to 4-desktop-apps/desktop-winui/README.md index 1228fc3..7955d89 100644 --- a/desktop-winui/README.md +++ b/4-desktop-apps/desktop-winui/README.md @@ -1,96 +1,96 @@ ---- -# Metadata required by https://docs.microsoft.com/samples/browse/ -# Metadata properties: https://review.docs.microsoft.com/help/contribute/samples/process/onboarding?branch=main#add-metadata-to-readme -languages: -- csharp -page_type: sample -name: WinUI 3 Packaged Desktop App that makes a request to the Graph API after signing in the user -description: This .NET 8 (C#) WinUI 3 Packaged Desktop App signs in the user and then makes a request to Microsoft Graph for the user's profile data. -products: -- azure -- entra-id -- ms-graph -urlFragment: ms-identity-docs-code-app-csharp-winui ---- - -# .NET (C#) | WinUI 3 Packaged Desktop App | user sign-in, protected web API access (Microsoft Graph) | Microsoft identity platform - - - -This .NET WinUI 3 Packaged Desktop App authenticates a user and then makes a request to the Graph API as the authenticated user. The response to the request is presented to the user. - -![A screenshot of a WinUI 3 Packaged Desktop App displaying a response from Microsoft Graph.](./app.png) - -## Prerequisites - -- Microsoft Entra tenant and the permissions or role required for managing app registrations in the tenant. -- Visual Studio 2022, [configured with WinUI 3 workload and components](https://docs.microsoft.com/windows/apps/windows-app-sdk/set-up-your-development-environment?tabs=vs-2022-17-1-a%2Cvs-2022-17-1-b#required-workloads-and-components) - -## Setup - -### 1. Register the app - -First, complete the steps in [Register an application with the Microsoft identity platform](https://docs.microsoft.com/azure/active-directory/develop/quickstart-register-app) to register the application. - -Use these settings in your app registration. - -| App registration
setting | Value for this sample app | Notes | -|--------------------------------:|:--------------------------------------------------------------------|:--------------------------------------------------------------------------------| -| **Name** | `active-directory-dotnet-winui3` | Suggested value for this sample.
You can change the app name at any time. | -| **Supported account types** | **Accounts in this organizational directory only (Single tenant)** | Suggested value for this sample. | -| **Platform type** | **Mobile and desktop applications** | Required value for this sample | -| **Redirect URIs** | `https://login.microsoftonline.com/common/oauth2/nativeclient` | Required value for this sample | - -> :information_source: **Bold text** in the tables above matches (or is similar to) a UI element in the Microsoft Entra admin center, while `code formatting` indicates a value you enter into a text box in the Microsoft Entra admin center. - -### 2. Open the project in Visual Studio - -Next, open the _WinUIApp.csproj_ project in Visual Studio. - -### 3. Update code sample in _MainWindow.xaml.cs_ with app registration values - -Finally, set the following values in _MainWindow.xaml.cs_. - - -```csharp -// 'Tenant ID' of your Microsoft Entra instance - this value is a GUID -TenantId = "", - -// 'Application (client) ID' of app registration in Microsoft Entra admin center - this value is a GUID -ClientId = "" -``` - -## Run the application - -Run the application by pressing F5 in Visual Studio. - -The application will open allowing you to click the **Sign In (if needed) & Call Graph** button to use the authentication flow. - -![A screenshot of a WinUI 3 Packaged Desktop App guiding the user to click the "Sign In" button.](./app-launch.png) - -## About the code - -This .NET 8 WinUI 3 Packaged Desktop App presents a button that initiates an authentication flow using the Microsoft Authentication Library (MSAL). The user completes this flow in their default web browser. Upon successful authentication, an HTTP GET request to the Microsoft Graph /me endpoint is issued with the user's access token in the HTTP header. The response from the GET request is then displayed to the user. The MSAL client first looks to its token cache, refreshing if necessary, before acquiring a new access token. - -## Reporting problems - -### Sample app not working? - -If you can't get the sample working, you've checked [Stack Overflow](https://stackoverflow.com/questions/tagged/msal), and you've already searched the issues in this sample's repository, open an issue report the problem. - -1. Search the [GitHub issues](../../../issues) in the repository - your problem might already have been reported or have an answer. -1. Nothing similar? [Open an issue](../../../issues/new) that clearly explains the problem you're having running the sample app. - -### All other issues - -> :warning: WARNING: Any issue in this repository _not_ limited to running one of its sample apps will be closed without being addressed. - -For all other requests, see [Support and help options for developers | Microsoft identity platform](https://docs.microsoft.com/azure/active-directory/develop/developer-support-help-options). - -## Contributing - -If you'd like to contribute to this sample, see [CONTRIBUTING.MD](/CONTRIBUTING.md). - -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information, see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. +--- +# Metadata required by https://learn.microsoft.com/samples/browse/ +# Metadata properties: https://review.learn.microsoft.com/help/contribute/samples/process/onboarding?branch=main#add-metadata-to-readme +languages: +- csharp +page_type: sample +name: WinUI 3 Packaged Desktop App that makes a request to the Graph API after signing in the user +description: This .NET 8 (C#) WinUI 3 Packaged Desktop App signs in the user and then makes a request to Microsoft Graph for the user's profile data. +products: +- azure +- entra-id +- ms-graph +urlFragment: ms-identity-docs-code-app-csharp-winui +--- + +# .NET (C#) | WinUI 3 Packaged Desktop App | user sign-in, protected web API access (Microsoft Graph) | Microsoft identity platform + + + +This .NET WinUI 3 Packaged Desktop App authenticates a user and then makes a request to the Graph API as the authenticated user. The response to the request is presented to the user. + +![A screenshot of a WinUI 3 Packaged Desktop App displaying a response from Microsoft Graph.](./app.png) + +## Prerequisites + +- Microsoft Entra tenant and the permissions or role required for managing app registrations in the tenant. +- Visual Studio 2022, [configured with WinUI 3 workload and components](https://learn.microsoft.com/windows/apps/windows-app-sdk/set-up-your-development-environment?tabs=vs-2022-17-1-a%2Cvs-2022-17-1-b#required-workloads-and-components) + +## Setup + +### 1. Register the app + +First, complete the steps in [Register an application with the Microsoft identity platform](https://learn.microsoft.com/entra/identity-platform/quickstart-register-app) to register the application. + +Use these settings in your app registration. + +| App registration
setting | Value for this sample app | Notes | +|--------------------------------:|:--------------------------------------------------------------------|:--------------------------------------------------------------------------------| +| **Name** | `active-directory-dotnet-winui3` | Suggested value for this sample.
You can change the app name at any time. | +| **Supported account types** | **Accounts in this organizational directory only (Single tenant)** | Suggested value for this sample. | +| **Platform type** | **Mobile and desktop applications** | Required value for this sample | +| **Redirect URIs** | `https://login.microsoftonline.com/common/oauth2/nativeclient` | Required value for this sample | + +> :information_source: **Bold text** in the tables above matches (or is similar to) a UI element in the Microsoft Entra admin center, while `code formatting` indicates a value you enter into a text box in the Microsoft Entra admin center. + +### 2. Open the project in Visual Studio + +Next, open the _WinUIApp.csproj_ project in Visual Studio. + +### 3. Update code sample in _MainWindow.xaml.cs_ with app registration values + +Finally, set the following values in _MainWindow.xaml.cs_. + + +```csharp +// 'Tenant ID' of your Microsoft Entra instance - this value is a GUID +TenantId = "", + +// 'Application (client) ID' of app registration in Microsoft Entra admin center - this value is a GUID +ClientId = "" +``` + +## Run the application + +Run the application by pressing F5 in Visual Studio. + +The application will open allowing you to click the **Sign In (if needed) & Call Graph** button to use the authentication flow. + +![A screenshot of a WinUI 3 Packaged Desktop App guiding the user to click the "Sign In" button.](./app-launch.png) + +## About the code + +This .NET 8 WinUI 3 Packaged Desktop App presents a button that initiates an authentication flow using the Microsoft Authentication Library (MSAL). The user completes this flow in their default web browser. Upon successful authentication, an HTTP GET request to the Microsoft Graph /me endpoint is issued with the user's access token in the HTTP header. The response from the GET request is then displayed to the user. The MSAL client first looks to its token cache, refreshing if necessary, before acquiring a new access token. + +## Reporting problems + +### Sample app not working? + +If you can't get the sample working, you've checked [Stack Overflow](https://stackoverflow.com/questions/tagged/msal), and you've already searched the issues in this sample's repository, open an issue report the problem. + +1. Search the [GitHub issues](../../../issues) in the repository - your problem might already have been reported or have an answer. +1. Nothing similar? [Open an issue](../../../issues/new) that clearly explains the problem you're having running the sample app. + +### All other issues + +> :warning: WARNING: Any issue in this repository _not_ limited to running one of its sample apps will be closed without being addressed. + +For all other requests, see [Support and help options for developers | Microsoft identity platform](https://learn.microsoft.com/entra/identity-platform/developer-support-help-options). + +## Contributing + +If you'd like to contribute to this sample, see [CONTRIBUTING.MD](/CONTRIBUTING.md). + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information, see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. diff --git a/desktop-winui/WinUIApp.csproj b/4-desktop-apps/desktop-winui/WinUIApp.csproj similarity index 97% rename from desktop-winui/WinUIApp.csproj rename to 4-desktop-apps/desktop-winui/WinUIApp.csproj index 02d1678..24427be 100644 --- a/desktop-winui/WinUIApp.csproj +++ b/4-desktop-apps/desktop-winui/WinUIApp.csproj @@ -1,41 +1,41 @@ - - - WinExe - net8.0-windows10.0.17763.0 - 10.0.17763.0 - WinUIApp - app.manifest - x86;x64;arm64 - win-x86;win-x64;win-arm64 - win-$(Platform).pubxml - true - true - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + WinExe + net8.0-windows10.0.17763.0 + 10.0.17763.0 + WinUIApp + app.manifest + x86;x64;arm64 + win-x86;win-x64;win-arm64 + win-$(Platform).pubxml + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/desktop-winui/app-launch.png b/4-desktop-apps/desktop-winui/app-launch.png similarity index 100% rename from desktop-winui/app-launch.png rename to 4-desktop-apps/desktop-winui/app-launch.png diff --git a/desktop-winui/app.manifest b/4-desktop-apps/desktop-winui/app.manifest similarity index 97% rename from desktop-winui/app.manifest rename to 4-desktop-apps/desktop-winui/app.manifest index 120383f..2fbfbfd 100644 --- a/desktop-winui/app.manifest +++ b/4-desktop-apps/desktop-winui/app.manifest @@ -1,15 +1,15 @@ - - - - - - - - true/PM - PerMonitorV2, PerMonitor - - - + + + + + + + + true/PM + PerMonitorV2, PerMonitor + + + diff --git a/desktop-winui/app.png b/4-desktop-apps/desktop-winui/app.png similarity index 100% rename from desktop-winui/app.png rename to 4-desktop-apps/desktop-winui/app.png diff --git a/desktop-wpf/App.xaml b/4-desktop-apps/desktop-wpf/App.xaml similarity index 97% rename from desktop-wpf/App.xaml rename to 4-desktop-apps/desktop-wpf/App.xaml index bd280a2..ca78f68 100644 --- a/desktop-wpf/App.xaml +++ b/4-desktop-apps/desktop-wpf/App.xaml @@ -1,9 +1,9 @@ - - - - - + + + + + diff --git a/desktop-wpf/App.xaml.cs b/4-desktop-apps/desktop-wpf/App.xaml.cs similarity index 94% rename from desktop-wpf/App.xaml.cs rename to 4-desktop-apps/desktop-wpf/App.xaml.cs index bce38aa..1318666 100644 --- a/desktop-wpf/App.xaml.cs +++ b/4-desktop-apps/desktop-wpf/App.xaml.cs @@ -1,11 +1,11 @@ -using System.Windows; - -namespace MsalExample -{ - /// - /// Interaction logic for App.xaml - /// - public partial class App : Application - { - } -} +using System.Windows; + +namespace MsalExample +{ + /// + /// Interaction logic for App.xaml + /// + public partial class App : Application + { + } +} diff --git a/desktop-wpf/AssemblyInfo.cs b/4-desktop-apps/desktop-wpf/AssemblyInfo.cs similarity index 98% rename from desktop-wpf/AssemblyInfo.cs rename to 4-desktop-apps/desktop-wpf/AssemblyInfo.cs index 74087a1..8b5504e 100644 --- a/desktop-wpf/AssemblyInfo.cs +++ b/4-desktop-apps/desktop-wpf/AssemblyInfo.cs @@ -1,10 +1,10 @@ -using System.Windows; - -[assembly: ThemeInfo( - ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located - //(used if a resource is not found in the page, - // or application resource dictionaries) - ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located - //(used if a resource is not found in the page, - // app, or any theme specific resource dictionaries) -)] +using System.Windows; + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/desktop-wpf/MainWindow.xaml b/4-desktop-apps/desktop-wpf/MainWindow.xaml similarity index 98% rename from desktop-wpf/MainWindow.xaml rename to 4-desktop-apps/desktop-wpf/MainWindow.xaml index fa6accc..a19dc9f 100644 --- a/desktop-wpf/MainWindow.xaml +++ b/4-desktop-apps/desktop-wpf/MainWindow.xaml @@ -1,27 +1,27 @@ - - -