From d56fa1a766cc039a9ae12e1bb2152e4a057dfb51 Mon Sep 17 00:00:00 2001 From: BluMichele <106175315+BluMichele@users.noreply.github.com> Date: Tue, 19 Jul 2022 17:58:21 +0200 Subject: [PATCH 1/2] Create a mix of ScopesRequiredHttpContextExtensions and RolesRequiredHttpContextExtensions --- ...esOrScopesRequiredHttpContextExtensions.cs | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 src/Microsoft.Identity.Web/Resource/RolesOrScopesRequiredHttpContextExtensions.cs diff --git a/src/Microsoft.Identity.Web/Resource/RolesOrScopesRequiredHttpContextExtensions.cs b/src/Microsoft.Identity.Web/Resource/RolesOrScopesRequiredHttpContextExtensions.cs new file mode 100644 index 000000000..79a875b21 --- /dev/null +++ b/src/Microsoft.Identity.Web/Resource/RolesOrScopesRequiredHttpContextExtensions.cs @@ -0,0 +1,94 @@ + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Security.Claims; +using global::Microsoft.AspNetCore.Http; +using global::Microsoft.Identity.Web; +using Bludata.Blu.Web.Security.Entities; +using Bludata.Blu.Web.Security.Interface; + +namespace Bludata.Blu.Web.Security.Utils; + +/// +/// Extension class providing the extension methods for that +/// can be used in web APIs to validate scopes and roles in controller actions. +/// +public static class RolesOrScopesRequiredHttpContextExtensions +{ + /// + /// When applied to an , verifies that the user authenticated in the + /// web API has any of the accepted scopes or roles. + /// If there is no authenticated user, the response is a 401 (Unauthenticated). + /// If the authenticated user does not have any of these or , the + /// method updates the HTTP response providing a status code 403 (Forbidden) + /// and writes to the response body a message telling which scopes or roles are expected in the token. + /// + /// HttpContext (from the controller). + /// Scopes accepted by this web API. + /// Roles accepted by this web API. + + public static void ValidateAppRolesOrScopes(this HttpContext context, string[] acceptedScopes, string[] acceptedRoles) + { + if (acceptedScopes?.Any() != true && acceptedRoles?.Any() != true) + { + throw new ArgumentException($"{nameof(acceptedScopes)} and {nameof(acceptedRoles)} are null or empty"); + } + ArgumentNullException.ThrowIfNull(context); + + IEnumerable userClaims; + ClaimsPrincipal user; + + // Need to lock due to https://docs.microsoft.com/en-us/aspnet/core/performance/performance-best-practices?#do-not-access-httpcontext-from-multiple-threads + lock (context) + { + user = context.User; + userClaims = user.Claims; + } + + if (user == null || userClaims == null || !userClaims.Any()) + { + lock (context) + { + context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; + } + //throw new UnauthorizedAccessException(IDWebErrorMessage.UnauthenticatedUser); + throw new UnauthorizedAccessException("IDW10204: The user is unauthenticated. The HttpContext does not contain any claims. "); + } + + var hasScopeOrRole = false; + if (acceptedScopes?.Any() == true) + { + var scpClaim = user.FindFirst(ClaimConstants.Scp)?.Value?.Split(' '); + var scopeClaim = user.FindFirst(ClaimConstants.Scope)?.Value?.Split(' '); + + hasScopeOrRole = scpClaim?.Any(acceptedScopes.Contains) == true || scopeClaim?.Any(acceptedScopes.Contains) == true; + } + if (acceptedRoles?.Any() == true) + { + var rolesClaim = userClaims.Where(claims => claims.Type == ClaimConstants.Roles || claims.Type == ClaimConstants.Role) + .SelectMany(roles => roles.Value.Split(' ')); + hasScopeOrRole = rolesClaim?.Any(acceptedRoles.Contains) == true; + } + if (hasScopeOrRole) + { + return; + } + + var message = string.Format(CultureInfo.InvariantCulture, + $"The 'scope' or 'scp' claim does not contain scopes '{0}' nor " + + $"the 'roles' or 'role' claim does not contain roles '{1}'", + new string[] { string.Join(",", acceptedScopes), string.Join(",", acceptedRoles) }); + + lock (context) + { + context.Response.StatusCode = (int)HttpStatusCode.Forbidden; + context.Response.WriteAsync(message); + context.Response.CompleteAsync(); + } + throw new UnauthorizedAccessException(message); + } +} From b8210ff56b6a246b1d35a26632a6d0f77408b498 Mon Sep 17 00:00:00 2001 From: BluMichele <106175315+BluMichele@users.noreply.github.com> Date: Thu, 21 Jul 2022 14:32:13 +0200 Subject: [PATCH 2/2] Added license and copyright, fixed namespace and removed unused usings --- .../RolesOrScopesRequiredHttpContextExtensions.cs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.Identity.Web/Resource/RolesOrScopesRequiredHttpContextExtensions.cs b/src/Microsoft.Identity.Web/Resource/RolesOrScopesRequiredHttpContextExtensions.cs index 79a875b21..d55e5ceaa 100644 --- a/src/Microsoft.Identity.Web/Resource/RolesOrScopesRequiredHttpContextExtensions.cs +++ b/src/Microsoft.Identity.Web/Resource/RolesOrScopesRequiredHttpContextExtensions.cs @@ -1,17 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. -using System; -using System.Collections.Generic; +using global::Microsoft.AspNetCore.Http; +using global::Microsoft.Identity.Web; using System.Globalization; -using System.Linq; using System.Net; -using System.Net.Http; using System.Security.Claims; -using global::Microsoft.AspNetCore.Http; -using global::Microsoft.Identity.Web; -using Bludata.Blu.Web.Security.Entities; -using Bludata.Blu.Web.Security.Interface; -namespace Bludata.Blu.Web.Security.Utils; +namespace Microsoft.Identity.Web.Resource; /// /// Extension class providing the extension methods for that