WinForms/WPF 앱에 Entra ID 인증 넣기 ── MSAL.NET과 WAM 브로커 실무 구성
· 小村 豪 · Windows, C#, .NET, WinForms, WPF, Entra ID, 인증, 보안, 기술 상담
“사내 업무 앱마다 로그인 화면을 만들어서, 비밀번호를 자체 데이터베이스로 관리하고 있다. 퇴사자가 나올 때마다 앱마다 계정을 정지시키러 다니는 게 힘들다.” “Microsoft 365는 전사에 도입했으니, 그 계정으로 그대로 로그인시킬 수는 없나.” 데스크톱 앱 개선 상담에서 최근 몇 년 사이 꾸준히 늘고 있는 주제입니다. 비밀번호 유출 사고나 제로 트러스트 대응이라는 맥락에서 정보 시스템 부서로부터 “자체 비밀번호 관리를 그만두라”는 말을 들었다는 형태로 오는 경우도 있습니다.
결론부터 말하면, Microsoft 365를 사용하는 조직이라면 WinForms / WPF 사내 앱의 로그인을 Entra ID(구 Azure AD) 로 옮기는 것은 합리적인 투자입니다. 앱은 비밀번호를 전혀 보관하지 않게 되고, 다중 인증・조건부 액세스・로그인 로그 같은 테넌트 쪽 방어와 감사가 그대로 사내 앱에도 적용됩니다. 구현도 MSAL.NET이라는 라이브러리와 수십 줄의 코드로 끝나는 범위입니다.
다만 데스크톱 앱 특유의 함정이 몇 가지 있습니다. “사용자 이름과 비밀번호를 텍스트박스로 받아 뒤에서 인증하는” 옛날식 구현(ROPC)은 공식적으로 폐지 방향이며, 신규로 채택해서는 안 됩니다. 토큰 캐시를 영속화하지 않으면 실행할 때마다 로그인 화면이 나오고, Windows에서는 브로커(WAM)를 사용하는지 여부에 따라 경험과 보안이 크게 달라집니다. 이 글에서는 개념의 최소한의 정리부터 앱 등록, MSAL.NET 구현, WAM, 캐시, 도입 판단, 운영상의 함정까지를 한 번에 정리합니다.
1. 먼저 결론
- ID와 비밀번호의 자체 관리를 그만두고 Entra ID로 옮기면, 비밀번호 보관・재발급 대응・퇴사자 계정 정지・로그인 감사가 전부 테넌트 쪽 업무가 됩니다. 앱 쪽의 책임 범위가 극적으로 줄어드는 것이 가장 큰 이점입니다.
- 데스크톱 앱은 퍼블릭 클라이언트입니다. exe는 배포된 곳에서 분석될 수 있으므로 클라이언트 시크릿을 가질 수 없습니다(가져서도 안 됩니다). 앱 등록도 퍼블릭 클라이언트로 구성합니다. 1
- 사용자 이름과 비밀번호를 앱이 직접 받는 ROPC(Resource Owner Password Credentials)는 공식적으로 「폐지(deprecated)」라고 명시되어 있고, 마이그레이션 가이드가 나와 있습니다. MFA・조건부 액세스와 공존하지 못해, 실질적으로 쓸 수 없게 되는 방향입니다. 신규 도입은 금지라고 생각하십시오. 23
- 구현은 MSAL.NET(Microsoft.Identity.Client)으로, 먼저
AcquireTokenSilent를 호출하고,MsalUiRequiredException이 나왔을 때만AcquireTokenInteractive를 호출하는 패턴이 거의 유일한 기본형입니다. 4 - Windows에서는 WAM(Web Account Manager) 브로커를 통한 인증이 권장됩니다. Windows에 로그인되어 있는 계정과의 SSO, 조건부 액세스・Windows Hello・FIDO 키 지원, 리프레시 토큰의 디바이스 바인딩을
WithBroker한 줄로 얻을 수 있습니다. 5 - 토큰 캐시 영속화를 잊으면, 앱을 재시작할 때마다 로그인 화면이 나옵니다.
Microsoft.Identity.Client.Extensions.Msal의 암호화 캐시를 처음부터 넣어 두십시오. 6 - Entra 인증은 네트워크를 전제로 하는 방식입니다. 완전히 오프라인으로 동작해야 하는 현장 앱에서는 성립하지 않으므로, 8장의 판단표로 도입 가능 여부를 먼저 판단하십시오.
2. 전체 그림 ── 자체 비밀번호 관리를 그만둔다는 것
2.1 자체 관리의 무엇이 문제인가
업무 앱이 자체 사용자 테이블로 비밀번호를 관리하고 있으면, 다음 책임이 모두 앱(=개발한 우리)에게 쏠립니다.
- 보관: 해시 방식의 선정과 구현(솔트 없는 MD5로 방치된 15년 된 테이블을 아직도 볼 수 있습니다)
- 운영: 비밀번호 재발급 문의 대응, 잠금, 초기 비밀번호 배포
- 라이프사이클: 퇴사・부서 이동 시 계정 정지. 앱이 5개면 5번 정지시키러 다녀야 합니다
- 감사: 누가 언제 로그인했는지 기록하고 보존. 다중 인증은 사실상 구현이 불가능합니다
Entra ID에 인증을 위임하면, 이 4가지가 앱의 코드에서 사라지고 테넌트 관리로 일원화됩니다. 퇴사자는 Entra ID 계정을 비활성화하면 모든 앱에서 즉시 로그인이 불가능해지고, 로그인 로그도 자동으로 남습니다. Microsoft 365를 이미 도입한 조직에서 자체 인증을 계속 유지할 이유는 거의 없습니다. 참고로 Google Workspace 조직에서 Windows 로그온 자체를 Google 계정으로 옮기는 대응 방안은 「GCPW란 무엇인가」에 정리했습니다.
2.2 최소한의 개념 ── 퍼블릭 클라이언트와 토큰
OAuth 2.0 / OpenID Connect의 교과서적인 설명은 생략하고, 데스크톱 앱 구현에 필요한 개념만 나열합니다.
| 개념 | 데스크톱 앱에서의 의미 |
|---|---|
| 퍼블릭 클라이언트 | exe・모바일 앱처럼 비밀(클라이언트 시크릿)을 안전하게 보관할 수 없는 앱. 사용자를 대신해서만 토큰을 얻을 수 있음 |
| 기밀 클라이언트(confidential client) | 웹 서버나 데몬처럼 시크릿이나 인증서를 보관할 수 있는 앱. 데스크톱 앱은 이쪽이 아닙니다 |
| ID 토큰 | “이 사람이 누구인지”를 나타내는 JWT. 로그인 기능만 필요하다면 이것으로 충분 |
| 액세스 토큰 | 특정 API(Microsoft Graph나 자사 웹 API)를 호출하기 위한 통행증. 대상(audience)과 스코프가 담겨 있음 |
| 리프레시 토큰 | 위 두 가지를 대화 없이 갱신하기 위한 토큰. MSAL이 캐시 안에서 자동으로 관리하며 앱에서 직접 보이지 않음 |
중요한 것은 첫 번째 줄입니다. exe는 배포된 곳에서 분석・역컴파일될 수 있으므로, 심어 넣은 “비밀”은 비밀이 되지 못합니다. 그래서 시크릿 없이 동작하는 퍼블릭 클라이언트로 등록하고, 인증 자체(비밀번호 입력이나 MFA)는 브라우저 또는 OS의 브로커에 맡기고, 앱은 토큰만 받는 설계가 됩니다. 앱이 사용자의 비밀번호를 건드리지 않는 것 자체가 이 구조의 근간입니다.
2.3 ROPC는 끝난 방식 ── 확인해 본 결과
옛날식 사고방식으로는 “우리 로그인 화면에서 사용자 이름과 비밀번호를 받고, 뒤에서 Entra ID에 검증을 맡기면 된다”고 생각하기 쉽습니다. 이것이 ROPC(사용자 이름・비밀번호 직접 전달)이며, MSAL.NET에도 AcquireTokenByUsernamePassword로 남아 있기는 하지만, 현재 공식 문서의 서술은 명확합니다.
- 퍼블릭 클라이언트용 ROPC는 「보안 위험 때문에 폐지(deprecated)되었다」고 명시되어 있고, 더 안전한 흐름으로의 마이그레이션 가이드가 공개되어 있습니다. 3
- ROPC는 MFA・조건부 액세스와 호환되지 않습니다. 테넌트에서 MFA가 필수로 설정된 사용자는 이 흐름에서는 차단되어 로그인할 수 없습니다. 2
- SSO가 동작하지 않고, 개인 Microsoft 계정도 사용할 수 없으며, 패스워드리스(FIDO, Authenticator) 계정도 로그인할 수 없습니다. 2
- Microsoft의 웹 API 쪽에서도 MFA를 마친 토큰만 받는 움직임이 진행되고 있으며, 공식 문서 자체가 “ROPC에 의존하는 앱은 차단된다(locked out). 데스크톱 앱은 브로커 기반 인증으로 이전해야 한다”고 적고 있습니다. 2
MFA 필수화는 테넌트 쪽 설정으로 언제든 일어날 수 있으므로, “지금은 동작하니까”라며 ROPC를 채택하면 어느 날 갑자기 전체 사용자가 로그인할 수 없게 됩니다. 기존 앱이 ROPC로 동작하고 있는 경우도 이전을 전제로 계획을 세우십시오. 데스크톱 앱에서 사용해도 되는 토큰 획득 방식은 실질적으로 다음 2가지입니다.
| 흐름 | 사용처 |
|---|---|
| 대화형(브로커 / 브라우저) | 일반적인 GUI 앱. 기본 선택지 |
| 디바이스 코드 흐름 | 브라우저를 표시할 수 없는 환경(SSH로 접속한 콘솔 등). URL과 코드를 표시하고, 다른 디바이스의 브라우저에서 로그인하게 함 |
3. 앱 등록 ── Entra 관리 센터에서의 설정
코드를 작성하기 전에, 테넌트에 앱을 등록합니다. 개발자가 직접 할 수 없는 경우, 이 절의 내용을 그대로 정보 시스템 부서에 보낼 의뢰서로 사용하십시오.
3.1 등록 본체
Microsoft Entra 관리 센터(entra.microsoft.com)의 [앱 등록] → [새 등록]에서 만듭니다. 7
- 이름: 동의 화면이나 로그인 로그에 표시되므로, “재고 관리 시스템”처럼 업무 쪽에서 알아볼 수 있는 이름으로 합니다.
- 지원되는 계정 유형: 사내 앱이라면 “이 조직 디렉터리에만 있는 계정“(단일 테넌트) 하나뿐입니다. 다중 테넌트는 여러 조직에 배포하는 제품일 때만입니다.
- 등록 후 표시되는 애플리케이션(클라이언트) ID와 디렉터리(테넌트) ID를 기록해 앱 설정에 넣습니다(둘 다 비밀 정보가 아닙니다).
3.2 리디렉션 URI ── 플랫폼은 “모바일 및 데스크톱 애플리케이션”
인증 후 토큰을 받을 위치를 선언하는 것입니다. [인증] → [플랫폼 추가] → [모바일 및 데스크톱 애플리케이션]을 선택하고, 사용할 인증 방식에 맞는 URI를 등록합니다. 1
| 인증 방식 | 등록할 리디렉션 URI |
|---|---|
| WAM 브로커(기본 선택지, 5장) | ms-appx-web://microsoft.aad.brokerplugin/{클라이언트 ID} |
| 시스템 브라우저 | http://localhost |
| 임베디드 브라우저 | https://login.microsoftonline.com/common/oauth2/nativeclient |
WAM용 ms-appx-web://...는 MSAL 쪽 코드에는 쓰지 않지만, 앱 등록 쪽에는 필수입니다. 8 WAM을 쓸 수 없는 환경에서의 브라우저 폴백(5장)을 고려하면, 표의 3가지를 처음부터 전부 등록해 두는 것이 실무적입니다. 특히 주의할 것은 WithDefaultRedirectUri()의 동작으로, 해석되는 값이 플랫폼에 따라 다릅니다. .NET Framework에서는 https://login.microsoftonline.com/common/oauth2/nativeclient, .NET(Core 이후)에서는 http://localhost로 해석됩니다. 9 ms-appx-web과 http://localhost만 등록한 .NET Framework 앱이 WAM에서 브라우저로 폴백하면, nativeclient와의 불일치로 인증 오류가 됩니다. 3가지를 모두 등록하거나, WithRedirectUri(...)로 명시적으로 고정하십시오. “Web” 플랫폼 쪽에 등록해 버려서 인증 오류가 나는 것도 흔한 실수입니다.
3.3 API 액세스 권한과 관리자 동의
[API 권한]에서 앱이 호출할 API의 위임된 액세스 권한(delegated permission)을 추가합니다. 로그인과 프로필 표시만 필요하다면, 기본으로 부여되는 Microsoft Graph의 User.Read로 충분합니다.
추가했다면 [(테넌트 이름)에 대한 관리자 동의 부여]를 실행하게 합니다. 7 이렇게 하면 최초 로그인 시 사용자별 동의 대화상자가 나오지 않게 됩니다. 사용자 자신의 동의가 비활성화된 테넌트에서는 관리자 동의 없이는 최초 로그인이 “관리자 승인이 필요합니다”에서 멈추므로, 사내 배포 앱에서는 배포 전에 관리자 동의까지 끝내 두는 것이 원칙입니다.
3.4 “퍼블릭 클라이언트 흐름 허용” 플래그
[인증]의 고급 설정에 있는 “퍼블릭 클라이언트 흐름 허용“은 디바이스 코드 흐름이나 통합 Windows 인증처럼 리디렉션 URI를 사용하지 않는 흐름을 쓸 때 “예”로 설정합니다. 1 대화형(브라우저 / 브로커)만 쓴다면 필수는 아닙니다. 참고로 이 앱 등록에는 클라이언트 시크릿도 인증서도 만들지 않습니다. “인증서 및 비밀”란이 비어 있는 것이 퍼블릭 클라이언트의 올바른 상태입니다(혼동하는 상담이 많아 9장에서 다시 다룹니다).
4. MSAL.NET으로 구현 ── Silent→Interactive의 기본형
NuGet으로 Microsoft.Identity.Client를 추가합니다. 구현 패턴은 하나만 외우면 됩니다. 반드시 AcquireTokenSilent를 먼저 호출하고, MsalUiRequiredException을 받았을 때만 대화형으로 폴백합니다. AcquireTokenInteractive는 캐시를 전혀 보지 않는 설계이므로, 바로 호출하면 매번 로그인 화면이 나옵니다. 4
using Microsoft.Identity.Client;
public sealed class AuthService
{
private const string ClientId = "애플리케이션(클라이언트) ID";
private const string TenantId = "디렉터리(테넌트) ID";
private static readonly string[] Scopes = { "User.Read" };
private readonly IPublicClientApplication _app;
public AuthService()
{
_app = PublicClientApplicationBuilder.Create(ClientId)
.WithAuthority(AzureCloudInstance.AzurePublic, TenantId)
.WithRedirectUri("http://localhost") // 시스템 브라우저용
.Build();
// 실제 운영에서는 여기서 토큰 캐시 영속화를 등록한다(6장)
}
public async Task<AuthenticationResult> SignInAsync(IntPtr ownerHwnd)
{
// 1. 캐시된 계정으로의 사일런트 획득을 반드시 먼저 시도
var accounts = await _app.GetAccountsAsync();
var account = accounts.FirstOrDefault();
try
{
return await _app.AcquireTokenSilent(Scopes, account)
.ExecuteAsync();
}
catch (MsalUiRequiredException)
{
// 2. 대화가 필요할 때만 로그인 화면을 표시한다.
// .NET Framework의 기본값은 옛날식 임베디드 WebView이므로,
// http://localhost 리디렉션 = 시스템 브라우저를 명시한다
// (.NET 6+는 원래 시스템 브라우저만 사용)
return await _app.AcquireTokenInteractive(Scopes)
.WithAccount(account)
.WithParentActivityOrWindow(ownerHwnd)
.WithUseEmbeddedWebView(false)
.ExecuteAsync();
}
}
}
호출하는 쪽에서 소유 창의 핸들을 넘겨줍니다. 인증 대화상자가 앱 뒤에 숨어버리는 사고를 막기 위한 것으로, WAM에서는 필수입니다. 5 한 가지 더, WithUseEmbeddedWebView(false)는 .NET Framework 앱에서는 생략하지 마십시오. .NET Framework의 대화형 인증 기본값은 임베디드 WebView이고, http://localhost 리디렉션은 시스템 브라우저용이기 때문입니다(조합이 어긋나면 조건부 액세스나 Windows Hello / FIDO가 동작하지 않는 옛날식 임베디드 브라우저로 떨어지거나, 리디렉션 URI 불일치가 됩니다). 10 .NET 6 이후는 임베디드 WebView 자체가 없어서 항상 시스템 브라우저이므로, 이 지정은 중복이지만 해가 되지는 않습니다.
// WinForms (Form의 메서드 안)
var result = await _authService.SignInAsync(this.Handle);
// WPF
var hwnd = new System.Windows.Interop.WindowInteropHelper(this).Handle;
var result = await _authService.SignInAsync(hwnd);
this.Text = $"로그인 중: {result.Account.Username}";
짚어 둘 포인트를 보충합니다.
IPublicClientApplication은 앱에서 인스턴스 하나를 계속 사용합니다. 인스턴스마다 캐시를 가지므로, 호출할 때마다Create하면 사일런트 획득이 동작하지 않습니다.MsalUiRequiredException은 “오류”가 아니라 “대화가 필요하다”는 정상적인 제어 흐름입니다. 최초 실행, 리프레시 토큰 만료, 조건부 액세스 요구 변경 등에서 발생합니다.- API를 호출하기 바로 직전에 매번
AcquireTokenSilent를 호출하는 것이 올바른 사용법입니다. 캐시에 유효한 토큰이 있으면 즉시 반환되고, 만료가 가까우면 자동으로 갱신됩니다. 4 액세스 토큰을 직접 보관하며 수명을 관리해서는 안 됩니다. - UI 스레드에서
.Result나.Wait()로 기다리면 데드락이 됩니다(「WPF/WinForms의 async와 UI 스레드를 한 장으로 정리」 참조).
5. WAM 브로커 ── Windows에서의 권장 구성
4장의 코드는 브라우저를 여는 구성이지만, Windows에서는 한 단계 더 나은 방법이 있습니다. WAM(Web Account Manager)은 Windows 10(1703 이후)과 Windows Server 2019 이후에 내장된 인증 브로커로, 공식 문서가 꼽는 이점은 다음 4가지입니다. 5
- 보안 강화: 리프레시 토큰이 디바이스에 바인딩되어, 탈취되어도 다른 디바이스에서는 쓸 수 없게 됩니다(토큰 보호). 보안 개선이 OS 쪽 업데이트로 계속 들어옵니다.
- 기능 지원: Windows Hello, 조건부 액세스, FIDO 키 같은 OS・서비스 연동 인증 기능을, 추가 코드 없이 사용할 수 있습니다.
- 시스템 통합: Windows에 로그인되어 있는 계정이 내장 계정 선택기에 표시되므로, 많은 경우 비밀번호 입력 없이 로그인이 완료됩니다. 실질적인 SSO입니다.
- 토큰 보호: 조건부 액세스의 토큰 보호 정책에 대응할 수 있습니다.
사내 PC가 Entra 참가(또는 하이브리드 참가)되어 있으면, “앱 실행 → Windows 계정 선택 → 즉시 로그인 완료”라는 경험이 되어, 비밀번호를 아무 데도 입력하지 않습니다.
5.1 구현 ── WithBroker와 패키지
WAM을 사용하려면 MSAL.NET 4.52.0 이상과 추가 패키지 Microsoft.Identity.Client.Broker가 필요합니다. 5 4장의 빌더에 WithBroker를 추가합니다.
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.Broker; // WithBroker(BrokerOptions)용
var brokerOptions = new BrokerOptions(BrokerOptions.OperatingSystems.Windows)
{
Title = "재고 관리 시스템" // 계정 선택기에 표시되는 제목
};
_app = PublicClientApplicationBuilder.Create(ClientId)
.WithAuthority(AzureCloudInstance.AzurePublic, TenantId)
.WithDefaultRedirectUri()
.WithParentActivityOrWindow(() => _ownerHwnd) // WAM에서는 필수
.WithBroker(brokerOptions)
.Build();
사일런트 획득 쪽도 한 줄로 더 강화할 수 있습니다. 캐시에 계정이 없을 때, PublicClientApplication.OperatingSystemAccount를 넘기면 “지금 Windows에 로그인되어 있는 계정”으로 사일런트 로그인을 시도할 수 있습니다. 최초 실행부터 대화상자 없이 로그인이 결정되는, 공식 권장 패턴입니다. 8
var accounts = await _app.GetAccountsAsync();
var account = accounts.FirstOrDefault()
?? PublicClientApplication.OperatingSystemAccount;
try
{
return await _app.AcquireTokenSilent(Scopes, account).ExecuteAsync();
}
catch (MsalUiRequiredException)
{
return await _app.AcquireTokenInteractive(Scopes).ExecuteAsync();
}
이와 함께, 3.2절에서 다룬 대로 앱 등록 쪽에 ms-appx-web://microsoft.aad.brokerplugin/{클라이언트 ID}를 “모바일 및 데스크톱 애플리케이션” 플랫폼으로 등록해 두어야 합니다. 5 이것을 잊으면 브로커 오류로 대화형 인증이 실패합니다. 한 가지 더, 위 예제의 WithDefaultRedirectUri()는 WAM을 사용할 수 없어 브라우저로 폴백했을 때의 리디렉션 URI를 결정하는 것으로, 해석되는 값이 플랫폼에 따라 다릅니다(.NET Framework → nativeclient, .NET → http://localhost). 9 3.2절 표의 3가지를 모두 등록해 두었다면 어느 쪽이든 통과하지만, 등록을 좁히고 싶다면 WithRedirectUri(...)로 명시하십시오.
5.2 WAM의 제약 ── 모르면 걸려 넘어지는 부분
| 제약 | 내용 |
|---|---|
| OS | Windows 10 (1703)+ / Windows Server 2019+. 그 이전・Mac・Linux에서는 자동으로 브라우저로 폴백합니다5 |
| ID 공급자 | Entra ID 전용. Azure AD B2C・AD FS의 authority는 지원되지 않습니다(브라우저로 폴백)5 |
| 실행 컨텍스트 | 대화형 사용자 세션에서 UI를 표시할 수 있다는 것이 전제입니다. Windows 서비스, 태스크 스케줄러(사용자 세션 밖), runas로 다른 사용자로 실행할 때는 설계상 오류가 됩니다5 |
세 번째 줄이 특히 중요합니다. “화면이 있는 앱에서는 동작하는데, 같은 코드를 야간 배치에 재사용하면 실패한다”는 것은 사양입니다. 무인 실행은 사용자 위임이 아니라 애플리케이션 권한(기밀 클라이언트)으로 설계를 나눠야 하는 영역입니다. 폴백이 사양으로 내장되어 있으므로, “WAM을 먼저 시도하고, 안 되면 브라우저”를 코드 한 줄로 구현할 수 있는 것이 MSAL의 좋은 점입니다.
6. 토큰 캐시 영속화 ── 재시작할 때마다 로그인 화면을 띄우지 않으려면
MSAL.NET의 토큰 캐시는 기본적으로 메모리상에만 존재하며, 데스크톱 앱에서는 영속화 구현이 앱 쪽의 책임입니다. 영속화하지 않으면, 프로세스를 재시작할 때마다 AcquireTokenSilent가 실패해 대화형 로그인이 됩니다. 4 “도입 테스트에서는 괜찮았는데, 매일 아침 로그인 화면이 나온다는 현장의 불만이 왔다”는 상담의 원인은 거의 이것입니다.
공식 권장은 크로스 플랫폼 캐시 라이브러리 Microsoft.Identity.Client.Extensions.Msal(NuGet)을 사용하는 것입니다. 6
using Microsoft.Identity.Client.Extensions.Msal;
var storageProperties = new StorageCreationPropertiesBuilder(
"msal_cache.dat",
Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"KomuraSoft", "InventoryApp"))
.Build();
var cacheHelper = await MsalCacheHelper.CreateAsync(storageProperties);
cacheHelper.RegisterCache(_app.UserTokenCache); // Build() 직후에 한 번만 등록
Windows에서는 캐시가 암호화되어 저장됩니다. 공식 문서에 실린 자체 구현 예시는 ProtectedData(DPAPI, DataProtectionScope.CurrentUser)로 토큰을 암호화해 파일로 저장하는 형태로, Extensions.Msal은 그것을 제품 품질로 마무리한 라이브러리라는 위치입니다. 6 “사용자별 비밀은 사용자 범위의 DPAPI로 지킨다”는 원칙은 「Windows 앱의 민감 정보 저장 - DPAPI로 평문 설정을 피한다」에서 다룬 설정 파일 이야기와 같습니다. 토큰 캐시를 평문 JSON으로 저장하는 자체 구현은, 연결 문자열을 평문으로 저장하는 것과 같은 심각도로 다루십시오.
운영 측면의 주의점을 3가지 짚습니다.
- WAM을 사용하는 경우에도 캐시 영속화는 필요합니다. MSAL은 ID 토큰과 계정 메타데이터를 여전히 자체 캐시에 저장하기 때문입니다. 8
- 저장 위치는
%LOCALAPPDATA%\회사명\앱명이 기본입니다. DPAPI의 바인딩 관계로 다른 PC・다른 사용자에서는 복호화할 수 없지만, 사일런트 획득이 실패해 재로그인이 되는 것뿐이라 실제 피해는 없습니다. - “로그아웃”은
GetAccountsAsync로 나열한 계정을RemoveAsync로 지워서 구현합니다. 캐시 파일 삭제는 필요 없습니다. 다만RemoveAsync가 지우는 것은 MSAL의 로컬 캐시뿐이며, WAM・브라우저・Windows 로그인 쪽의 세션은 그대로 남습니다. 다음 대화형 로그인에서 같은 계정이 사일런트로 재로그인될 수 있으므로, 공유 PC에서 계정 전환이 확실히 이루어지도록 하려면AcquireTokenInteractive에WithPrompt(Prompt.SelectAccount)를 붙여 반드시 계정 선택 화면을 표시하거나, 요구 사항에 따라 테넌트의 로그아웃 엔드포인트도 함께 사용하는 등, “로컬 캐시 삭제”와 “진짜 로그아웃”을 구분해서 설계하십시오.
7. 획득한 토큰으로 무엇을 하는가 ── 3가지 구성
인증이 통과한 뒤의 활용 방식은 3가지 패턴으로 나뉩니다. 어디까지 할지에 따라 필요한 설정도 달라집니다.
| 구성 | 사용하는 토큰 | 추가로 필요한 것 |
|---|---|---|
| (1) 로그인만 | ID 토큰(AuthenticationResult.Account / ClaimsPrincipal) |
없음(User.Read만으로 충분) |
| (2) Microsoft Graph 호출 | Graph용 액세스 토큰 | 호출할 API에 맞는 Graph 액세스 권한과 동의 |
| (3) 자사 웹 API 보호 | 자사 API용 액세스 토큰 | API 쪽의 앱 등록과 스코프 공개, API 쪽에서의 토큰 검증 |
7.1 로그인만 하는 구성 ── 가장 작게 시작하기
“자체 비밀번호 검증만 교체하고 싶고, 클라우드 API는 호출하지 않는다”면, 로그인 결과의 계정 정보를 앱 내 권한 테이블과 대조하는 것만으로 성립합니다. users 테이블의 키를 Entra의 오브젝트 ID(성이 바뀌는 등 UPN이 바뀌어도 변하지 않음)로 바꾸고, 비밀번호 열을 삭제합니다. 로컬 DB 설계를 바꾸지 않고 인증만 교체할 수 있으므로, 첫걸음으로 가장 추천하기 쉬운 구성입니다.
7.2 Microsoft Graph 호출하기
User.Read의 액세스 토큰을 그대로 Microsoft Graph에 던지면, 로그인한 사용자의 프로필이나 사진을 가져올 수 있습니다.
var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", result.AccessToken);
var me = await http.GetStringAsync("https://graph.microsoft.com/v1.0/me");
일정・메일 발송・Teams 알림 등으로 확장하는 경우, 해당하는 액세스 권한(Mail.Send 등)을 추가하고 관리자 동의를 다시 받습니다. 사내 앱의 알림 메일을 Graph로 옮기는 설계는 「중소기업을 위한 대량 메일 발송을 특정 서비스에 얽매이지 않고 설계하는 방법」에서 다룬 흐름과도 잘 맞습니다.
7.3 자사 웹 API 보호하기 ── audience와 스코프 검증까지
데스크톱 앱이 자사 웹 API를 호출하는 구성이라면, API 쪽에도 별도의 앱 등록을 만들고, api://{API의 클라이언트 ID}/access_as_user 같은 스코프를 공개해서, 데스크톱 쪽은 그 스코프로 토큰을 요청합니다. API 쪽에서 중요한 것은, [Authorize]를 붙이는 것만으로는 충분하지 않다고 공식적으로 명시되어 있다는 점입니다. 11 검증해야 할 것은 다음 3단계입니다.
- 서명과 발급자: 올바른 테넌트의 Entra ID가 발급한 JWT인지(ASP.NET Core + Microsoft.Identity.Web이라면 미들웨어가 처리)
- audience(
aud): 토큰의 대상이 이 API 자신인지. Graph용 토큰을 자사 API에 재사용시키지 않기 - 스코프(
scp클레임): 기대하는 스코프가 들어 있는지. Microsoft.Identity.Web이라면[RequiredScope("access_as_user")]속성으로 선언할 수 있습니다11
2와 3을 생략하면 “Entra의 토큰처럼 보이면 누구든 통과하는 API”가 만들어집니다. 「Windows 앱 개발의 보안 최소 체크리스트」의 통신・입력 검증 항목과 함께 설계 리뷰 대상으로 삼으십시오.
8. 도입 판단 ── 완전 사내 도구에 Entra 인증을 넣어야 하는가
모든 사내 앱에 넣어야 하는 것은 아닙니다. 판단표를 정리합니다.
| 상황 | 권장 | 이유 |
|---|---|---|
| Microsoft 365 / Entra ID를 전사 도입했고+앱에 로그인 개념이 있다 | 도입한다 | 자체 비밀번호 관리의 부채가 통째로 사라진다. 구현 비용이 작다 |
| 앱이 자사 웹 API나 클라우드 자원을 호출한다 | 도입한다 | API 보호에 인증 기반은 필수. 자체 토큰을 만드는 것보다 확실하다 |
| 감사 요건이 있다(누가 언제 사용했는지 기록, MFA 필수화) | 도입한다 | 로그인 로그・조건부 액세스가 테넌트 쪽에서 일원화된다 |
| 로그인 개념이 없는 단일 기능 도구(변환 도구, 뷰어 등) | 불필요 | 인증을 추가할 동기가 없다. Windows 로그온으로 충분하다 |
| 완전 오프라인 환경(폐쇄된 생산 라인, 반출 PC)에서 동작 | 불가 또는 설계 필요 | 최초 로그인과 토큰 갱신에 네트워크가 필수 |
| Entra ID 미도입(온프레미스 AD만, Google Workspace만) | 다른 방법을 검토 | 전자는 AD 인증(통합 Windows 인증), 후자는 Google 쪽 구조가 자연스러움 |
오프라인 요건은 특히 주의하십시오. AcquireTokenSilent는 캐시된 액세스 토큰이 유효한 동안(경험상 1시간 조금 넘는 정도)은 오프라인에서도 반환할 수 있지만, 만료되면 갱신에 네트워크가 필요합니다. 도입 전에, 이 수명 전제로 업무가 돌아가는지를 현장의 사용 패턴과 맞춰 볼 필요가 있습니다.
9. 운영상의 함정 ── 도입 후에 오는 문의
도입해서 끝나는 것이 아니라, 운영 단계에서 정기적으로 오는 문의가 있습니다. 미리 적어 둡니다.
- “어제까지 되던 게 갑자기 로그인이 안 된다”: 가장 의심할 대상은 조건부 액세스 정책 변경입니다. 정보 시스템 부서가 “등록되지 않은 디바이스 차단” 같은 것을 활성화하면, 앱은 아무것도 바꾸지 않았는데 로그인이 실패하기 시작합니다. 가장 빠른 배제 방법은 Entra 관리 센터의 로그인 로그에서 해당 사용자의 오류 원인을 보는 것입니다. WAM 구성으로 해 두면 정책 요구에 대한 대응력이 올라가, 이런 종류의 마찰 자체가 줄어듭니다. 5
- “시크릿 만료 알림이 왔는데, 이 앱은 괜찮은가”: 퍼블릭 클라이언트에는 시크릿도 인증서도 원래 없으므로, 만료도 없습니다. 이 문의가 오면, 기밀 클라이언트의 앱 등록과 혼동한 것이거나, 누군가가 퍼블릭 클라이언트용 등록에 불필요한 시크릿을 만든 것입니다(후자라면 지워도 됩니다). 시크릿 만료로 인한 중단 사고가 구조적으로 일어나지 않는 것이 이 구성의 숨은 이점입니다.
- “최초 실행 시 ‘관리자 승인이 필요합니다’라고 나온다”: 3.3절의 관리자 동의 누락입니다. 액세스 권한을 나중에 추가한 경우도, 추가분의 동의를 다시 받을 때까지 같은 메시지가 나옵니다.
- “야간 배치에 넣었더니 동작하지 않는다”: 5.2절대로 WAM은 대화형 세션을 전제로 합니다. 무인 처리는 위임된 사용자 토큰을 재사용하는 것이 아니라, 애플리케이션 권한으로 별도 설계해야 합니다.
- 배포와 업데이트: MSAL 쪽은 수정이 활발해서, 라이브러리 업데이트를 모든 단말에 배포하는 체계가 필요합니다. 「자동 업데이트의 보안 설계」에서 다룬 업데이트 경로 검증과 함께 생각하십시오.
10. 정리
WinForms / WPF 앱의 Entra ID 인증 대응은 다음 6가지로 정리됩니다.
- 자체 비밀번호 관리를 그만두는 것 자체가 목적. 보관・재발급・퇴사자 대응・감사가 테넌트 쪽으로 일원화됩니다
- 데스크톱 앱은 퍼블릭 클라이언트입니다. 시크릿을 가질 수 없고, 필요도 없습니다
- ROPC는 폐지 방향입니다. 사용자 이름・비밀번호를 받는 화면은 신규로 만들지 않습니다
- 구현은 MSAL.NET의
AcquireTokenSilent→AcquireTokenInteractive패턴 하나로 정해집니다 - Windows에서는 WAM 브로커(
WithBroker)로 SSO・조건부 액세스・Windows Hello 대응을 얻습니다 - 토큰 캐시 영속화(Extensions.Msal / DPAPI 보호)를 처음부터 넣어 둡니다
“로그인만 교체한다”는 최소 구성(7.1절)이라면, 기존 앱에 대한 영향을 로그인 화면과 사용자 테이블 주변으로 한정할 수 있어, 며칠 규모의 개선으로 끝나는 경우가 많습니다. 반면 조건부 액세스나 오프라인 요건이 얽히면, 테넌트 쪽 설정과 업무 실태를 바탕으로 한 설계 판단이 필요합니다. 현재 앱을 어느 구성까지 가져가야 할지, 자체 인증에서의 이전 절차를 어떻게 나눌지 판단이 어려우시다면 도와드릴 수 있습니다.
관련 글
- GCPW란 무엇인가 - Windows 로그온을 Google 인증으로 처리하는 방법
- Windows 앱의 민감 정보 저장 - DPAPI로 평문 설정을 피한다
- Windows 앱 개발의 보안 최소 체크리스트
- 자동 업데이트의 보안 설계 - HTTPS만으로는 부족한 이유
관련 상담 영역
합동회사 코무라소프트는 기존 WinForms / WPF 앱에 Entra ID 인증을 통합하는 작업(앱 등록 설계, MSAL.NET 구현, 자체 인증에서의 이전 계획), 자사 웹 API의 토큰 검증 설계 리뷰, 조건부 액세스 관련 로그인 장애의 원인 분석을 다루고 있습니다.
참고 링크
-
Microsoft Learn, Desktop app that calls web APIs: Code configuration. 데스크톱 앱의 리디렉션 URI(모바일과 데스크톱 플랫폼, nativeclient / localhost), “퍼블릭 클라이언트 흐름 허용” 설정의 의미에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, Microsoft identity platform and OAuth 2.0 Resource Owner Password Credentials. ROPC를 사용해서는 안 되는 이유, MFA와 호환되지 않아 차단되는 점, ROPC에 의존하는 앱이 차단되는 방향인 점, 데스크톱 앱은 브로커 기반 인증으로 이전해야 한다는 점에 대해. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Desktop app that calls web APIs: Acquire a token using username and password. 사용자 이름・비밀번호 흐름(ROPC)이 보안 위험 때문에 폐지(deprecated)된 점, 마이그레이션 가이드 안내, MFA・조건부 액세스・SSO 미지원 제약에 대해. ↩ ↩2
-
Microsoft Learn, Get a token from the token cache using MSAL.NET. AcquireTokenSilent를 먼저 호출하고 MsalUiRequiredException에서 대화형으로 폴백하는 권장 패턴, 캐시와 리프레시 토큰에 의한 자동 갱신, 계정 삭제로 캐시를 지우는 것에 대해. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Using MSAL.NET with Web Account Manager (WAM). 브로커의 이점(보안 강화, Windows Hello・조건부 액세스・FIDO 대응, 계정 선택기, 토큰 보호), MSAL.NET 4.52.0+와 Microsoft.Identity.Client.Broker 패키지, WithBroker와 부모 창 핸들 필수, ms-appx-web 리디렉션 URI, 지원 OS와 폴백, 대화형 세션 필수 등의 제약에 대해. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9
-
Microsoft Learn, Token cache serialization. 데스크톱 앱은 Microsoft.Identity.Client.Extensions.Msal의 크로스 플랫폼 캐시를 사용하라는 권장, MsalCacheHelper 사용법, ProtectedData(DPAPI, CurrentUser 범위)에 의한 자체 직렬화 예시에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, Register an application with the Microsoft identity platform. Entra 관리 센터에서의 앱 등록 절차, 지원되는 계정 유형 선택, 클라이언트 ID 취득, 관리자 동의에 대해. ↩ ↩2
-
Microsoft Learn, Desktop app that calls web APIs: Acquire a token by using WAM. WAM 사용 시에도 토큰 캐시 영속화가 필요한 점, OperatingSystemAccount에 의한 사일런트 로그인 권장 패턴, 앱 등록 쪽의 리디렉션 URI 설정에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, Default reply URI. WithDefaultRedirectUri가 설정하는 리디렉션 URI가 플랫폼에 따라 다른 점(.NET Framework 데스크톱은 https://login.microsoftonline.com/common/oauth2/nativeclient, .NET Core는 http://localhost)에 대해. ↩ ↩2
-
Microsoft Learn, Using web browsers (MSAL.NET). 프레임워크별 브라우저 지원표(.NET Framework 4.6.2+의 기본값이 임베디드, .NET 6+는 시스템 브라우저만), 시스템 브라우저에는 http://localhost 리디렉션 URI가 필요한 점, WithUseEmbeddedWebView에 의한 전환에 대해. ↩
-
Microsoft Learn, Protected web API: Verify scopes and app roles. [Authorize] 속성만으로는 충분하지 않은 점, scp 클레임(스코프) 검증이 필요한 점, Microsoft.Identity.Web의 RequiredScope 속성에 의한 선언적 검증에 대해. ↩ ↩2
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
Windows 데스크톱 앱의 UI 자동 테스트 ── UI Automation의 원리와 FlaUI로 만드는 잘 깨지지 않는 테스트
WinForms/WPF 앱의 UI 자동 테스트를 Windows UI Automation의 원리(트리·AutomationId·컨트롤 패턴)부터 정리합니다. FlaUI를 이용한 최소 구현, WinAppDriver의 현재 상황, AutomationId...
WPF의 고DPI 대응 ── 「DPI에 강할 텐데」인데도 흐릿하고・번지는 원인과 대처
WPF는 DIP(1/96인치)로 레이아웃되어 처음부터 System DPI Aware이지만, DPI가 다른 모니터로 옮기면 전체가 번지고, 비트맵이나 단선은 흐릿해집니다. 원인 구분, .NET Framework 4.6.2/.NET에서의 Per-Mo...
WinForms의 고DPI 대응 - 4K 모니터에서 흐려지거나 깨지는 원인과 현실적인 대처법
4K 모니터나 150% 스케일링 환경에서 WinForms 앱이 흐려지거나 레이아웃이 깨지는 원인을 DPI 가상화와 DPI 인식 모드(System Aware / Per-Monitor V2) 관점에서 정리합니다. .NET과 .NET Framework...
Windows 앱 외주·수탁 개발을 의뢰하기 전에 정리해야 할 것
Windows 앱의 외주·수탁 개발을 의뢰하기 전에, 기존 소프트웨어 개수, 장치 연계, COM/ActiveX, 배포·업데이트, 보수와 관련해 정리해야 할 포인트를 설명합니다.
Windows 업무 앱의 인쇄와 PDF 출력 - System.Drawing.Printing / WPF / 보고서 라이브러리 구분 사용법
PrintDocument를 이용한 WinForms 인쇄, WPF의 FlowDocument/FixedDocument 인쇄, PDF 출력의 선택지를 요구사항별 판단표로 정리합니다. 페이지 나눔 제어나 DPI 차이 같은 구현의 함정, Windows 서...
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
UI 스레드 & 타이머
WPF / WinForms UI 스레드, async 흐름, Dispatcher 사용, 타이머 판단을 정리한 토픽 페이지입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
Windows 앱 개발
상주 처리, 장비 연동, 운영 로그, 유지 보수 가능한 구조가 필요한 Windows 데스크톱 애플리케이션을 지원합니다.
기술 상담 & 설계 리뷰
설계 방향, 아키텍처 경계, 수명 관리, 기존 Windows 자산 처리 방법을 정리하는 데 도움을 드립니다.
자주 묻는 질문
이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.
- WinForms/WPF 앱에 Entra ID 인증을 넣으면 어떤 이점이 있나요?
- 비밀번호 보관・재발급 대응・퇴사자 계정 정지・로그인 감사가 모두 테넌트 쪽 업무가 되어, 앱 쪽의 책임 범위가 극적으로 줄어듭니다. 퇴사자는 Entra ID 계정을 비활성화하면 모든 앱에서 즉시 로그인이 불가능해지고, 다중 인증이나 조건부 액세스도 그대로 사내 앱에 적용됩니다. 구현도 MSAL.NET이라는 라이브러리와 수십 줄의 코드로 끝나는 범위입니다. Microsoft 365를 도입한 조직에서 자체 인증을 계속 유지할 이유는 거의 없습니다.
- 사용자 이름과 비밀번호를 자체 화면에서 입력받아 인증하는 방식(ROPC)은 쓸 수 있나요?
- 신규 도입은 금지라고 생각하십시오. 퍼블릭 클라이언트용 ROPC는 공식적으로 「보안 위험 때문에 폐지(deprecated)」라고 명시되어 있고, 마이그레이션 가이드도 공개되어 있습니다. MFA・조건부 액세스와 호환되지 않아, 테넌트에서 MFA가 필수로 설정된 사용자는 차단되어 로그인할 수 없습니다. MFA 필수화는 테넌트 쪽 설정으로 언제든 일어날 수 있으므로, 지금 동작하고 있어도 어느 날 갑자기 모든 사용자가 로그인할 수 없게 되는 위험이 있습니다.
- WAM 브로커는 사용하는 편이 좋은가요?
- Windows에서는 권장합니다. WithBroker 한 줄로 Windows에 로그인되어 있는 계정과의 SSO, 조건부 액세스・Windows Hello・FIDO 키 지원, 리프레시 토큰의 디바이스 바인딩을 얻을 수 있습니다. 사내 PC가 Entra에 참가되어 있으면 앱을 실행한 뒤 Windows 계정을 선택하기만 하면 비밀번호 입력 없이 로그인이 완료됩니다. 다만 Entra ID 전용이며, Windows 서비스나 태스크 스케줄러처럼 대화형 사용자 세션 밖에서는 설계상 오류가 나는 제약이 있습니다.
- 앱을 재시작할 때마다 로그인 화면이 나오는 이유는 무엇인가요?
- 토큰 캐시를 영속화하지 않았기 때문입니다. MSAL.NET의 토큰 캐시는 기본적으로 메모리상에만 존재하며, 데스크톱 앱에서는 영속화 구현이 앱 쪽의 책임입니다. 공식 권장은 Microsoft.Identity.Client.Extensions.Msal 패키지로, Windows에서는 캐시가 암호화되어 저장됩니다. WAM을 사용하는 경우에도 ID 토큰과 계정 메타데이터 저장을 위해 캐시 영속화가 필요합니다.
저자 프로필
기사 저자의 프로필 페이지입니다.
Go Komura
합동회사 코무라소프트 대표
Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.
공개 링크