Skip to content

Commit 3f93fc2

Browse files
authored
Merge pull request Azure-Samples#2 from Azure-Samples/dev
New chapter: advanced scenarios
2 parents a847b1d + 41ffcb0 commit 3f93fc2

File tree

128 files changed

+20854
-585
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

128 files changed

+20854
-585
lines changed

6-Multitenancy/1-call-api-mt/README-incremental.md

Lines changed: 0 additions & 430 deletions
This file was deleted.
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Linq;
6+
using System.Net;
7+
using System.Threading.Tasks;
8+
using Microsoft.AspNetCore.Authorization;
9+
using Microsoft.AspNetCore.Http;
10+
using Microsoft.AspNetCore.Mvc;
11+
using Microsoft.Identity.Client;
12+
using Microsoft.Identity.Web;
13+
using Microsoft.Identity.Web.Resource;
14+
using Microsoft.EntityFrameworkCore;
15+
using Microsoft.Graph;
16+
using ProfileAPI.Models;
17+
using Newtonsoft.Json;
18+
using Microsoft.Extensions.Options;
19+
20+
namespace ProfileAPI.Controllers
21+
{
22+
[Authorize]
23+
[Route("api/[controller]")]
24+
[ApiController]
25+
public class ProfileController : ControllerBase
26+
{
27+
/// <summary>
28+
/// The Web API will only accept tokens 1) for users, and
29+
/// 2) having the access_as_user scope for this API
30+
/// </summary>
31+
static readonly string[] scopeRequiredByApi = new string[] { "access_as_user" };
32+
33+
private readonly ProfileContext _context;
34+
private readonly ITokenAcquisition _tokenAcquisition;
35+
private readonly GraphServiceClient _graphServiceClient;
36+
private readonly IOptions<MicrosoftGraphOptions> _graphOptions;
37+
38+
public ProfileController(ProfileContext context, ITokenAcquisition tokenAcquisition, GraphServiceClient graphServiceClient, IOptions<MicrosoftGraphOptions> graphOptions)
39+
{
40+
_context = context;
41+
_tokenAcquisition = tokenAcquisition;
42+
_graphServiceClient = graphServiceClient;
43+
_graphOptions = graphOptions;
44+
}
45+
46+
// GET: api/ProfileItems/5
47+
[HttpGet("{id}")]
48+
public async Task<ActionResult<ProfileItem>> GetProfileItem(string id)
49+
{
50+
HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);
51+
52+
var profileItem = await _context.ProfileItems.FindAsync(id);
53+
54+
if (profileItem == null)
55+
{
56+
return NotFound();
57+
}
58+
59+
return profileItem;
60+
}
61+
62+
// POST api/values
63+
[HttpPost]
64+
public async Task<ActionResult<ProfileItem>> PostProfileItem(ProfileItem profileItem)
65+
{
66+
HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);
67+
68+
profileItem.FirstLogin = false;
69+
70+
// This is a synchronous call, so that the clients know, when they call Get, that the
71+
// call to the downstream API (Microsoft Graph) has completed.
72+
try
73+
{
74+
User profile = await _graphServiceClient.Me.Request().GetAsync();
75+
76+
profileItem.Id = profile.Id;
77+
profileItem.UserPrincipalName = profile.UserPrincipalName;
78+
profileItem.GivenName = profile.GivenName;
79+
profileItem.Surname = profile.Surname;
80+
profileItem.JobTitle = profile.JobTitle;
81+
profileItem.MobilePhone = profile.MobilePhone;
82+
profileItem.PreferredLanguage = profile.PreferredLanguage;
83+
}
84+
catch (MsalException ex)
85+
{
86+
HttpContext.Response.ContentType = "application/json";
87+
HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
88+
await HttpContext.Response.WriteAsync(JsonConvert.SerializeObject("An authentication error occurred while acquiring a token for downstream API\n" + ex.ErrorCode + "\n" + ex.Message));
89+
}
90+
catch (Exception ex)
91+
{
92+
if (ex.InnerException is MicrosoftIdentityWebChallengeUserException challengeException)
93+
{
94+
await _tokenAcquisition.ReplyForbiddenWithWwwAuthenticateHeaderAsync(_graphOptions.Value.Scopes.Split(' '),
95+
challengeException.MsalUiRequiredException);
96+
HttpContext.Response.ContentType = "application/json";
97+
HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;
98+
await HttpContext.Response.WriteAsync(JsonConvert.SerializeObject("interaction required"));
99+
}
100+
else
101+
{
102+
HttpContext.Response.ContentType = "application/json";
103+
HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;
104+
await HttpContext.Response.WriteAsync(JsonConvert.SerializeObject("An error occurred while calling the downstream API\n" + ex.Message));
105+
}
106+
}
107+
108+
_context.ProfileItems.Add(profileItem);
109+
await _context.SaveChangesAsync();
110+
111+
return CreatedAtAction("GetProfileItem", new { id = profileItem.Id }, profileItem);
112+
}
113+
114+
115+
116+
// PUT: api/ProfileItems/5
117+
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
118+
// more details see https://aka.ms/RazorPagesCRUD.
119+
[HttpPut("{id}")]
120+
public async Task<IActionResult> PutProfileItem(string id, ProfileItem profileItem)
121+
{
122+
HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);
123+
124+
if (id != profileItem.Id)
125+
{
126+
return BadRequest();
127+
}
128+
129+
_context.Entry(profileItem).State = EntityState.Modified;
130+
131+
try
132+
{
133+
await _context.SaveChangesAsync();
134+
}
135+
catch (DbUpdateConcurrencyException)
136+
{
137+
if (!ProfileItemExists(id))
138+
{
139+
return NotFound();
140+
}
141+
else
142+
{
143+
throw;
144+
}
145+
}
146+
147+
return NoContent();
148+
}
149+
150+
private bool ProfileItemExists(string id)
151+
{
152+
return _context.ProfileItems.Any(e => e.Id == id);
153+
}
154+
}
155+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using Microsoft.EntityFrameworkCore;
2+
3+
namespace ProfileAPI.Models
4+
{
5+
public class ProfileContext : DbContext
6+
{
7+
public ProfileContext(DbContextOptions<ProfileContext> options)
8+
: base(options)
9+
{
10+
11+
}
12+
public DbSet<ProfileItem> ProfileItems { get; set; }
13+
}
14+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System.ComponentModel.DataAnnotations;
5+
6+
namespace ProfileAPI.Models
7+
{
8+
public class ProfileItem
9+
{
10+
[Key]
11+
public string Id { get; set; }
12+
public string UserPrincipalName { get; set; }
13+
public string GivenName { get; set; }
14+
public string Surname { get; set; }
15+
public string JobTitle { get; set; }
16+
public string MobilePhone { get; set; }
17+
public string PreferredLanguage { get; set; }
18+
public bool FirstLogin { get; set; }
19+
}
20+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netcoreapp3.1</TargetFramework>
5+
<UserSecretsId>aspnet-ProfileAPI-03230DB1-5145-408C-A48B-BE3DAFC56C30</UserSecretsId>
6+
<WebProject_DirectoryAccessLevelKey>0</WebProject_DirectoryAccessLevelKey>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.12" />
11+
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="3.1.12" Condition="'$(Configuration)' == 'Debug'" />
12+
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.12" />
13+
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="3.1.12" />
14+
</ItemGroup>
15+
16+
<ItemGroup>
17+
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.12" />
18+
<PackageReference Include="Microsoft.Identity.Web" Version="1.8.2" />
19+
<PackageReference Include="Microsoft.Identity.Web.MicrosoftGraph" Version="1.8.2" />
20+
</ItemGroup>
21+
22+
</Project>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using Microsoft.AspNetCore.Hosting;
5+
using Microsoft.Extensions.Hosting;
6+
7+
namespace ProfileAPI
8+
{
9+
public class Program
10+
{
11+
public static void Main(string[] args)
12+
{
13+
CreateHostBuilder(args).Build().Run();
14+
}
15+
16+
public static IHostBuilder CreateHostBuilder(string[] args) =>
17+
Host.CreateDefaultBuilder(args)
18+
.ConfigureWebHostDefaults(webBuilder =>
19+
{
20+
webBuilder.UseStartup<Startup>();
21+
});
22+
}
23+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"$schema": "http://json.schemastore.org/launchsettings.json",
3+
"iisSettings": {
4+
"windowsAuthentication": false,
5+
"anonymousAuthentication": true,
6+
"iisExpress": {
7+
"applicationUrl": "https://localhost:44351",
8+
"sslPort": 44351
9+
}
10+
},
11+
"profiles": {
12+
"IIS Express": {
13+
"commandName": "IISExpress",
14+
"launchBrowser": true,
15+
"launchUrl": "https://localhost:44351/api/profile",
16+
"environmentVariables": {
17+
"ASPNETCORE_ENVIRONMENT": "Development"
18+
}
19+
},
20+
"ProfileAPI": {
21+
"commandName": "Project",
22+
"launchBrowser": true,
23+
"environmentVariables": {
24+
"ASPNETCORE_ENVIRONMENT": "Development"
25+
},
26+
"applicationUrl": "https://localhost:44351/",
27+
"sslPort": 44351
28+
}
29+
}
30+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using Microsoft.EntityFrameworkCore;
5+
using Microsoft.AspNetCore.Builder;
6+
using Microsoft.AspNetCore.Hosting;
7+
using Microsoft.Extensions.Configuration;
8+
using Microsoft.Extensions.DependencyInjection;
9+
using Microsoft.Extensions.Hosting;
10+
using Microsoft.Identity.Web;
11+
using ProfileAPI.Models;
12+
using Microsoft.AspNetCore.Authentication.JwtBearer;
13+
14+
namespace ProfileAPI
15+
{
16+
public class Startup
17+
{
18+
public Startup(IConfiguration configuration)
19+
{
20+
Configuration = configuration;
21+
}
22+
23+
public IConfiguration Configuration { get; }
24+
25+
// This method gets called by the runtime. Use this method to add services to the container.
26+
public void ConfigureServices(IServiceCollection services)
27+
{
28+
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
29+
.AddMicrosoftIdentityWebApi(Configuration)
30+
.EnableTokenAcquisitionToCallDownstreamApi()
31+
.AddMicrosoftGraph(Configuration.GetSection("DownstreamAPI"))
32+
.AddInMemoryTokenCaches();
33+
34+
services.AddDbContext<ProfileContext>(opt => opt.UseInMemoryDatabase("Profile"));
35+
36+
services.AddControllers();
37+
38+
// Allowing CORS for all domains and methods for the purpose of sample
39+
services.AddCors(o => o.AddPolicy("default", builder =>
40+
{
41+
builder.AllowAnyOrigin()
42+
.AllowAnyMethod()
43+
.AllowAnyHeader()
44+
.WithExposedHeaders("WWW-Authenticate");
45+
}));
46+
}
47+
48+
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
49+
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
50+
{
51+
if (env.IsDevelopment())
52+
{
53+
// Since IdentityModel version 5.2.1 (or since Microsoft.AspNetCore.Authentication.JwtBearer version 2.2.0),
54+
// PII hiding in log files is enabled by default for GDPR concerns.
55+
// For debugging/development purposes, one can enable additional detail in exceptions by setting IdentityModelEventSource.ShowPII to true.
56+
// Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true;
57+
app.UseDeveloperExceptionPage();
58+
}
59+
else
60+
{
61+
app.UseHsts();
62+
}
63+
64+
app.UseCors("default");
65+
app.UseHttpsRedirection();
66+
app.UseRouting();
67+
68+
app.UseAuthentication();
69+
app.UseAuthorization();
70+
71+
app.UseEndpoints(endpoints =>
72+
{
73+
endpoints.MapControllers();
74+
});
75+
}
76+
}
77+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Debug",
5+
"System": "Information",
6+
"Microsoft": "Information"
7+
}
8+
}
9+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"AzureAd": {
3+
"Instance": "https://login.microsoftonline.com/",
4+
"Domain": "Enter the domain of your Azure AD tenant, e.g. 'contoso.onmicrosoft.com'",
5+
"ClientId": "Enter the Client ID (aka 'Application ID')",
6+
"TenantId": "Enter the Tenant ID",
7+
"ClientSecret": "Enter the Client Secret"
8+
},
9+
"DownstreamAPI": {
10+
"Scopes": "User.Read",
11+
"BaseUrl": "https://graph.microsoft.com/v1.0/"
12+
},
13+
"https_port": 44351,
14+
"Logging": {
15+
"LogLevel": {
16+
"Default": "Information",
17+
"Microsoft": "Warning",
18+
"Microsoft.Hosting.Lifetime": "Information"
19+
}
20+
},
21+
"AllowedHosts": "*"
22+
}

0 commit comments

Comments
 (0)