1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
|
---|
2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
|
---|
3 |
|
---|
4 |
|
---|
5 | using IdentityServer4.Events;
|
---|
6 | using IdentityServer4.Models;
|
---|
7 | using IdentityServer4.Services;
|
---|
8 | using IdentityServer4.Extensions;
|
---|
9 | using Microsoft.AspNetCore.Authorization;
|
---|
10 | using Microsoft.AspNetCore.Mvc;
|
---|
11 | using Microsoft.Extensions.Logging;
|
---|
12 | using System.Linq;
|
---|
13 | using System.Threading.Tasks;
|
---|
14 | using IdentityServer4.Validation;
|
---|
15 | using System.Collections.Generic;
|
---|
16 | using System;
|
---|
17 |
|
---|
18 | namespace IdentityServerHost.Quickstart.UI
|
---|
19 | {
|
---|
20 | /// <summary>
|
---|
21 | /// This controller processes the consent UI
|
---|
22 | /// </summary>
|
---|
23 | [SecurityHeaders]
|
---|
24 | [Authorize]
|
---|
25 | public class ConsentController : Controller
|
---|
26 | {
|
---|
27 | private readonly IIdentityServerInteractionService _interaction;
|
---|
28 | private readonly IEventService _events;
|
---|
29 | private readonly ILogger<ConsentController> _logger;
|
---|
30 |
|
---|
31 | public ConsentController(
|
---|
32 | IIdentityServerInteractionService interaction,
|
---|
33 | IEventService events,
|
---|
34 | ILogger<ConsentController> logger)
|
---|
35 | {
|
---|
36 | _interaction = interaction;
|
---|
37 | _events = events;
|
---|
38 | _logger = logger;
|
---|
39 | }
|
---|
40 |
|
---|
41 | /// <summary>
|
---|
42 | /// Shows the consent screen
|
---|
43 | /// </summary>
|
---|
44 | /// <param name="returnUrl"></param>
|
---|
45 | /// <returns></returns>
|
---|
46 | [HttpGet]
|
---|
47 | public async Task<IActionResult> Index(string returnUrl)
|
---|
48 | {
|
---|
49 | var vm = await BuildViewModelAsync(returnUrl);
|
---|
50 | if (vm != null)
|
---|
51 | {
|
---|
52 | return View("Index", vm);
|
---|
53 | }
|
---|
54 |
|
---|
55 | return View("Error");
|
---|
56 | }
|
---|
57 |
|
---|
58 | /// <summary>
|
---|
59 | /// Handles the consent screen postback
|
---|
60 | /// </summary>
|
---|
61 | [HttpPost]
|
---|
62 | [ValidateAntiForgeryToken]
|
---|
63 | public async Task<IActionResult> Index(ConsentInputModel model)
|
---|
64 | {
|
---|
65 | var result = await ProcessConsent(model);
|
---|
66 |
|
---|
67 | if (result.IsRedirect)
|
---|
68 | {
|
---|
69 | var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl);
|
---|
70 | if (context?.IsNativeClient() == true)
|
---|
71 | {
|
---|
72 | // The client is native, so this change in how to
|
---|
73 | // return the response is for better UX for the end user.
|
---|
74 | return this.LoadingPage("Redirect", result.RedirectUri);
|
---|
75 | }
|
---|
76 |
|
---|
77 | return Redirect(result.RedirectUri);
|
---|
78 | }
|
---|
79 |
|
---|
80 | if (result.HasValidationError)
|
---|
81 | {
|
---|
82 | ModelState.AddModelError(string.Empty, result.ValidationError);
|
---|
83 | }
|
---|
84 |
|
---|
85 | if (result.ShowView)
|
---|
86 | {
|
---|
87 | return View("Index", result.ViewModel);
|
---|
88 | }
|
---|
89 |
|
---|
90 | return View("Error");
|
---|
91 | }
|
---|
92 |
|
---|
93 | /*****************************************/
|
---|
94 | /* helper APIs for the ConsentController */
|
---|
95 | /*****************************************/
|
---|
96 | private async Task<ProcessConsentResult> ProcessConsent(ConsentInputModel model)
|
---|
97 | {
|
---|
98 | var result = new ProcessConsentResult();
|
---|
99 |
|
---|
100 | // validate return url is still valid
|
---|
101 | var request = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl);
|
---|
102 | if (request == null) return result;
|
---|
103 |
|
---|
104 | ConsentResponse grantedConsent = null;
|
---|
105 |
|
---|
106 | // user clicked 'no' - send back the standard 'access_denied' response
|
---|
107 | if (model?.Button == "no")
|
---|
108 | {
|
---|
109 | grantedConsent = new ConsentResponse { Error = AuthorizationError.AccessDenied };
|
---|
110 |
|
---|
111 | // emit event
|
---|
112 | await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues));
|
---|
113 | }
|
---|
114 | // user clicked 'yes' - validate the data
|
---|
115 | else if (model?.Button == "yes")
|
---|
116 | {
|
---|
117 | // if the user consented to some scope, build the response model
|
---|
118 | if (model.ScopesConsented != null && model.ScopesConsented.Any())
|
---|
119 | {
|
---|
120 | var scopes = model.ScopesConsented;
|
---|
121 | if (ConsentOptions.EnableOfflineAccess == false)
|
---|
122 | {
|
---|
123 | scopes = scopes.Where(x => x != IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess);
|
---|
124 | }
|
---|
125 |
|
---|
126 | grantedConsent = new ConsentResponse
|
---|
127 | {
|
---|
128 | RememberConsent = model.RememberConsent,
|
---|
129 | ScopesValuesConsented = scopes.ToArray(),
|
---|
130 | Description = model.Description
|
---|
131 | };
|
---|
132 |
|
---|
133 | // emit event
|
---|
134 | await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues, grantedConsent.ScopesValuesConsented, grantedConsent.RememberConsent));
|
---|
135 | }
|
---|
136 | else
|
---|
137 | {
|
---|
138 | result.ValidationError = ConsentOptions.MustChooseOneErrorMessage;
|
---|
139 | }
|
---|
140 | }
|
---|
141 | else
|
---|
142 | {
|
---|
143 | result.ValidationError = ConsentOptions.InvalidSelectionErrorMessage;
|
---|
144 | }
|
---|
145 |
|
---|
146 | if (grantedConsent != null)
|
---|
147 | {
|
---|
148 | // communicate outcome of consent back to identityserver
|
---|
149 | await _interaction.GrantConsentAsync(request, grantedConsent);
|
---|
150 |
|
---|
151 | // indicate that's it ok to redirect back to authorization endpoint
|
---|
152 | result.RedirectUri = model.ReturnUrl;
|
---|
153 | result.Client = request.Client;
|
---|
154 | }
|
---|
155 | else
|
---|
156 | {
|
---|
157 | // we need to redisplay the consent UI
|
---|
158 | result.ViewModel = await BuildViewModelAsync(model.ReturnUrl, model);
|
---|
159 | }
|
---|
160 |
|
---|
161 | return result;
|
---|
162 | }
|
---|
163 |
|
---|
164 | private async Task<ConsentViewModel> BuildViewModelAsync(string returnUrl, ConsentInputModel model = null)
|
---|
165 | {
|
---|
166 | var request = await _interaction.GetAuthorizationContextAsync(returnUrl);
|
---|
167 | if (request != null)
|
---|
168 | {
|
---|
169 | return CreateConsentViewModel(model, returnUrl, request);
|
---|
170 | }
|
---|
171 | else
|
---|
172 | {
|
---|
173 | _logger.LogError("No consent request matching request: {0}", returnUrl);
|
---|
174 | }
|
---|
175 |
|
---|
176 | return null;
|
---|
177 | }
|
---|
178 |
|
---|
179 | private ConsentViewModel CreateConsentViewModel(
|
---|
180 | ConsentInputModel model, string returnUrl,
|
---|
181 | AuthorizationRequest request)
|
---|
182 | {
|
---|
183 | var vm = new ConsentViewModel
|
---|
184 | {
|
---|
185 | RememberConsent = model?.RememberConsent ?? true,
|
---|
186 | ScopesConsented = model?.ScopesConsented ?? Enumerable.Empty<string>(),
|
---|
187 | Description = model?.Description,
|
---|
188 |
|
---|
189 | ReturnUrl = returnUrl,
|
---|
190 |
|
---|
191 | ClientName = request.Client.ClientName ?? request.Client.ClientId,
|
---|
192 | ClientUrl = request.Client.ClientUri,
|
---|
193 | ClientLogoUrl = request.Client.LogoUri,
|
---|
194 | AllowRememberConsent = request.Client.AllowRememberConsent
|
---|
195 | };
|
---|
196 |
|
---|
197 | vm.IdentityScopes = request.ValidatedResources.Resources.IdentityResources.Select(x => CreateScopeViewModel(x, vm.ScopesConsented.Contains(x.Name) || model == null)).ToArray();
|
---|
198 |
|
---|
199 | var apiScopes = new List<ScopeViewModel>();
|
---|
200 | foreach(var parsedScope in request.ValidatedResources.ParsedScopes)
|
---|
201 | {
|
---|
202 | var apiScope = request.ValidatedResources.Resources.FindApiScope(parsedScope.ParsedName);
|
---|
203 | if (apiScope != null)
|
---|
204 | {
|
---|
205 | var scopeVm = CreateScopeViewModel(parsedScope, apiScope, vm.ScopesConsented.Contains(parsedScope.RawValue) || model == null);
|
---|
206 | apiScopes.Add(scopeVm);
|
---|
207 | }
|
---|
208 | }
|
---|
209 | if (ConsentOptions.EnableOfflineAccess && request.ValidatedResources.Resources.OfflineAccess)
|
---|
210 | {
|
---|
211 | apiScopes.Add(GetOfflineAccessScope(vm.ScopesConsented.Contains(IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess) || model == null));
|
---|
212 | }
|
---|
213 | vm.ApiScopes = apiScopes;
|
---|
214 |
|
---|
215 | return vm;
|
---|
216 | }
|
---|
217 |
|
---|
218 | private ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool check)
|
---|
219 | {
|
---|
220 | return new ScopeViewModel
|
---|
221 | {
|
---|
222 | Value = identity.Name,
|
---|
223 | DisplayName = identity.DisplayName ?? identity.Name,
|
---|
224 | Description = identity.Description,
|
---|
225 | Emphasize = identity.Emphasize,
|
---|
226 | Required = identity.Required,
|
---|
227 | Checked = check || identity.Required
|
---|
228 | };
|
---|
229 | }
|
---|
230 |
|
---|
231 | public ScopeViewModel CreateScopeViewModel(ParsedScopeValue parsedScopeValue, ApiScope apiScope, bool check)
|
---|
232 | {
|
---|
233 | var displayName = apiScope.DisplayName ?? apiScope.Name;
|
---|
234 | if (!String.IsNullOrWhiteSpace(parsedScopeValue.ParsedParameter))
|
---|
235 | {
|
---|
236 | displayName += ":" + parsedScopeValue.ParsedParameter;
|
---|
237 | }
|
---|
238 |
|
---|
239 | return new ScopeViewModel
|
---|
240 | {
|
---|
241 | Value = parsedScopeValue.RawValue,
|
---|
242 | DisplayName = displayName,
|
---|
243 | Description = apiScope.Description,
|
---|
244 | Emphasize = apiScope.Emphasize,
|
---|
245 | Required = apiScope.Required,
|
---|
246 | Checked = check || apiScope.Required
|
---|
247 | };
|
---|
248 | }
|
---|
249 |
|
---|
250 | private ScopeViewModel GetOfflineAccessScope(bool check)
|
---|
251 | {
|
---|
252 | return new ScopeViewModel
|
---|
253 | {
|
---|
254 | Value = IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess,
|
---|
255 | DisplayName = ConsentOptions.OfflineAccessDisplayName,
|
---|
256 | Description = ConsentOptions.OfflineAccessDescription,
|
---|
257 | Emphasize = true,
|
---|
258 | Checked = check
|
---|
259 | };
|
---|
260 | }
|
---|
261 | }
|
---|
262 | } |
---|