From 63217aef4a88d0cd846ceecb880b7940caa46352 Mon Sep 17 00:00:00 2001 From: Brock Allen Date: Fri, 12 Jun 2020 09:03:43 -0400 Subject: [PATCH] Clarify semantics of resource validator and scope parser (#4452) * attempt to clarify semantics * another possible improvement? * minor feedback * make individual scope validation easier * update other hosts to match resource changes * updates from code review/testing --- .../host/Configuration/ClientsConsole.cs | 20 +-- .../host/Configuration/ClientsWeb.cs | 4 +- .../host/Configuration/Resources.cs | 25 ++- .../Quickstart/Consent/ConsentController.cs | 14 +- .../Device/DeviceAuthorizationInputModel.cs | 2 +- .../Device/DeviceAuthorizationViewModel.cs | 2 +- .../Quickstart/Device/DeviceController.cs | 12 +- .../Views/Device/UserCodeConfirmation.cshtml | 2 +- .../Extensions/ExtensionGrantValidator.cs | 35 ---- .../NoSubjectExtensionGrantValidator.cs | 35 ---- .../Extensions/RequestLoggerMiddleware.cs | 72 -------- .../Quickstart/Consent/ConsentController.cs | 14 +- .../Device/DeviceAuthorizationInputModel.cs | 2 +- .../Device/DeviceAuthorizationViewModel.cs | 2 +- .../Quickstart/Device/DeviceController.cs | 12 +- src/EntityFramework/host/Startup.cs | 1 - .../Views/Device/UserCodeConfirmation.cshtml | 2 +- .../SqlServer/Configuration/ClientsConsole.cs | 20 +-- .../SqlServer/Configuration/ClientsWeb.cs | 4 +- .../SqlServer/Configuration/Resources.cs | 25 ++- .../host/Extensions/HostProfileService.cs | 6 +- .../Extensions/ParameterizedScopeParser.cs | 47 +++++ ...ParameterizedScopeTokenRequestValidator.cs | 6 +- .../Extensions/ParameterizedScopeValidator.cs | 30 ---- .../Quickstart/Consent/ConsentController.cs | 14 +- .../Quickstart/Device/DeviceController.cs | 10 +- src/IdentityServer4/host/Startup.cs | 2 +- .../BuilderExtensions/Additional.cs | 14 ++ .../BuilderExtensions/Core.cs | 3 +- .../Events/DeviceAuthorizationSuccessEvent.cs | 2 +- .../src/Events/TokenIssuedSuccessEvent.cs | 2 +- .../Extensions/IResourceStoreExtensions.cs | 22 +-- .../src/Extensions/ResourceExtensions.cs | 2 +- .../src/Models/Messages/ConsentRequest.cs | 2 +- .../AuthorizeInteractionResponseGenerator.cs | 2 +- .../Default/AuthorizeResponseGenerator.cs | 2 +- .../DeviceAuthorizationResponseGenerator.cs | 2 +- .../Default/TokenResponseGenerator.cs | 47 ++--- .../Models/AuthorizeResponse.cs | 2 +- .../Services/Default/DefaultClaimsService.cs | 2 +- .../Services/Default/DefaultConsentService.cs | 6 +- .../DefaultDeviceFlowInteractionService.cs | 10 +- .../Default/AuthorizeRequestValidator.cs | 3 +- ...lidator.cs => DefaultResourceValidator.cs} | 72 ++++---- .../Validation/Default/DefaultScopeParser.cs | 160 ++++++++++++++++++ .../DeviceAuthorizationRequestValidator.cs | 3 +- .../Default/TokenRequestValidator.cs | 3 +- .../src/Validation/IResourceValidator.cs | 8 +- .../src/Validation/IScopeParser.cs | 20 +++ .../Models/ParsedScopeValidationError.cs | 45 +++++ .../src/Validation/Models/ParsedScopeValue.cs | 47 +++-- .../Validation/Models/ParsedScopesResult.cs | 30 ++++ .../Models/ResourceValidationRequest.cs | 2 +- .../Models/ResourceValidationResult.cs | 8 +- .../Endpoints/Authorize/ConsentTests.cs | 2 +- .../Common/MockConsentService.cs | 2 +- .../Default/DefaultClaimsServiceTests.cs | 2 +- .../Validation/ResourceValidation.cs | 33 ++-- .../Validation/Setup/Factory.cs | 2 +- 59 files changed, 552 insertions(+), 430 deletions(-) delete mode 100644 src/EntityFramework/host/Extensions/ExtensionGrantValidator.cs delete mode 100644 src/EntityFramework/host/Extensions/NoSubjectExtensionGrantValidator.cs delete mode 100644 src/EntityFramework/host/Extensions/RequestLoggerMiddleware.cs create mode 100644 src/IdentityServer4/host/Extensions/ParameterizedScopeParser.cs delete mode 100644 src/IdentityServer4/host/Extensions/ParameterizedScopeValidator.cs rename src/IdentityServer4/src/Validation/Default/{ResourceValidator.cs => DefaultResourceValidator.cs} (80%) create mode 100644 src/IdentityServer4/src/Validation/Default/DefaultScopeParser.cs create mode 100644 src/IdentityServer4/src/Validation/IScopeParser.cs create mode 100644 src/IdentityServer4/src/Validation/Models/ParsedScopeValidationError.cs create mode 100644 src/IdentityServer4/src/Validation/Models/ParsedScopesResult.cs diff --git a/src/AspNetIdentity/host/Configuration/ClientsConsole.cs b/src/AspNetIdentity/host/Configuration/ClientsConsole.cs index ef45caff05..bb62b210ee 100644 --- a/src/AspNetIdentity/host/Configuration/ClientsConsole.cs +++ b/src/AspNetIdentity/host/Configuration/ClientsConsole.cs @@ -26,7 +26,7 @@ public static IEnumerable Get() ClientId = "client", ClientSecrets = {new Secret("secret".Sha256())}, AllowedGrantTypes = GrantTypes.ClientCredentials, - AllowedScopes = { "scope1", "scope2", IdentityServerConstants.LocalApi.ScopeName} + AllowedScopes = { "resource1.scope1", "resource2.scope1", IdentityServerConstants.LocalApi.ScopeName} }, /////////////////////////////////////////// @@ -60,7 +60,7 @@ public static IEnumerable Get() AccessTokenType = AccessTokenType.Jwt, AllowedGrantTypes = GrantTypes.ClientCredentials, - AllowedScopes = { "scope1", "scope2" } + AllowedScopes = { "resource1.scope1", "resource2.scope1" } }, /////////////////////////////////////////// @@ -86,7 +86,7 @@ public static IEnumerable Get() }, AllowedGrantTypes = GrantTypes.ClientCredentials, - AllowedScopes = { "scope1", "scope2" } + AllowedScopes = { "resource1.scope1", "resource2.scope1" } }, /////////////////////////////////////////// @@ -97,7 +97,7 @@ public static IEnumerable Get() ClientId = "client.custom", ClientSecrets = {new Secret("secret".Sha256())}, AllowedGrantTypes = {"custom", "custom.nosubject"}, - AllowedScopes = { "scope1", "scope2" } + AllowedScopes = { "resource1.scope1", "resource2.scope1" } }, /////////////////////////////////////////// @@ -113,7 +113,7 @@ public static IEnumerable Get() { IdentityServerConstants.StandardScopes.OpenId, "custom.profile", - "scope1", "scope2" + "resource1.scope1", "resource2.scope1" } }, @@ -130,7 +130,7 @@ public static IEnumerable Get() { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Email, - "scope1", "scope2" + "resource1.scope1", "resource2.scope1" } }, @@ -151,7 +151,7 @@ public static IEnumerable Get() IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, IdentityServerConstants.StandardScopes.Email, - "scope1", "scope2" + "resource1.scope1", "resource2.scope1" } }, /////////////////////////////////////////// @@ -173,7 +173,7 @@ public static IEnumerable Get() IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, IdentityServerConstants.StandardScopes.Email, - "scope1", "scope2" + "resource1.scope1", "resource2.scope1" } }, @@ -186,7 +186,7 @@ public static IEnumerable Get() ClientId = "roclient.reference", ClientSecrets = {new Secret("secret".Sha256())}, AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, - AllowedScopes = {"scope1", "scope2"}, + AllowedScopes = { "resource1.scope1", "resource2.scope1" }, AccessTokenType = AccessTokenType.Reference }, @@ -208,7 +208,7 @@ public static IEnumerable Get() IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, IdentityServerConstants.StandardScopes.Email, - "scope1", "scope2" + "resource1.scope1", "resource2.scope1" } } }; diff --git a/src/AspNetIdentity/host/Configuration/ClientsWeb.cs b/src/AspNetIdentity/host/Configuration/ClientsWeb.cs index 70baf17bf8..962c02ea81 100644 --- a/src/AspNetIdentity/host/Configuration/ClientsWeb.cs +++ b/src/AspNetIdentity/host/Configuration/ClientsWeb.cs @@ -14,8 +14,8 @@ public static class ClientsWeb IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, IdentityServerConstants.StandardScopes.Email, - "scope1", - "scope2", + "resource1.scope1", + "resource2.scope1", "transaction" }; diff --git a/src/AspNetIdentity/host/Configuration/Resources.cs b/src/AspNetIdentity/host/Configuration/Resources.cs index 1a5356e567..d536f1aa69 100644 --- a/src/AspNetIdentity/host/Configuration/Resources.cs +++ b/src/AspNetIdentity/host/Configuration/Resources.cs @@ -28,16 +28,20 @@ public class Resources public static readonly IEnumerable ApiScopes = new[] { - // local feature + // local API scope new ApiScope(LocalApi.ScopeName), - // some generic scopes - new ApiScope("scope1"), - new ApiScope("scope2"), + // resource specific scopes + new ApiScope("resource1.scope1"), + new ApiScope("resource2.scope1"), + + // a scope without resource association new ApiScope("scope3"), + + // a scope shared by multiple resources new ApiScope("shared.scope"), - // used as a dynamic scope + // a parameterized scope new ApiScope("transaction", "Transaction") { Description = "Some Transaction" @@ -48,15 +52,11 @@ public class Resources public static readonly IEnumerable ApiResources = new[] { - // simple version with ctor new ApiResource("resource1", "Resource 1") { - // this is needed for introspection when using reference tokens ApiSecrets = { new Secret("secret".Sha256()) }, - //AllowedSigningAlgorithms = { "RS256", "ES256" } - - Scopes = { "scope1", "shared.scope" } + Scopes = { "resource1.scope1", "shared.scope" } }, // expanded version if more control is needed @@ -67,15 +67,14 @@ public class Resources new Secret("secret".Sha256()) }, - //AllowedSigningAlgorithms = { "PS256", "ES256", "RS256" }, - + // additional claims to put into access token UserClaims = { JwtClaimTypes.Name, JwtClaimTypes.Email }, - Scopes = { "scope2", "shared.scope" } + Scopes = { "resource2.scope1", "shared.scope" } } }; } diff --git a/src/AspNetIdentity/host/Quickstart/Consent/ConsentController.cs b/src/AspNetIdentity/host/Quickstart/Consent/ConsentController.cs index 4f630142f2..03b3c0e2b1 100644 --- a/src/AspNetIdentity/host/Quickstart/Consent/ConsentController.cs +++ b/src/AspNetIdentity/host/Quickstart/Consent/ConsentController.cs @@ -109,7 +109,7 @@ private async Task ProcessConsent(ConsentInputModel model) grantedConsent = new ConsentResponse { Error = AuthorizationError.AccessDenied }; // emit event - await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.ScopeValues)); + await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues)); } // user clicked 'yes' - validate the data else if (model?.Button == "yes") @@ -131,7 +131,7 @@ private async Task ProcessConsent(ConsentInputModel model) }; // emit event - await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.ScopeValues, grantedConsent.ScopesValuesConsented, grantedConsent.RememberConsent)); + await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues, grantedConsent.ScopesValuesConsented, grantedConsent.RememberConsent)); } else { @@ -199,10 +199,10 @@ private ConsentViewModel CreateConsentViewModel( var apiScopes = new List(); foreach(var parsedScope in request.ValidatedResources.ParsedScopes) { - var apiScope = request.ValidatedResources.Resources.FindApiScope(parsedScope.Name); + var apiScope = request.ValidatedResources.Resources.FindApiScope(parsedScope.ParsedName); if (apiScope != null) { - var scopeVm = CreateScopeViewModel(parsedScope, apiScope, vm.ScopesConsented.Contains(parsedScope.Value) || model == null); + var scopeVm = CreateScopeViewModel(parsedScope, apiScope, vm.ScopesConsented.Contains(parsedScope.RawValue) || model == null); apiScopes.Add(scopeVm); } } @@ -231,14 +231,14 @@ private ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool chec public ScopeViewModel CreateScopeViewModel(ParsedScopeValue parsedScopeValue, ApiScope apiScope, bool check) { var displayName = apiScope.DisplayName ?? apiScope.Name; - if (!String.IsNullOrWhiteSpace(parsedScopeValue.ParameterValue)) + if (!String.IsNullOrWhiteSpace(parsedScopeValue.ParsedParameter)) { - displayName += ":" + parsedScopeValue.ParameterValue; + displayName += ":" + parsedScopeValue.ParsedParameter; } return new ScopeViewModel { - Value = parsedScopeValue.Value, + Value = parsedScopeValue.RawValue, DisplayName = displayName, Description = apiScope.Description, Emphasize = apiScope.Emphasize, diff --git a/src/AspNetIdentity/host/Quickstart/Device/DeviceAuthorizationInputModel.cs b/src/AspNetIdentity/host/Quickstart/Device/DeviceAuthorizationInputModel.cs index 2760213195..f95d4e86d7 100644 --- a/src/AspNetIdentity/host/Quickstart/Device/DeviceAuthorizationInputModel.cs +++ b/src/AspNetIdentity/host/Quickstart/Device/DeviceAuthorizationInputModel.cs @@ -2,7 +2,7 @@ // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -namespace IdentityServer4.Quickstart.UI.Device +namespace IdentityServer4.Quickstart.UI { public class DeviceAuthorizationInputModel : ConsentInputModel { diff --git a/src/AspNetIdentity/host/Quickstart/Device/DeviceAuthorizationViewModel.cs b/src/AspNetIdentity/host/Quickstart/Device/DeviceAuthorizationViewModel.cs index 42f6412067..02d0a81c29 100644 --- a/src/AspNetIdentity/host/Quickstart/Device/DeviceAuthorizationViewModel.cs +++ b/src/AspNetIdentity/host/Quickstart/Device/DeviceAuthorizationViewModel.cs @@ -2,7 +2,7 @@ // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -namespace IdentityServer4.Quickstart.UI.Device +namespace IdentityServer4.Quickstart.UI { public class DeviceAuthorizationViewModel : ConsentViewModel { diff --git a/src/AspNetIdentity/host/Quickstart/Device/DeviceController.cs b/src/AspNetIdentity/host/Quickstart/Device/DeviceController.cs index 328b436233..e6f636d7f8 100644 --- a/src/AspNetIdentity/host/Quickstart/Device/DeviceController.cs +++ b/src/AspNetIdentity/host/Quickstart/Device/DeviceController.cs @@ -17,7 +17,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -namespace IdentityServer4.Quickstart.UI.Device +namespace IdentityServer4.Quickstart.UI { [Authorize] [SecurityHeaders] @@ -91,7 +91,7 @@ private async Task ProcessConsent(DeviceAuthorizationInput grantedConsent = new ConsentResponse { Error = AuthorizationError.AccessDenied }; // emit event - await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.ScopeValues)); + await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues)); } // user clicked 'yes' - validate the data else if (model.Button == "yes") @@ -113,7 +113,7 @@ private async Task ProcessConsent(DeviceAuthorizationInput }; // emit event - await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.ScopeValues, grantedConsent.ScopesValuesConsented, grantedConsent.RememberConsent)); + await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues, grantedConsent.ScopesValuesConsented, grantedConsent.RememberConsent)); } else { @@ -175,10 +175,10 @@ private DeviceAuthorizationViewModel CreateConsentViewModel(string userCode, Dev var apiScopes = new List(); foreach (var parsedScope in request.ValidatedResources.ParsedScopes) { - var apiScope = request.ValidatedResources.Resources.FindApiScope(parsedScope.Name); + var apiScope = request.ValidatedResources.Resources.FindApiScope(parsedScope.ParsedName); if (apiScope != null) { - var scopeVm = CreateScopeViewModel(parsedScope, apiScope, vm.ScopesConsented.Contains(parsedScope.Value) || model == null); + var scopeVm = CreateScopeViewModel(parsedScope, apiScope, vm.ScopesConsented.Contains(parsedScope.RawValue) || model == null); apiScopes.Add(scopeVm); } } @@ -208,7 +208,7 @@ public ScopeViewModel CreateScopeViewModel(ParsedScopeValue parsedScopeValue, Ap { return new ScopeViewModel { - Value = parsedScopeValue.Value, + Value = parsedScopeValue.RawValue, // todo: use the parsed scope value in the display? DisplayName = apiScope.DisplayName ?? apiScope.Name, Description = apiScope.Description, diff --git a/src/AspNetIdentity/host/Views/Device/UserCodeConfirmation.cshtml b/src/AspNetIdentity/host/Views/Device/UserCodeConfirmation.cshtml index 194cf3ed17..e1d3b19686 100644 --- a/src/AspNetIdentity/host/Views/Device/UserCodeConfirmation.cshtml +++ b/src/AspNetIdentity/host/Views/Device/UserCodeConfirmation.cshtml @@ -1,4 +1,4 @@ -@model IdentityServer4.Quickstart.UI.Device.DeviceAuthorizationViewModel +@model DeviceAuthorizationViewModel
diff --git a/src/EntityFramework/host/Extensions/ExtensionGrantValidator.cs b/src/EntityFramework/host/Extensions/ExtensionGrantValidator.cs deleted file mode 100644 index da1f7f4df6..0000000000 --- a/src/EntityFramework/host/Extensions/ExtensionGrantValidator.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. -// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. - - -using IdentityServer4.Models; -using IdentityServer4.Validation; -using System.Threading.Tasks; - -namespace Host.Extensions -{ - public class ExtensionGrantValidator : IExtensionGrantValidator - { - public Task ValidateAsync(ExtensionGrantValidationContext context) - { - var credential = context.Request.Raw.Get("custom_credential"); - - if (credential != null) - { - context.Result = new GrantValidationResult(subject: "818727", authenticationMethod: "custom"); - } - else - { - // custom error message - context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "invalid custom credential"); - } - - return Task.CompletedTask; - } - - public string GrantType - { - get { return "custom"; } - } - } -} \ No newline at end of file diff --git a/src/EntityFramework/host/Extensions/NoSubjectExtensionGrantValidator.cs b/src/EntityFramework/host/Extensions/NoSubjectExtensionGrantValidator.cs deleted file mode 100644 index 7078e87eb5..0000000000 --- a/src/EntityFramework/host/Extensions/NoSubjectExtensionGrantValidator.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. -// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. - - -using IdentityServer4.Models; -using IdentityServer4.Validation; -using System.Threading.Tasks; - -namespace Host.Extensions -{ - public class NoSubjectExtensionGrantValidator : IExtensionGrantValidator - { - public Task ValidateAsync(ExtensionGrantValidationContext context) - { - var credential = context.Request.Raw.Get("custom_credential"); - - if (credential != null) - { - context.Result = new GrantValidationResult(); - } - else - { - // custom error message - context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "invalid custom credential"); - } - - return Task.CompletedTask; - } - - public string GrantType - { - get { return "custom.nosubject"; } - } - } -} \ No newline at end of file diff --git a/src/EntityFramework/host/Extensions/RequestLoggerMiddleware.cs b/src/EntityFramework/host/Extensions/RequestLoggerMiddleware.cs deleted file mode 100644 index 5fbb26c563..0000000000 --- a/src/EntityFramework/host/Extensions/RequestLoggerMiddleware.cs +++ /dev/null @@ -1,72 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Serilog; -using Serilog.Events; -using System; -using System.Diagnostics; -using System.Linq; -using System.Threading.Tasks; - -namespace Host.Logging -{ - internal class RequestLoggerMiddleware - { - const string MessageTemplate = "HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000} ms"; - - static readonly ILogger Log = Serilog.Log.ForContext(); - - readonly RequestDelegate _next; - - public RequestLoggerMiddleware(RequestDelegate next) - { - _next = next ?? throw new ArgumentNullException(nameof(next)); - } - - public async Task Invoke(HttpContext httpContext) - { - if (httpContext == null) throw new ArgumentNullException(nameof(httpContext)); - - var start = Stopwatch.GetTimestamp(); - try - { - await _next(httpContext); - var elapsedMs = GetElapsedMilliseconds(start, Stopwatch.GetTimestamp()); - - var statusCode = httpContext.Response?.StatusCode; - var level = statusCode > 499 ? LogEventLevel.Error : LogEventLevel.Information; - - var log = level == LogEventLevel.Error ? LogForErrorContext(httpContext) : Log; - log.Write(level, MessageTemplate, httpContext.Request.Method, httpContext.Request.Path, statusCode, elapsedMs); - } - // Never caught, because `LogException()` returns false. - catch (Exception ex) when (LogException(httpContext, GetElapsedMilliseconds(start, Stopwatch.GetTimestamp()), ex)) { } - } - - static bool LogException(HttpContext httpContext, double elapsedMs, Exception ex) - { - LogForErrorContext(httpContext) - .Error(ex, MessageTemplate, httpContext.Request.Method, httpContext.Request.Path, 500, elapsedMs); - - return false; - } - - static ILogger LogForErrorContext(HttpContext httpContext) - { - var request = httpContext.Request; - - var result = Log - .ForContext("RequestHeaders", request.Headers.ToDictionary(h => h.Key, h => h.Value.ToString()), destructureObjects: true) - .ForContext("RequestHost", request.Host) - .ForContext("RequestProtocol", request.Protocol); - - if (request.HasFormContentType) - result = result.ForContext("RequestForm", request.Form.ToDictionary(v => v.Key, v => v.Value.ToString())); - - return result; - } - - static double GetElapsedMilliseconds(long start, long stop) - { - return (stop - start) * 1000 / (double)Stopwatch.Frequency; - } - } -} \ No newline at end of file diff --git a/src/EntityFramework/host/Quickstart/Consent/ConsentController.cs b/src/EntityFramework/host/Quickstart/Consent/ConsentController.cs index 4f630142f2..03b3c0e2b1 100644 --- a/src/EntityFramework/host/Quickstart/Consent/ConsentController.cs +++ b/src/EntityFramework/host/Quickstart/Consent/ConsentController.cs @@ -109,7 +109,7 @@ private async Task ProcessConsent(ConsentInputModel model) grantedConsent = new ConsentResponse { Error = AuthorizationError.AccessDenied }; // emit event - await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.ScopeValues)); + await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues)); } // user clicked 'yes' - validate the data else if (model?.Button == "yes") @@ -131,7 +131,7 @@ private async Task ProcessConsent(ConsentInputModel model) }; // emit event - await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.ScopeValues, grantedConsent.ScopesValuesConsented, grantedConsent.RememberConsent)); + await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues, grantedConsent.ScopesValuesConsented, grantedConsent.RememberConsent)); } else { @@ -199,10 +199,10 @@ private ConsentViewModel CreateConsentViewModel( var apiScopes = new List(); foreach(var parsedScope in request.ValidatedResources.ParsedScopes) { - var apiScope = request.ValidatedResources.Resources.FindApiScope(parsedScope.Name); + var apiScope = request.ValidatedResources.Resources.FindApiScope(parsedScope.ParsedName); if (apiScope != null) { - var scopeVm = CreateScopeViewModel(parsedScope, apiScope, vm.ScopesConsented.Contains(parsedScope.Value) || model == null); + var scopeVm = CreateScopeViewModel(parsedScope, apiScope, vm.ScopesConsented.Contains(parsedScope.RawValue) || model == null); apiScopes.Add(scopeVm); } } @@ -231,14 +231,14 @@ private ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool chec public ScopeViewModel CreateScopeViewModel(ParsedScopeValue parsedScopeValue, ApiScope apiScope, bool check) { var displayName = apiScope.DisplayName ?? apiScope.Name; - if (!String.IsNullOrWhiteSpace(parsedScopeValue.ParameterValue)) + if (!String.IsNullOrWhiteSpace(parsedScopeValue.ParsedParameter)) { - displayName += ":" + parsedScopeValue.ParameterValue; + displayName += ":" + parsedScopeValue.ParsedParameter; } return new ScopeViewModel { - Value = parsedScopeValue.Value, + Value = parsedScopeValue.RawValue, DisplayName = displayName, Description = apiScope.Description, Emphasize = apiScope.Emphasize, diff --git a/src/EntityFramework/host/Quickstart/Device/DeviceAuthorizationInputModel.cs b/src/EntityFramework/host/Quickstart/Device/DeviceAuthorizationInputModel.cs index 2760213195..f95d4e86d7 100644 --- a/src/EntityFramework/host/Quickstart/Device/DeviceAuthorizationInputModel.cs +++ b/src/EntityFramework/host/Quickstart/Device/DeviceAuthorizationInputModel.cs @@ -2,7 +2,7 @@ // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -namespace IdentityServer4.Quickstart.UI.Device +namespace IdentityServer4.Quickstart.UI { public class DeviceAuthorizationInputModel : ConsentInputModel { diff --git a/src/EntityFramework/host/Quickstart/Device/DeviceAuthorizationViewModel.cs b/src/EntityFramework/host/Quickstart/Device/DeviceAuthorizationViewModel.cs index 42f6412067..02d0a81c29 100644 --- a/src/EntityFramework/host/Quickstart/Device/DeviceAuthorizationViewModel.cs +++ b/src/EntityFramework/host/Quickstart/Device/DeviceAuthorizationViewModel.cs @@ -2,7 +2,7 @@ // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -namespace IdentityServer4.Quickstart.UI.Device +namespace IdentityServer4.Quickstart.UI { public class DeviceAuthorizationViewModel : ConsentViewModel { diff --git a/src/EntityFramework/host/Quickstart/Device/DeviceController.cs b/src/EntityFramework/host/Quickstart/Device/DeviceController.cs index 328b436233..e6f636d7f8 100644 --- a/src/EntityFramework/host/Quickstart/Device/DeviceController.cs +++ b/src/EntityFramework/host/Quickstart/Device/DeviceController.cs @@ -17,7 +17,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -namespace IdentityServer4.Quickstart.UI.Device +namespace IdentityServer4.Quickstart.UI { [Authorize] [SecurityHeaders] @@ -91,7 +91,7 @@ private async Task ProcessConsent(DeviceAuthorizationInput grantedConsent = new ConsentResponse { Error = AuthorizationError.AccessDenied }; // emit event - await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.ScopeValues)); + await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues)); } // user clicked 'yes' - validate the data else if (model.Button == "yes") @@ -113,7 +113,7 @@ private async Task ProcessConsent(DeviceAuthorizationInput }; // emit event - await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.ScopeValues, grantedConsent.ScopesValuesConsented, grantedConsent.RememberConsent)); + await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues, grantedConsent.ScopesValuesConsented, grantedConsent.RememberConsent)); } else { @@ -175,10 +175,10 @@ private DeviceAuthorizationViewModel CreateConsentViewModel(string userCode, Dev var apiScopes = new List(); foreach (var parsedScope in request.ValidatedResources.ParsedScopes) { - var apiScope = request.ValidatedResources.Resources.FindApiScope(parsedScope.Name); + var apiScope = request.ValidatedResources.Resources.FindApiScope(parsedScope.ParsedName); if (apiScope != null) { - var scopeVm = CreateScopeViewModel(parsedScope, apiScope, vm.ScopesConsented.Contains(parsedScope.Value) || model == null); + var scopeVm = CreateScopeViewModel(parsedScope, apiScope, vm.ScopesConsented.Contains(parsedScope.RawValue) || model == null); apiScopes.Add(scopeVm); } } @@ -208,7 +208,7 @@ public ScopeViewModel CreateScopeViewModel(ParsedScopeValue parsedScopeValue, Ap { return new ScopeViewModel { - Value = parsedScopeValue.Value, + Value = parsedScopeValue.RawValue, // todo: use the parsed scope value in the display? DisplayName = apiScope.DisplayName ?? apiScope.Name, Description = apiScope.Description, diff --git a/src/EntityFramework/host/Startup.cs b/src/EntityFramework/host/Startup.cs index 55536ed111..45c9764df3 100644 --- a/src/EntityFramework/host/Startup.cs +++ b/src/EntityFramework/host/Startup.cs @@ -49,7 +49,6 @@ public void ConfigureServices(IServiceCollection services) public void Configure(IApplicationBuilder app) { - app.UseMiddleware(); app.UseDeveloperExceptionPage(); app.UseStaticFiles(); diff --git a/src/EntityFramework/host/Views/Device/UserCodeConfirmation.cshtml b/src/EntityFramework/host/Views/Device/UserCodeConfirmation.cshtml index 194cf3ed17..e1d3b19686 100644 --- a/src/EntityFramework/host/Views/Device/UserCodeConfirmation.cshtml +++ b/src/EntityFramework/host/Views/Device/UserCodeConfirmation.cshtml @@ -1,4 +1,4 @@ -@model IdentityServer4.Quickstart.UI.Device.DeviceAuthorizationViewModel +@model DeviceAuthorizationViewModel
diff --git a/src/EntityFramework/migrations/SqlServer/Configuration/ClientsConsole.cs b/src/EntityFramework/migrations/SqlServer/Configuration/ClientsConsole.cs index ef45caff05..bb62b210ee 100644 --- a/src/EntityFramework/migrations/SqlServer/Configuration/ClientsConsole.cs +++ b/src/EntityFramework/migrations/SqlServer/Configuration/ClientsConsole.cs @@ -26,7 +26,7 @@ public static IEnumerable Get() ClientId = "client", ClientSecrets = {new Secret("secret".Sha256())}, AllowedGrantTypes = GrantTypes.ClientCredentials, - AllowedScopes = { "scope1", "scope2", IdentityServerConstants.LocalApi.ScopeName} + AllowedScopes = { "resource1.scope1", "resource2.scope1", IdentityServerConstants.LocalApi.ScopeName} }, /////////////////////////////////////////// @@ -60,7 +60,7 @@ public static IEnumerable Get() AccessTokenType = AccessTokenType.Jwt, AllowedGrantTypes = GrantTypes.ClientCredentials, - AllowedScopes = { "scope1", "scope2" } + AllowedScopes = { "resource1.scope1", "resource2.scope1" } }, /////////////////////////////////////////// @@ -86,7 +86,7 @@ public static IEnumerable Get() }, AllowedGrantTypes = GrantTypes.ClientCredentials, - AllowedScopes = { "scope1", "scope2" } + AllowedScopes = { "resource1.scope1", "resource2.scope1" } }, /////////////////////////////////////////// @@ -97,7 +97,7 @@ public static IEnumerable Get() ClientId = "client.custom", ClientSecrets = {new Secret("secret".Sha256())}, AllowedGrantTypes = {"custom", "custom.nosubject"}, - AllowedScopes = { "scope1", "scope2" } + AllowedScopes = { "resource1.scope1", "resource2.scope1" } }, /////////////////////////////////////////// @@ -113,7 +113,7 @@ public static IEnumerable Get() { IdentityServerConstants.StandardScopes.OpenId, "custom.profile", - "scope1", "scope2" + "resource1.scope1", "resource2.scope1" } }, @@ -130,7 +130,7 @@ public static IEnumerable Get() { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Email, - "scope1", "scope2" + "resource1.scope1", "resource2.scope1" } }, @@ -151,7 +151,7 @@ public static IEnumerable Get() IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, IdentityServerConstants.StandardScopes.Email, - "scope1", "scope2" + "resource1.scope1", "resource2.scope1" } }, /////////////////////////////////////////// @@ -173,7 +173,7 @@ public static IEnumerable Get() IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, IdentityServerConstants.StandardScopes.Email, - "scope1", "scope2" + "resource1.scope1", "resource2.scope1" } }, @@ -186,7 +186,7 @@ public static IEnumerable Get() ClientId = "roclient.reference", ClientSecrets = {new Secret("secret".Sha256())}, AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, - AllowedScopes = {"scope1", "scope2"}, + AllowedScopes = { "resource1.scope1", "resource2.scope1" }, AccessTokenType = AccessTokenType.Reference }, @@ -208,7 +208,7 @@ public static IEnumerable Get() IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, IdentityServerConstants.StandardScopes.Email, - "scope1", "scope2" + "resource1.scope1", "resource2.scope1" } } }; diff --git a/src/EntityFramework/migrations/SqlServer/Configuration/ClientsWeb.cs b/src/EntityFramework/migrations/SqlServer/Configuration/ClientsWeb.cs index 70baf17bf8..962c02ea81 100644 --- a/src/EntityFramework/migrations/SqlServer/Configuration/ClientsWeb.cs +++ b/src/EntityFramework/migrations/SqlServer/Configuration/ClientsWeb.cs @@ -14,8 +14,8 @@ public static class ClientsWeb IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, IdentityServerConstants.StandardScopes.Email, - "scope1", - "scope2", + "resource1.scope1", + "resource2.scope1", "transaction" }; diff --git a/src/EntityFramework/migrations/SqlServer/Configuration/Resources.cs b/src/EntityFramework/migrations/SqlServer/Configuration/Resources.cs index 1a5356e567..d536f1aa69 100644 --- a/src/EntityFramework/migrations/SqlServer/Configuration/Resources.cs +++ b/src/EntityFramework/migrations/SqlServer/Configuration/Resources.cs @@ -28,16 +28,20 @@ public class Resources public static readonly IEnumerable ApiScopes = new[] { - // local feature + // local API scope new ApiScope(LocalApi.ScopeName), - // some generic scopes - new ApiScope("scope1"), - new ApiScope("scope2"), + // resource specific scopes + new ApiScope("resource1.scope1"), + new ApiScope("resource2.scope1"), + + // a scope without resource association new ApiScope("scope3"), + + // a scope shared by multiple resources new ApiScope("shared.scope"), - // used as a dynamic scope + // a parameterized scope new ApiScope("transaction", "Transaction") { Description = "Some Transaction" @@ -48,15 +52,11 @@ public class Resources public static readonly IEnumerable ApiResources = new[] { - // simple version with ctor new ApiResource("resource1", "Resource 1") { - // this is needed for introspection when using reference tokens ApiSecrets = { new Secret("secret".Sha256()) }, - //AllowedSigningAlgorithms = { "RS256", "ES256" } - - Scopes = { "scope1", "shared.scope" } + Scopes = { "resource1.scope1", "shared.scope" } }, // expanded version if more control is needed @@ -67,15 +67,14 @@ public class Resources new Secret("secret".Sha256()) }, - //AllowedSigningAlgorithms = { "PS256", "ES256", "RS256" }, - + // additional claims to put into access token UserClaims = { JwtClaimTypes.Name, JwtClaimTypes.Email }, - Scopes = { "scope2", "shared.scope" } + Scopes = { "resource2.scope1", "shared.scope" } } }; } diff --git a/src/IdentityServer4/host/Extensions/HostProfileService.cs b/src/IdentityServer4/host/Extensions/HostProfileService.cs index d4cb2aef0c..95a4596729 100644 --- a/src/IdentityServer4/host/Extensions/HostProfileService.cs +++ b/src/IdentityServer4/host/Extensions/HostProfileService.cs @@ -17,10 +17,10 @@ public override async Task GetProfileDataAsync(ProfileDataRequestContext context { await base.GetProfileDataAsync(context); - var transaction = context.RequestedResources.ParsedScopes.FirstOrDefault(x => x.Name == "transaction"); - if (transaction?.ParameterValue != null) + var transaction = context.RequestedResources.ParsedScopes.FirstOrDefault(x => x.ParsedName == "transaction"); + if (transaction?.ParsedParameter != null) { - context.IssuedClaims.Add(new Claim("transaction_id", transaction.ParameterValue)); + context.IssuedClaims.Add(new Claim("transaction_id", transaction.ParsedParameter)); } } } diff --git a/src/IdentityServer4/host/Extensions/ParameterizedScopeParser.cs b/src/IdentityServer4/host/Extensions/ParameterizedScopeParser.cs new file mode 100644 index 0000000000..74ab0a1bac --- /dev/null +++ b/src/IdentityServer4/host/Extensions/ParameterizedScopeParser.cs @@ -0,0 +1,47 @@ +using IdentityServer4.Validation; +using Microsoft.Extensions.Logging; +using System; + +namespace Host.Extensions +{ + public class ParameterizedScopeParser : DefaultScopeParser + { + public ParameterizedScopeParser(ILogger logger) : base(logger) + { + } + + public override void ParseScopeValue(ParseScopeContext scopeContext) + { + const string transactionScopeName = "transaction"; + const string separator = ":"; + const string transactionScopePrefix = transactionScopeName + separator; + + var scopeValue = scopeContext.RawValue; + + if (scopeValue.StartsWith(transactionScopePrefix)) + { + // we get in here with a scope like "transaction:something" + var parts = scopeValue.Split(separator, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length == 2) + { + scopeContext.SetParsedValues(transactionScopeName, parts[1]); + } + else + { + scopeContext.SetError("transaction scope missing transaction parameter value"); + } + } + else if (scopeValue != transactionScopeName) + { + // we get in here with a scope not like "transaction" + base.ParseScopeValue(scopeContext); + } + else + { + // we get in here with a scope exactly "transaction", which is to say we're ignoring it + // and not including it in the results + scopeContext.SetIgnore(); + } + } + } +} diff --git a/src/IdentityServer4/host/Extensions/ParameterizedScopeTokenRequestValidator.cs b/src/IdentityServer4/host/Extensions/ParameterizedScopeTokenRequestValidator.cs index d4bdb87c0d..9a610502b5 100644 --- a/src/IdentityServer4/host/Extensions/ParameterizedScopeTokenRequestValidator.cs +++ b/src/IdentityServer4/host/Extensions/ParameterizedScopeTokenRequestValidator.cs @@ -9,10 +9,10 @@ public class ParameterizedScopeTokenRequestValidator : ICustomTokenRequestValida { public Task ValidateAsync(CustomTokenRequestValidationContext context) { - var transaction = context.Result.ValidatedRequest.ValidatedResources.ParsedScopes.FirstOrDefault(x => x.Name == "transaction"); - if (transaction?.ParameterValue != null) + var transaction = context.Result.ValidatedRequest.ValidatedResources.ParsedScopes.FirstOrDefault(x => x.ParsedName == "transaction"); + if (transaction?.ParsedParameter != null) { - context.Result.ValidatedRequest.ClientClaims.Add(new Claim(transaction.Name, transaction.ParameterValue)); + context.Result.ValidatedRequest.ClientClaims.Add(new Claim(transaction.ParsedName, transaction.ParsedParameter)); } return Task.CompletedTask; diff --git a/src/IdentityServer4/host/Extensions/ParameterizedScopeValidator.cs b/src/IdentityServer4/host/Extensions/ParameterizedScopeValidator.cs deleted file mode 100644 index 570c8adf91..0000000000 --- a/src/IdentityServer4/host/Extensions/ParameterizedScopeValidator.cs +++ /dev/null @@ -1,30 +0,0 @@ -using IdentityServer4.Stores; -using IdentityServer4.Validation; -using Microsoft.Extensions.Logging; -using System; -using System.Threading.Tasks; - -namespace Host.Extensions -{ - public class ParameterizedScopeValidator : ResourceValidator - { - public ParameterizedScopeValidator(IResourceStore store, ILogger logger) : base(store, logger) - { - } - - public override Task ParseScopeValue(string scopeValue) - { - const string transactionScopeName = "transaction"; - const string separator = ":"; - const string transactionScopePrefix = transactionScopeName + separator; - - if (scopeValue.StartsWith(transactionScopePrefix)) - { - var parts = scopeValue.Split(separator, StringSplitOptions.RemoveEmptyEntries); - return Task.FromResult(new ParsedScopeValue(transactionScopeName, scopeValue, parts[1])); - } - - return base.ParseScopeValue(scopeValue); - } - } -} diff --git a/src/IdentityServer4/host/Quickstart/Consent/ConsentController.cs b/src/IdentityServer4/host/Quickstart/Consent/ConsentController.cs index ef82edf35f..926cfe03c5 100644 --- a/src/IdentityServer4/host/Quickstart/Consent/ConsentController.cs +++ b/src/IdentityServer4/host/Quickstart/Consent/ConsentController.cs @@ -109,7 +109,7 @@ private async Task ProcessConsent(ConsentInputModel model) grantedConsent = new ConsentResponse { Error = AuthorizationError.AccessDenied }; // emit event - await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.ScopeValues)); + await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues)); } // user clicked 'yes' - validate the data else if (model?.Button == "yes") @@ -131,7 +131,7 @@ private async Task ProcessConsent(ConsentInputModel model) }; // emit event - await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.ScopeValues, grantedConsent.ScopesValuesConsented, grantedConsent.RememberConsent)); + await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues, grantedConsent.ScopesValuesConsented, grantedConsent.RememberConsent)); } else { @@ -199,10 +199,10 @@ private ConsentViewModel CreateConsentViewModel( var apiScopes = new List(); foreach(var parsedScope in request.ValidatedResources.ParsedScopes) { - var apiScope = request.ValidatedResources.Resources.FindApiScope(parsedScope.Name); + var apiScope = request.ValidatedResources.Resources.FindApiScope(parsedScope.ParsedName); if (apiScope != null) { - var scopeVm = CreateScopeViewModel(parsedScope, apiScope, vm.ScopesConsented.Contains(parsedScope.Value) || model == null); + var scopeVm = CreateScopeViewModel(parsedScope, apiScope, vm.ScopesConsented.Contains(parsedScope.RawValue) || model == null); apiScopes.Add(scopeVm); } } @@ -231,14 +231,14 @@ private ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool chec public ScopeViewModel CreateScopeViewModel(ParsedScopeValue parsedScopeValue, ApiScope apiScope, bool check) { var displayName = apiScope.DisplayName ?? apiScope.Name; - if (!String.IsNullOrWhiteSpace(parsedScopeValue.ParameterValue)) + if (!String.IsNullOrWhiteSpace(parsedScopeValue.ParsedParameter)) { - displayName += ":" + parsedScopeValue.ParameterValue; + displayName += ":" + parsedScopeValue.ParsedParameter; } return new ScopeViewModel { - Value = parsedScopeValue.Value, + Value = parsedScopeValue.RawValue, DisplayName = displayName, Description = apiScope.Description, Emphasize = apiScope.Emphasize, diff --git a/src/IdentityServer4/host/Quickstart/Device/DeviceController.cs b/src/IdentityServer4/host/Quickstart/Device/DeviceController.cs index d268ad1680..9f9bb590b0 100644 --- a/src/IdentityServer4/host/Quickstart/Device/DeviceController.cs +++ b/src/IdentityServer4/host/Quickstart/Device/DeviceController.cs @@ -91,7 +91,7 @@ private async Task ProcessConsent(DeviceAuthorizationInput grantedConsent = new ConsentResponse { Error = AuthorizationError.AccessDenied }; // emit event - await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.ScopeValues)); + await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues)); } // user clicked 'yes' - validate the data else if (model.Button == "yes") @@ -113,7 +113,7 @@ private async Task ProcessConsent(DeviceAuthorizationInput }; // emit event - await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.ScopeValues, grantedConsent.ScopesValuesConsented, grantedConsent.RememberConsent)); + await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues, grantedConsent.ScopesValuesConsented, grantedConsent.RememberConsent)); } else { @@ -175,10 +175,10 @@ private DeviceAuthorizationViewModel CreateConsentViewModel(string userCode, Dev var apiScopes = new List(); foreach (var parsedScope in request.ValidatedResources.ParsedScopes) { - var apiScope = request.ValidatedResources.Resources.FindApiScope(parsedScope.Name); + var apiScope = request.ValidatedResources.Resources.FindApiScope(parsedScope.ParsedName); if (apiScope != null) { - var scopeVm = CreateScopeViewModel(parsedScope, apiScope, vm.ScopesConsented.Contains(parsedScope.Value) || model == null); + var scopeVm = CreateScopeViewModel(parsedScope, apiScope, vm.ScopesConsented.Contains(parsedScope.RawValue) || model == null); apiScopes.Add(scopeVm); } } @@ -208,7 +208,7 @@ public ScopeViewModel CreateScopeViewModel(ParsedScopeValue parsedScopeValue, Ap { return new ScopeViewModel { - Value = parsedScopeValue.Value, + Value = parsedScopeValue.RawValue, // todo: use the parsed scope value in the display? DisplayName = apiScope.DisplayName ?? apiScope.Name, Description = apiScope.Description, diff --git a/src/IdentityServer4/host/Startup.cs b/src/IdentityServer4/host/Startup.cs index 9224d713bc..870d2d4047 100644 --- a/src/IdentityServer4/host/Startup.cs +++ b/src/IdentityServer4/host/Startup.cs @@ -66,7 +66,7 @@ public void ConfigureServices(IServiceCollection services) .AddTestUsers(TestUsers.Users) .AddProfileService() .AddCustomTokenRequestValidator() - .AddResourceValidator() + .AddScopeParser() .AddMutualTlsSecretValidators(); // use this for persisted grants store diff --git a/src/IdentityServer4/src/Configuration/DependencyInjection/BuilderExtensions/Additional.cs b/src/IdentityServer4/src/Configuration/DependencyInjection/BuilderExtensions/Additional.cs index 1f60b9990d..08547e3336 100644 --- a/src/IdentityServer4/src/Configuration/DependencyInjection/BuilderExtensions/Additional.cs +++ b/src/IdentityServer4/src/Configuration/DependencyInjection/BuilderExtensions/Additional.cs @@ -100,6 +100,20 @@ public static IIdentityServerBuilder AddResourceValidator(this IIdentityServe return builder; } + /// + /// Adds a scope parser. + /// + /// + /// The builder. + /// + public static IIdentityServerBuilder AddScopeParser(this IIdentityServerBuilder builder) + where T : class, IScopeParser + { + builder.Services.AddTransient(); + + return builder; + } + /// /// Adds a client store. /// diff --git a/src/IdentityServer4/src/Configuration/DependencyInjection/BuilderExtensions/Core.cs b/src/IdentityServer4/src/Configuration/DependencyInjection/BuilderExtensions/Core.cs index 56d711eaa8..2f9fd16b64 100644 --- a/src/IdentityServer4/src/Configuration/DependencyInjection/BuilderExtensions/Core.cs +++ b/src/IdentityServer4/src/Configuration/DependencyInjection/BuilderExtensions/Core.cs @@ -172,7 +172,8 @@ public static IIdentityServerBuilder AddPluggableServices(this IIdentityServerBu builder.Services.TryAddTransient(); builder.Services.TryAddTransient(); builder.Services.TryAddTransient(); - builder.Services.TryAddTransient(); + builder.Services.TryAddTransient(); + builder.Services.TryAddTransient(); builder.AddJwtRequestUriHttpClient(); builder.AddBackChannelLogoutHttpClient(); diff --git a/src/IdentityServer4/src/Events/DeviceAuthorizationSuccessEvent.cs b/src/IdentityServer4/src/Events/DeviceAuthorizationSuccessEvent.cs index 4f5a8d6230..f39e102a2e 100644 --- a/src/IdentityServer4/src/Events/DeviceAuthorizationSuccessEvent.cs +++ b/src/IdentityServer4/src/Events/DeviceAuthorizationSuccessEvent.cs @@ -25,7 +25,7 @@ public DeviceAuthorizationSuccessEvent(DeviceAuthorizationResponse response, Dev ClientId = request.ValidatedRequest.Client?.ClientId; ClientName = request.ValidatedRequest.Client?.ClientName; Endpoint = Constants.EndpointNames.DeviceAuthorization; - Scopes = request.ValidatedRequest.ValidatedResources?.ScopeValues.ToSpaceSeparatedString(); + Scopes = request.ValidatedRequest.ValidatedResources?.RawScopeValues.ToSpaceSeparatedString(); } /// diff --git a/src/IdentityServer4/src/Events/TokenIssuedSuccessEvent.cs b/src/IdentityServer4/src/Events/TokenIssuedSuccessEvent.cs index 82199e480b..82fbcc41b1 100644 --- a/src/IdentityServer4/src/Events/TokenIssuedSuccessEvent.cs +++ b/src/IdentityServer4/src/Events/TokenIssuedSuccessEvent.cs @@ -72,7 +72,7 @@ public TokenIssuedSuccessEvent(TokenResponse response, TokenRequestValidationRes } else { - Scopes = request.ValidatedRequest.ValidatedResources?.ScopeValues.ToSpaceSeparatedString(); + Scopes = request.ValidatedRequest.ValidatedResources?.RawScopeValues.ToSpaceSeparatedString(); } var tokens = new List(); diff --git a/src/IdentityServer4/src/Extensions/IResourceStoreExtensions.cs b/src/IdentityServer4/src/Extensions/IResourceStoreExtensions.cs index 699f6a0d72..09c1e2796e 100644 --- a/src/IdentityServer4/src/Extensions/IResourceStoreExtensions.cs +++ b/src/IdentityServer4/src/Extensions/IResourceStoreExtensions.cs @@ -103,26 +103,14 @@ public static async Task FindEnabledResourcesByScopeAsync(this IResou /// Creates a resource validation result. /// /// The store. - /// The parsed scopes. + /// The parsed scopes. /// - public static async Task CreateResourceValidationResult(this IResourceStore store, IEnumerable parsedScopeValues) + public static async Task CreateResourceValidationResult(this IResourceStore store, ParsedScopesResult parsedScopesResult) { - var scopes = parsedScopeValues.Select(x => x.Name).ToArray(); + var validScopeValues = parsedScopesResult.ParsedScopes; + var scopes = validScopeValues.Select(x => x.ParsedName).ToArray(); var resources = await store.FindEnabledResourcesByScopeAsync(scopes); - return new ResourceValidationResult(resources, parsedScopeValues); - } - - /// - /// Gets the names of the scopes - /// - /// - /// The scope names. - /// - public static async Task<(IEnumerable parsedScopes, IEnumerable scopeNames)> GetParsedScopes(this IResourceValidator resourceValidator, IEnumerable scopeValues) - { - var parsedScopes = await resourceValidator.ParseRequestedScopesAsync(scopeValues); - var scopeNames = parsedScopes.Select(x => x.Name).ToArray(); - return (parsedScopes, scopeNames); + return new ResourceValidationResult(resources, validScopeValues); } /// diff --git a/src/IdentityServer4/src/Extensions/ResourceExtensions.cs b/src/IdentityServer4/src/Extensions/ResourceExtensions.cs index cb2cf037dd..88c37ae4bf 100644 --- a/src/IdentityServer4/src/Extensions/ResourceExtensions.cs +++ b/src/IdentityServer4/src/Extensions/ResourceExtensions.cs @@ -25,7 +25,7 @@ public static IEnumerable GetRequiredScopeValues(this ResourceValidation var names = resourceValidationResult.Resources.IdentityResources.Where(x => x.Required).Select(x => x.Name).ToList(); names.AddRange(resourceValidationResult.Resources.ApiScopes.Where(x => x.Required).Select(x => x.Name)); - var values = resourceValidationResult.ParsedScopes.Where(x => names.Contains(x.Name)).Select(x => x.Value); + var values = resourceValidationResult.ParsedScopes.Where(x => names.Contains(x.ParsedName)).Select(x => x.RawValue); return values; } diff --git a/src/IdentityServer4/src/Models/Messages/ConsentRequest.cs b/src/IdentityServer4/src/Models/Messages/ConsentRequest.cs index 41d7c0988c..e4067b003b 100644 --- a/src/IdentityServer4/src/Models/Messages/ConsentRequest.cs +++ b/src/IdentityServer4/src/Models/Messages/ConsentRequest.cs @@ -26,7 +26,7 @@ public ConsentRequest(AuthorizationRequest request, string subject) { ClientId = request.Client.ClientId; Nonce = request.Parameters[OidcConstants.AuthorizeRequest.Nonce]; - ScopesRequested = request.ValidatedResources.ScopeValues; + ScopesRequested = request.Parameters[OidcConstants.AuthorizeRequest.Scope].ParseScopesString(); Subject = subject; } diff --git a/src/IdentityServer4/src/ResponseHandling/Default/AuthorizeInteractionResponseGenerator.cs b/src/IdentityServer4/src/ResponseHandling/Default/AuthorizeInteractionResponseGenerator.cs index 58f424f59a..8b278978b9 100644 --- a/src/IdentityServer4/src/ResponseHandling/Default/AuthorizeInteractionResponseGenerator.cs +++ b/src/IdentityServer4/src/ResponseHandling/Default/AuthorizeInteractionResponseGenerator.cs @@ -312,7 +312,7 @@ protected internal virtual async Task ProcessConsentAsync(V if (consent.RememberConsent) { // remember what user actually selected - scopes = request.ValidatedResources.ScopeValues; + scopes = request.ValidatedResources.RawScopeValues; Logger.LogDebug("User indicated to remember consent for scopes: {scopes}", scopes); } diff --git a/src/IdentityServer4/src/ResponseHandling/Default/AuthorizeResponseGenerator.cs b/src/IdentityServer4/src/ResponseHandling/Default/AuthorizeResponseGenerator.cs index 24025d0f01..8a9f470f0f 100644 --- a/src/IdentityServer4/src/ResponseHandling/Default/AuthorizeResponseGenerator.cs +++ b/src/IdentityServer4/src/ResponseHandling/Default/AuthorizeResponseGenerator.cs @@ -250,7 +250,7 @@ protected virtual async Task CreateCodeAsync(ValidatedAuthori CodeChallengeMethod = request.CodeChallengeMethod, IsOpenId = request.IsOpenIdRequest, - RequestedScopes = request.ValidatedResources.ScopeValues, + RequestedScopes = request.ValidatedResources.RawScopeValues, RedirectUri = request.RedirectUri, Nonce = request.Nonce, StateHash = stateHash, diff --git a/src/IdentityServer4/src/ResponseHandling/Default/DeviceAuthorizationResponseGenerator.cs b/src/IdentityServer4/src/ResponseHandling/Default/DeviceAuthorizationResponseGenerator.cs index 5de9a0e25e..f48f150653 100644 --- a/src/IdentityServer4/src/ResponseHandling/Default/DeviceAuthorizationResponseGenerator.cs +++ b/src/IdentityServer4/src/ResponseHandling/Default/DeviceAuthorizationResponseGenerator.cs @@ -134,7 +134,7 @@ public virtual async Task ProcessAsync(DeviceAuthor IsOpenId = validationResult.ValidatedRequest.IsOpenIdRequest, Lifetime = response.DeviceCodeLifetime, CreationTime = Clock.UtcNow.UtcDateTime, - RequestedScopes = validationResult.ValidatedRequest.ValidatedResources.ScopeValues + RequestedScopes = validationResult.ValidatedRequest.ValidatedResources.RawScopeValues }); return response; diff --git a/src/IdentityServer4/src/ResponseHandling/Default/TokenResponseGenerator.cs b/src/IdentityServer4/src/ResponseHandling/Default/TokenResponseGenerator.cs index 0a0f779634..12f4863b9e 100644 --- a/src/IdentityServer4/src/ResponseHandling/Default/TokenResponseGenerator.cs +++ b/src/IdentityServer4/src/ResponseHandling/Default/TokenResponseGenerator.cs @@ -38,9 +38,9 @@ public class TokenResponseGenerator : ITokenResponseGenerator protected readonly IRefreshTokenService RefreshTokenService; /// - /// The resource validator + /// The scope parser /// - public IResourceValidator ResourceValidator { get; } + public IScopeParser ScopeParser { get; } /// /// The resource store @@ -63,16 +63,16 @@ public class TokenResponseGenerator : ITokenResponseGenerator /// The clock. /// The token service. /// The refresh token service. - /// The resource validator. + /// The scope parser. /// The resources. /// The clients. /// The logger. - public TokenResponseGenerator(ISystemClock clock, ITokenService tokenService, IRefreshTokenService refreshTokenService, IResourceValidator resourceValidator, IResourceStore resources, IClientStore clients, ILogger logger) + public TokenResponseGenerator(ISystemClock clock, ITokenService tokenService, IRefreshTokenService refreshTokenService, IScopeParser scopeParser, IResourceStore resources, IClientStore clients, ILogger logger) { Clock = clock; TokenService = tokenService; RefreshTokenService = refreshTokenService; - ResourceValidator = resourceValidator; + ScopeParser = scopeParser; Resources = resources; Clients = clients; Logger = logger; @@ -172,8 +172,8 @@ protected virtual async Task ProcessAuthorizationCodeRequestAsync throw new InvalidOperationException("Client does not exist anymore."); } - var parsedScopes = await ResourceValidator.ParseRequestedScopesAsync(request.ValidatedRequest.AuthorizationCode.RequestedScopes); - var validatedResources = await Resources.CreateResourceValidationResult(parsedScopes); + var parsedScopesResult = ScopeParser.ParseScopeValues(request.ValidatedRequest.AuthorizationCode.RequestedScopes); + var validatedResources = await Resources.CreateResourceValidationResult(parsedScopesResult); var tokenRequest = new TokenCreationRequest { @@ -209,8 +209,10 @@ protected virtual async Task ProcessRefreshTokenRequestAsync(Toke { var subject = request.ValidatedRequest.RefreshToken.Subject; - var parsedScopes = await ResourceValidator.ParseRequestedScopesAsync(oldAccessToken.Scopes); - var validatedResources = await Resources.CreateResourceValidationResult(parsedScopes); + // todo: do we want to just parse here and build up validated result + // or do we want to fully re-run validation here. + var parsedScopesResult = ScopeParser.ParseScopeValues(oldAccessToken.Scopes); + var validatedResources = await Resources.CreateResourceValidationResult(parsedScopesResult); var creationRequest = new TokenCreationRequest { @@ -289,8 +291,8 @@ protected virtual async Task ProcessDeviceCodeRequestAsync(TokenR throw new InvalidOperationException("Client does not exist anymore."); } - var parsedScopes = await ResourceValidator.ParseRequestedScopesAsync(request.ValidatedRequest.DeviceCode.AuthorizedScopes); - var validatedResources = await Resources.CreateResourceValidationResult(parsedScopes); + var parsedScopesResult = ScopeParser.ParseScopeValues(request.ValidatedRequest.DeviceCode.AuthorizedScopes); + var validatedResources = await Resources.CreateResourceValidationResult(parsedScopesResult); var tokenRequest = new TokenCreationRequest { @@ -333,7 +335,7 @@ protected virtual async Task ProcessTokenRequestAsync(TokenReques AccessToken = accessToken, AccessTokenLifetime = validationResult.ValidatedRequest.AccessTokenLifetime, Custom = validationResult.CustomResponse, - Scope = validationResult.ValidatedRequest.ValidatedResources.ScopeValues.ToSpaceSeparatedString() + Scope = validationResult.ValidatedRequest.ValidatedResources.RawScopeValues.ToSpaceSeparatedString() }; if (refreshToken.IsPresent()) @@ -370,8 +372,8 @@ protected virtual async Task ProcessTokenRequestAsync(TokenReques throw new InvalidOperationException("Client does not exist anymore."); } - var parsedScopes = await ResourceValidator.ParseRequestedScopesAsync(request.AuthorizationCode.RequestedScopes); - var validatedResources = await Resources.CreateResourceValidationResult(parsedScopes); + var parsedScopesResult = ScopeParser.ParseScopeValues(request.AuthorizationCode.RequestedScopes); + var validatedResources = await Resources.CreateResourceValidationResult(parsedScopesResult); tokenRequest = new TokenCreationRequest { @@ -395,8 +397,8 @@ protected virtual async Task ProcessTokenRequestAsync(TokenReques throw new InvalidOperationException("Client does not exist anymore."); } - var parsedScopes = await ResourceValidator.ParseRequestedScopesAsync(request.DeviceCode.AuthorizedScopes); - var validatedResources = await Resources.CreateResourceValidationResult(parsedScopes); + var parsedScopesResult = ScopeParser.ParseScopeValues(request.DeviceCode.AuthorizedScopes); + var validatedResources = await Resources.CreateResourceValidationResult(parsedScopesResult); tokenRequest = new TokenCreationRequest { @@ -438,13 +440,16 @@ protected virtual async Task ProcessTokenRequestAsync(TokenReques /// protected virtual async Task CreateIdTokenFromRefreshTokenRequestAsync(ValidatedTokenRequest request, string newAccessToken) { - var resources = await Resources.FindEnabledResourcesByScopeAsync(request.RefreshToken.Scopes); - if (resources.IdentityResources.Any()) + // todo: can we just check for "openid" scope? + //var identityResources = await Resources.FindEnabledIdentityResourcesByScopeAsync(request.RefreshToken.Scopes); + //if (identityResources.Any()) + + if (request.RefreshToken.Scopes.Contains(OidcConstants.StandardScopes.OpenId)) { var oldAccessToken = request.RefreshToken.AccessToken; - - var parsedScopes = await ResourceValidator.ParseRequestedScopesAsync(oldAccessToken.Scopes); - var validatedResources = await Resources.CreateResourceValidationResult(parsedScopes); + + var parsedScopesResult = ScopeParser.ParseScopeValues(oldAccessToken.Scopes); + var validatedResources = await Resources.CreateResourceValidationResult(parsedScopesResult); var tokenRequest = new TokenCreationRequest { diff --git a/src/IdentityServer4/src/ResponseHandling/Models/AuthorizeResponse.cs b/src/IdentityServer4/src/ResponseHandling/Models/AuthorizeResponse.cs index f997d6ff40..40e825e98c 100644 --- a/src/IdentityServer4/src/ResponseHandling/Models/AuthorizeResponse.cs +++ b/src/IdentityServer4/src/ResponseHandling/Models/AuthorizeResponse.cs @@ -14,7 +14,7 @@ public class AuthorizeResponse public ValidatedAuthorizeRequest Request { get; set; } public string RedirectUri => Request?.RedirectUri; public string State => Request?.State; - public string Scope => Request?.ValidatedResources?.ScopeValues.ToSpaceSeparatedString(); + public string Scope => Request?.ValidatedResources?.RawScopeValues.ToSpaceSeparatedString(); public string IdentityToken { get; set; } public string AccessToken { get; set; } diff --git a/src/IdentityServer4/src/Services/Default/DefaultClaimsService.cs b/src/IdentityServer4/src/Services/Default/DefaultClaimsService.cs index 279e8a76b8..8bbb4e76e0 100644 --- a/src/IdentityServer4/src/Services/Default/DefaultClaimsService.cs +++ b/src/IdentityServer4/src/Services/Default/DefaultClaimsService.cs @@ -147,7 +147,7 @@ public virtual async Task> GetAccessTokenClaimsAsync(ClaimsPr // add scopes (filter offline_access) // we use the ScopeValues collection rather than the Resources.Scopes because we support dynamic scope values // from the request, so this issues those in the token. - foreach (var scope in resourceResult.ScopeValues.Where(x => x != IdentityServerConstants.StandardScopes.OfflineAccess)) + foreach (var scope in resourceResult.RawScopeValues.Where(x => x != IdentityServerConstants.StandardScopes.OfflineAccess)) { outputClaims.Add(new Claim(JwtClaimTypes.Scope, scope)); } diff --git a/src/IdentityServer4/src/Services/Default/DefaultConsentService.cs b/src/IdentityServer4/src/Services/Default/DefaultConsentService.cs index 0c983e91fe..0753f1f10b 100644 --- a/src/IdentityServer4/src/Services/Default/DefaultConsentService.cs +++ b/src/IdentityServer4/src/Services/Default/DefaultConsentService.cs @@ -87,13 +87,13 @@ public virtual async Task RequiresConsentAsync(ClaimsPrincipal subject, Cl return true; } - if (parsedScopes.Any(x => x.Name != x.Value)) + if (parsedScopes.Any(x => x.ParsedName != x.RawValue)) { Logger.LogDebug("Scopes contains parameterized values, consent is required"); return true; } - var scopes = parsedScopes.Select(x => x.Value).ToArray(); + var scopes = parsedScopes.Select(x => x.RawValue).ToArray(); // we always require consent for offline access if // the client has not disabled RequireConsent @@ -162,7 +162,7 @@ public virtual async Task UpdateConsentAsync(ClaimsPrincipal subject, Client cli var subjectId = subject.GetSubjectId(); var clientId = client.ClientId; - var scopes = parsedScopes?.Select(x => x.Value).ToArray(); + var scopes = parsedScopes?.Select(x => x.RawValue).ToArray(); if (scopes != null && scopes.Any()) { Logger.LogDebug("Client allows remembering consent, and consent given. Updating consent store for subject: {subject}", subject.GetSubjectId()); diff --git a/src/IdentityServer4/src/Services/Default/DefaultDeviceFlowInteractionService.cs b/src/IdentityServer4/src/Services/Default/DefaultDeviceFlowInteractionService.cs index 4c3ed93331..72cbcfdab4 100644 --- a/src/IdentityServer4/src/Services/Default/DefaultDeviceFlowInteractionService.cs +++ b/src/IdentityServer4/src/Services/Default/DefaultDeviceFlowInteractionService.cs @@ -17,7 +17,7 @@ internal class DefaultDeviceFlowInteractionService : IDeviceFlowInteractionServi private readonly IUserSession _session; private readonly IDeviceFlowCodeService _devices; private readonly IResourceStore _resourceStore; - private readonly IResourceValidator _resourceValidator; + private readonly IScopeParser _scopeParser; private readonly ILogger _logger; public DefaultDeviceFlowInteractionService( @@ -25,14 +25,14 @@ public DefaultDeviceFlowInteractionService( IUserSession session, IDeviceFlowCodeService devices, IResourceStore resourceStore, - IResourceValidator resourceValidator, + IScopeParser scopeParser, ILogger logger) { _clients = clients; _session = session; _devices = devices; _resourceStore = resourceStore; - _resourceValidator = resourceValidator; + _scopeParser = scopeParser; _logger = logger; } @@ -44,8 +44,8 @@ public async Task GetAuthorizationContextAsync(s var client = await _clients.FindClientByIdAsync(deviceAuth.ClientId); if (client == null) return null; - var parsedScopes = await _resourceValidator.ParseRequestedScopesAsync(deviceAuth.RequestedScopes); - var validatedResources = await _resourceStore.CreateResourceValidationResult(parsedScopes); + var parsedScopesResult = _scopeParser.ParseScopeValues(deviceAuth.RequestedScopes); + var validatedResources = await _resourceStore.CreateResourceValidationResult(parsedScopesResult); return new DeviceFlowAuthorizationRequest { diff --git a/src/IdentityServer4/src/Validation/Default/AuthorizeRequestValidator.cs b/src/IdentityServer4/src/Validation/Default/AuthorizeRequestValidator.cs index 97c1ce7471..a019a318c8 100644 --- a/src/IdentityServer4/src/Validation/Default/AuthorizeRequestValidator.cs +++ b/src/IdentityServer4/src/Validation/Default/AuthorizeRequestValidator.cs @@ -574,11 +574,10 @@ private async Task ValidateScopeAsync(Validate ////////////////////////////////////////////////////////// // check if scopes are valid/supported and check for resource scopes ////////////////////////////////////////////////////////// - var parasedScopes = await _resourceValidator.ParseRequestedScopesAsync(request.RequestedScopes); var validatedResources = await _resourceValidator.ValidateRequestedResourcesAsync(new ResourceValidationRequest { Client = request.Client, - ParsedScopeValues = parasedScopes + Scopes = request.RequestedScopes }); if (!validatedResources.Succeeded) diff --git a/src/IdentityServer4/src/Validation/Default/ResourceValidator.cs b/src/IdentityServer4/src/Validation/Default/DefaultResourceValidator.cs similarity index 80% rename from src/IdentityServer4/src/Validation/Default/ResourceValidator.cs rename to src/IdentityServer4/src/Validation/Default/DefaultResourceValidator.cs index 4f660461b4..ebceeb70f1 100644 --- a/src/IdentityServer4/src/Validation/Default/ResourceValidator.cs +++ b/src/IdentityServer4/src/Validation/Default/DefaultResourceValidator.cs @@ -6,7 +6,6 @@ using IdentityServer4.Stores; using Microsoft.Extensions.Logging; using System; -using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -15,19 +14,22 @@ namespace IdentityServer4.Validation /// /// Default implementation of IResourceValidator. /// - public class ResourceValidator : IResourceValidator + public class DefaultResourceValidator : IResourceValidator { private readonly ILogger _logger; + private readonly IScopeParser _scopeParser; private readonly IResourceStore _store; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The store. + /// /// The logger. - public ResourceValidator(IResourceStore store, ILogger logger) + public DefaultResourceValidator(IResourceStore store, IScopeParser scopeParser, ILogger logger) { _logger = logger; + _scopeParser = scopeParser; _store = store; } @@ -36,11 +38,25 @@ public virtual async Task ValidateRequestedResourcesAs { if (request == null) throw new ArgumentNullException(nameof(request)); - var scopeNames = request.ParsedScopeValues.Select(x => x.Name).Distinct().ToArray(); - var resourcesFromStore = await _store.FindEnabledResourcesByScopeAsync(scopeNames); + var parsedScopesResult = _scopeParser.ParseScopeValues(request.Scopes); var result = new ResourceValidationResult(); - foreach (var scope in request.ParsedScopeValues) + + if (!parsedScopesResult.Succeeded) + { + foreach (var invalidScope in parsedScopesResult.Errors) + { + _logger.LogError("Invalid parsed scope {scope}, message: {error}", invalidScope.RawValue, invalidScope.Error); + result.InvalidScopes.Add(invalidScope.RawValue); + } + + return result; + } + + var scopeNames = parsedScopesResult.ParsedScopes.Select(x => x.ParsedName).Distinct().ToArray(); + var resourcesFromStore = await _store.FindEnabledResourcesByScopeAsync(scopeNames); + + foreach (var scope in parsedScopesResult.ParsedScopes) { await ValidateScopeAsync(request.Client, resourcesFromStore, scope, result); } @@ -56,34 +72,6 @@ public virtual async Task ValidateRequestedResourcesAs return result; } - /// - /// Parses the requested scopes. - /// - /// - /// - public virtual async Task> ParseRequestedScopesAsync(IEnumerable scopeValues) - { - var list = new List(); - - foreach(var scopeValue in scopeValues) - { - var parsedScopeValue = await ParseScopeValue(scopeValue); - list.Add(parsedScopeValue); - } - - return list; - } - - /// - /// Parses a scope value. - /// - /// - /// - public virtual Task ParseScopeValue(string scopeValue) - { - return Task.FromResult(new ParsedScopeValue(scopeValue)); - } - /// /// Validates that the requested scopes is contained in the store, and the client is allowed to request it. /// @@ -98,7 +86,7 @@ protected virtual async Task ValidateScopeAsync( ParsedScopeValue requestedScope, ResourceValidationResult result) { - if (requestedScope.Name == IdentityServerConstants.StandardScopes.OfflineAccess) + if (requestedScope.ParsedName == IdentityServerConstants.StandardScopes.OfflineAccess) { if (await IsClientAllowedOfflineAccessAsync(client)) { @@ -112,7 +100,7 @@ protected virtual async Task ValidateScopeAsync( } else { - var identity = resourcesFromStore.FindIdentityResourcesByScope(requestedScope.Name); + var identity = resourcesFromStore.FindIdentityResourcesByScope(requestedScope.ParsedName); if (identity != null) { if (await IsClientAllowedIdentityResourceAsync(client, identity)) @@ -122,12 +110,12 @@ protected virtual async Task ValidateScopeAsync( } else { - result.InvalidScopes.Add(requestedScope.Value); + result.InvalidScopes.Add(requestedScope.RawValue); } } else { - var apiScope = resourcesFromStore.FindApiScope(requestedScope.Name); + var apiScope = resourcesFromStore.FindApiScope(requestedScope.ParsedName); if (apiScope != null) { if (await IsClientAllowedApiScopeAsync(client, apiScope)) @@ -143,13 +131,13 @@ protected virtual async Task ValidateScopeAsync( } else { - result.InvalidScopes.Add(requestedScope.Value); + result.InvalidScopes.Add(requestedScope.RawValue); } } else { - _logger.LogError("Scope {scope} not found in store.", requestedScope.Name); - result.InvalidScopes.Add(requestedScope.Value); + _logger.LogError("Scope {scope} not found in store.", requestedScope.ParsedName); + result.InvalidScopes.Add(requestedScope.RawValue); } } } diff --git a/src/IdentityServer4/src/Validation/Default/DefaultScopeParser.cs b/src/IdentityServer4/src/Validation/Default/DefaultScopeParser.cs new file mode 100644 index 0000000000..6d8cef31b3 --- /dev/null +++ b/src/IdentityServer4/src/Validation/Default/DefaultScopeParser.cs @@ -0,0 +1,160 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + + +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; + +namespace IdentityServer4.Validation +{ + /// + /// Default implementation of IScopeParser. + /// + public class DefaultScopeParser : IScopeParser + { + private readonly ILogger _logger; + + /// + /// Ctor. + /// + /// + public DefaultScopeParser(ILogger logger) + { + _logger = logger; + } + + /// + public ParsedScopesResult ParseScopeValues(IEnumerable scopeValues) + { + if (scopeValues == null) throw new ArgumentNullException(nameof(scopeValues)); + + var result = new ParsedScopesResult(); + + foreach (var scopeValue in scopeValues) + { + var ctx = new ParseScopeContext(scopeValue); + ParseScopeValue(ctx); + + if (ctx.Succeeded) + { + var parsedScope = ctx.ParsedName != null ? + new ParsedScopeValue(ctx.RawValue, ctx.ParsedName, ctx.ParsedParameter) : + new ParsedScopeValue(ctx.RawValue); + + result.ParsedScopes.Add(parsedScope); + } + else if (!ctx.Ignore) + { + result.Errors.Add(new ParsedScopeValidationError(scopeValue, ctx.Error)); + } + else + { + _logger.LogDebug("Scope parsing ignoring scope {scope}", scopeValue); + } + } + + return result; + } + + /// + /// Parses a scope value. + /// + /// + /// + public virtual void ParseScopeValue(ParseScopeContext scopeContext) + { + // nop leaves the raw scope value as a success result. + } + + /// + /// Models the context for parsing a scope. + /// + public class ParseScopeContext + { + /// + /// The original (raw) value of the scope. + /// + public string RawValue { get; } + + /// + /// The parsed name of the scope. + /// + public string ParsedName { get; private set; } + + /// + /// The parsed parameter value of the scope. + /// + public string ParsedParameter { get; private set; } + + /// + /// The error encountered parsing the scope. + /// + public string Error { get; private set; } + + /// + /// Indicates if the scope should be excluded from the parsed results. + /// + public bool Ignore { get; private set; } + + /// + /// Indicates if parsing the scope was successful. + /// + public bool Succeeded => !Ignore && Error == null; + + + /// + /// Ctor. Indicates success, but the scope should not be included in result. + /// + internal ParseScopeContext(string rawScopeValue) + { + RawValue = rawScopeValue; + } + + /// + /// Sets the parsed name and parsed parameter value for the scope. + /// + /// + /// + public void SetParsedValues(string parsedName, string parsedParameter) + { + if (String.IsNullOrWhiteSpace(parsedName)) + { + throw new ArgumentNullException(nameof(parsedName)); + } + if (String.IsNullOrWhiteSpace(parsedParameter)) + { + throw new ArgumentNullException(nameof(parsedParameter)); + } + + ParsedName = parsedName; + ParsedParameter = parsedParameter; + Error = null; + Ignore = false; + } + + /// + /// Set the error encountered parsing the scope. + /// + /// + public void SetError(string error) + { + ParsedName = null; + ParsedParameter = null; + Error = error; + Ignore = false; + } + + /// + /// Sets that the scope is to be ignore/excluded from the parsed results. + /// + public void SetIgnore() + { + ParsedName = null; + ParsedParameter = null; + Error = null; + Ignore = true; + } + } + } +} \ No newline at end of file diff --git a/src/IdentityServer4/src/Validation/Default/DeviceAuthorizationRequestValidator.cs b/src/IdentityServer4/src/Validation/Default/DeviceAuthorizationRequestValidator.cs index 469b109bd3..089a156662 100644 --- a/src/IdentityServer4/src/Validation/Default/DeviceAuthorizationRequestValidator.cs +++ b/src/IdentityServer4/src/Validation/Default/DeviceAuthorizationRequestValidator.cs @@ -152,10 +152,9 @@ private async Task ValidateScopeAsyn ////////////////////////////////////////////////////////// // check if scopes are valid/supported ////////////////////////////////////////////////////////// - var parsedScopes = await _resourceValidator.ParseRequestedScopesAsync(request.RequestedScopes); var validatedResources = await _resourceValidator.ValidateRequestedResourcesAsync(new ResourceValidationRequest{ Client = request.Client, - ParsedScopeValues = parsedScopes + Scopes = request.RequestedScopes }); if (!validatedResources.Succeeded) diff --git a/src/IdentityServer4/src/Validation/Default/TokenRequestValidator.cs b/src/IdentityServer4/src/Validation/Default/TokenRequestValidator.cs index 05190fdfb9..ecdb1de98b 100644 --- a/src/IdentityServer4/src/Validation/Default/TokenRequestValidator.cs +++ b/src/IdentityServer4/src/Validation/Default/TokenRequestValidator.cs @@ -711,10 +711,9 @@ private async Task ValidateRequestedScopesAsync(NameValueCollection parame return false; } - var parasedScopes = await _resourceValidator.ParseRequestedScopesAsync(requestedScopes); var resourceValidationResult = await _resourceValidator.ValidateRequestedResourcesAsync(new ResourceValidationRequest { Client = _validatedRequest.Client, - ParsedScopeValues = parasedScopes + Scopes = requestedScopes }); if (!resourceValidationResult.Succeeded) diff --git a/src/IdentityServer4/src/Validation/IResourceValidator.cs b/src/IdentityServer4/src/Validation/IResourceValidator.cs index 39dc7fdff7..9f3442e855 100644 --- a/src/IdentityServer4/src/Validation/IResourceValidator.cs +++ b/src/IdentityServer4/src/Validation/IResourceValidator.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -using System.Collections.Generic; using System.Threading.Tasks; namespace IdentityServer4.Validation @@ -12,11 +11,8 @@ namespace IdentityServer4.Validation /// public interface IResourceValidator { - /// - /// Parses the requested scopes. - /// - Task> ParseRequestedScopesAsync(IEnumerable scopeValues); - + // todo: should this be used anywhere we re-create tokens? do we need to re-run scope validation? + /// /// Validates the requested resources for the client. /// diff --git a/src/IdentityServer4/src/Validation/IScopeParser.cs b/src/IdentityServer4/src/Validation/IScopeParser.cs new file mode 100644 index 0000000000..4061fe9b31 --- /dev/null +++ b/src/IdentityServer4/src/Validation/IScopeParser.cs @@ -0,0 +1,20 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + + +using System.Collections.Generic; + +namespace IdentityServer4.Validation +{ + /// + /// Allows parsing raw scopes values into structured scope values. + /// + public interface IScopeParser + { + // todo: test return no error, and no parsed scopes. how do callers behave? + /// + /// Parses the requested scopes. + /// + ParsedScopesResult ParseScopeValues(IEnumerable scopeValues); + } +} diff --git a/src/IdentityServer4/src/Validation/Models/ParsedScopeValidationError.cs b/src/IdentityServer4/src/Validation/Models/ParsedScopeValidationError.cs new file mode 100644 index 0000000000..8f3b4f9294 --- /dev/null +++ b/src/IdentityServer4/src/Validation/Models/ParsedScopeValidationError.cs @@ -0,0 +1,45 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + + +using System; + +namespace IdentityServer4.Validation +{ + /// + /// Models an error parsing a scope. + /// + public class ParsedScopeValidationError + { + /// + /// Ctor + /// + /// + /// + public ParsedScopeValidationError(string rawValue, string error) + { + if (String.IsNullOrWhiteSpace(rawValue)) + { + throw new ArgumentNullException(nameof(rawValue)); + } + + if (String.IsNullOrWhiteSpace(error)) + { + throw new ArgumentNullException(nameof(error)); + } + + RawValue = rawValue; + Error = error; + } + + /// + /// The original (raw) value of the scope. + /// + public string RawValue { get; set; } + + /// + /// Error message describing why the raw scope failed to be parsed. + /// + public string Error { get; set; } + } +} \ No newline at end of file diff --git a/src/IdentityServer4/src/Validation/Models/ParsedScopeValue.cs b/src/IdentityServer4/src/Validation/Models/ParsedScopeValue.cs index c121e79a68..4e0e48095e 100644 --- a/src/IdentityServer4/src/Validation/Models/ParsedScopeValue.cs +++ b/src/IdentityServer4/src/Validation/Models/ParsedScopeValue.cs @@ -2,6 +2,8 @@ // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. +using System; + namespace IdentityServer4.Validation { /// @@ -12,40 +14,49 @@ public class ParsedScopeValue /// /// Ctor /// - /// - public ParsedScopeValue(string name) - : this(name, name, null) + /// + public ParsedScopeValue(string rawValue) + : this(rawValue, rawValue, null) { } /// /// Ctor /// - /// - /// - /// - public ParsedScopeValue(string name, string value, string paramaterValue) + /// + /// + /// + public ParsedScopeValue(string rawValue, string parsedName, string parsedParameter) { - Name = name; - Value = value; - ParameterValue = paramaterValue; + if (String.IsNullOrWhiteSpace(rawValue)) + { + throw new ArgumentNullException(nameof(rawValue)); + } + + if (String.IsNullOrWhiteSpace(parsedName)) + { + throw new ArgumentNullException(nameof(parsedName)); + } + + RawValue = rawValue; + ParsedName = parsedName; + ParsedParameter = parsedParameter; } /// - /// The name of the scope. + /// The original (raw) value of the scope. /// - public string Name { get; set; } - - // future: maybe this should be something w/ more structure? dictionary? + public string RawValue { get; set; } /// - /// The value of the parsed scope. If the scope has no structure, then the value will be the same as the name. + /// The parsed name of the scope. If the scope has no structure, the parsed name will be the same as the raw value. /// - public string Value { get; set; } - + public string ParsedName { get; set; } + + // future: maybe this should be something w/ more structure? dictionary? /// /// The parameter value of the parsed scope. If the scope has no structure, then the value will be null. /// - public string ParameterValue { get; set; } + public string ParsedParameter { get; set; } } } \ No newline at end of file diff --git a/src/IdentityServer4/src/Validation/Models/ParsedScopesResult.cs b/src/IdentityServer4/src/Validation/Models/ParsedScopesResult.cs new file mode 100644 index 0000000000..69ddc4d4ad --- /dev/null +++ b/src/IdentityServer4/src/Validation/Models/ParsedScopesResult.cs @@ -0,0 +1,30 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + + +using System.Collections.Generic; +using System.Linq; + +namespace IdentityServer4.Validation +{ + /// + /// Represents the result of scope parsing. + /// + public class ParsedScopesResult + { + /// + /// The valid parsed scopes. + /// + public ICollection ParsedScopes { get; set; } = new HashSet(); + + /// + /// The errors encountered while parsing. + /// + public ICollection Errors { get; set; } = new HashSet(); + + /// + /// Indicates if the result of parsing the scopes was successful. + /// + public bool Succeeded => Errors == null || !Errors.Any(); + } +} \ No newline at end of file diff --git a/src/IdentityServer4/src/Validation/Models/ResourceValidationRequest.cs b/src/IdentityServer4/src/Validation/Models/ResourceValidationRequest.cs index 3d5c74842f..6fe3fcd01e 100644 --- a/src/IdentityServer4/src/Validation/Models/ResourceValidationRequest.cs +++ b/src/IdentityServer4/src/Validation/Models/ResourceValidationRequest.cs @@ -20,7 +20,7 @@ public class ResourceValidationRequest /// /// The requested scope values. /// - public IEnumerable ParsedScopeValues { get; set; } + public IEnumerable Scopes { get; set; } // /// // /// The requested resource indicators. diff --git a/src/IdentityServer4/src/Validation/Models/ResourceValidationResult.cs b/src/IdentityServer4/src/Validation/Models/ResourceValidationResult.cs index e9dd21095b..e069bb72e5 100644 --- a/src/IdentityServer4/src/Validation/Models/ResourceValidationResult.cs +++ b/src/IdentityServer4/src/Validation/Models/ResourceValidationResult.cs @@ -57,9 +57,9 @@ public ResourceValidationResult(Resources resources, IEnumerable ParsedScopes { get; set; } = new HashSet(); /// - /// All scope values represented by the result. + /// The original (raw) scope values represented by the validated result. /// - public IEnumerable ScopeValues => ParsedScopes.Select(x => x.Value); + public IEnumerable RawScopeValues => ParsedScopes.Select(x => x.RawValue); /// /// The requested scopes that are invalid. @@ -77,8 +77,8 @@ public ResourceValidationResult Filter(IEnumerable scopeValues) var offline = scopeValues.Contains(IdentityServerConstants.StandardScopes.OfflineAccess); - var parsedScopesToKeep = ParsedScopes.Where(x => scopeValues.Contains(x.Value)).ToArray(); - var parsedScopeNamesToKeep = parsedScopesToKeep.Select(x => x.Name).ToArray(); + var parsedScopesToKeep = ParsedScopes.Where(x => scopeValues.Contains(x.RawValue)).ToArray(); + var parsedScopeNamesToKeep = parsedScopesToKeep.Select(x => x.ParsedName).ToArray(); var identityToKeep = Resources.IdentityResources.Where(x => parsedScopeNamesToKeep.Contains(x.Name)); var apiScopesToKeep = Resources.ApiScopes.Where(x => parsedScopeNamesToKeep.Contains(x.Name)); diff --git a/src/IdentityServer4/test/IdentityServer.IntegrationTests/Endpoints/Authorize/ConsentTests.cs b/src/IdentityServer4/test/IdentityServer.IntegrationTests/Endpoints/Authorize/ConsentTests.cs index 02f57ec2c4..b620b19599 100644 --- a/src/IdentityServer4/test/IdentityServer.IntegrationTests/Endpoints/Authorize/ConsentTests.cs +++ b/src/IdentityServer4/test/IdentityServer.IntegrationTests/Endpoints/Authorize/ConsentTests.cs @@ -159,7 +159,7 @@ public async Task consent_page_should_have_authorization_params(Type storeType) _mockPipeline.ConsentRequest.AcrValues.Should().BeEquivalentTo(new string[] { "acr_2", "acr_1" }); _mockPipeline.ConsentRequest.Parameters.AllKeys.Should().Contain("custom_foo"); _mockPipeline.ConsentRequest.Parameters["custom_foo"].Should().Be("foo_value"); - _mockPipeline.ConsentRequest.ValidatedResources.ScopeValues.Should().BeEquivalentTo(new string[] { "api2", "openid", "api1" }); + _mockPipeline.ConsentRequest.ValidatedResources.RawScopeValues.Should().BeEquivalentTo(new string[] { "api2", "openid", "api1" }); } [Theory] diff --git a/src/IdentityServer4/test/IdentityServer.UnitTests/Common/MockConsentService.cs b/src/IdentityServer4/test/IdentityServer.UnitTests/Common/MockConsentService.cs index 74c19d5fd8..e4ac504ccf 100644 --- a/src/IdentityServer4/test/IdentityServer.UnitTests/Common/MockConsentService.cs +++ b/src/IdentityServer4/test/IdentityServer.UnitTests/Common/MockConsentService.cs @@ -29,7 +29,7 @@ public Task UpdateConsentAsync(ClaimsPrincipal subject, Client client, IEnumerab { ConsentSubject = subject; ConsentClient = client; - ConsentScopes = parsedScopes?.Select(x => x.Value); + ConsentScopes = parsedScopes?.Select(x => x.RawValue); return Task.CompletedTask; } diff --git a/src/IdentityServer4/test/IdentityServer.UnitTests/Services/Default/DefaultClaimsServiceTests.cs b/src/IdentityServer4/test/IdentityServer.UnitTests/Services/Default/DefaultClaimsServiceTests.cs index 15c8904fcd..e5c1432a61 100644 --- a/src/IdentityServer4/test/IdentityServer.UnitTests/Services/Default/DefaultClaimsServiceTests.cs +++ b/src/IdentityServer4/test/IdentityServer.UnitTests/Services/Default/DefaultClaimsServiceTests.cs @@ -186,7 +186,7 @@ public async Task GetAccessTokenClaimsAsync_should_contain_parameterized_scope_v var resourceResult = new ResourceValidationResult() { Resources = _resources, - ParsedScopes = { new ParsedScopeValue("api", "api:123", "123") } + ParsedScopes = { new ParsedScopeValue("api:123", "api", "123") } }; var claims = await _subject.GetAccessTokenClaimsAsync(_user, resourceResult, _validatedRequest); diff --git a/src/IdentityServer4/test/IdentityServer.UnitTests/Validation/ResourceValidation.cs b/src/IdentityServer4/test/IdentityServer.UnitTests/Validation/ResourceValidation.cs index 9e3bda9792..b829d22380 100644 --- a/src/IdentityServer4/test/IdentityServer.UnitTests/Validation/ResourceValidation.cs +++ b/src/IdentityServer4/test/IdentityServer.UnitTests/Validation/ResourceValidation.cs @@ -78,11 +78,6 @@ public ResourceValidation() _store = new InMemoryResourcesStore(_identityResources, _apiResources, _scopes); } - IEnumerable ParseScopes(IEnumerable scopes) - { - return scopes.Select(x => new ParsedScopeValue(x)); - } - [Fact] [Trait("Category", Category)] public void Parse_Scopes_with_Empty_Scope_List() @@ -141,7 +136,7 @@ public async Task Only_Offline_Access_Requested() var result = await validator.ValidateRequestedResourcesAsync(new IdentityServer4.Validation.ResourceValidationRequest { Client = _restrictedClient, - ParsedScopeValues = ParseScopes(scopes) + Scopes = scopes }); result.Succeeded.Should().BeFalse(); @@ -158,7 +153,7 @@ public async Task All_Scopes_Valid() var result = await validator.ValidateRequestedResourcesAsync(new IdentityServer4.Validation.ResourceValidationRequest { Client = _restrictedClient, - ParsedScopeValues = ParseScopes(scopes) + Scopes = scopes }); result.Succeeded.Should().BeTrue(); @@ -176,7 +171,7 @@ public async Task Invalid_Scope() var result = await validator.ValidateRequestedResourcesAsync(new IdentityServer4.Validation.ResourceValidationRequest { Client = _restrictedClient, - ParsedScopeValues = ParseScopes(scopes) + Scopes = scopes }); result.Succeeded.Should().BeFalse(); @@ -190,7 +185,7 @@ public async Task Invalid_Scope() var result = await validator.ValidateRequestedResourcesAsync(new IdentityServer4.Validation.ResourceValidationRequest { Client = _restrictedClient, - ParsedScopeValues = ParseScopes(scopes) + Scopes = scopes }); result.Succeeded.Should().BeFalse(); @@ -203,7 +198,7 @@ public async Task Invalid_Scope() var result = await validator.ValidateRequestedResourcesAsync(new IdentityServer4.Validation.ResourceValidationRequest { Client = _restrictedClient, - ParsedScopeValues = ParseScopes(scopes) + Scopes = scopes }); result.Succeeded.Should().BeFalse(); @@ -221,7 +216,7 @@ public async Task Disabled_Scope() var result = await validator.ValidateRequestedResourcesAsync(new IdentityServer4.Validation.ResourceValidationRequest { Client = _restrictedClient, - ParsedScopeValues = ParseScopes(scopes) + Scopes = scopes }); result.Succeeded.Should().BeFalse(); @@ -238,7 +233,7 @@ public async Task All_Scopes_Allowed_For_Restricted_Client() var result = await validator.ValidateRequestedResourcesAsync(new IdentityServer4.Validation.ResourceValidationRequest { Client = _restrictedClient, - ParsedScopeValues = ParseScopes(scopes) + Scopes = scopes }); result.Succeeded.Should().BeTrue(); @@ -255,7 +250,7 @@ public async Task Restricted_Scopes() var result = await validator.ValidateRequestedResourcesAsync(new IdentityServer4.Validation.ResourceValidationRequest { Client = _restrictedClient, - ParsedScopeValues = ParseScopes(scopes) + Scopes = scopes }); result.Succeeded.Should().BeFalse(); @@ -273,7 +268,7 @@ public async Task Contains_Resource_and_Identity_Scopes() var result = await validator.ValidateRequestedResourcesAsync(new IdentityServer4.Validation.ResourceValidationRequest { Client = _restrictedClient, - ParsedScopeValues = ParseScopes(scopes) + Scopes = scopes }); result.Succeeded.Should().BeTrue(); @@ -291,7 +286,7 @@ public async Task Contains_Resource_Scopes_Only() var result = await validator.ValidateRequestedResourcesAsync(new IdentityServer4.Validation.ResourceValidationRequest { Client = _restrictedClient, - ParsedScopeValues = ParseScopes(scopes) + Scopes = scopes }); result.Succeeded.Should().BeTrue(); @@ -309,7 +304,7 @@ public async Task Contains_Identity_Scopes_Only() var result = await validator.ValidateRequestedResourcesAsync(new IdentityServer4.Validation.ResourceValidationRequest { Client = _restrictedClient, - ParsedScopeValues = ParseScopes(scopes) + Scopes = scopes }); result.Succeeded.Should().BeTrue(); @@ -331,14 +326,14 @@ public async Task Scope_matches_multipls_apis_should_succeed() var result = await validator.ValidateRequestedResourcesAsync(new IdentityServer4.Validation.ResourceValidationRequest { Client = new Client { AllowedScopes = { "resource" } }, - ParsedScopeValues = ParseScopes(new[] { "resource" }) + Scopes = new[] { "resource" } }); result.Succeeded.Should().BeTrue(); result.Resources.ApiResources.Count.Should().Be(2); result.Resources.ApiResources.Select(x => x.Name).Should().BeEquivalentTo(new[] { "api1", "api2" }); - result.ScopeValues.Count().Should().Be(1); - result.ScopeValues.Should().BeEquivalentTo(new[] { "resource" }); + result.RawScopeValues.Count().Should().Be(1); + result.RawScopeValues.Should().BeEquivalentTo(new[] { "resource" }); } } } \ No newline at end of file diff --git a/src/IdentityServer4/test/IdentityServer.UnitTests/Validation/Setup/Factory.cs b/src/IdentityServer4/test/IdentityServer.UnitTests/Validation/Setup/Factory.cs index d9ae5a665f..2723eb2da0 100644 --- a/src/IdentityServer4/test/IdentityServer.UnitTests/Validation/Setup/Factory.cs +++ b/src/IdentityServer4/test/IdentityServer.UnitTests/Validation/Setup/Factory.cs @@ -138,7 +138,7 @@ private static IRefreshTokenService CreateRefreshTokenService(IRefreshTokenStore internal static IResourceValidator CreateResourceValidator(IResourceStore store = null) { store = store ?? new InMemoryResourcesStore(TestScopes.GetIdentity(), TestScopes.GetApis(), TestScopes.GetScopes()); - return new ResourceValidator(store, TestLogger.Create()); + return new DefaultResourceValidator(store, new DefaultScopeParser(TestLogger.Create()), TestLogger.Create()); } internal static ITokenCreationService CreateDefaultTokenCreator(IdentityServerOptions options = null)