첨부 실행 코드는 나눔고딕코딩 폰트를 사용합니다.
본 블로그는 광고를 포함하고 있습니다.
광고 클릭에서 발생하는 수익금은 모두 블로그 콘텐츠 향상을 위해 쓰여집니다.

728x90
반응형
728x170

TestSolution.zip
다운로드

[TestIdentityServer 프로젝트]

▶ Properties.launchSettins.json

{
    "iisSettings" :
    {
        "windowsAuthentication"   : false,
        "anonymousAuthentication" : true,
        "iisExpress"              :
        {
            "applicationUrl" : "http://localhost:50000",
            "sslPort"        : 44300
        }
    },
    "profiles" :
    {
        "IIS Express" :
        {
            "commandName"          : "IISExpress",
            "launchBrowser"        : true,
            "environmentVariables" :
            {
                "ASPNETCORE_ENVIRONMENT" : "Development"
            }
        },
        "TestIdentityServer" :
        {
            "commandName"          : "Project",
            "launchBrowser"        : true,
            "applicationUrl"       : "https://localhost:5001;http://localhost:5000",
            "environmentVariables" :
            {
                "ASPNETCORE_ENVIRONMENT" : "Development"
            }
        }
    }
}

 

▶ appsettings.json

{
    "Logging" :
    {
        "LogLevel" :
        {
            "Default"                    : "Information",
            "Microsoft"                  : "Warning",
            "Microsoft.Hosting.Lifetime" : "Information"
        }
    },
    "AllowedHosts" : "*",
    "ConnectionStrings" :
    {
        "DefaultConnection" : "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=TestDB;Integrated Security=True;"
    }
}

 

▶ Configuration.cs

using System.Collections.Generic;

using IdentityServer4;
using IdentityServer4.Models;

namespace TestIdentityServer
{
    /// <summary>
    /// 구성
    /// </summary>
    public static class Configuration
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Static
        //////////////////////////////////////////////////////////////////////////////// Public

        #region 신원 리소스 리스트 구하기 - GetIdentityResourceList()

        /// <summary>
        /// 신원 리소스 리스트 구하기
        /// </summary>
        /// <returns>신원 리소스 리스트</returns>
        public static List<IdentityResource> GetIdentityResourceList()
        {
            return new List<IdentityResource>
            { 
                new IdentityResources.OpenId(),
                new IdentityResources.Profile()
            };
        }

        #endregion
        #region API 범위 리스트 구하기 - GetAPIScopeList()

        /// <summary>
        /// API 범위 리스트 구하기
        /// </summary>
        /// <returns>API 범위 리스트</returns>
        public static IEnumerable<ApiScope> GetAPIScopeList()
        {
            return new List<ApiScope>
            {
                new ApiScope("API1", "API 1")
            };
        }

        #endregion
        #region 클라이언트 리스트 구하기 - GetClientList()

        /// <summary>
        /// 클라이언트 리스트 구하기
        /// </summary>
        /// <return>클라이언트 리스트</return>
        public static IEnumerable<Client> GetClientList()
        {
            return new List<Client>
            {
                new Client
                {
                    AllowedGrantTypes = GrantTypes.ClientCredentials,
                    ClientId          = "CLIENTID0001",
                    ClientSecrets     = { new Secret("CLIENTSECRET0001".Sha256()) },
                    AllowedScopes     = { "API1" }
                },
                new Client
                {
                    AllowedGrantTypes      = GrantTypes.Code,
                    ClientId               = "CLIENTID0002",
                    ClientSecrets          = { new Secret("CLIENTSECRET0002".Sha256()) },
                    RedirectUris           = { "https://localhost:44320/signin-oidc" },
                    PostLogoutRedirectUris = { "https://localhost:44320/signout-callback-oidc" },
                    AllowedScopes          = new List<string>
                    {
                        IdentityServerConstants.StandardScopes.OpenId,
                        IdentityServerConstants.StandardScopes.Profile
                        "API1"
                    }
                }
            };
        }

        #endregion
    }
}

 

▶ Models/TestUserData.cs

using System.Collections.Generic;
using System.Security.Claims;
using System.Text.Json;

using IdentityModel;
using IdentityServer4;
using IdentityServer4.Test;

namespace TestIdentityServer.Models
{
    /// <summary>
    /// 테스트 사용자 데이터
    /// </summary>
    public class TestUserData
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Property
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 사용자 리스트 - UserList

        /// <summary>
        /// 사용자 리스트
        /// </summary>
        public static List<TestUser> UserList
        {
            get
            {
                var address = new
                {
                    street_address = "One Hacker Way",
                    locality       = "Heidelberg",
                    postal_code    = 69118,
                    country        = "Germany"
                };
                
                return new List<TestUser>
                {
                    new TestUser
                    {
                        SubjectId = "818727",
                        Username  = "alice",
                        Password  = "alice",
                        Claims    =
                        {
                            new Claim(JwtClaimTypes.Name         , "Alice Smith"),
                            new Claim(JwtClaimTypes.GivenName    , "Alice"),
                            new Claim(JwtClaimTypes.FamilyName   , "Smith"),
                            new Claim(JwtClaimTypes.Email        , "AliceSmith@email.com"),
                            new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),
                            new Claim(JwtClaimTypes.WebSite      , "http://alice.com"),
                            new Claim
                            (
                                JwtClaimTypes.Address,
                                JsonSerializer.Serialize(address),
                                IdentityServerConstants.ClaimValueTypes.Json
                            )
                        }
                    },
                    new TestUser
                    {
                        SubjectId = "88421113",
                        Username  = "bob",
                        Password  = "bob",
                        Claims    =
                        {
                            new Claim(JwtClaimTypes.Name         , "Bob Smith"),
                            new Claim(JwtClaimTypes.GivenName    , "Bob"),
                            new Claim(JwtClaimTypes.FamilyName   , "Smith"),
                            new Claim(JwtClaimTypes.Email        , "BobSmith@email.com"),
                            new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),
                            new Claim(JwtClaimTypes.WebSite      , "http://bob.com"),
                            new Claim
                            (
                                JwtClaimTypes.Address,
                                JsonSerializer.Serialize(address),
                                IdentityServerConstants.ClaimValueTypes.Json
                            )
                        }
                    }
                };
            }
        }

        #endregion
    }
}

 

▶ Startup.cs

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.Linq;
using System.Reflection;

using IdentityServer4.EntityFramework.DbContexts;
using IdentityServer4.EntityFramework.Mappers;
using IdentityServer4.Models;

using TestIdentityServer.Models;

namespace TestIdentityServer
{
    /// <summary>
    /// 시작
    /// </summary>
    public class Startup
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Property
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 구성 - Configuration

        /// <summary>
        /// 구성
        /// </summary>
        public IConfiguration Configuration { get; }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 생성자 - Startup(configuration)

        /// <summary>
        /// 생성자
        /// </summary>
        /// <param name="configuration">구성</param>
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 서비스 컬렉션 구성하기 - ConfigureServices(services)

        /// <summary>
        /// 서비스 컬렉션 구성하기
        /// </summary>
        /// <param name="services">서비스 컬렉션</param>
        public void ConfigureServices(IServiceCollection services)
        {
            string connectionString   = Configuration.GetConnectionString("DefaultConnection");
            string migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;

            services.AddIdentityServer()
                .AddTestUsers(TestUserData.UserList)
                .AddDeveloperSigningCredential()
                .AddConfigurationStore
                (
                    options =>
                    {
                        options.ConfigureDbContext = builder => builder.UseSqlServer
                        (
                            connectionString,
                            sqlServerBuilder => sqlServerBuilder.MigrationsAssembly(migrationsAssembly)
                        );
                    }
                )
                .AddOperationalStore
                (
                    options =>
                    {
                        options.ConfigureDbContext = builder => builder.UseSqlServer
                        (
                            connectionString,
                            sqlServerBuilder => sqlServerBuilder.MigrationsAssembly(migrationsAssembly)
                        );
                    }
                );

            services.AddControllersWithViews();
        }

        #endregion
        #region 구성하기 - Configure(app, environment)

        /// <summary>
        /// 구성하기
        /// </summary>
        /// <param name="app">애플리케이션 빌더</param>
        /// <param name="environment">웹 호스트 환경</param>
        public void Configure(IApplicationBuilder app, IWebHostEnvironment environment)
        {
            InitializeDatabase(app);

            if(environment.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseStaticFiles();

            app.UseRouting();
            
            app.UseIdentityServer();

            app.UseAuthorization();

            app.UseEndpoints
            (
                endpoints =>
                {
                    endpoints.MapControllerRoute
                    (
                        name    : "default",
                        pattern : "{controller=Home}/{action=Index}/{id?}"
                    );
                }
            );
        }

        #endregion

        ////////////////////////////////////////////////////////////////////////////////////////// Private

        #region 데이터베이스 초기화하기 - InitializeDatabase(app)

        /// <summary>
        /// 데이터베이스 초기화하기
        /// </summary>
        /// <param name="app">애플리케이션 빌더</param>
        private void InitializeDatabase(IApplicationBuilder app)
        {
            IServiceScopeFactory serviceScopeFactory = app.ApplicationServices.GetService<IServiceScopeFactory>();

            using(IServiceScope serviceScope = serviceScopeFactory.CreateScope())
            {
                serviceScope.ServiceProvider.GetRequiredService<PersistedGrantDbContext>().Database.Migrate();

                ConfigurationDbContext context = serviceScope.ServiceProvider.GetRequiredService<ConfigurationDbContext>();

                context.Database.Migrate();

                if(!context.Clients.Any())
                {
                    foreach(Client client in TestIdentityServer.Configuration.GetClientList())
                    {
                        context.Clients.Add(client.ToEntity());
                    }

                    context.SaveChanges();
                }

                if(!context.IdentityResources.Any())
                {
                    foreach(IdentityResource resource in TestIdentityServer.Configuration.GetIdentityResourceList())
                    {
                        context.IdentityResources.Add(resource.ToEntity());
                    }

                    context.SaveChanges();
                }

                if(!context.ApiScopes.Any())
                {
                    foreach(ApiScope resource in TestIdentityServer.Configuration.GetAPIScopeList())
                    {
                        context.ApiScopes.Add(resource.ToEntity());
                    }

                    context.SaveChanges();
                }
            }
        }

        #endregion
    }
}

 

▶ Extensions/ControllerExtension.cs

using Microsoft.AspNetCore.Mvc;
using System;

using IdentityServer4.Models;

using TestIdentityServer.Models;

namespace TestIdentityServer.Extensions
{
    /// <summary>
    /// 컨트롤러 확장
    /// </summary>
    public static class ControllerExtension
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Static
        //////////////////////////////////////////////////////////////////////////////// Public

        #region 네이티브 클라이언트 여부 구하기 - IsNativeClient(authorizationRequest)

        /// <summary>
        /// 네이티브 클라이언트 여부 구하기
        /// </summary>
        /// <returns>네이티브 클라이언트 여부</returns>
        /// <remarks>리디렉션 URI가 네이티브 클라이언트용인지 확인한다.</remarks>
        public static bool IsNativeClient(this AuthorizationRequest authorizationRequest)
        {
            return !authorizationRequest.RedirectUri.StartsWith("https", StringComparison.Ordinal) &&
                   !authorizationRequest.RedirectUri.StartsWith("http" , StringComparison.Ordinal);
        }

        #endregion
        #region 로딩 페이지 처리하기 - LoadingPage(controller, viewName, redirectURI)

        /// <summary>
        /// 로딩 페이지 처리하기
        /// </summary>
        /// <param name="controller">컨트롤러</param>
        /// <param name="viewName">뷰 명칭</param>
        /// <param name="redirectURI">재전송 URI</param>
        /// <returns>액션 결과</returns>
        public static IActionResult LoadingPage(this Controller controller, string viewName, string redirectURI)
        {
            controller.HttpContext.Response.StatusCode = 200;

            controller.HttpContext.Response.Headers["Location"] = string.Empty;
            
            return controller.View(viewName, new RedirectViewModel { RedirectURL = redirectURI });
        }

        #endregion
    }
}

 

▶ Attributes/SecurityHeadersAttribute.cs

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

namespace TestIdentityServer.Attributes
{
    /// <summary>
    /// 보안 헤더 어트리뷰트
    /// </summary>
    public class SecurityHeadersAttribute : ActionFilterAttribute
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 결과 실행시 처리하기 - OnResultExecuting(context)

        /// <summary>
        /// 결과 실행시 처리하기
        /// </summary>
        /// <param name="context">결과 실행시 컨텍스트</param>
        public override void OnResultExecuting(ResultExecutingContext context)
        {
            IActionResult result = context.Result;

            if(result is ViewResult)
            {
                // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options
                if(!context.HttpContext.Response.Headers.ContainsKey("X-Content-Type-Options"))
                {
                    context.HttpContext.Response.Headers.Add("X-Content-Type-Options", "nosniff");
                }

                // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options
                if(!context.HttpContext.Response.Headers.ContainsKey("X-Frame-Options"))
                {
                    context.HttpContext.Response.Headers.Add("X-Frame-Options", "SAMEORIGIN");
                }

                // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
                string csp = "default-src 'self'; object-src 'none'; frame-ancestors 'none'; " +
                             "sandbox allow-forms allow-same-origin allow-scripts; base-uri 'self';";

                // 또한 프로덕션을 위해 HTTPS를 확보한 후 업그레이드-비보안-요청 추가를 고려한다.
                // csp += "upgrade-insecure-requests;";
                // 또한 트위터에서 클라이언트 이미지를 표시해야 하는 경우의 예이다.
                // csp += "img-src 'self' https://pbs.twimg.com;";

                // 표준 호환 브라우저의 경우 실행한다.
                if(!context.HttpContext.Response.Headers.ContainsKey("Content-Security-Policy"))
                {
                    context.HttpContext.Response.Headers.Add("Content-Security-Policy", csp);
                }

                // IE인 경우 실행한다.
                if(!context.HttpContext.Response.Headers.ContainsKey("X-Content-Security-Policy"))
                {
                    context.HttpContext.Response.Headers.Add("X-Content-Security-Policy", csp);
                }

                // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
                string referrerPolicy = "no-referrer";

                if(!context.HttpContext.Response.Headers.ContainsKey("Referrer-Policy"))
                {
                    context.HttpContext.Response.Headers.Add("Referrer-Policy", referrerPolicy);
                }
            }
        }

        #endregion
    }
}

 

▶ Controllers/AccountController.cs

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

using IdentityModel;
using IdentityServer4;
using IdentityServer4.Events;
using IdentityServer4.Extensions;
using IdentityServer4.Models;
using IdentityServer4.Services;
using IdentityServer4.Stores;
using IdentityServer4.Test;

using TestIdentityServer.Attributes;
using TestIdentityServer.Extensions;
using TestIdentityServer.Models;

namespace TestIdentityServer
{
    /// <summary>
    /// 계정 컨트롤러
    /// </summary>
    [SecurityHeaders]
    [AllowAnonymous]
    public class AccountController : Controller
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Field
        ////////////////////////////////////////////////////////////////////////////////////////// Private

        #region Field

        /// <summary>
        /// 신원 서버 대화형 서비스
        /// </summary>
        private readonly IIdentityServerInteractionService identityServerInteractionService;

        /// <summary>
        /// 클라이언트 저장소
        /// </summary>
        private readonly IClientStore clientStore;

        /// <summary>
        /// 인증 계획 공급자
        /// </summary>
        private readonly IAuthenticationSchemeProvider authenticationSchemeProvider;

        /// <summary>
        /// 이벤트 저장소
        /// </summary>
        private readonly IEventService eventService;

        /// <summary>
        /// 테스트 사용자 저장소
        /// </summary>
        private readonly TestUserStore testUserStore;

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 생성자 - AccountController(identityServerInteractionService, clientStore, authenticationSchemeProvider, eventService, testUserStore)

        /// <summary>
        /// 생성자
        /// </summary>
        /// <param name="identityServerInteractionService">신원 서버 대화형 서비스</param>
        /// <param name="clientStore">클라이언트 저장소</param>
        /// <param name="authenticationSchemeProvider">인증 계획 공급자</param>
        /// <param name="eventService">이벤트 서비스</param>
        /// <param name="testUserStore">테스트 사용자 저장소</param>
        public AccountController
        (
            IIdentityServerInteractionService identityServerInteractionService,
            IClientStore                      clientStore,
            IAuthenticationSchemeProvider     authenticationSchemeProvider,
            IEventService                     eventService,
            TestUserStore                     testUserStore = null
        )
        {
            this.identityServerInteractionService = identityServerInteractionService;
            this.clientStore                      = clientStore;
            this.authenticationSchemeProvider     = authenticationSchemeProvider;
            this.eventService                     = eventService;
            this.testUserStore                    = testUserStore ?? new TestUserStore(TestUserData.UserList);
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 로그인 페이지 처리하기 - Login(returnURL)

        /// <summary>
        /// 로그인 페이지 처리하기
        /// </summary>
        /// <param name="returnURL">반환 URL</param>
        /// <returns>액션 결과 태스크</returns>
        [HttpGet]
        public async Task<IActionResult> Login(string returnURL)
        {
            // 로그인 페이지에 표시할 내용을 알 수 있도록 모델을 작성한다.
            LoginViewModel vm = await BuildLoginViewModelAsync(returnURL);

            if(vm.IsExternalLoginOnly)
            {
                // 로그인 옵션은 하나 뿐이며 외부 공급자이다.
                return RedirectToAction("Challenge", "External", new { scheme = vm.ExternalLoginScheme, returnURL });
            }

            return View(vm);
        }

        #endregion
        #region 로그인 페이지 처리하기 - (model, button)

        /// <summary>
        /// 로그인 페이지 처리하기
        /// </summary>
        /// <param name="model">로그인 입력 모델</param>
        /// <param name="button">버튼 타입</param>
        /// <returns>액션 결과 태스크</returns>
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Login(LoginInputModel model, string button)
        {
            // 권한 요청 컨텍스트에 있는지 확인한다.
            AuthorizationRequest authorizationRequest = await this.identityServerInteractionService.GetAuthorizationContextAsync
            (
                model.ReturnURL
            );

            // 사용자가 "취소" 버튼을 클릭한 경우 처리한다.
            if(button != "login")
            {
                if(authorizationRequest != null)
                {
                    // 사용자가 취소하면 동의를 거부한 것처럼 결과를 IdentityServer로 다시 보낸다
                    // (이 클라이언트에 동의가 필요하지 않은 경우에도 해당).
                    // 그러면 액세스 거부 OIDC 오류 응답이 클라이언트에 다시 전송된다. 
                    await this.identityServerInteractionService.DenyAuthorizationAsync
                    (
                        authorizationRequest,
                        AuthorizationError.AccessDenied
                    );

                    // GetAuthorizationContextAsync가 null이 아닌 값을 반환했기 때문에 model.ReturnUrl을 신뢰할 수 있다.
                    if(authorizationRequest.IsNativeClient())
                    {
                        // 클라이언트가 네이티브이므로 응답을 반환하는 방법의 이러한 변경은
                        // 최종 사용자에게 더 나은 UX를 제공하기 위한 것이다.
                        return this.LoadingPage("Redirect", model.ReturnURL);
                    }

                    return Redirect(model.ReturnURL);
                }
                else
                {
                    // 올바른 컨텍스트가 없기 때문에 홈 페이지로 돌아간다.
                    return Redirect("~/");
                }
            }

            if(ModelState.IsValid)
            {
                // 메모리 내 저장소에 대해 사용자 이름/암호를 검증한다.
                if(this.testUserStore.ValidateCredentials(model.UserName, model.Password))
                {
                    TestUser testUser = this.testUserStore.FindByUsername(model.UserName);

                    await this.eventService.RaiseAsync
                    (
                        new UserLoginSuccessEvent
                        (
                            testUser.Username,
                            testUser.SubjectId,
                            testUser.Username,
                            clientId : authorizationRequest?.Client.ClientId
                        )
                    );

                    // 사용자가 "기억하기"를 선택한 경우에만 여기에서 명시적 만료를 설정한다.
                    // 그렇지 않으면 쿠키 미들웨어에 구성된 만료에 의존한다.
                    AuthenticationProperties authenticationProperties = null;

                    if(AccountOptions.AllowRememberLogin && model.RememberLogin)
                    {
                        authenticationProperties = new AuthenticationProperties
                        {
                            IsPersistent = true,
                            ExpiresUtc   = DateTimeOffset.UtcNow.Add(AccountOptions.RememberLoginDuration)
                        };
                    };

                    // 서브젝트 ID 및 사용자 이름으로 인증 쿠키를 발급한다.
                    IdentityServerUser identityServerUser = new IdentityServerUser(testUser.SubjectId)
                    {
                        DisplayName = testUser.Username
                    };

                    await HttpContext.SignInAsync(identityServerUser, authenticationProperties);

                    if(authorizationRequest != null)
                    {
                        if(authorizationRequest.IsNativeClient())
                        {
                            // 클라이언트는 기본이므로 응답을 반환하는 방법의 이러한 변경은
                            // 최종 사용자에게 더 나은 UX를 제공하기 위한 것이다.
                            return this.LoadingPage("Redirect", model.ReturnURL);
                        }

                        // GetAuthorizationContextAsync가 null이 아닌 값을 반환했기 때문에 model.ReturnUrl을 신뢰할 수 있다.
                        return Redirect(model.ReturnURL);
                    }

                    // 로컬 페이지를 요청한다.
                    if(Url.IsLocalUrl(model.ReturnURL))
                    {
                        return Redirect(model.ReturnURL);
                    }
                    else if(string.IsNullOrEmpty(model.ReturnURL))
                    {
                        return Redirect("~/");
                    }
                    else
                    {
                        // 사용자가 악성 링크를 클릭했을 수 있다 - 기록되어야 한다.
                        throw new Exception("invalid return URL");
                    }
                }

                await this.eventService.RaiseAsync
                (
                    new UserLoginFailureEvent
                    (
                        model.UserName,
                        "invalid credentials",
                        clientId : authorizationRequest?.Client.ClientId
                    )
                );

                ModelState.AddModelError(string.Empty, AccountOptions.InvalidCredentialsErrorMessage);
            }

            // 문제가 발생했다. 오류가 있는 양식을 표시한다.
            LoginViewModel vm = await BuildLoginViewModelAsync(model);

            return View(vm);
        }

        #endregion
        #region 로그아웃 페이지 처리하기 - Logout(logoutID)

        /// <summary>
        /// 로그아웃 페이지 처리하기
        /// </summary>
        /// <param name="logoutID">로그아웃 ID</param>
        /// <returns>액션 결과 태스크</returns>
        [HttpGet]
        public async Task<IActionResult> Logout(string logoutID)
        {
            // 로그 아웃 페이지가 표시할 내용을 알 수 있도록 로그아웃 뷰 모델을 만든다.
            LogoutViewModel vm = await BuildLogoutViewModelAsync(logoutID);

            if(vm.ShowLogoutPrompt == false)
            {
                // 로그 아웃 요청이 IdentityServer에서 제대로 인증된 경우
                // 프롬프트를 표시할 필요가 없으며 사용자를 직접 로그아웃 할 수 있다.
                return await Logout(vm);
            }

            return View(vm);
        }

        #endregion
        #region 로그아웃 페이지 처리하기 - Logout(model)

        /// <summary>
        /// 로그아웃 페이지 처리하기
        /// </summary>
        /// <param name="model">로그아웃 입력 모델</param>
        /// <returns>액션 결과 태스크</returns>
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Logout(LogoutInputModel model)
        {
            // 로그아웃한 페이지가 표시할 내용을 알 수 있도록 로그아웃 완료시 뷰 모델을 만든다.
            LoggedOutViewModel vm = await BuildLoggedOutViewModelAsync(model.LogoutID);

            if(User?.Identity.IsAuthenticated == true)
            {
                // 로컬 인증 쿠키를 삭제한다.
                await HttpContext.SignOutAsync();

                // 로그아웃 이벤트를 발생시킨다.
                await this.eventService.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName()));
            }

            // 업스트림 ID 공급자에서 로그아웃을 트리거해야 하는지 확인한다.
            if(vm.TriggerExternalLogout)
            {
                // 사용자가 로그아웃한 후 업스트림 공급자가 다시 당사로 리디렉션되도록 반환 URL을 작성한다.
                // 이를 통해 싱글 사인 아웃 처리를 완료할 수 있다.
                string url = Url.Action("Logout", new { logoutID = vm.LogoutID });

                // 이렇게 하면 로그아웃을 위해 외부 공급자로의 리디렉션이 트리거된다.
                return SignOut(new AuthenticationProperties { RedirectUri = url }, vm.ExternalAuthenticationScheme);
            }

            return View("LoggedOut", vm);
        }

        #endregion
        #region 액세스 거부시 페이지 처리하기 - AccessDenied()

        /// <summary>
        /// 액세스 거부시 페이지 처리하기
        /// </summary>
        /// <returns>액션 결과</returns>
        [HttpGet]
        public IActionResult AccessDenied()
        {
            return View();
        }

        #endregion

        ////////////////////////////////////////////////////////////////////////////////////////// Private

        #region 로그인 뷰 모델 만들기 (비동기) - BuildLoginViewModelAsync(returnURL)

        /// <summary>
        /// 로그인 뷰 모델 만들기 (비동기)
        /// </summary>
        /// <param name="returnURL">반환 URL</param>
        /// <returns>로그인 뷰 모델 태스크</returns>
        private async Task<LoginViewModel> BuildLoginViewModelAsync(string returnURL)
        {
            AuthorizationRequest authorizationRequest = await this.identityServerInteractionService.GetAuthorizationContextAsync
            (
                returnURL
            );

            if
            (
                authorizationRequest?.IdP != null &&
                await this.authenticationSchemeProvider.GetSchemeAsync(authorizationRequest.IdP) != null
            )
            {
                bool isLocalIdentityProvider = authorizationRequest.IdP == IdentityServerConstants.LocalIdentityProvider;

                // 이는 UI를 단락시키고 하나의 외부 IdP만 트리거하기 위한 것이다.
                LoginViewModel vm = new LoginViewModel
                {
                    EnableLocalLogin = isLocalIdentityProvider,
                    ReturnURL        = returnURL,
                    UserName         = authorizationRequest?.LoginHint,
                };

                if(!isLocalIdentityProvider)
                {
                    vm.ExternalProviderEnumerable = new[]
                    {
                        new ExternalProvider { AuthenticationScheme = authorizationRequest.IdP }
                    };
                }

                return vm;
            }

            IEnumerable<AuthenticationScheme> authenticationSchemeEnumerable = await this.authenticationSchemeProvider.GetAllSchemesAsync();

            List<ExternalProvider> externalProviderList = authenticationSchemeEnumerable
                .Where(x => x.DisplayName != null)
                .Select
                (
                    x => new ExternalProvider
                    {
                        DisplayName          = x.DisplayName ?? x.Name,
                        AuthenticationScheme = x.Name
                    }
                )
                .ToList();

            bool allowLocal = true;

            if(authorizationRequest?.Client.ClientId != null)
            {
                Client client = await this.clientStore.FindEnabledClientByIdAsync(authorizationRequest.Client.ClientId);

                if(client != null)
                {
                    allowLocal = client.EnableLocalLogin;

                    if(client.IdentityProviderRestrictions != null && client.IdentityProviderRestrictions.Any())
                    {
                        externalProviderList = externalProviderList.Where
                        (
                            provider => client.IdentityProviderRestrictions.Contains(provider.AuthenticationScheme)
                        )
                        .ToList();
                    }
                }
            }

            return new LoginViewModel
            {
                AllowRememberLogin         = AccountOptions.AllowRememberLogin,
                EnableLocalLogin           = allowLocal && AccountOptions.AllowLocalLogin,
                ReturnURL                  = returnURL,
                UserName                   = authorizationRequest?.LoginHint,
                ExternalProviderEnumerable = externalProviderList.ToArray()
            };
        }

        #endregion
        #region 로그인 뷰 모델 만들기 (비동기) - BuildLoginViewModelAsync(model)

        /// <summary>
        /// 로그인 뷰 모델 만들기 (비동기)
        /// </summary>
        /// <param name="model">로그인 입력 모델</param>
        /// <returns>로그인 뷰 모델 태스크</returns>
        private async Task<LoginViewModel> BuildLoginViewModelAsync(LoginInputModel model)
        {
            LoginViewModel vm = await BuildLoginViewModelAsync(model.ReturnURL);

            vm.UserName      = model.UserName;
            vm.RememberLogin = model.RememberLogin;

            return vm;
        }

        #endregion
        #region 로그아웃 뷰 모델 만들기 (비동기) - BuildLogoutViewModelAsync(logoutID)

        /// <summary>
        /// 로그아웃 뷰 모델 만들기 (비동기)
        /// </summary>
        /// <param name="logoutID">로그아웃 ID</param>
        /// <returns>로그아웃 뷰 모델 태스크</returns>
        private async Task<LogoutViewModel> BuildLogoutViewModelAsync(string logoutID)
        {
            LogoutViewModel vm = new LogoutViewModel
            {
                LogoutID         = logoutID,
                ShowLogoutPrompt = AccountOptions.ShowLogoutPrompt
            };

            if(User?.Identity.IsAuthenticated != true)
            {
                // 사용자가 인증되지 않은 경우 로그아웃된 페이지만 표시한다.
                vm.ShowLogoutPrompt = false;

                return vm;
            }

            LogoutRequest logoutRequest = await this.identityServerInteractionService.GetLogoutContextAsync(logoutID);

            if(logoutRequest?.ShowSignoutPrompt == false)
            {
                // 자동으로 로그아웃해도 안전하다.
                vm.ShowLogoutPrompt = false;

                return vm;
            }

            // 로그아웃 프롬프트를 표시한다.
            // 이것은 사용자가 다른 악성 웹 페이지에 의해 자동으로 로그아웃되는 공격을 방지한다.
            return vm;
        }

        #endregion
        #region 로그아웃 완료시 뷰 모델 만들기 (비동기) - BuildLoggedOutViewModelAsync(logoutID)

        /// <summary>
        /// 로그아웃 완료시 뷰 모델 만들기 (비동기)
        /// </summary>
        /// <param name="logoutID">로그아웃 ID</param>
        /// <returns>로그아웃 완료시 뷰 모델 태스크</returns>
        private async Task<LoggedOutViewModel> BuildLoggedOutViewModelAsync(string logoutID)
        {
            // 컨텍스트 정보를 가져온다(클라이언트명, 포스트 로그아웃 재전송 URI 및 페더레이션된 로그아웃을 위한 iframe).
            LogoutRequest logoutRequest = await this.identityServerInteractionService.GetLogoutContextAsync(logoutID);

            LoggedOutViewModel vm = new LoggedOutViewModel
            {
                AutomaticRedirectAfterLogOut = AccountOptions.AutomaticRedirectAfterLogout,
                PostLogoutRedirectURI        = logoutRequest?.PostLogoutRedirectUri,
                ClientName                   = string.IsNullOrEmpty(logoutRequest?.ClientName)
                                                   ? logoutRequest?.ClientId : logoutRequest?.ClientName,
                LogOutIframeURL              = logoutRequest?.SignOutIFrameUrl,
                LogoutID                     = logoutID
            };

            if(User?.Identity.IsAuthenticated == true)
            {
                string idp = User.FindFirst(JwtClaimTypes.IdentityProvider)?.Value;

                if(idp != null && idp != IdentityServerConstants.LocalIdentityProvider)
                {
                    bool providerSupportsLogout = await HttpContext.GetSchemeSupportsSignOutAsync(idp);

                    if(providerSupportsLogout)
                    {
                        if(vm.LogoutID == null)
                        {
                            // 현재 로그아웃 컨텍스트가 없는 경우 새로 만들어야 한다.
                            // 이렇게 하면 로그아웃하기 전에 현재 로그인한 사용자로부터 필요한 정보를 캡처하고
                            // 로그 아웃을 위해 외부 IdP로 재전송한다.
                            vm.LogoutID = await this.identityServerInteractionService.CreateLogoutContextAsync();
                        }

                        vm.ExternalAuthenticationScheme = idp;
                    }
                }
            }

            return vm;
        }

        #endregion
    }
}

 

▶ Controllers/ExternalController.cs

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;

using IdentityModel;
using IdentityServer4;
using IdentityServer4.Events;
using IdentityServer4.Services;
using IdentityServer4.Stores;
using IdentityServer4.Test;
using IdentityServer4.Models;

using TestIdentityServer.Attributes;
using TestIdentityServer.Extensions;
using TestIdentityServer.Models;

namespace TestIdentityServer
{
    /// <summary>
    /// 외부 컨트롤러
    /// </summary>
    [SecurityHeaders]
    [AllowAnonymous]
    public class ExternalController : Controller
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Field
        ////////////////////////////////////////////////////////////////////////////////////////// Private

        #region Field

        /// <summary>
        /// 신원 서버 대화형 서비스
        /// </summary>
        private readonly IIdentityServerInteractionService identityServerInteractionService;

        /// <summary>
        /// 클라이언트 저장소
        /// </summary>
        private readonly IClientStore clientStore;

        /// <summary>
        /// 로그 기록기
        /// </summary>
        private readonly ILogger<ExternalController> logger;

        /// <summary>
        /// 이벤트 서비스
        /// </summary>
        private readonly IEventService eventService;

        /// <summary>
        /// 테스트 사용자 저장소
        /// </summary>
        private readonly TestUserStore testUserStore;

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 생성자 - ExternalController(identityServerInteractionService, clientStore, eventService, logger, testUserStore)

        /// <summary>
        /// 생성자
        /// </summary>
        /// <param name="identityServerInteractionService">신원 서버 대화형 서비스</param>
        /// <param name="clientStore">클라이언트 저장소</param>
        /// <param name="eventService">이벤트 서비스</param>
        /// <param name="logger">로그 기록기</param>
        /// <param name="testUserStore">테스트 사용자 저장소</param>
        public ExternalController
        (
            IIdentityServerInteractionService identityServerInteractionService,
            IClientStore                      clientStore,
            IEventService                     eventService,
            ILogger<ExternalController>       logger,
            TestUserStore                     testUserStore = null
        )
        {
            this.identityServerInteractionService = identityServerInteractionService;
            this.clientStore                      = clientStore;
            this.eventService                     = eventService;
            this.logger                           = logger;
            // TestUserStore가 DI에 없는 경우 글로벌 사용자 컬렉션만 사용한다.
            // 여기에 사용자 지정 ID 관리 라이브러리(예 : ASP.NET ID)를 연결할 수 있다.
            this.testUserStore                    = testUserStore ?? new TestUserStore(TestUserData.UserList);
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 도전 페이지 처리하기 - Challenge(scheme, returnURL)

        /// <summary>
        /// 도전 페이지 처리하기
        /// </summary>
        /// <param name="scheme">계획</param>
        /// <param name="returnURL">반환 URL</param>
        /// <returns>액션 결과</returns>
        /// <remarks>외부 인증 공급자에 대한 왕복 초기화한다.</remarks>
        [HttpGet]
        public IActionResult Challenge(string scheme, string returnURL)
        {
            if(string.IsNullOrEmpty(returnURL))
            {
                returnURL = "~/";
            }

            // returnURL을 검사한다 - 유효한 OIDC URL이거나 로컬 페이지로 돌아간다.
            if(Url.IsLocalUrl(returnURL) == false && this.identityServerInteractionService.IsValidReturnUrl(returnURL) == false)
            {
                // 사용자가 악성 링크를 클릭했을 수 있다 - 기록되어야 한다.
                throw new Exception("invalid return URL");
            }
            
            // 도전을 시작하고 반환 URL 및 체계를 왕복한다.
            AuthenticationProperties authenticationProperties = new AuthenticationProperties
            {
                RedirectUri = Url.Action(nameof(Callback)), 
                Items       =
                {
                    { "returnUrl", returnURL }, 
                    { "scheme"   , scheme    }
                }
            };

            return Challenge(authenticationProperties, scheme);
        }

        #endregion
        #region 콜백 페이지 처리하기 - Callback()

        /// <summary>
        /// 콜백 페이지 처리하기
        /// </summary>
        /// <returns>액션 결과 태스크</returns>
        /// <remarks>외부 인증 후 처리한다.</remarks>
        [HttpGet]
        public async Task<IActionResult> Callback()
        {
            // 임시 쿠키에서 외부 ID를 읽는다.
            AuthenticateResult authenticateResult = await HttpContext.AuthenticateAsync
            (
                IdentityServerConstants.ExternalCookieAuthenticationScheme
            );

            if(authenticateResult?.Succeeded != true)
            {
                throw new Exception("External authentication error");
            }

            if(this.logger.IsEnabled(LogLevel.Debug))
            {
                IEnumerable<string> externalClaimEnumberable = authenticateResult.Principal.Claims.Select
                (
                    claim => $"{claim.Type} : {claim.Value}"
                );

                this.logger.LogDebug("External claims : {@claims}", externalClaimEnumberable);
            }

            // 사용자 및 외부 공급자 정보를 조회한다.
            var (testUser, provider, providerUserID, claimEnumerable) = FindUserFromExternalProvider(authenticateResult);

            if(testUser == null)
            {
                // 이 샘플에서는 사용자 등록을 위한 사용자 지정 워크 플로를 시작할 수 있다.
                // 이 샘플에서는 수행 방법을 보여주지 않는다.
                // 새로운 외부 사용자를 자동 프로비저닝하기만 하면 된다.
                testUser = AutoProvisionUser(provider, providerUserID, claimEnumerable);
            }

            // 이를 통해 사용된 특정 프로토콜에 대한 추가 클레임 또는 속성을 수집하고 로컬 인증 쿠키에 저장할 수 있다.
            // 일반적으로 해당 프로토콜에서 로그아웃하는 데 필요한 데이터를 저장하는 데 사용된다.
            List<Claim> additionalLocalClaimList = new List<Claim>();

            AuthenticationProperties localLoginAuthenticationProperties = new AuthenticationProperties();

            ProcessLoginCallback(authenticateResult, additionalLocalClaimList, localLoginAuthenticationProperties);
            
            // 사용자에 대한 인증 쿠키를 발급한다.
            IdentityServerUser identityServerUser = new IdentityServerUser(testUser.SubjectId)
            {
                DisplayName      = testUser.Username,
                IdentityProvider = provider,
                AdditionalClaims = additionalLocalClaimList
            };

            await HttpContext.SignInAsync(identityServerUser, localLoginAuthenticationProperties);

            // 외부 인증시 사용되는 임시 쿠키를 삭제한다.
            await HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);

            // 반환 URL을 구한다.
            string returnURL = authenticateResult.Properties.Items["returnUrl"] ?? "~/";

            // 외부 로그인이 OIDC 요청 컨텍스트에 있는지 확인한다.
            AuthorizationRequest authorizationRequest = await this.identityServerInteractionService.GetAuthorizationContextAsync(returnURL);

            await this.eventService.RaiseAsync
            (
                new UserLoginSuccessEvent
                (
                    provider,
                    providerUserID,
                    testUser.SubjectId,
                    testUser.Username,
                    true,
                    authorizationRequest?.Client.ClientId
                )
            );

            if(authorizationRequest != null)
            {
                if(authorizationRequest.IsNativeClient())
                {
                    // 클라이언트는 기본이므로 응답을 반환하는 방법의 이러한 변경은 최종 사용자에게 더 나은 UX를 제공하기 위한 것이다.
                    return this.LoadingPage("Redirect", returnURL);
                }
            }

            return Redirect(returnURL);
        }

        #endregion

        ////////////////////////////////////////////////////////////////////////////////////////// Private

        #region 외부 공급자에서 사용자 찾기 - FindUserFromExternalProvider(authenticateResult)

        /// <summary>
        /// 외부 공급자에서 사용자 찾기
        /// </summary>
        /// <param name="authenticateResult">인증 결과</param>
        /// <returns>(테스트 사용자, 공급자, 공급자 사용자 ID, 클레임 열거 가능형)</returns>
        private (TestUser testUser, string provider, string providerUserID, IEnumerable<Claim> claimEnumerable) FindUserFromExternalProvider
        (
            AuthenticateResult authenticateResult
        )
        {
            ClaimsPrincipal externalUserClaimsPrincipal = authenticateResult.Principal;

            // 공급자가 발급한 외부 사용자의 고유 ID를 확인한다.
            // 가장 일반적인 클레임 유형은 외부 공급자에 따라 sub 클레임 및 NameIdentifier이며 다른 클레임 유형이 사용될 수 있다.
            Claim userIDClaim = externalUserClaimsPrincipal.FindFirst(JwtClaimTypes.Subject) ??
                              externalUserClaimsPrincipal.FindFirst(ClaimTypes.NameIdentifier) ??
                              throw new Exception("Unknown userid");

            // 사용자를 프로비저닝할 때 추가 클레임으로 포함하지 않도록 사용자 ID 클레임을 제거한다.
            List<Claim> claimList = externalUserClaimsPrincipal.Claims.ToList();

            claimList.Remove(userIDClaim);

            string provider       = authenticateResult.Properties.Items["scheme"];
            string providerUserID = userIDClaim.Value;

            // 외부 사용자를 찾는다.
            TestUser testUser = this.testUserStore.FindByExternalProvider(provider, providerUserID);

            return (testUser, provider, providerUserID, claimList);
        }

        #endregion
        #region 사용자 자동 프로비저닝하기 - AutoProvisionUser(provider, providerUserID, claimEnumerable)

        /// <summary>
        /// 사용자 자동 프로비저닝하기
        /// </summary>
        /// <param name="provider">공급자</param>
        /// <param name="providerUserID">공급자 사용자 ID</param>
        /// <param name="claimEnumerable">클레임 열거 가능형</param>
        /// <returns>테스트 사용자</returns>
        private TestUser AutoProvisionUser(string provider, string providerUserID, IEnumerable<Claim> claimEnumerable)
        {
            TestUser testUser = this.testUserStore.AutoProvisionUser(provider, providerUserID, claimEnumerable.ToList());

            return testUser;
        }

        #endregion
        #region 로그인 콜백 처리하기 - ProcessLoginCallback(externalAuthenticateResult, localClaimList, localLoginAuthenticationProperties)

        /// <summary>
        /// 로그인 콜백 처리하기
        /// </summary>
        /// <param name="externalAuthenticateResult">외부 인증 결과</param>
        /// <param name="localClaimList">로컬 클레임 리스트</param>
        /// <param name="localLoginAuthenticationProperties">로컬 로그인 인증 속성</param>
        /// <remarks>
        ///  외부 로그인이 OIDC 기반인 경우 로그아웃 작업을 위해 보존해야 할 특정 사항이 있다.
        /// 이것은 WS-Fed, SAML2p 또는 기타 프로토콜에 따라 다르다.
        /// </remarks>
        private void ProcessLoginCallback
        (
            AuthenticateResult       externalAuthenticateResult,
            List<Claim>              localClaimList,
            AuthenticationProperties localLoginAuthenticationProperties
        )
        {
            // 외부 시스템이 세션 ID 클레임을 보낸 경우 단일 사인 아웃에 사용할 수 있도록 복사한다.
            Claim sessionIDClaim = externalAuthenticateResult.Principal.Claims.FirstOrDefault(x => x.Type == JwtClaimTypes.SessionId);

            if(sessionIDClaim != null)
            {
                localClaimList.Add(new Claim(JwtClaimTypes.SessionId, sessionIDClaim.Value));
            }

            // 외부 공급자가 id_token을 발행한 경우 로그 아웃을 위해 보관한다.
            string idToken = externalAuthenticateResult.Properties.GetTokenValue("id_token");

            if(idToken != null)
            {
                localLoginAuthenticationProperties.StoreTokens(new[] { new AuthenticationToken { Name = "id_token", Value = idToken } });
            }
        }

        #endregion
    }
}

 

▶ Views/Account/Login.cshtml

@model LoginViewModel
<div class="login-page">
    <div class="lead">
        <h1>Login</h1>
        <p>Choose how to login</p>
    </div>
    <partial name="_ValidationSummary" />
    <div class="row">
        @if(Model.EnableLocalLogin)
        {
            <div class="col-sm-6">
                <div class="card">
                    <div class="card-header">
                        <h2>Local Account</h2>
                    </div>
                    <div class="card-body">
                        <form asp-route="Login">
                            <input type="hidden" asp-for="ReturnURL" />
                            <div class="form-group">
                                <label asp-for="UserName"></label>
                                <input type="text" asp-for="UserName"
                                    class="form-control"
                                    autofocus
                                    placeholder="Username" />
                            </div>
                            <div class="form-group">
                                <label asp-for="Password"></label>
                                <input type="password" asp-for="Password"
                                    class="form-control"
                                    autocomplete="off"
                                    placeholder="Password" />
                            </div>
                            @if(Model.AllowRememberLogin)
                            {
                                <div class="form-group">
                                    <div class="form-check">
                                        <input asp-for="RememberLogin" class="form-check-input">
                                        <label asp-for="RememberLogin" class="form-check-label" >
                                            Remember My Login
                                        </label>
                                    </div>
                                </div>
                            }
                            <button name="button"
                                class="btn btn-primary"
                                value="login">
                                Login
                            </button>
                            <button name="button"
                                class="btn btn-secondary"
                                value="cancel">
                                Cancel
                            </button>
                        </form>
                    </div>
                </div>
            </div>
        }
        @if(Model.VisibleExternalProviderEnumerable.Any())
        {
            <div class="col-sm-6">
                <div class="card">
                    <div class="card-header">
                        <h2>External Account</h2>
                    </div>
                    <div class="card-body">
                        <ul class="list-inline">
                            @foreach(ExternalProvider provider in Model.VisibleExternalProviderEnumerable)
                            {
                                <li class="list-inline-item">
                                    <a asp-controller="External" asp-action="Challenge"
                                        asp-route-scheme="@provider.AuthenticationScheme"
                                        asp-route-returnUrl="@Model.ReturnURL"
                                        class="btn btn-secondary">
                                        @provider.DisplayName
                                    </a>
                                </li>
                            }
                        </ul>
                    </div>
                </div>
            </div>
        }
        @if(!Model.EnableLocalLogin && !Model.VisibleExternalProviderEnumerable.Any())
        {
            <div class="alert alert-warning">
                <strong>Invalid login request</strong>
                There are no login schemes configured for this request.
            </div>
        }
    </div>
</div>

 

▶ Views/Account/Logout.cshtml

@model LogoutViewModel
<div class="logout-page">
    <div class="lead">
        <h1>Logout</h1>
        <p>Would you like to logut of IdentityServer?</p>
    </div>
    <form asp-action="Logout">
        <input type="hidden" name="logoutID" value="@Model.LogoutID" />
        <div class="form-group">
            <button class="btn btn-primary">Yes</button>
        </div>
    </form>
</div>

 

▶ Views/Account/LoggedOut.cshtml

@model LoggedOutViewModel
@{ 
    ViewData["signed-out"] = true;
}
<div class="logged-out-page">
    <h1>
        Logout
        <small>You are now logged out</small>
    </h1>
    @if(Model.PostLogoutRedirectURI != null)
    {
        <div>
            Click <a class="PostLogoutRedirectUri" href="@Model.PostLogoutRedirectURI">here</a> to return to the
            <span>@Model.ClientName</span> application.
        </div>
    }
    @if(Model.LogOutIframeURL != null)
    {
        <iframe
            class="signout"
            width="0"
            height="0"
            src="@Model.LogOutIframeURL">
        </iframe>
    }
</div>
@section scripts
{
    @if(Model.AutomaticRedirectAfterLogOut)
    {
        <script src="~/js/signout-redirect.js"></script>
    }
}

 

▶ Views/Account/AccessDenied.cshtml

<div class="container">
    <div class="lead">
        <h1>Access Denied</h1>
        <p>You do not have access to that resource.</p>
    </div>
</div>

 

[TestAPIServer 프로젝트]

▶ Properties/launchSettings.json

{
    "iisSettings" :
    {
        "windowsAuthentication"   : false, 
        "anonymousAuthentication" : true, 
        "iisExpress"              :
        {
            "applicationUrl" : "http://localhost:50010",
            "sslPort"        : 44310
        }
    },
    "profiles" :
    {
        "IIS Express" :
        {
            "commandName"          : "IISExpress",
            "launchBrowser"        : true,
            "environmentVariables" :
            {
                "ASPNETCORE_ENVIRONMENT" : "Development"
            }
        },
        "TestIdentityServer" :
        {
            "commandName"          : "Project",
            "launchBrowser"        : true,
            "applicationUrl"       : "https://localhost:5001;http://localhost:5000",
            "environmentVariables" :
            {
                "ASPNETCORE_ENVIRONMENT" : "Development"
            }
        }
    }
}

 

▶ Startup.cs

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;

namespace TestAPIServer
{
    /// <summary>
    /// 시작
    /// </summary>
    public class Startup
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 서비스 컬렉션 구성하기 - ConfigureServices(services)

        /// <summary>
        /// 서비스 컬렉션 구성하기
        /// </summary>
        /// <param name="services">서비스 컬렉션</param>
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthentication("Bearer")
                .AddJwtBearer
                (
                    "Bearer",
                    options =>
                    {
                        options.Authority = "https://localhost:44300";

                        options.TokenValidationParameters = new TokenValidationParameters
                        {
                            ValidateAudience = false
                        };
                    }
                );

            services.AddAuthorization
            (
                options =>
                {
                    options.AddPolicy
                    (
                        "APIScope",
                        policy =>
                        {
                            policy.RequireAuthenticatedUser();
                            policy.RequireClaim("scope", "API1");
                        }
                    );
                }
            );

            services.AddControllersWithViews();
        }

        #endregion
        #region 구성하기 - Configure(app, environment)

        /// <summary>
        /// 구성하기
        /// </summary>
        /// <param name="app">애플리케이션 빌더</param>
        /// <param name="environment">웹 호스트 환경</param>
        public void Configure(IApplicationBuilder app, IWebHostEnvironment environment)
        {
            if(environment.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

            app.UseAuthentication();

            app.UseAuthorization();

            app.UseEndpoints
            (
                endpoints =>
                {
                    endpoints.MapDefaultControllerRoute();
                }
            );
        }

        #endregion
    }
}

 

▶ Controllers/IdentityController.cs

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Linq;

namespace TestAPIServer.Controllers
{
    /// <summary>
    /// 신원 컨트롤러
    /// </summary>
    public class IdentityController : Controller
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 인덱스 페이지 처리하기 - Index()

        /// <summary>
        /// 인덱스 페이지 처리하기
        /// </summary>
        /// <returns>액션 결과</returns>
        [HttpGet]
        [Authorize("APIScope")]
        public IActionResult Index()
        {
            return new JsonResult(from claim in User.Claims select new { claim.Type, claim.Value });
        }

        #endregion
    }
}

 

[TestClient 프로젝트]

▶ Properties/launchSettings.json

{
    "iisSettings" :
    {
        "windowsAuthentication"   : false,
        "anonymousAuthentication" : true,
        "iisExpress"              :
        {
            "applicationUrl" : "http://localhost:50020",
            "sslPort"        : 44320
        }
    },
    "profiles" :
    {
        "IIS Express" :
        {
            "commandName"          : "IISExpress",
            "launchBrowser"        : true,
            "environmentVariables" :
            {
                "ASPNETCORE_ENVIRONMENT" : "Development"
            }
        },
        "TestClient" :
        {
            "commandName"          : "Project",
            "launchBrowser"        : true,
            "applicationUrl"       : "https://localhost:5001;http://localhost:5000",
            "environmentVariables" :
            {
                "ASPNETCORE_ENVIRONMENT" : "Development"
            }
        }
    }
}

 

▶ Startup.cs

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.IdentityModel.Tokens.Jwt;

namespace TestClient
{
    /// <summary>
    /// 시작
    /// </summary>
    public class Startup
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 서비스 컬렉션 구성하기 - ConfigureServices(services)

        /// <summary>
        /// 서비스 컬렉션 구성하기
        /// </summary>
        /// <param name="services">서비스 컬렉션</param>
        public void ConfigureServices(IServiceCollection services)
        {
            JwtSecurityTokenHandler.DefaultMapInboundClaims = false;

            services.AddAuthentication
            (
                options =>
                {
                    options.DefaultScheme          = "Cookies";
                    options.DefaultChallengeScheme = "oidc";
                }
            )
            .AddCookie("Cookies")
            .AddOpenIdConnect
            (
                "oidc",
                options =>
                {
                    options.Authority                     = "https://localhost:44300";
                    options.ClientId                      = "CLIENTID0002";
                    options.ClientSecret                  = "CLIENTSECRET0002";
                    options.ResponseType                  = "code";
                    options.SaveTokens                    = true;
                    options.GetClaimsFromUserInfoEndpoint = true;

                    options.Scope.Add("profile"       );
                    options.Scope.Add("API1"          );
                    options.Scope.Add("offline_access");
                }
            );

            services.AddControllersWithViews();
        }

        #endregion
        #region 구성하기 - Configure(app, environment)

        /// <summary>
        /// 구성하기
        /// </summary>
        /// <param name="app">애플리케이션 빌더</param>
        /// <param name="environment">웹 호스트 환경</param>
        public void Configure(IApplicationBuilder app, IWebHostEnvironment environment)
        {
            if(environment.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");

                app.UseHsts();
            }

            app.UseHttpsRedirection();

            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthentication();

            app.UseAuthorization();

            app.UseEndpoints
            (
                endpoints =>
                {
                    endpoints.MapDefaultControllerRoute().RequireAuthorization();
                }
            );
        }

        #endregion
    }
}

 

▶ Controllers/HomeController.cs

using Microsoft.AspNetCore.Mvc;
using System.Diagnostics;

using TestClient.Models;

namespace TestClient.Controllers
{
    /// <summary>
    /// 홈 컨트롤러
    /// </summary>
    public class HomeController : Controller
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 인덱스 페이지 처리하기 - Index()

        /// <summary>
        /// 인덱스 페이지 처리하기
        /// </summary>
        /// <returns>액션 결과</returns>
        public IActionResult Index()
        {
            return View();
        }

        #endregion
        #region 개인 정보 보호 정책 페이지 처리하기 - Privacy()

        /// <summary>
        /// 개인 정보 보호 정책 페이지 처리하기
        /// </summary>
        /// <returns>액션 결과</returns>
        public IActionResult Privacy()
        {
            return View();
        }

        #endregion
        #region 에러 페이지 처리하기 - Error()

        /// <summary>
        /// 에러 페이지 처리하기
        /// </summary>
        /// <returns>액션 결과</returns>
        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
        public IActionResult Error()
        {
            return View(new ErrorViewModel { RequestID = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
        }

        #endregion
        #region 로그아웃 페이지 처리하기 - Logout()

        /// <summary>
        /// 로그아웃 페이지 처리하기
        /// </summary>
        /// <returns>액션 결과</returns>
        public IActionResult Logout()
        {
            return SignOut("Cookies", "oidc");
        }

        #endregion
        #region API 호출 페이지 처리하기 - CallAPI()

        /// <summary>
        /// API 호출 페이지 처리하기
        /// </summary>
        /// <returns>액션 결과 태스크</returns>
        public async Task<IActionResult> CallAPI()
        {
            string accessToken = await HttpContext.GetTokenAsync("access_token");

            HttpClient client = new HttpClient();

            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

            string content = await client.GetStringAsync("https://localhost:44310/identity/index");

            ViewData["JSON"] = JArray.Parse(content).ToString();

            return View();
        }

        #endregion
    }
}

 

▶ Views/Home/CallAPI.cshtml

<h1>API 호출 페이지</h1>
<hr />
<pre>@ViewData["JSON"]</pre>
728x90
반응형
그리드형
Posted by 사용자 icodebroker

댓글을 달아 주세요