IdentityServer4를 활용한 OIDC 기반 내부 MVC 클라이언트 인증 - Consent 구현

개요 이 예제에서는 이전 예제에서 다룬 로그인 프로세스에 추가적으로 Consent 확인 단계를 포함시킵니다. Consent 페이지는 외부 클라이언트가 우리 권한 중앙 서버의 승인을 필요로 하는 상황에 적합합니다. 본 예제에서는 여전히 암묵적(Implicit) 인증 모드를 사용하며, 보호된 MVC 클라이언트 페이지에 접근할 때 로그인 페이지로 리다이렉션되고 이후 Consent 페이지로 다시 전환됩니다.

AuthServer 측 Consent 컨트롤러 및 뷰 추가 2.1 Consent 컨트롤러 및 뷰 구성 아래는 Consent 뷰의 핵심 코드입니다.

@model ConsentViewModel

<div class="page-consent">
    <div class="lead">
        @if (Model.ClientLogoUrl != null)
        {
            <div class="client-logo">![](@Model.ClientLogoUrl)</div>
        }
        <h1>@Model.ClientName<small class="text-muted">의 요청 승인</small></h1>
        <p>허용하지 않는 권한은 선택 해제하세요.</p>
    </div>

    <form method="post" asp-action="Index">
        <input type="hidden" asp-for="ReturnUrl" />
        <div class="row">
            <div class="col-sm-8">
                @if (Model.IdentityScopes.Any())
                {
                    <div class="card">
                        <div class="card-header">개인 정보</div>
                         @foreach (var scope in Model.IdentityScopes) { <partial model="scope" name="_ScopeListItem"></partial> } 
                    </div>
                }

                @if (Model.ApiScopes.Any())
                {
                    <div class="card">
                        <div class="card-header">애플리케이션 접근 권한</div>
                         @foreach (var scope in Model.ApiScopes) { <partial model="scope" name="_ScopeListItem"></partial> } 
                    </div>
                }

                <div class="card">
                    <div class="card-header">설명</div>
                    <div class="card-body">
                        <input class="form-control" placeholder="장치 설명 또는 이름" asp-for="Description" autofocus />
                    </div>
                </div>

                @if (Model.AllowRememberConsent)
                {
                    <div class="form-check">
                        <input class="form-check-input" asp-for="RememberConsent" />
                        <label class="form-check-label" asp-for="RememberConsent">결정 기억하기</label>
                    </div>
                }
            </div>
        </div>

        <div class="row">
            <div class="col-sm-4">
                <button type="submit" name="button" value="yes" class="btn btn-primary">승인</button>
                <button type="submit" name="button" value="no" class="btn btn-secondary">거절</button>
            </div>
        </div>
    </form>
</div>

컨트롤러 POST 처리 로직 아래는 Consent 입력 데이터를 처리하는 컨트롤러 메서드입니다.

private async Task<ProcessConsentResult> HandleConsent(ConsentInputModel input)
{
    var result = new ProcessConsentResult();
    var request = await _interaction.GetAuthorizationContextAsync(input.ReturnUrl);

    if (request == null) return result;

    ConsentResponse grantedConsent = null;

    if (input.Button == "no")
    {
        grantedConsent = new ConsentResponse { Error = AuthorizationError.AccessDenied };
        await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues));
    }
    else if (input.Button == "yes")
    {
        if (input.ScopesConsented?.Any() == true)
        {
            var scopes = input.ScopesConsented;

            if (!ConsentOptions.EnableOfflineAccess)
            {
                scopes = scopes.Where(x => x != IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess);
            }

            grantedConsent = new ConsentResponse
            {
                RememberConsent = input.RememberConsent,
                ScopesValuesConsented = scopes.ToArray(),
                Description = input.Description
            };

            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)
    {
        await _interaction.GrantConsentAsync(request, grantedConsent);
        result.RedirectUri = input.ReturnUrl;
        result.Client = request.Client;
    }
    else
    {
        result.ViewModel = await BuildViewModelAsync(input.ReturnUrl, input);
    }

    return result;
}

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Index(ConsentInputModel input)
{
    var result = await HandleConsent(input);

    if (result.IsRedirect)
    {
        return Redirect(result.RedirectUri);
    }

    if (result.HasValidationError)
    {
        ModelState.AddModelError(string.Empty, result.ValidationError);
    }

    if (result.ShowView)
    {
        return View("Index", result.ViewModel);
    }

    return View("Error");
}

테스트

  • localhost:5010에 접속하면 로그인 페이지로 리다이렉션됩니다.
  • localhost:5030에서 성공적으로 로그인한 후, 정상적인 권한 부여가 이루어져 홈 페이지로 리다이렉션됩니다.

질문

  • Consent 동의 이후 내부 처리 절차는 무엇인가?
  • Consent 거절 시 클라이언트 측에서 어떻게 처리되는가?

태그: IdentityServer4 oidc MVC

7월 14일 06:56에 게시됨