Skip to content

Commit eb19669

Browse files
committed
first commit
1 parent fea6955 commit eb19669

Some content is hidden

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

42 files changed

+5610
-10397
lines changed

3-Authorization-II/2-call-api-b2c/API/TodoListAPI.Tests/ConfigurationTests.cs

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using System;
22
using Xunit;
3-
using System.Text.RegularExpressions;
43
using Microsoft.Extensions.Configuration;
54

65
namespace TodoListAPI.Tests
@@ -20,23 +19,27 @@ public static IConfiguration InitConfiguration()
2019
public void ShouldNotContainClientId()
2120
{
2221
var myConfiguration = ConfigurationTests.InitConfiguration();
23-
string clientId = myConfiguration.GetSection("AzureAdB2C")["ClientId"];
22+
var clientId = myConfiguration.GetSection("AzureAd")["ClientId"];
2423

25-
string pattern = @"(\{){0,1}[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}(\}){0,1}";
26-
var regex = new Regex(pattern);
27-
Assert.Matches(regex, clientId);
24+
Assert.True(Guid.TryParse(clientId, out var theGuid));
2825
}
2926

3027
[Fact]
31-
public void ShouldNotContainDomain()
28+
public void ShouldNotContainTenantId()
3229
{
3330
var myConfiguration = ConfigurationTests.InitConfiguration();
34-
string domain = myConfiguration.GetSection("AzureAdB2C")["Domain"];
31+
var tenantId = myConfiguration.GetSection("AzureAd")["TenantId"];
32+
33+
Assert.True(Guid.TryParse(tenantId, out var theGuid));
34+
}
3535

36-
string pattern = @"(^http[s]?:\/\/|[a-z]*\.[a-z]{3}\.[a-z]{2})|([a-z]*\.[a-z]{3}$)";
37-
var regex = new Regex(pattern);
36+
[Fact]
37+
public void ShouldNotContainDomain()
38+
{
39+
var myConfiguration = ConfigurationTests.InitConfiguration();
40+
var domain = $"https://{myConfiguration.GetSection("AzureAd")["Domain"]}";
3841

39-
Assert.Matches(regex, domain);
42+
Assert.True(Uri.TryCreate(domain, UriKind.Absolute, out var uri));
4043
}
4144
}
4245
}
Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFramework>netcoreapp3.1</TargetFramework>
4+
<TargetFramework>net6.0</TargetFramework>
55

66
<IsPackable>false</IsPackable>
77
</PropertyGroup>
88

9+
<ItemGroup>
10+
<Content Include="..\TodoListAPI\appsettings.json" Link="appsettings.json">
11+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
12+
</Content>
13+
</ItemGroup>
14+
915
<ItemGroup>
1016
<PackageReference Include="Microsoft.Extensions.Configuration" Version="3.1.17" />
1117
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.17" />
@@ -21,10 +27,4 @@
2127
</PackageReference>
2228
</ItemGroup>
2329

24-
<ItemGroup>
25-
<None Update="appsettings.json">
26-
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
27-
</None>
28-
</ItemGroup>
29-
3030
</Project>

3-Authorization-II/2-call-api-b2c/API/TodoListAPI.Tests/appsettings.json

Lines changed: 0 additions & 18 deletions
This file was deleted.

3-Authorization-II/2-call-api-b2c/API/TodoListAPI.sln

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,29 @@ Microsoft Visual Studio Solution File, Format Version 12.00
33
# Visual Studio Version 16
44
VisualStudioVersion = 16.0.31005.135
55
MinimumVisualStudioVersion = 10.0.40219.1
6-
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoListAPI.Tests", "TodoListAPI.Tests\TodoListAPI.Tests.csproj", "{ED40B596-FC4A-4BBF-948B-68D0BAE70299}"
6+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoListAPI", "TodoListAPI\TodoListAPI.csproj", "{3E0BC18D-E25D-4E0E-9F6C-712C30350DF8}"
77
EndProject
8-
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoListAPI", "TodoListAPI\TodoListAPI.csproj", "{C82EE059-1ABB-4B5A-9907-770EF562F946}"
8+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoListAPI.Tests", "TodoListAPI.Tests\TodoListAPI.Tests.csproj", "{FD40F6A5-7735-4AA5-ACA8-ADE3FBE65378}"
99
EndProject
1010
Global
1111
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1212
Debug|Any CPU = Debug|Any CPU
1313
Release|Any CPU = Release|Any CPU
1414
EndGlobalSection
1515
GlobalSection(ProjectConfigurationPlatforms) = postSolution
16-
{ED40B596-FC4A-4BBF-948B-68D0BAE70299}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
17-
{ED40B596-FC4A-4BBF-948B-68D0BAE70299}.Debug|Any CPU.Build.0 = Debug|Any CPU
18-
{ED40B596-FC4A-4BBF-948B-68D0BAE70299}.Release|Any CPU.ActiveCfg = Release|Any CPU
19-
{ED40B596-FC4A-4BBF-948B-68D0BAE70299}.Release|Any CPU.Build.0 = Release|Any CPU
20-
{C82EE059-1ABB-4B5A-9907-770EF562F946}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21-
{C82EE059-1ABB-4B5A-9907-770EF562F946}.Debug|Any CPU.Build.0 = Debug|Any CPU
22-
{C82EE059-1ABB-4B5A-9907-770EF562F946}.Release|Any CPU.ActiveCfg = Release|Any CPU
23-
{C82EE059-1ABB-4B5A-9907-770EF562F946}.Release|Any CPU.Build.0 = Release|Any CPU
16+
{3E0BC18D-E25D-4E0E-9F6C-712C30350DF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
17+
{3E0BC18D-E25D-4E0E-9F6C-712C30350DF8}.Debug|Any CPU.Build.0 = Debug|Any CPU
18+
{3E0BC18D-E25D-4E0E-9F6C-712C30350DF8}.Release|Any CPU.ActiveCfg = Release|Any CPU
19+
{3E0BC18D-E25D-4E0E-9F6C-712C30350DF8}.Release|Any CPU.Build.0 = Release|Any CPU
20+
{FD40F6A5-7735-4AA5-ACA8-ADE3FBE65378}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21+
{FD40F6A5-7735-4AA5-ACA8-ADE3FBE65378}.Debug|Any CPU.Build.0 = Debug|Any CPU
22+
{FD40F6A5-7735-4AA5-ACA8-ADE3FBE65378}.Release|Any CPU.ActiveCfg = Release|Any CPU
23+
{FD40F6A5-7735-4AA5-ACA8-ADE3FBE65378}.Release|Any CPU.Build.0 = Release|Any CPU
2424
EndGlobalSection
2525
GlobalSection(SolutionProperties) = preSolution
2626
HideSolutionNode = FALSE
2727
EndGlobalSection
2828
GlobalSection(ExtensibilityGlobals) = postSolution
29-
SolutionGuid = {10F069FB-DCC7-48B6-9575-4B6C47FFD4C8}
29+
SolutionGuid = {44AB506D-AF3A-4AE5-90E9-CFECC92533A6}
3030
EndGlobalSection
3131
EndGlobal
Lines changed: 123 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
using System;
2-
using System.Collections.Generic;
1+
using System.Collections.Generic;
32
using System.Linq;
3+
using System.Security.Claims;
44
using System.Threading.Tasks;
5+
using Microsoft.AspNetCore.Authorization;
56
using Microsoft.AspNetCore.Http;
67
using Microsoft.AspNetCore.Mvc;
7-
using Microsoft.AspNetCore.Authorization;
88
using Microsoft.EntityFrameworkCore;
9-
using TodoListAPI.Models;
10-
using System.Security.Claims;
9+
using Microsoft.Identity.Web;
1110
using Microsoft.Identity.Web.Resource;
11+
using TodoListAPI.Models;
1212

1313
namespace TodoListAPI.Controllers
1414
{
@@ -17,70 +17,98 @@ namespace TodoListAPI.Controllers
1717
[ApiController]
1818
public class TodoListController : ControllerBase
1919
{
20-
// The Web API will only accept tokens 1) for users, and
21-
// 2) having the demo.read scope for this API
22-
static readonly string[] scopeRequiredByApi = new string[] { "demo.read" };
20+
private readonly TodoContext _TodoListContext;
21+
private readonly IHttpContextAccessor _contextAccessor;
22+
private ClaimsPrincipal _currentPrincipal;
2323

24-
private readonly TodoContext _context;
24+
/// <summary>
25+
/// We store the object id of the user/app derived from the presented Access token
26+
/// </summary>
27+
private string _currentPrincipalId = string.Empty;
2528

26-
public TodoListController(TodoContext context)
29+
public TodoListController(TodoContext context, IHttpContextAccessor contextAccessor)
2730
{
28-
_context = context;
31+
_TodoListContext = context;
32+
_contextAccessor = contextAccessor;
33+
34+
// We seek the details of the user/app represented by the access token presented to this API, This can be empty unless authN succeeded
35+
// If a user signed-in, the value will be the unique identifier of the user.
36+
_currentPrincipal = GetCurrentClaimsPrincipal();
37+
38+
if (!IsAppOnlyToken() && _currentPrincipal != null)
39+
{
40+
_currentPrincipalId = _currentPrincipal.GetObjectId();
41+
PopulateDefaultToDos(_currentPrincipalId);
42+
}
43+
}
44+
45+
// GET: api/todolist/getAll
46+
[HttpGet]
47+
[Route("getAll")]
48+
[RequiredScope(RequiredScopesConfigurationKey = "AzureAd:Scopes:Read")]
49+
public async Task<ActionResult<IEnumerable<TodoItem>>> GetAll()
50+
{
51+
return await _TodoListContext.TodoItems.ToListAsync();
2952
}
3053

3154
// GET: api/TodoItems
3255
[HttpGet]
56+
[RequiredScope(RequiredScopesConfigurationKey = "AzureAd:Scopes:Read")]
3357
public async Task<ActionResult<IEnumerable<TodoItem>>> GetTodoItems()
3458
{
35-
HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);
36-
string owner = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
37-
return await _context.TodoItems.Where(item => item.Owner == owner).ToListAsync();
59+
/// <summary>
60+
/// The 'oid' (object id) is the only claim that should be used to uniquely identify
61+
/// a user in an Azure AD tenant. The token might have one or more of the following claim,
62+
/// that might seem like a unique identifier, but is not and should not be used as such:
63+
///
64+
/// - upn (user principal name): might be unique amongst the active set of users in a tenant
65+
/// but tend to get reassigned to new employees as employees leave the organization and others
66+
/// take their place or might change to reflect a personal change like marriage.
67+
///
68+
/// - email: might be unique amongst the active set of users in a tenant but tend to get reassigned
69+
/// to new employees as employees leave the organization and others take their place.
70+
/// </summary>
71+
return await _TodoListContext.TodoItems.Where(x => x.Owner == _currentPrincipalId).ToListAsync();
3872
}
3973

4074
// GET: api/TodoItems/5
4175
[HttpGet("{id}")]
76+
[RequiredScope(RequiredScopesConfigurationKey = "AzureAd:Scopes:Read")]
4277
public async Task<ActionResult<TodoItem>> GetTodoItem(int id)
4378
{
44-
HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);
45-
46-
var todoItem = await _context.TodoItems.FindAsync(id);
47-
48-
if (todoItem == null)
49-
{
50-
return NotFound();
51-
}
52-
53-
return todoItem;
79+
return await _TodoListContext.TodoItems.FirstOrDefaultAsync(t => t.Id == id && t.Owner == _currentPrincipalId);
5480
}
5581

5682
// PUT: api/TodoItems/5
5783
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
5884
// more details see https://aka.ms/RazorPagesCRUD.
5985
[HttpPut("{id}")]
86+
[RequiredScope(RequiredScopesConfigurationKey = "AzureAd:Scopes:Write")]
6087
public async Task<IActionResult> PutTodoItem(int id, TodoItem todoItem)
6188
{
62-
HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);
63-
64-
if (id != todoItem.Id)
89+
if (id != todoItem.Id || !_TodoListContext.TodoItems.Any(x => x.Id == id))
6590
{
66-
return BadRequest();
91+
return NotFound();
6792
}
6893

69-
_context.Entry(todoItem).State = EntityState.Modified;
70-
71-
try
94+
if (_TodoListContext.TodoItems.Any(x => x.Id == id && x.Owner == _currentPrincipalId))
7295
{
73-
await _context.SaveChangesAsync();
74-
}
75-
catch (DbUpdateConcurrencyException)
76-
{
77-
if (!TodoItemExists(id))
96+
_TodoListContext.Entry(todoItem).State = EntityState.Modified;
97+
98+
try
7899
{
79-
return NotFound();
100+
await _TodoListContext.SaveChangesAsync();
80101
}
81-
else
102+
catch (DbUpdateConcurrencyException)
82103
{
83-
throw;
104+
if (!_TodoListContext.TodoItems.Any(e => e.Id == id))
105+
{
106+
return NotFound();
107+
}
108+
else
109+
{
110+
throw;
111+
}
84112
}
85113
}
86114

@@ -91,40 +119,83 @@ public async Task<IActionResult> PutTodoItem(int id, TodoItem todoItem)
91119
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
92120
// more details see https://aka.ms/RazorPagesCRUD.
93121
[HttpPost]
122+
[RequiredScope(RequiredScopesConfigurationKey = "AzureAd:Scopes:Write")]
94123
public async Task<ActionResult<TodoItem>> PostTodoItem(TodoItem todoItem)
95124
{
96-
HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);
97-
string owner = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
98-
todoItem.Owner = owner;
125+
todoItem.Owner = _currentPrincipalId;
99126
todoItem.Status = false;
100127

101-
_context.TodoItems.Add(todoItem);
102-
await _context.SaveChangesAsync();
128+
_TodoListContext.TodoItems.Add(todoItem);
129+
await _TodoListContext.SaveChangesAsync();
103130

104131
return CreatedAtAction("GetTodoItem", new { id = todoItem.Id }, todoItem);
105132
}
106133

107134
// DELETE: api/TodoItems/5
108135
[HttpDelete("{id}")]
136+
[RequiredScope(RequiredScopesConfigurationKey = "AzureAd:Scopes:Write")]
109137
public async Task<ActionResult<TodoItem>> DeleteTodoItem(int id)
110138
{
111-
HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);
139+
TodoItem todoItem = await _TodoListContext.TodoItems.FindAsync(id);
112140

113-
var todoItem = await _context.TodoItems.FindAsync(id);
114141
if (todoItem == null)
115142
{
116143
return NotFound();
117144
}
118145

119-
_context.TodoItems.Remove(todoItem);
120-
await _context.SaveChangesAsync();
146+
if (_TodoListContext.TodoItems.Any(x => x.Id == id && x.Owner == _currentPrincipalId))
147+
{
148+
_TodoListContext.TodoItems.Remove(todoItem);
149+
await _TodoListContext.SaveChangesAsync();
150+
}
151+
152+
return NoContent();
153+
}
154+
155+
private async void PopulateDefaultToDos(string _currentPrincipalId)
156+
{
157+
//Pre - populate with sample data
158+
if (_TodoListContext.TodoItems.Count() == 0 && !string.IsNullOrEmpty(_currentPrincipalId))
159+
{
160+
_TodoListContext.TodoItems.Add(new TodoItem() { Owner = $"{_currentPrincipalId}", Description = "Pick up groceries", Status = false });
161+
_TodoListContext.TodoItems.Add(new TodoItem() { Owner = $"{_currentPrincipalId}", Description = "Finish invoice report", Status = false });
162+
163+
await _TodoListContext.SaveChangesAsync();
164+
}
165+
}
166+
167+
/// <summary>
168+
/// returns the current claimsPrincipal (user/Client app) dehydrated from the Access token
169+
/// </summary>
170+
/// <returns></returns>
171+
private ClaimsPrincipal GetCurrentClaimsPrincipal()
172+
{
173+
// Irrespective of whether a user signs in or not, the AspNet security middleware dehydrates
174+
// the claims in the HttpContext.User.Claims collection
175+
if (_contextAccessor.HttpContext != null && _contextAccessor.HttpContext.User != null)
176+
{
177+
return _contextAccessor.HttpContext.User;
178+
}
121179

122-
return todoItem;
180+
return null;
123181
}
124182

125-
private bool TodoItemExists(int id)
183+
/// <summary>
184+
/// Indicates of the AT presented was for an app-only token or not.
185+
/// </summary>
186+
/// <returns></returns>
187+
private bool IsAppOnlyToken()
126188
{
127-
return _context.TodoItems.Any(e => e.Id == id);
189+
// Add in the optional 'idtyp' claim to check if the access token is coming from an application or user.
190+
//
191+
// See: https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-optional-claims
192+
193+
if (GetCurrentClaimsPrincipal() != null)
194+
{
195+
return GetCurrentClaimsPrincipal().Claims.Any(c => c.Type == "idtyp" && c.Value == "app");
196+
}
197+
198+
return false;
128199
}
129200
}
130-
}
201+
}

0 commit comments

Comments
 (0)