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

Update 3.1 to follow BASHER and Zero Trust guidelines #182

Merged
merged 26 commits into from
Aug 25, 2022
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:

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

steps:
Expand Down Expand Up @@ -65,7 +65,7 @@ jobs:
npm ci
npm audit --production
npm run test

- run: |
cd 7-AdvancedScenarios/1-call-api-obo/SPA
npm ci
Expand All @@ -76,4 +76,4 @@ jobs:
cd 7-AdvancedScenarios/2-call-api-pop/SPA
npm ci
npm audit --production
npm run test
npm run test
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,7 @@ obj/
.vs

# VS Code cache
.vscode
.vscode

# Angular cache
.angular
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net6.0</TargetFramework>

<IsPackable>false</IsPackable>
</PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
using TodoListAPI.Models;
using System.Security.Claims;
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.Resource;
using TodoListAPI.Models;

namespace TodoListAPI.Controllers
{
Expand All @@ -17,55 +16,108 @@ namespace TodoListAPI.Controllers
[ApiController]
public class TodoListController : ControllerBase
{
// The Web API will only accept tokens 1) for users, and
// 2) having the access_as_user scope for this API
static readonly string[] scopeRequiredByApi = new string[] { "access_as_user" };

private readonly TodoContext _context;

private const string _todoListRead = "TodoList.Read";
private const string _todoListReadWrite = "TodoList.ReadWrite";
private const string _todoListReadAll = "TodoList.Read.All";
private const string _todoListReadWriteAll = "TodoList.ReadWrite.All";

public TodoListController(TodoContext context)
{
_context = context;
}

// GET: api/TodoItems
[HttpGet]
/// <summary>
/// Access tokens that have neither the 'scp' (for delegated permissions) nor
/// 'roles' (for application permissions) claim are not to be honored.
///
/// An access token issued by Azure AD will have at least one of the two claims. Access tokens
/// issued to a user will have the 'scp' claim. Access tokens issued to an application will have
/// the roles claim. Access tokens that contain both claims are issued only to users, where the scp
/// claim designates the delegated permissions, while the roles claim designates the user's role.
///
/// To determine whether an access token was issued to a user (i.e delegated) or an application
/// more easily, we recommend enabling the optional claim 'idtyp'. For more information, see:
/// https://docs.microsoft.com/azure/active-directory/develop/access-tokens#user-and-application-tokens
/// </summary>
[RequiredScopeOrAppPermission(
AcceptedScope = new string[] { _todoListRead, _todoListReadWrite },
AcceptedAppPermission = new string[] { _todoListReadAll, _todoListReadWriteAll }
)]
public async Task<ActionResult<IEnumerable<TodoItem>>> GetTodoItems()
{
HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);
string owner = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
return await _context.TodoItems.Where(item => item.Owner == owner).ToListAsync();
if (HasDelegatedPermissions(new string[] { _todoListRead, _todoListReadWrite }))
{
/// <summary>
/// The 'oid' (object id) is the only claim that should be used to uniquely identify
/// a user in an Azure AD tenant. The token might have one or more of the following claim,
/// that might seem like a unique identifier, but is not and should not be used as such:
///
/// - upn (user principal name): might be unique amongst the active set of users in a tenant
/// but tend to get reassigned to new employees as employees leave the organization and others
/// take their place or might change to reflect a personal change like marriage.
///
/// - email: might be unique amongst the active set of users in a tenant but tend to get reassigned
/// to new employees as employees leave the organization and others take their place.
/// </summary>
return await _context.TodoItems.Where(x => x.Owner == HttpContext.User.GetObjectId()).ToListAsync();
}
else if (HasApplicationPermissions(new string[] { _todoListReadAll, _todoListReadWriteAll }))
{
return await _context.TodoItems.ToListAsync();
}

return null;
}

// GET: api/TodoItems/5
[HttpGet("{id}")]
[RequiredScopeOrAppPermission(
AcceptedScope = new string[] { _todoListRead, _todoListReadWrite },
AcceptedAppPermission = new string[] { _todoListReadAll, _todoListReadWriteAll }
)]
public async Task<ActionResult<TodoItem>> GetTodoItem(int id)
{
HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);

var todoItem = await _context.TodoItems.FindAsync(id);

if (todoItem == null)
// if it only has delegated permissions, then it will be t.id==id && x.Owner == owner
// if it has app permissions the it will return t.id==id
if (HasDelegatedPermissions(new string[] { _todoListRead, _todoListReadWrite }))
{
return NotFound();
return await _context.TodoItems.FirstOrDefaultAsync(t => t.Id == id && t.Owner == HttpContext.User.GetObjectId());
}
else if (HasApplicationPermissions(new string[] { _todoListReadAll, _todoListReadWriteAll }))
{
return await _context.TodoItems.FirstOrDefaultAsync(t => t.Id == id);
}

return todoItem;
return null;
}

// PUT: api/TodoItems/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see https://aka.ms/RazorPagesCRUD.
[HttpPut("{id}")]
[RequiredScopeOrAppPermission(
AcceptedScope = new string[] { _todoListReadWrite },
AcceptedAppPermission = new string[] { _todoListReadWriteAll }
)]
public async Task<IActionResult> PutTodoItem(int id, TodoItem todoItem)
{
HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);

if (id != todoItem.Id)
if (id != todoItem.Id || !_context.TodoItems.Any(x => x.Id == id))
{
return BadRequest();
return NotFound();
}


if (HasDelegatedPermissions(new string[] { _todoListReadWrite })
&& _context.TodoItems.Any(x => x.Id == id && x.Owner == HttpContext.User.GetObjectId())
&& todoItem.Owner == HttpContext.User.GetObjectId()
||
HasApplicationPermissions(new string[] { _todoListReadWriteAll })
)
{
_context.Entry(todoItem).State = EntityState.Modified;

try
Expand All @@ -74,7 +126,7 @@ public async Task<IActionResult> PutTodoItem(int id, TodoItem todoItem)
}
catch (DbUpdateConcurrencyException)
{
if (!TodoItemExists(id))
if (!_context.TodoItems.Any(e => e.Id == id))
{
return NotFound();
}
Expand All @@ -83,6 +135,7 @@ public async Task<IActionResult> PutTodoItem(int id, TodoItem todoItem)
throw;
}
}
}

return NoContent();
}
Expand All @@ -91,10 +144,20 @@ public async Task<IActionResult> PutTodoItem(int id, TodoItem todoItem)
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see https://aka.ms/RazorPagesCRUD.
[HttpPost]
[RequiredScopeOrAppPermission(
AcceptedScope = new string[] { _todoListReadWrite },
AcceptedAppPermission = new string[] { _todoListReadWriteAll }
)]
public async Task<ActionResult<TodoItem>> PostTodoItem(TodoItem todoItem)
{
HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);
string owner = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
string owner = HttpContext.User.GetObjectId();

if (HasApplicationPermissions(new string[] { _todoListReadWriteAll }))
{
// with such a permission any owner name is accepted
owner = todoItem.Owner;
}

todoItem.Owner = owner;
todoItem.Status = false;

Expand All @@ -106,25 +169,52 @@ public async Task<ActionResult<TodoItem>> PostTodoItem(TodoItem todoItem)

// DELETE: api/TodoItems/5
[HttpDelete("{id}")]
[RequiredScopeOrAppPermission(
AcceptedScope = new string[] { _todoListReadWrite },
AcceptedAppPermission = new string[] { _todoListReadWriteAll }
)]
public async Task<ActionResult<TodoItem>> DeleteTodoItem(int id)
{
HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);
TodoItem todoItem = await _context.TodoItems.FindAsync(id);

var todoItem = await _context.TodoItems.FindAsync(id);
if (todoItem == null)
{
return NotFound();
}

_context.TodoItems.Remove(todoItem);
await _context.SaveChangesAsync();
if ((HasDelegatedPermissions(new string[] { _todoListReadWrite })
&& _context.TodoItems.Any(x => x.Id == id && x.Owner == HttpContext.User.GetObjectId()))
|| HasApplicationPermissions(new string[] { _todoListReadWriteAll }))
{
_context.TodoItems.Remove(todoItem);
await _context.SaveChangesAsync();
return todoItem;
}
else
{
return BadRequest();
}
}

return todoItem;
// Checks if the presented token has application permissions
private bool HasApplicationPermissions(string[] permissionsNames)
{
var rolesClaim = User.Claims.Where(
c => c.Type == ClaimConstants.Roles || c.Type == ClaimConstants.Role)
.SelectMany(c => c.Value.Split(' '));

var result = rolesClaim.Any(v => permissionsNames.Any(p => p.Equals(v)));

return result;
}

private bool TodoItemExists(int id)
// Checks if the presented token has delegated permissions
private bool HasDelegatedPermissions(string[] scopesNames)
{
return _context.TodoItems.Any(e => e.Id == id);
var result = (User.FindFirst(ClaimConstants.Scp) ?? User.FindFirst(ClaimConstants.Scope))?
.Value.Split(' ').Any(v => scopesNames.Any(s => s.Equals(v)));

return result ?? false;
}
}
}
23 changes: 13 additions & 10 deletions 3-Authorization-II/1-call-api/API/TodoListAPI/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.EntityFrameworkCore;
using Microsoft.Identity.Web;
using System.IdentityModel.Tokens.Jwt;

using TodoListAPI.Models;
using Microsoft.AspNetCore.Authentication.JwtBearer;

namespace TodoListAPI
{
Expand All @@ -23,19 +24,21 @@ public Startup(IConfiguration configuration)
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Setting configuration for protected web api
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(Configuration);
// This is required to be instantiated before the OpenIdConnectOptions starts getting configured.
// By default, the claims mapping will map claim names in the old format to accommodate older SAML applications.
// 'http://schemas.microsoft.com/ws/2008/06/identity/claims/role' instead of 'roles'
// This flag ensures that the ClaimsIdentity claims collection will be built from the claims in the token
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;

// Creating policies that wraps the authorization requirements
services.AddAuthorization();
// Adds Microsoft Identity platform (AAD v2.0) support to protect this Api
services.AddMicrosoftIdentityWebApiAuthentication(Configuration);

services.AddDbContext<TodoContext>(opt => opt.UseInMemoryDatabase("TodoList"));

services.AddControllers();
// Allowing CORS for all domains and methods for the purpose of the sample
// In production, modify this with the actual domains you want to allow

// Allowing CORS for all domains and HTTP methods for the purpose of the sample
// In production, modify this with the actual domains and HTTP methods you want to allow
services.AddCors(o => o.AddPolicy("default", builder =>
{
builder.AllowAnyOrigin()
Expand Down Expand Up @@ -72,4 +75,4 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
});
}
}
}
}
16 changes: 8 additions & 8 deletions 3-Authorization-II/1-call-api/API/TodoListAPI/TodoListAPI.csproj
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<UserSecretsId>aspnet-TodoListAPI-BA938C29-8BAB-4664-A688-8FD54049C1C3</UserSecretsId>
<WebProject_DirectoryAccessLevelKey>1</WebProject_DirectoryAccessLevelKey>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="3.1.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.9">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="6.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Identity.Web" Version="1.8.0" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.4" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="3.1.9" Condition="'$(Configuration)' == 'Debug'" />
<PackageReference Include="Microsoft.Identity.Web" Version="1.25.1" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="6.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="6.0.6" Condition="'$(Configuration)' == 'Debug'" />
</ItemGroup>

</Project>
Loading