using IdentityServer4.Models; using IdentityServer4.Services; using IdentityServer4.Stores; using IdentityServer4.Validation; using Microsoft.Extensions.Logging; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Teknik.IdentityServer.Models; using Teknik.IdentityServer.Options; using Teknik.IdentityServer.ViewModels; namespace Teknik.IdentityServer.Services { public class ConsentService { private readonly IClientStore _clientStore; private readonly IResourceStore _resourceStore; private readonly IIdentityServerInteractionService _interaction; private readonly ILogger _logger; public ConsentService( IIdentityServerInteractionService interaction, IClientStore clientStore, IResourceStore resourceStore, ILogger logger) { _interaction = interaction; _clientStore = clientStore; _resourceStore = resourceStore; _logger = logger; } public async Task ProcessConsent(ConsentInputModel model) { var result = new ProcessConsentResult(); ConsentResponse grantedConsent = null; // user clicked 'no' - send back the standard 'access_denied' response if (model.Button == "no") { grantedConsent = new ConsentResponse() { Error = AuthorizationError.AccessDenied }; } // user clicked 'yes' - validate the data else if (model.Button == "yes" && model != null) { // if the user consented to some scope, build the response model if (model.ScopesConsented != null && model.ScopesConsented.Any()) { var scopes = model.ScopesConsented; if (ConsentOptions.EnableOfflineAccess == false) { scopes = scopes.Where(x => x != IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess); } grantedConsent = new ConsentResponse { RememberConsent = model.RememberConsent, ScopesValuesConsented = scopes.ToArray() }; } else { result.ValidationError = ConsentOptions.MustChooseOneErrorMessage; } } else { result.ValidationError = ConsentOptions.InvalidSelectionErrorMessage; } if (grantedConsent != null) { // validate return url is still valid var request = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl); if (request == null) return result; // communicate outcome of consent back to identityserver await _interaction.GrantConsentAsync(request, grantedConsent); // indicate that's it ok to redirect back to authorization endpoint result.RedirectUri = model.ReturnUrl; } else { // we need to redisplay the consent UI result.ViewModel = await BuildViewModelAsync(model.ReturnUrl, model); } return result; } public async Task BuildViewModelAsync(string returnUrl, ConsentInputModel model = null) { var request = await _interaction.GetAuthorizationContextAsync(returnUrl); if (request != null) { var client = await _clientStore.FindEnabledClientByIdAsync(request.Client.ClientId); if (client != null) { var resources = await _resourceStore.FindEnabledResourcesByScopeAsync(request.ValidatedResources.RawScopeValues); if (resources != null && (resources.IdentityResources.Any() || resources.ApiResources.Any())) { return CreateConsentViewModel(model, returnUrl, request, client, resources); } else { _logger.LogError("No scopes matching: {0}", request.ValidatedResources.RawScopeValues.Aggregate((x, y) => x + ", " + y)); } } else { _logger.LogError("Invalid client id: {0}", request.Client.ClientId); } } else { _logger.LogError("No consent request matching request: {0}", returnUrl); } return null; } private ConsentViewModel CreateConsentViewModel( ConsentInputModel model, string returnUrl, AuthorizationRequest request, Client client, Resources resources) { var vm = new ConsentViewModel(); vm.RememberConsent = model?.RememberConsent ?? true; vm.ScopesConsented = model?.ScopesConsented ?? Enumerable.Empty(); vm.ReturnUrl = returnUrl; vm.ClientName = client.ClientName ?? client.ClientId; vm.ClientUrl = client.ClientUri; vm.ClientLogoUrl = client.LogoUri; vm.AllowRememberConsent = client.AllowRememberConsent; vm.IdentityScopes = resources.IdentityResources.Select(x => CreateScopeViewModel(x, vm.ScopesConsented.Contains(x.Name) || model == null)).ToArray(); var apiScopes = new List(); foreach (var parsedScope in request.ValidatedResources.ParsedScopes) { var apiScope = request.ValidatedResources.Resources.FindApiScope(parsedScope.ParsedName); if (apiScope != null) { var scopeVm = CreateScopeViewModel(parsedScope, apiScope, vm.ScopesConsented.Contains(parsedScope.RawValue) || model == null); apiScopes.Add(scopeVm); } } if (ConsentOptions.EnableOfflineAccess && request.ValidatedResources.Resources.OfflineAccess) { apiScopes.Add(GetOfflineAccessScope(vm.ScopesConsented.Contains(IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess) || model == null)); } vm.ResourceScopes = apiScopes; return vm; } public ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool check) { return new ScopeViewModel { Name = identity.Name, DisplayName = identity.DisplayName, Description = identity.Description, Emphasize = identity.Emphasize, Required = identity.Required, Checked = check || identity.Required }; } public ScopeViewModel CreateScopeViewModel(ParsedScopeValue parsedScopeValue, ApiScope apiScope, bool check) { var displayName = apiScope.DisplayName ?? apiScope.Name; if (!string.IsNullOrWhiteSpace(parsedScopeValue.ParsedParameter)) { displayName += ":" + parsedScopeValue.ParsedParameter; } return new ScopeViewModel { Name = parsedScopeValue.RawValue, DisplayName = displayName, Description = apiScope.Description, Emphasize = apiScope.Emphasize, Required = apiScope.Required, Checked = check || apiScope.Required }; } private ScopeViewModel GetOfflineAccessScope(bool check) { return new ScopeViewModel { Name = IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess, DisplayName = ConsentOptions.OfflineAccessDisplayName, Description = ConsentOptions.OfflineAccessDescription, Emphasize = true, Checked = check }; } } }