Skip to content
This repository was archived by the owner on May 17, 2024. It is now read-only.

Commit a7656cb

Browse files
authored
Merge branch 'main' into basher-2-1
2 parents f9c10a8 + 1f493cf commit a7656cb

Some content is hidden

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

53 files changed

+6970
-10650
lines changed

.github/workflows/node.js.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414

1515
strategy:
1616
matrix:
17-
node-version: [12.x]
17+
node-version: [16.x]
1818
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
1919

2020
steps:
@@ -65,7 +65,7 @@ jobs:
6565
npm ci
6666
npm audit --production
6767
npm run test
68-
68+
6969
- run: |
7070
cd 7-AdvancedScenarios/1-call-api-obo/SPA
7171
npm ci
@@ -76,4 +76,4 @@ jobs:
7676
cd 7-AdvancedScenarios/2-call-api-pop/SPA
7777
npm ci
7878
npm audit --production
79-
npm run test
79+
npm run test

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,4 +114,5 @@ obj/
114114
.vscode
115115

116116
# Angular cache
117-
.angular
117+
.angular
118+

1-Authentication/1-sign-in/SPA/package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

1-Authentication/2-sign-in-b2c/SPA/package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 9 additions & 17 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
@@ -17,37 +16,30 @@ public static IConfiguration InitConfiguration()
1716
}
1817

1918
[Fact]
20-
public void ShouldNotContainClientId()
19+
public void ShouldContainClientId()
2120
{
2221
var myConfiguration = ConfigurationTests.InitConfiguration();
23-
string clientId = myConfiguration.GetSection("AzureAd")["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.DoesNotMatch(regex, clientId);
24+
Assert.True(Guid.TryParse(clientId, out var theGuid));
2825
}
2926

3027
[Fact]
31-
public void ShouldNotContainTenantId()
28+
public void ShouldContainTenantId()
3229
{
3330
var myConfiguration = ConfigurationTests.InitConfiguration();
34-
string tenantId = myConfiguration.GetSection("AzureAd")["TenantId"];
31+
var tenantId = myConfiguration.GetSection("AzureAd")["TenantId"];
3532

36-
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}";
37-
var regex = new Regex(pattern);
38-
Assert.DoesNotMatch(regex, tenantId);
33+
Assert.True(Guid.TryParse(tenantId, out var theGuid));
3934
}
4035

4136
[Fact]
42-
public void ShouldNotContainDomain()
37+
public void ShouldContainDomain()
4338
{
4439
var myConfiguration = ConfigurationTests.InitConfiguration();
45-
string domain = myConfiguration.GetSection("AzureAd")["Domain"];
40+
var domain = $"https://{myConfiguration.GetSection("AzureAd")["Domain"]}";
4641

47-
string pattern = @"(^http[s]?:\/\/|[a-z]*\.[a-z]{3}\.[a-z]{2})|([a-z]*\.[a-z]{3}$)";
48-
var regex = new Regex(pattern);
49-
50-
Assert.DoesNotMatch(regex, domain);
42+
Assert.True(Uri.TryCreate(domain, UriKind.Absolute, out var uri));
5143
}
5244
}
5345
}

3-Authorization-II/1-call-api/API/TodoListAPI.Tests/TodoListAPI.Tests.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
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>
Lines changed: 128 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1-
using System;
1+
using System.Linq;
22
using System.Collections.Generic;
3-
using System.Linq;
43
using System.Threading.Tasks;
54
using Microsoft.AspNetCore.Http;
65
using Microsoft.AspNetCore.Mvc;
76
using Microsoft.AspNetCore.Authorization;
87
using Microsoft.EntityFrameworkCore;
9-
using TodoListAPI.Models;
10-
using System.Security.Claims;
8+
using Microsoft.Identity.Web;
119
using Microsoft.Identity.Web.Resource;
10+
using TodoListAPI.Models;
1211

1312
namespace TodoListAPI.Controllers
1413
{
@@ -17,70 +16,139 @@ namespace TodoListAPI.Controllers
1716
[ApiController]
1817
public class TodoListController : ControllerBase
1918
{
20-
// The Web API will only accept tokens 1) for users, and
21-
// 2) having the access_as_user scope for this API
22-
static readonly string[] scopeRequiredByApi = new string[] { "access_as_user" };
23-
2419
private readonly TodoContext _context;
2520

21+
private const string _todoListRead = "TodoList.Read";
22+
private const string _todoListReadWrite = "TodoList.ReadWrite";
23+
private const string _todoListReadAll = "TodoList.Read.All";
24+
private const string _todoListReadWriteAll = "TodoList.ReadWrite.All";
25+
2626
public TodoListController(TodoContext context)
2727
{
2828
_context = context;
2929
}
3030

31+
/// <summary>
32+
/// Indicates if the AT presented has application or delegated permissions.
33+
/// </summary>
34+
/// <returns></returns>
35+
private bool IsAppOnlyToken()
36+
{
37+
// Add in the optional 'idtyp' claim to check if the access token is coming from an application or user.
38+
// See: https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-optional-claims
39+
if (HttpContext.User.Claims.Any(c => c.Type == "idtyp"))
40+
{
41+
return HttpContext.User.Claims.Any(c => c.Type == "idtyp" && c.Value == "app");
42+
}
43+
else
44+
{
45+
// alternatively, if an AT contains the roles claim but no scp claim, that indicates it's an app token
46+
return HttpContext.User.Claims.Any(c => c.Type == "roles") && HttpContext.User.Claims.Any(c => c.Type != "scp");
47+
}
48+
}
49+
3150
// GET: api/TodoItems
3251
[HttpGet]
52+
/// <summary>
53+
/// Access tokens that have neither the 'scp' (for delegated permissions) nor
54+
/// 'roles' (for application permissions) claim are not to be honored.
55+
///
56+
/// An access token issued by Azure AD will have at least one of the two claims. Access tokens
57+
/// issued to a user will have the 'scp' claim. Access tokens issued to an application will have
58+
/// the roles claim. Access tokens that contain both claims are issued only to users, where the scp
59+
/// claim designates the delegated permissions, while the roles claim designates the user's role.
60+
///
61+
/// To determine whether an access token was issued to a user (i.e delegated) or an application
62+
/// more easily, we recommend enabling the optional claim 'idtyp'. For more information, see:
63+
/// https://docs.microsoft.com/azure/active-directory/develop/access-tokens#user-and-application-tokens
64+
/// </summary>
65+
[RequiredScopeOrAppPermission(
66+
AcceptedScope = new string[] { _todoListRead, _todoListReadWrite },
67+
AcceptedAppPermission = new string[] { _todoListReadAll, _todoListReadWriteAll }
68+
)]
3369
public async Task<ActionResult<IEnumerable<TodoItem>>> GetTodoItems()
3470
{
35-
HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);
36-
string owner = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
37-
return await _context.TodoItems.Where(item => item.Owner == owner).ToListAsync();
71+
if (!IsAppOnlyToken())
72+
{
73+
/// <summary>
74+
/// The 'oid' (object id) is the only claim that should be used to uniquely identify
75+
/// a user in an Azure AD tenant. The token might have one or more of the following claim,
76+
/// that might seem like a unique identifier, but is not and should not be used as such:
77+
///
78+
/// - upn (user principal name): might be unique amongst the active set of users in a tenant
79+
/// but tend to get reassigned to new employees as employees leave the organization and others
80+
/// take their place or might change to reflect a personal change like marriage.
81+
///
82+
/// - email: might be unique amongst the active set of users in a tenant but tend to get reassigned
83+
/// to new employees as employees leave the organization and others take their place.
84+
/// </summary>
85+
return await _context.TodoItems.Where(x => x.Owner == HttpContext.User.GetObjectId()).ToListAsync();
86+
}
87+
else
88+
{
89+
return await _context.TodoItems.ToListAsync();
90+
}
3891
}
3992

4093
// GET: api/TodoItems/5
4194
[HttpGet("{id}")]
95+
[RequiredScopeOrAppPermission(
96+
AcceptedScope = new string[] { _todoListRead, _todoListReadWrite },
97+
AcceptedAppPermission = new string[] { _todoListReadAll, _todoListReadWriteAll }
98+
)]
4299
public async Task<ActionResult<TodoItem>> GetTodoItem(int id)
43100
{
44-
HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);
45-
46-
var todoItem = await _context.TodoItems.FindAsync(id);
47-
48-
if (todoItem == null)
101+
// if it only has delegated permissions, then it will be t.id==id && x.Owner == owner
102+
// if it has app permissions the it will return t.id==id
103+
if (!IsAppOnlyToken())
49104
{
50-
return NotFound();
105+
return await _context.TodoItems.FirstOrDefaultAsync(t => t.Id == id && t.Owner == HttpContext.User.GetObjectId());
106+
}
107+
else
108+
{
109+
return await _context.TodoItems.FirstOrDefaultAsync(t => t.Id == id);
51110
}
52-
53-
return todoItem;
54111
}
55112

56113
// PUT: api/TodoItems/5
57114
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
58115
// more details see https://aka.ms/RazorPagesCRUD.
59116
[HttpPut("{id}")]
117+
[RequiredScopeOrAppPermission(
118+
AcceptedScope = new string[] { _todoListReadWrite },
119+
AcceptedAppPermission = new string[] { _todoListReadWriteAll }
120+
)]
60121
public async Task<IActionResult> PutTodoItem(int id, TodoItem todoItem)
61122
{
62-
HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);
63-
64-
if (id != todoItem.Id)
123+
if (id != todoItem.Id || !_context.TodoItems.Any(x => x.Id == id))
65124
{
66-
return BadRequest();
125+
return NotFound();
67126
}
68127

69-
_context.Entry(todoItem).State = EntityState.Modified;
70128

71-
try
72-
{
73-
await _context.SaveChangesAsync();
74-
}
75-
catch (DbUpdateConcurrencyException)
129+
if ((!IsAppOnlyToken() && _context.TodoItems.Any(x => x.Id == id && x.Owner == HttpContext.User.GetObjectId()))
130+
||
131+
IsAppOnlyToken())
76132
{
77-
if (!TodoItemExists(id))
78-
{
79-
return NotFound();
80-
}
81-
else
133+
if (_context.TodoItems.Any(x => x.Id == id && x.Owner == HttpContext.User.GetObjectId()))
82134
{
83-
throw;
135+
_context.Entry(todoItem).State = EntityState.Modified;
136+
137+
try
138+
{
139+
await _context.SaveChangesAsync();
140+
}
141+
catch (DbUpdateConcurrencyException)
142+
{
143+
if (!_context.TodoItems.Any(e => e.Id == id))
144+
{
145+
return NotFound();
146+
}
147+
else
148+
{
149+
throw;
150+
}
151+
}
84152
}
85153
}
86154

@@ -91,10 +159,20 @@ public async Task<IActionResult> PutTodoItem(int id, TodoItem todoItem)
91159
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
92160
// more details see https://aka.ms/RazorPagesCRUD.
93161
[HttpPost]
162+
[RequiredScopeOrAppPermission(
163+
AcceptedScope = new string[] { _todoListReadWrite },
164+
AcceptedAppPermission = new string[] { _todoListReadWriteAll }
165+
)]
94166
public async Task<ActionResult<TodoItem>> PostTodoItem(TodoItem todoItem)
95167
{
96-
HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);
97-
string owner = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
168+
string owner = HttpContext.User.GetObjectId();
169+
170+
if (IsAppOnlyToken())
171+
{
172+
// with such a permission any owner name is accepted
173+
owner = todoItem.Owner;
174+
}
175+
98176
todoItem.Owner = owner;
99177
todoItem.Status = false;
100178

@@ -106,25 +184,28 @@ public async Task<ActionResult<TodoItem>> PostTodoItem(TodoItem todoItem)
106184

107185
// DELETE: api/TodoItems/5
108186
[HttpDelete("{id}")]
187+
[RequiredScopeOrAppPermission(
188+
AcceptedScope = new string[] { _todoListReadWrite },
189+
AcceptedAppPermission = new string[] { _todoListReadWriteAll }
190+
)]
109191
public async Task<ActionResult<TodoItem>> DeleteTodoItem(int id)
110192
{
111-
HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);
193+
TodoItem todoItem = await _context.TodoItems.FindAsync(id);
112194

113-
var todoItem = await _context.TodoItems.FindAsync(id);
114195
if (todoItem == null)
115196
{
116197
return NotFound();
117198
}
118199

119-
_context.TodoItems.Remove(todoItem);
120-
await _context.SaveChangesAsync();
121-
122-
return todoItem;
123-
}
200+
if ((!IsAppOnlyToken() && _context.TodoItems.Any(x => x.Id == id && x.Owner == HttpContext.User.GetObjectId()))
201+
||
202+
IsAppOnlyToken())
203+
{
204+
_context.TodoItems.Remove(todoItem);
205+
await _context.SaveChangesAsync();
206+
}
124207

125-
private bool TodoItemExists(int id)
126-
{
127-
return _context.TodoItems.Any(e => e.Id == id);
208+
return NoContent();
128209
}
129210
}
130211
}

0 commit comments

Comments
 (0)