[C#/ASP.NET MVC/.NETCORE] IdentityServer4 신원 서버에서 사용자 인증하고 API 호출하기
C#/ASP.NET MVC 2020. 11. 4. 22:52728x90
반응형
728x170
[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"
}
}
}
}
728x90
▶ 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
}
}
300x250
▶ 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.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using TestIdentityServer.Models;
namespace TestIdentityServer
{
/// <summary>
/// 시작
/// </summary>
public class Startup
{
//////////////////////////////////////////////////////////////////////////////////////////////////// Method
////////////////////////////////////////////////////////////////////////////////////////// Public
#region 서비스 컬렉션 구성하기 - ConfigureServices(services)
/// <summary>
/// 서비스 컬렉션 구성하기
/// </summary>
/// <param name="services">서비스 컬렉션</param>
public void ConfigureServices(IServiceCollection services)
{
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryIdentityResources(Configuration.GetIdentityResourceList())
.AddInMemoryApiScopes(Configuration.GetAPIScopeList())
.AddInMemoryClients(Configuration.GetClientList())
.AddTestUsers(TestUserData.UserList);
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.UseStaticFiles();
app.UseRouting();
app.UseIdentityServer();
app.UseAuthorization();
app.UseEndpoints
(
endpoints =>
{
endpoints.MapControllerRoute
(
name : "default",
pattern : "{controller=Home}/{action=Index}/{id?}"
);
}
);
}
#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
반응형
그리드형(광고전용)
댓글을 달아 주세요