feat (protect-api): [contoso.service] protect endpoints (#5)
This commit is contained in:
Родитель
e5e7b48bf2
Коммит
caf5ff7d8a
|
@ -23,6 +23,7 @@
|
||||||
// ---------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------
|
||||||
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Media.Animation;
|
using Microsoft.UI.Xaml.Media.Animation;
|
||||||
|
@ -34,7 +35,6 @@ using Contoso.App.Views;
|
||||||
using Contoso.Repository;
|
using Contoso.Repository;
|
||||||
using Contoso.Repository.Rest;
|
using Contoso.Repository.Rest;
|
||||||
using Contoso.Repository.Sql;
|
using Contoso.Repository.Sql;
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace Contoso.App
|
namespace Contoso.App
|
||||||
{
|
{
|
||||||
|
@ -125,7 +125,11 @@ namespace Contoso.App
|
||||||
/// Configures the app to use the REST data source. For convenience, a read-only source is provided.
|
/// Configures the app to use the REST data source. For convenience, a read-only source is provided.
|
||||||
/// You can also deploy your own copy of the REST service locally or to Azure. See the README for details.
|
/// You can also deploy your own copy of the REST service locally or to Azure. See the README for details.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static void UseRest() =>
|
public static void UseRest()
|
||||||
Repository = new RestContosoRepository($"{Constants.ApiUrl}/api/");
|
{
|
||||||
|
var accessToken = Task.Run(async () => await MsalHelper.GetTokenAsync(Constants.WebApiScopes)).Result;
|
||||||
|
|
||||||
|
Repository = new RestContosoRepository($"{Constants.ApiUrl}/api/", accessToken);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,118 @@
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
//
|
||||||
|
// The MIT License (MIT)
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Identity.Client;
|
||||||
|
using Microsoft.Identity.Client.Extensions.Msal;
|
||||||
|
|
||||||
|
namespace Contoso.App
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Handles user authentication and getting user info from the Microsoft Graph API.
|
||||||
|
/// </summary>
|
||||||
|
public class MsalHelper
|
||||||
|
{
|
||||||
|
private static readonly IPublicClientApplication _msalPublicClientApp;
|
||||||
|
|
||||||
|
static MsalHelper()
|
||||||
|
{
|
||||||
|
_msalPublicClientApp = PublicClientApplicationBuilder
|
||||||
|
.Create(Repository.Constants.AccountClientId)
|
||||||
|
.WithAuthority(AadAuthorityAudience.AzureAdMultipleOrgs)
|
||||||
|
.WithDefaultRedirectUri()
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
Task.Run(ConfigureCachingAsync);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async void ConfigureCachingAsync()
|
||||||
|
{
|
||||||
|
// Configuring the token cache
|
||||||
|
var storageProperties =
|
||||||
|
new StorageCreationPropertiesBuilder(Repository.Constants.CacheFileName, MsalCacheHelper.UserRootDirectory)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
// This hooks up the cache into MSAL
|
||||||
|
var cacheHelper = await MsalCacheHelper.CreateAsync(storageProperties);
|
||||||
|
cacheHelper.RegisterCache(_msalPublicClientApp.UserTokenCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<IEnumerable<IAccount>> GetAccountsAsync()
|
||||||
|
=> await _msalPublicClientApp.GetAccountsAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets an auth token for the user, which can be used to call the Microsoft Graph API.
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<string> GetTokenAsync(IEnumerable<String> scopes)
|
||||||
|
{
|
||||||
|
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(
|
||||||
|
scopes,
|
||||||
|
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(
|
||||||
|
scopes)
|
||||||
|
.ExecuteAsync();
|
||||||
|
|
||||||
|
// TODO: [feat] when user cancel the authN flow, the UX will be as if the login had failed. This can be improved with a more friendly UI experience on top of this.
|
||||||
|
}
|
||||||
|
|
||||||
|
return msalAuthenticationResult.AccessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task RemoveCachedTokens()
|
||||||
|
{
|
||||||
|
// All cached tokens will be removed.
|
||||||
|
// The next token request will require the user to sign in.
|
||||||
|
foreach (var account in (await MsalHelper.GetAccountsAsync()).ToList())
|
||||||
|
{
|
||||||
|
await _msalPublicClientApp.RemoveAsync(account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,8 +31,6 @@ using System.Net.Http.Headers;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Graph;
|
using Microsoft.Graph;
|
||||||
using Microsoft.Identity.Client;
|
|
||||||
using Microsoft.Identity.Client.Extensions.Msal;
|
|
||||||
using Microsoft.UI.Dispatching;
|
using Microsoft.UI.Dispatching;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
using Microsoft.UI.Xaml.Media.Imaging;
|
using Microsoft.UI.Xaml.Media.Imaging;
|
||||||
|
@ -44,22 +42,11 @@ namespace Contoso.App.ViewModels
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class AuthenticationViewModel : BindableBase
|
public class AuthenticationViewModel : BindableBase
|
||||||
{
|
{
|
||||||
// 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;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new AuthenticationViewModel for logging users in and getting their info.
|
/// Creates a new AuthenticationViewModel for logging users in and getting their info.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public AuthenticationViewModel()
|
public AuthenticationViewModel()
|
||||||
{
|
{
|
||||||
_msalPublicClientApp = PublicClientApplicationBuilder
|
|
||||||
.Create(Repository.Constants.AccountClientId)
|
|
||||||
.WithAuthority(AadAuthorityAudience.AzureAdMultipleOrgs)
|
|
||||||
.WithDefaultRedirectUri()
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
Task.Run(PrepareAsync);
|
Task.Run(PrepareAsync);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,16 +165,7 @@ namespace Contoso.App.ViewModels
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task PrepareAsync()
|
public async Task PrepareAsync()
|
||||||
{
|
{
|
||||||
// Configuring the token cache
|
var accounts = await MsalHelper.GetAccountsAsync();
|
||||||
var storageProperties =
|
|
||||||
new StorageCreationPropertiesBuilder(Repository.Constants.CacheFileName, MsalCacheHelper.UserRootDirectory)
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
// This hooks up the cache into MSAL
|
|
||||||
var cacheHelper = await MsalCacheHelper.CreateAsync(storageProperties);
|
|
||||||
cacheHelper.RegisterCache(_msalPublicClientApp.UserTokenCache);
|
|
||||||
|
|
||||||
var accounts = await _msalPublicClientApp.GetAccountsAsync();
|
|
||||||
if (accounts.Any())
|
if (accounts.Any())
|
||||||
{
|
{
|
||||||
await LoginAsync();
|
await LoginAsync();
|
||||||
|
@ -207,7 +185,7 @@ namespace Contoso.App.ViewModels
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
SetVisible(vm => vm.ShowLoading);
|
SetVisible(vm => vm.ShowLoading);
|
||||||
string token = await GetTokenAsync();
|
string token = await MsalHelper.GetTokenAsync(Repository.Constants.GraphpApiScopes);
|
||||||
if (token != null)
|
if (token != null)
|
||||||
{
|
{
|
||||||
await SetUserInfoAsync(token);
|
await SetUserInfoAsync(token);
|
||||||
|
@ -226,53 +204,12 @@ namespace Contoso.App.ViewModels
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets an auth token for the user, which can be used to call the Microsoft Graph API.
|
|
||||||
/// </summary>
|
|
||||||
private async Task<string> GetTokenAsync()
|
|
||||||
{
|
|
||||||
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(
|
|
||||||
Repository.Constants.Scopes,
|
|
||||||
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(
|
|
||||||
Repository.Constants.Scopes)
|
|
||||||
.ExecuteAsync();
|
|
||||||
|
|
||||||
// TODO: [feat] when user cancel the authN flow, the UX will be as if the login had failed. This can be improved with a more friendly UI experience on top of this.
|
|
||||||
}
|
|
||||||
|
|
||||||
return msalAuthenticationResult.AccessToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets and processes the user's info from the Microsoft Graph API.
|
/// Gets and processes the user's info from the Microsoft Graph API.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task SetUserInfoAsync(string token)
|
private async Task SetUserInfoAsync(string token)
|
||||||
{
|
{
|
||||||
var accounts = await _msalPublicClientApp.GetAccountsAsync();
|
var accounts = await MsalHelper.GetAccountsAsync();
|
||||||
var domain = accounts?.First().Username.Split('@')[1] ?? string.Empty;
|
var domain = accounts?.First().Username.Split('@')[1] ?? string.Empty;
|
||||||
|
|
||||||
var graph = new GraphServiceClient(new DelegateAuthenticationProvider(message =>
|
var graph = new GraphServiceClient(new DelegateAuthenticationProvider(message =>
|
||||||
|
@ -347,12 +284,7 @@ namespace Contoso.App.ViewModels
|
||||||
};
|
};
|
||||||
signoutDialog.PrimaryButtonClick += async (_, _) =>
|
signoutDialog.PrimaryButtonClick += async (_, _) =>
|
||||||
{
|
{
|
||||||
// All cached tokens will be removed.
|
await MsalHelper.RemoveCachedTokens();
|
||||||
// The next token request will require the user to sign in.
|
|
||||||
foreach (var account in (await _msalPublicClientApp.GetAccountsAsync()).ToList())
|
|
||||||
{
|
|
||||||
await _msalPublicClientApp.RemoveAsync(account);
|
|
||||||
}
|
|
||||||
SetVisible(vm => vm.ShowWelcome);
|
SetVisible(vm => vm.ShowWelcome);
|
||||||
};
|
};
|
||||||
signoutDialog.XamlRoot = App.Window.Content.XamlRoot;
|
signoutDialog.XamlRoot = App.Window.Content.XamlRoot;
|
||||||
|
|
|
@ -43,6 +43,11 @@ namespace Contoso.Repository
|
||||||
/// The Azure Active Directory (AAD) client id.
|
/// The Azure Active Directory (AAD) client id.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string AccountClientId = "<TODO: Insert Azure client Id>";
|
public const string AccountClientId = "<TODO: Insert Azure client Id>";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The Azure Active Directory (AAD) rest api client id.
|
||||||
|
/// </summary>
|
||||||
|
public const string WebApiClientId = "< TODO: Insert Azure client Id>";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Connection string for a server-side SQL database.
|
/// Connection string for a server-side SQL database.
|
||||||
|
@ -52,7 +57,11 @@ namespace Contoso.Repository
|
||||||
// Cache settings
|
// Cache settings
|
||||||
public const string CacheFileName = "contosoapp_msal_cache.txt";
|
public const string CacheFileName = "contosoapp_msal_cache.txt";
|
||||||
|
|
||||||
// App settings
|
// Graph Api Scopes
|
||||||
public static readonly string[] Scopes = new[] { "https://graph.microsoft.com/User.Read" };
|
public static readonly string[] GraphpApiScopes = new[] { "https://graph.microsoft.com/User.Read" };
|
||||||
|
|
||||||
|
// Downstream Api Scopes
|
||||||
|
public static readonly string[] WebApiScopes = new[] { $"api://{WebApiClientId}/Contoso.ReadWrite" };
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using System;
|
using System;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
@ -48,10 +49,16 @@ namespace Contoso.Repository.Rest
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Makes an HTTP GET request to the given controller and returns the deserialized response content.
|
/// Makes an HTTP GET request to the given controller and returns the deserialized response content.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<TResult> GetAsync<TResult>(string controller)
|
public async Task<TResult> GetAsync<TResult>(string controller, string accessToken)
|
||||||
{
|
{
|
||||||
|
if (accessToken is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(accessToken));
|
||||||
|
}
|
||||||
|
|
||||||
using (var client = BaseClient())
|
using (var client = BaseClient())
|
||||||
{
|
{
|
||||||
|
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
|
||||||
var response = await client.GetAsync(controller);
|
var response = await client.GetAsync(controller);
|
||||||
string json = await response.Content.ReadAsStringAsync();
|
string json = await response.Content.ReadAsStringAsync();
|
||||||
TResult obj = JsonConvert.DeserializeObject<TResult>(json);
|
TResult obj = JsonConvert.DeserializeObject<TResult>(json);
|
||||||
|
@ -63,10 +70,16 @@ namespace Contoso.Repository.Rest
|
||||||
/// Makes an HTTP POST request to the given controller with the given object as the body.
|
/// Makes an HTTP POST request to the given controller with the given object as the body.
|
||||||
/// Returns the deserialized response content.
|
/// Returns the deserialized response content.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<TResult> PostAsync<TRequest, TResult>(string controller, TRequest body)
|
public async Task<TResult> PostAsync<TRequest, TResult>(string controller, TRequest body, string accessToken)
|
||||||
{
|
{
|
||||||
|
if (accessToken is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(accessToken));
|
||||||
|
}
|
||||||
|
|
||||||
using (var client = BaseClient())
|
using (var client = BaseClient())
|
||||||
{
|
{
|
||||||
|
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
|
||||||
var response = await client.PostAsync(controller, new JsonStringContent(body));
|
var response = await client.PostAsync(controller, new JsonStringContent(body));
|
||||||
string json = await response.Content.ReadAsStringAsync();
|
string json = await response.Content.ReadAsStringAsync();
|
||||||
TResult obj = JsonConvert.DeserializeObject<TResult>(json);
|
TResult obj = JsonConvert.DeserializeObject<TResult>(json);
|
||||||
|
@ -78,10 +91,16 @@ namespace Contoso.Repository.Rest
|
||||||
/// Makes an HTTP DELETE request to the given controller and includes all the given
|
/// Makes an HTTP DELETE request to the given controller and includes all the given
|
||||||
/// object's properties as URL parameters. Returns the deserialized response content.
|
/// object's properties as URL parameters. Returns the deserialized response content.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task DeleteAsync(string controller, Guid objectId)
|
public async Task DeleteAsync(string controller, Guid objectId, string accessToken)
|
||||||
{
|
{
|
||||||
|
if (accessToken is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(accessToken));
|
||||||
|
}
|
||||||
|
|
||||||
using (var client = BaseClient())
|
using (var client = BaseClient())
|
||||||
{
|
{
|
||||||
|
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
|
||||||
await client.DeleteAsync($"{controller}/{objectId}");
|
await client.DeleteAsync($"{controller}/{objectId}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,16 +32,18 @@ namespace Contoso.Repository.Rest
|
||||||
public class RestContosoRepository : IContosoRepository
|
public class RestContosoRepository : IContosoRepository
|
||||||
{
|
{
|
||||||
private readonly string _url;
|
private readonly string _url;
|
||||||
|
private readonly string _accessToken;
|
||||||
public RestContosoRepository(string url)
|
|
||||||
|
public RestContosoRepository(string url, string accessToken)
|
||||||
{
|
{
|
||||||
_url = url;
|
_url = url;
|
||||||
|
_accessToken = accessToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ICustomerRepository Customers => new RestCustomerRepository(_url);
|
public ICustomerRepository Customers => new RestCustomerRepository(_url, _accessToken);
|
||||||
|
|
||||||
public IOrderRepository Orders => new RestOrderRepository(_url);
|
public IOrderRepository Orders => new RestOrderRepository(_url, _accessToken);
|
||||||
|
|
||||||
public IProductRepository Products => new RestProductRepository(_url);
|
public IProductRepository Products => new RestProductRepository(_url, _accessToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,25 +36,27 @@ namespace Contoso.Repository.Rest
|
||||||
public class RestCustomerRepository : ICustomerRepository
|
public class RestCustomerRepository : ICustomerRepository
|
||||||
{
|
{
|
||||||
private readonly HttpHelper _http;
|
private readonly HttpHelper _http;
|
||||||
|
private readonly string _accessToken;
|
||||||
public RestCustomerRepository(string baseUrl)
|
|
||||||
|
public RestCustomerRepository(string baseUrl, string accessToken)
|
||||||
{
|
{
|
||||||
_http = new HttpHelper(baseUrl);
|
_http = new HttpHelper(baseUrl);
|
||||||
|
_accessToken = accessToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<Customer>> GetAsync() =>
|
public async Task<IEnumerable<Customer>> GetAsync() =>
|
||||||
await _http.GetAsync<IEnumerable<Customer>>("customer");
|
await _http.GetAsync<IEnumerable<Customer>>("customer", _accessToken);
|
||||||
|
|
||||||
public async Task<IEnumerable<Customer>> GetAsync(string search) =>
|
public async Task<IEnumerable<Customer>> GetAsync(string search) =>
|
||||||
await _http.GetAsync<IEnumerable<Customer>>($"customer/search?value={search}");
|
await _http.GetAsync<IEnumerable<Customer>>($"customer/search?value={search}", _accessToken);
|
||||||
|
|
||||||
public async Task<Customer> GetAsync(Guid id) =>
|
public async Task<Customer> GetAsync(Guid id) =>
|
||||||
await _http.GetAsync<Customer>($"customer/{id}");
|
await _http.GetAsync<Customer>($"customer/{id}", _accessToken);
|
||||||
|
|
||||||
public async Task<Customer> UpsertAsync(Customer customer) =>
|
public async Task<Customer> UpsertAsync(Customer customer) =>
|
||||||
await _http.PostAsync<Customer, Customer>("customer", customer);
|
await _http.PostAsync<Customer, Customer>("customer", customer, _accessToken);
|
||||||
|
|
||||||
public async Task DeleteAsync(Guid customerId) =>
|
public async Task DeleteAsync(Guid customerId) =>
|
||||||
await _http.DeleteAsync("customer", customerId);
|
await _http.DeleteAsync("customer", customerId, _accessToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,28 +36,30 @@ namespace Contoso.Repository.Rest
|
||||||
public class RestOrderRepository : IOrderRepository
|
public class RestOrderRepository : IOrderRepository
|
||||||
{
|
{
|
||||||
private readonly HttpHelper _http;
|
private readonly HttpHelper _http;
|
||||||
|
private readonly string _accessToken;
|
||||||
|
|
||||||
public RestOrderRepository(string baseUrl)
|
public RestOrderRepository(string baseUrl, string accessToken)
|
||||||
{
|
{
|
||||||
_http = new HttpHelper(baseUrl);
|
_http = new HttpHelper(baseUrl);
|
||||||
|
_accessToken = accessToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<Order>> GetAsync() =>
|
public async Task<IEnumerable<Order>> GetAsync() =>
|
||||||
await _http.GetAsync<IEnumerable<Order>>("order");
|
await _http.GetAsync<IEnumerable<Order>>("order", _accessToken);
|
||||||
|
|
||||||
public async Task<Order> GetAsync(Guid id) =>
|
public async Task<Order> GetAsync(Guid id) =>
|
||||||
await _http.GetAsync<Order>($"order/{id}");
|
await _http.GetAsync<Order>($"order/{id}", _accessToken);
|
||||||
|
|
||||||
public async Task<IEnumerable<Order>> GetForCustomerAsync(Guid customerId) =>
|
public async Task<IEnumerable<Order>> GetForCustomerAsync(Guid customerId) =>
|
||||||
await _http.GetAsync<IEnumerable<Order>>($"order/customer/{customerId}");
|
await _http.GetAsync<IEnumerable<Order>>($"order/customer/{customerId}", _accessToken);
|
||||||
|
|
||||||
public async Task<IEnumerable<Order>> GetAsync(string search) =>
|
public async Task<IEnumerable<Order>> GetAsync(string search) =>
|
||||||
await _http.GetAsync<IEnumerable<Order>>($"order/search?value={search}");
|
await _http.GetAsync<IEnumerable<Order>>($"order/search?value={search}", _accessToken);
|
||||||
|
|
||||||
public async Task<Order> UpsertAsync(Order order) =>
|
public async Task<Order> UpsertAsync(Order order) =>
|
||||||
await _http.PostAsync<Order, Order>("order", order);
|
await _http.PostAsync<Order, Order>("order", order, _accessToken);
|
||||||
|
|
||||||
public async Task DeleteAsync(Guid orderId) =>
|
public async Task DeleteAsync(Guid orderId) =>
|
||||||
await _http.DeleteAsync("order", orderId);
|
await _http.DeleteAsync("order", orderId, _accessToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,19 +35,21 @@ namespace Contoso.Repository.Rest
|
||||||
public class RestProductRepository : IProductRepository
|
public class RestProductRepository : IProductRepository
|
||||||
{
|
{
|
||||||
private readonly HttpHelper _http;
|
private readonly HttpHelper _http;
|
||||||
|
private readonly string _accessToken;
|
||||||
|
|
||||||
public RestProductRepository(string baseUrl)
|
public RestProductRepository(string baseUrl, string accessToken)
|
||||||
{
|
{
|
||||||
_http = new HttpHelper(baseUrl);
|
_http = new HttpHelper(baseUrl);
|
||||||
|
_accessToken = accessToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<Product>> GetAsync() =>
|
public async Task<IEnumerable<Product>> GetAsync() =>
|
||||||
await _http.GetAsync<IEnumerable<Product>>("product");
|
await _http.GetAsync<IEnumerable<Product>>("product", _accessToken);
|
||||||
|
|
||||||
public async Task<Product> GetAsync(Guid id) =>
|
public async Task<Product> GetAsync(Guid id) =>
|
||||||
await _http.GetAsync<Product>($"product/{id}");
|
await _http.GetAsync<Product>($"product/{id}", _accessToken);
|
||||||
|
|
||||||
public async Task<IEnumerable<Product>> GetAsync(string search) =>
|
public async Task<IEnumerable<Product>> GetAsync(string search) =>
|
||||||
await _http.GetAsync<IEnumerable<Product>>($"product/search?value={search}");
|
await _http.GetAsync<IEnumerable<Product>>($"product/search?value={search}", _accessToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.5" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.5" />
|
||||||
|
<PackageReference Include="Microsoft.Identity.Web" Version="1.*" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -24,7 +24,8 @@
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Contoso.Models;
|
using Contoso.Models;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
|
||||||
namespace Contoso.Service.Controllers
|
namespace Contoso.Service.Controllers
|
||||||
{
|
{
|
||||||
namespace Contoso.Service.Controllers
|
namespace Contoso.Service.Controllers
|
||||||
|
@ -32,7 +33,8 @@ namespace Contoso.Service.Controllers
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Contains methods for interacting with customer data.
|
/// Contains methods for interacting with customer data.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiController]
|
[ApiController]
|
||||||
|
[Authorize(Policy = "AuthZPolicy")]
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
public class CustomerController : ControllerBase
|
public class CustomerController : ControllerBase
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
// ---------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------
|
||||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
//
|
//
|
||||||
// The MIT License (MIT)
|
// The MIT License (MIT)
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
// in the Software without restriction, including without limitation the rights
|
// in the Software without restriction, including without limitation the rights
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
// copies of the Software, and to permit persons to whom the Software is
|
||||||
// furnished to do so, subject to the following conditions:
|
// furnished to do so, subject to the following conditions:
|
||||||
//
|
//
|
||||||
// The above copyright notice and this permission notice shall be included in
|
// The above copyright notice and this permission notice shall be included in
|
||||||
// all copies or substantial portions of the Software.
|
// all copies or substantial portions of the Software.
|
||||||
//
|
//
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
@ -22,11 +22,14 @@
|
||||||
// THE SOFTWARE.
|
// THE SOFTWARE.
|
||||||
// ---------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------
|
||||||
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Contoso.Models;
|
|
||||||
using Contoso.Repository;
|
|
||||||
using Contoso.Repository.Sql;
|
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Identity.Web;
|
||||||
|
using Contoso.Models;
|
||||||
|
using Constants = Contoso.Repository.Constants;
|
||||||
|
using Contoso.Repository.Sql;
|
||||||
|
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
@ -42,6 +45,8 @@ builder.Services.AddControllers().AddJsonOptions(x =>
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
app.UseAuthorization();
|
||||||
|
|
||||||
if (app.Environment.IsDevelopment())
|
if (app.Environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
app.UseDeveloperExceptionPage();
|
app.UseDeveloperExceptionPage();
|
||||||
|
@ -49,4 +54,4 @@ if (app.Environment.IsDevelopment())
|
||||||
|
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
app.Run(Constants.ApiUrl);
|
app.Run(Constants.ApiUrl);
|
||||||
|
|
|
@ -1,4 +1,10 @@
|
||||||
{
|
{
|
||||||
|
"AzureAd": {
|
||||||
|
"Instance": "https://login.microsoftonline.com/",
|
||||||
|
"ClientId": "[Enter_your_application_client_ID_from_Azure_Portal, _e.g._2ec40e65-ba09-4853-bcde-bcb60029e596]",
|
||||||
|
"TenantId": "common",
|
||||||
|
"Scepes": "Contoso.ReadWrite"
|
||||||
|
},
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"IncludeScopes": false,
|
"IncludeScopes": false,
|
||||||
"Debug": {
|
"Debug": {
|
||||||
|
|
40
README.md
40
README.md
|
@ -123,7 +123,7 @@ Set your startup project as **Contoso.App**, the architecture to x86 or x64, and
|
||||||
To fully explore the sample, you'll need to connect to your own Azure Active Directory and data source. Values you need to fill
|
To fully explore the sample, you'll need to connect to your own Azure Active Directory and data source. Values you need to fill
|
||||||
are in [Constants.cs](ContosoRepository/Constants.cs).
|
are in [Constants.cs](ContosoRepository/Constants.cs).
|
||||||
|
|
||||||
* **Client Id**: Set the *AccountClientId* field to your Azure account client Id.
|
* **Deskptop app Client Id**: Set the *AccountClientId* field to your Azure account client Id.
|
||||||
* **API endpoint**: Set the value of the *BaseUrl* constant to match the url the backing service is running on.
|
* **API endpoint**: Set the value of the *BaseUrl* constant to match the url the backing service is running on.
|
||||||
* **Set a database connection string**: Set the connection string to one of your own local or remote databases.
|
* **Set a database connection string**: Set the connection string to one of your own local or remote databases.
|
||||||
* **Associate this sample with the Store**: Authentication requires store association. To associate the app with the Store,
|
* **Associate this sample with the Store**: Authentication requires store association. To associate the app with the Store,
|
||||||
|
@ -133,22 +133,31 @@ right click the project in Visual Studio and select **Store** -> *Associate App
|
||||||
|
|
||||||
You can then either start the service running locally, or deploy it to Azure.
|
You can then either start the service running locally, or deploy it to Azure.
|
||||||
|
|
||||||
### Register the Azure Active Directory app
|
### Register the Azure Active Directory app (Web api)
|
||||||
|
|
||||||
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://docs.microsoft.com/azure/active-directory/develop/quickstart-register-app) to register the web application.
|
||||||
|
|
||||||
Use these settings in your app registration.
|
| App registration <br/> setting | Value for this sample app | Notes |
|
||||||
|
|-------------------------------:|:--------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------|
|
||||||
|
| **Name** | `active-directory-contoso-customer-oders-protected-api` | Suggested value for this sample. <br/> You can change the app name at any time. |
|
||||||
|
| **Supported account types** | **Accounts in this organizational directory only (Any Azure AD directory - Multitenant)** | Required for this sample. <br/> Support for the Single tenant. |
|
||||||
|
| **Expose an API** | **Scope name**: `Contoso.ReadWrite`<br/>**Who can consent?**: **Admins and users**<br/>**Admin consent display name**: `Act on behalf of the user`<br/>**Admin consent description**: `Allows the API to act on behalf of the user.`<br/>**User consent display name**: `Act on your behalf`<br/>**User consent description**: `Allows the API to act on your behalf.`<br/>**State**: **Enabled** | Add a new scope that reads as follows `api://{clientId}/Contoso.ReadWrite`. Required value for this sample. |
|
||||||
|
|
||||||
| App registration <br/> setting | Value for this sample app | Notes |
|
### Register the Azure Active Directory app (Client desktop app)
|
||||||
|--------------------------------:|:-------------------------------------------------------------------------------------|:--------------------------------------------------------------------------------|
|
|
||||||
| **Name** | `active-directory-contoso-customer-oders-winui3` | Suggested value for this sample. <br/> You can change the app name at any time. |
|
Then, 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 deskptop application.
|
||||||
| **Supported account types** | **Accounts in any organizational directory (Any Azure AD directory - Multitenant)** | Suggested value for this sample. |
|
|
||||||
| **Platform type** | **Mobile and desktop applications** | Required value for this sample |
|
| App registration <br/> setting | Value for this sample app | Notes |
|
||||||
| **Redirect URIs** | `https://login.microsoftonline.com/common/oauth2/nativeclient` | Required value for this sample
|
|--------------------------------:|:-------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------------------|
|
||||||
|
| **Name** | `active-directory-contoso-customer-oders-winui3` | Suggested value for this sample. <br/> You can change the app name at any time. |
|
||||||
|
| **Supported account types** | **Accounts in any organizational directory (Any Azure AD directory - Multitenant)** | 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 |
|
||||||
|
| **API Permissions** | `active-directory-contoso-customer-oders-protected-api` <br/> ` Contoso.ReadWrite` | Add a new delegated permission for `api://<application-id>/Contoso.ReadWrite`. Required value for this sample. |
|
||||||
|
|
||||||
### Run locally (SQLite)
|
### Run locally (SQLite)
|
||||||
|
|
||||||
1. Navigate to [Constants.cs](ContosoRepository/Constants.cs) and complete the following values:
|
1. Navigate to [Constants.cs](ContosoRepository/Constants.cs) and complete the following value using the `active-directory-contoso-customer-oders-winui3` Application (client) ID from Azure Portal:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
...
|
...
|
||||||
|
@ -191,12 +200,21 @@ Use these settings in your app registration.
|
||||||
```
|
```
|
||||||
|
|
||||||
1. Right-click the solution, choose *Properties*, and choose to start both **Contoso.App** and **Contoso.Service** at the same time.
|
1. Right-click the solution, choose *Properties*, and choose to start both **Contoso.App** and **Contoso.Service** at the same time.
|
||||||
|
1. Open the [appsettings.json](ContosoService/appsettings.json) file and modify the following field using the `active-directory-contoso-customer-oders-protected-api` Application (client) ID from Azure Portal:
|
||||||
|
|
||||||
|
```json
|
||||||
|
...
|
||||||
|
"ClientId": "Enter_the_Application_Id_here"
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
1. Navigate to [Constants.cs](ContosoRepository/Constants.cs) and complete the following values:
|
1. Navigate to [Constants.cs](ContosoRepository/Constants.cs) and complete the following values:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
public const string ApiUrl = @"http://localhost:65027";
|
public const string ApiUrl = @"http://localhost:65027";
|
||||||
...
|
...
|
||||||
public const string AccountClientId = "Application_Client_ID_From_Azure_Portal";
|
public const string AccountClientId = "Application_Client_ID_From_Azure_Portal";
|
||||||
|
public const string WebApiClientId = "Application_Client_ID_From_Azure_Portal";
|
||||||
...
|
...
|
||||||
public const string SqlAzureConnectionString = "Data Source=(LocalDB)\\ContosoDb;Initial Catalog=CONTOSODB;Integrated Security=True";
|
public const string SqlAzureConnectionString = "Data Source=(LocalDB)\\ContosoDb;Initial Catalog=CONTOSODB;Integrated Security=True";
|
||||||
...
|
...
|
||||||
|
|
Загрузка…
Ссылка в новой задаче