5. 使用IdentityServer4 实现 基于OIDC 的内部MVC客户端认证 - 实现Consent
1. 概述
-
本例在上一个示例的基础上,为客户端登录过程增加Consent 确认过程.
-
Consent 页面适合外部客户端登录时需要我们授权中心授权的场景。
-
本示例继续使用上一个示例中隐式授权码(Implicit)模式,当用户访问受保护的客户端页面时,MVC客户端会重定向到登录页,输入用户名密码后再重定向到Conset 页面。
2. AuthServer 端增加Consent控制器和视图
2.1 Consent 控制器和视图参考 QuickUI
View 核心代码:
@model ConsentViewModel
<div class="page-consent">
<div class="lead">
<!-- client logo-->
@if (Model.ClientLogoUrl != null)
{
<div class="client-logo"><img src="@Model.ClientLogoUrl"></div>
}
<h1>
@Model.ClientName
<small class="text-muted">is requesting your permission</small>
</h1>
<p>Uncheck the permissions you do not wish to grant.</p>
</div>
<div class="row">
<div class="col-sm-8">
</div>
</div>
<form asp-action="Index">
<input type="hidden" asp-for="ReturnUrl" />
<div class="row">
<div class="col-sm-8">
<!-- client scopes-->
@if (Model.IdentityScopes.Any())
{
<div class="form-group">
<div class="card">
<div class="card-header">
<span class="glyphicon glyphicon-user"></span>
Personal Information
</div>
<ul class="list-group list-group-flush">
@foreach (var scope in Model.IdentityScopes)
{
<partial name="_ScopeListItem" model="@scope" />
}
</ul>
</div>
</div>
}
@if (Model.ApiScopes.Any())
{
<div class="form-group">
<div class="card">
<div class="card-header">
<span class="glyphicon glyphicon-tasks"></span>
Application Access
</div>
<ul class="list-group list-group-flush">
@foreach (var scope in Model.ApiScopes)
{
<partial name="_ScopeListItem" model="scope" />
}
</ul>
</div>
</div>
}
<div class="form-group">
<div class="card">
<div class="card-header">
<span class="glyphicon glyphicon-tasks"></span>
Description
</div>
<div class="card-body">
<input class="form-control" placeholder="Description or name of device" asp-for="Description" autofocus>
</div>
</div>
</div>
@if (Model.AllowRememberConsent)
{
<div class="form-group">
<div class="form-check">
<input class="form-check-input" asp-for="RememberConsent">
<label class="form-check-label" asp-for="RememberConsent">
<strong>Remember My Decision</strong>
</label>
</div>
</div>
}
</div>
</div>
<div class="row">
<div class="col-sm-4">
<button name="button" value="yes" class="btn btn-primary" autofocus>Yes, Allow</button>
<button name="button" value="no" class="btn btn-secondary">No, Do Not Allow</button>
</div>
<div class="col-sm-4 col-lg-auto">
@if (Model.ClientUrl != null)
{
<a class="btn btn-outline-info" href="@Model.ClientUrl">
<span class="glyphicon glyphicon-info-sign"></span>
<strong>@Model.ClientName</strong>
</a>
}
</div>
</div>
</form>
</div>
Conroller Post 逻辑
private async Task<ProcessConsentResult> ProcessConsent(ConsentInputModel model)
{
var result = new ProcessConsentResult();
// 使用 IIdentityServerInteractionService 从returnUrl中得到请求上下文
var request = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl);
if (request == null) return result;
ConsentResponse grantedConsent = null;
// 当用户选择拒绝授权时 页面需要重定向到mvc客户端
if (model?.Button == "no")
{
grantedConsent = new ConsentResponse { Error = AuthorizationError.AccessDenied };
// 使用ids4 eventservice 发布拒绝事件
await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues));
}
// 当用户选择允许授权
else if (model?.Button == "yes")
{
// 授权客户端读取用户勾选的scope,即resourceowner 授予 第三方服务 访问 resource 的权限
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(),
Description = model.Description
};
// emit event
await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues, grantedConsent.ScopesValuesConsented, grantedConsent.RememberConsent));
}
else
{
result.ValidationError = ConsentOptions.MustChooseOneErrorMessage;
}
}
else
{
result.ValidationError = ConsentOptions.InvalidSelectionErrorMessage;
}
if (grantedConsent != null)
{
// 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;
result.Client = request.Client;
}
else
{
// we need to redisplay the consent UI
result.ViewModel = await BuildViewModelAsync(model.ReturnUrl, model);
}
return result;
}
// consent 确认表单提交
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Index(ConsentInputModel model)
{
var result = await ProcessConsent(model);
if (result.IsRedirect)
{
var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl);
return Redirect(result.RedirectUri);
}
if (result.HasValidationError)
{
ModelState.AddModelError(string.Empty, result.ValidationError);
}
if (result.ShowView)
{
return View("Index", result.ViewModel);
}
return View("Error");
}
3 测试
-
打开localhost:5010 重定向到登录页,然后从localhost:5030 登录成功
-
授权成功 重定向到 localhost:5030 首页
问题
-
consent 同意之后的内部处理流程?
-
consent 拒绝之后客户端处理?