Capacitor 앱에서 Microsoft Entra ID(구 Azure Active Directory)로 로그인하려면 Entra 관리 센터에서 공개 클라이언트 앱을 등록하고 모바일 및 데스크톱 애플리케이션 과 같은 리다이렉트 URI를 추가하고 com.example.app://oauth/azure, 시스템 브라우저에서 PKCE를 사용하여 OpenID Connect 인가 code 플로우를 실행합니다. @capgo/capacitor-social-login 즉, Microsoft ID 플랫폼 v2.0 엔드포인트를 사용하는 initialize 호출과 SocialLogin.login({ provider: 'oauth2', options: { providerId: 'azure' } }).
이 가이드는 앱 등록, 올바른 authority 선택, Graph 및 사용자 정의 API scope, 토큰 검증, 조건부 액세스, 로그아웃 및 AADSTS 오류를 다룹니다. 2026년 10월 Capacitor 8을 대상으로 합니다.
준비 사항
- Microsoft Entra 테넌트 및 앱 등록을 생성할 수 있는 역할(Application Developer 이상).
- Capacitor 8 앱과 그
appId이후 리다이렉트 스킴으로 재사용할 것입니다. - 선택적으로, Entra 토큰을 받아야 하는 백엔드 API.
WebView에서 MSAL.js를 사용하지 않는 이유
MSAL.js는 자체 origin을 소유하고 팝업을 열 수 있는 브라우저를 위해 설계되었습니다. Capacitor WebView에서는 origin이 capacitor://localhost iOS의 https://localhost Android에서는 팝업이 차단되고, 전체 페이지 리다이렉트를 사용하면 앱이 자체 번들 밖으로 이동하게 됩니다. Microsoft가 네이티브 앱에 권장하는 방식은 PKCE(RFC 8252)를 사용하는 시스템 브라우저이며, 이 플러그인도 바로 이 방식을 사용합니다:
| 플랫폼 | 사용 브라우저 | 추가 설정 |
|---|---|---|
| iOS | ASWebAuthenticationSession |
없음 |
| Android | Chrome Custom Tabs 사용 androidUseCustomTabs: true |
인텐트 필터 |
| 웹 | 팝업 또는 리다이렉트 | SPA 리다이렉트 URI |
Android에서는 Custom Tabs를 켜세요. 기본 내장 WebView는 Conditional Access가 자주 사용하는 Microsoft Authenticator, 패스키, Company Portal과 연동할 수 없습니다.
1단계: Entra ID에 앱 등록
- 다음을 엽니다: Microsoft Entra 관리 센터 그리고 다음으로 이동합니다: Entra ID > 앱 등록 > 새 등록.
- “Example Mobile”과 같은 이름을 입력하세요.
- 다음을 선택하세요: 지원되는 계정 유형. 이는 나중에 사용할 권한(authority)을 결정합니다.
- 리다이렉트 URI는 일단 건너뛰고 클릭하세요: 회원가입.
- 웹 개요, 다음을 복사하세요: 애플리케이션(클라이언트) ID 및 디렉터리(테넌트) ID.
Authority 선택
| 등록 시 계정 유형 | URL의 Authority 세그먼트 |
|---|---|
| 단일 테넌트(본인 조직 전용) | 본인 테넌트 ID 예: 8f1c... |
| 모든 조직 디렉터리 | organizations |
| 모든 조직 디렉터리 및 개인 Microsoft 계정 | common |
| 개인 Microsoft 계정만 | consumers |
가능할 때마다 tenant ID를 사용하세요. 예측 가능한 issuer를 생성하고 다른 tenant에서 의도치 않은 sign-in을 방지합니다.
redirect URI 추가
다음으로 이동: Authentication > Add a platform:
- 모바일 및 데스크톱 애플리케이션: 추가
com.example.app://oauth/azure을(를) 커스텀 리다이렉트 URI로 추가하세요. 추가하려면com.example.app://oauth/logout로그아웃 후 리다이렉트도 원하면 추가하세요. - 싱글 페이지 애플리케이션 (웹 빌드를 배포하는 경우에만): 추가하세요
https://app.example.com/auth/callback.
아래에 모바일 URI를 등록하지 마세요 웹. 웹 플랫폼 리다이렉트는 기밀 클라이언트이며 토큰 엔드포인트에서 시크릿이 필요합니다.
API 권한
API 권한 이미 포함하고 있습니다 Microsoft Graph > User.Read. 추가하세요 offline_access, openid, profile 및 email 목록에 없다면. 일부 테넌트는 사용자 동의가 차단되어 있습니다. 이 경우 관리자가 클릭해야 합니다 관리자 동의 부여, 그렇지 않으면 사용자가 AADSTS65001 오류나 “관리자 승인 필요” 화면을 봅니다.
자체 API에 스코프 노출 (선택 사항)
백엔드가 토큰을 받아야 한다면:
- 열기 API 노출 Application ID URI 설정, 보통
api://<client-id>. - 이름이 있는 스코프 추가
access_as_user, 동의 가능 대상: 관리자 및 사용자. - 다음에서 Manifest, 설정
"requestedAccessTokenVersion": 2하여 액세스 토큰이 v2.0 발급자 형식을 사용하도록 합니다.
2단계: 플러그인 설치 및 인텐트 필터 추가
bun add @capgo/capacitor-social-login
bunx cap sync
다음에서 android/app/src/main/AndroidManifest.xml내 MainActivity:
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="com.example.app" android:host="oauth" />
</intent-filter>
유지하세요 android:launchMode="singleTask" 켜짐 MainActivity (기본값인 Capacitor). Google, Apple 또는 Facebook 로그인을 사용하지 않는 경우, 아래에서 비활성화하세요. plugins.SocialLogin.providers 에서 capacitor.config.ts 빌드에서 해당 SDK를 제외하려면.
3단계: Entra ID 프로바이더 초기화
import { Capacitor } from '@capacitor/core';
import { SocialLogin } from '@capgo/capacitor-social-login';
const TENANT = 'YOUR_TENANT_ID'; // or organizations / common
const AUTHORITY = `https://login.microsoftonline.com/${TENANT}/oauth2/v2.0`;
const isWeb = Capacitor.getPlatform() === 'web';
export async function initAuth() {
await SocialLogin.initialize({
oauth2: {
azure: {
appId: 'YOUR_CLIENT_ID',
authorizationBaseUrl: `${AUTHORITY}/authorize`,
accessTokenEndpoint: `${AUTHORITY}/token`,
redirectUrl: isWeb
? 'https://app.example.com/auth/callback'
: 'com.example.app://oauth/azure',
scope: 'openid profile email offline_access User.Read',
pkceEnabled: true,
resourceUrl: 'https://graph.microsoft.com/v1.0/me',
logoutUrl: `${AUTHORITY}/logout`,
postLogoutRedirectUrl: isWeb ? 'https://app.example.com/' : 'com.example.app://oauth/logout',
androidUseCustomTabs: true,
},
},
});
}
사용하면 resourceUrl Graph를 가리키도록 /me, 플러그인은 로그인 직후 Graph를 호출하여 프로필을 반환합니다 resourceData.
Ionic Auth Connect에서 마이그레이션하는 경우, SocialLoginAuthConnect 에 azure 프리셋이 있으며, 이에는 tenantId, clientId 및 redirectUrl. 구현 세부 사항은 Entra ID 통합 페이지.
4단계: 로그인
import { SocialLogin } from '@capgo/capacitor-social-login';
export async function signInWithMicrosoft() {
try {
const { result } = await SocialLogin.login({
provider: 'oauth2',
options: { providerId: 'azure' },
});
const me = result.resourceData as {
id: string;
displayName?: string;
mail?: string | null;
userPrincipalName?: string;
} | null;
return {
idToken: result.idToken,
graphToken: result.accessToken?.token,
refreshToken: result.refreshToken,
user: me,
};
} catch (error: any) {
if (error?.code === 'USER_CANCELLED') return null;
throw error;
}
}
유용한 옵션:
loginHint: 'jane@contoso.com'계정을 미리 채우고 해당 사용자의 계정 선택기를 건너뜁니다.prompt: 'select_account'항상 계정 선택기를 표시하며, 공용 기기에 유용합니다.additionalParameters: { domain_hint: 'contoso.com' }사용자를 해당 연동 IdP로 바로 이동시킵니다.
mail 자주 null 메일박스가 없는 계정에 사용됩니다. 대신 userPrincipalName 또는 preferred_username ID 토큰의 claim을 사용합니다.
5단계: Microsoft Graph 호출
export async function getMyPhotoBlob(graphToken: string) {
const res = await fetch('https://graph.microsoft.com/v1.0/me/photo/$value', {
headers: { Authorization: `Bearer ${graphToken}` },
});
if (res.status === 404) return null; // no photo set
if (!res.ok) throw new Error(`Graph error ${res.status}`);
return res.blob();
}
필요한 각 Graph scope를 추가합니다 (예: Calendars.Read) 를 모두 API 권한 및 scope string.
6단계: 자체 API용 토큰 가져오기
액세스 토큰은 하나의 리소스를 대상으로 합니다. 백엔드를 호출하려면 다음 옆에 두 번째 항목을 추가하세요. azure 같은 oauth2 의 initialize 호출에서 Graph 대신 API 스코프를 요청하세요:
await SocialLogin.initialize({
oauth2: {
azure: graphConfig, // the object from step 3, moved into a const
azureApi: {
appId: 'YOUR_CLIENT_ID',
authorizationBaseUrl: `${AUTHORITY}/authorize`,
accessTokenEndpoint: `${AUTHORITY}/token`,
redirectUrl: 'com.example.app://oauth/azure-api', // register this URI too
scope: 'openid profile offline_access api://YOUR_CLIENT_ID/access_as_user',
pkceEnabled: true,
androidUseCustomTabs: true,
},
},
});
사용자는 이미 Microsoft 세션을 가지고 있으므로, 두 번째 로그인에는 일반적으로 프롬프트 없이 완료됩니다. 서버에서 토큰을 검증하세요:
import { createRemoteJWKSet, jwtVerify } from 'jose';
const TENANT_ID = 'YOUR_TENANT_ID';
const jwks = createRemoteJWKSet(
new URL(`https://login.microsoftonline.com/${TENANT_ID}/discovery/v2.0/keys`),
);
export async function verifyEntraToken(token: string) {
const { payload } = await jwtVerify(token, jwks, {
issuer: `https://login.microsoftonline.com/${TENANT_ID}/v2.0`,
audience: 'YOUR_CLIENT_ID', // v2 tokens use the client ID as aud
});
if (!String(payload.scp ?? '').split(' ').includes('access_as_user')) {
throw new Error('Missing scope');
}
return payload; // payload.oid is the stable user object ID
}
멀티테넌트 앱의 경우, 발급자(issuer)에는 각 고객의 테넌트 ID가 포함됩니다. tid 고정된 발급자가 아닌, 허용하는 테넌트 목록과 oid 및 tid클레임을 대조하여 검증하세요. 이메일이 아닌
7단계: 새로고침 및 로그아웃
// Get a valid Graph token, refreshing it when it has expired
export async function getGraphToken() {
const { isLoggedIn } = await SocialLogin.isLoggedIn({ provider: 'oauth2', providerId: 'azure' });
if (!isLoggedIn) {
try {
await SocialLogin.refresh({ provider: 'oauth2', options: { providerId: 'azure' } });
} catch {
// no session, or the refresh token expired or was revoked
const loginResult = await signInWithMicrosoft();
if (!loginResult) throw new Error('Sign-in cancelled');
}
}
const { accessToken } = await SocialLogin.getAuthorizationCode({ provider: 'oauth2', providerId: 'azure' });
return accessToken;
}
// Sign out
await SocialLogin.logout({ provider: 'oauth2', providerId: 'azure' });
공개 클라이언트의 Entra 리프레시 토큰은 슬라이딩 윈도우 방식으로 최대 90일까지 유효하며, 사용될 때마다 교체됩니다. 플러그인은 새 토큰을 저장합니다. 토큰을 직접 저장하는 경우 Keychain 또는 Keystore를 사용하세요. NativeBiometric.setData 과 @capgo/capacitor-native-biometric, 그리고 다음에 전달하세요 SocialLogin.refreshToken.
logout 로컬 토큰을 삭제하고 시스템 브라우저에서 v2.0 로그아웃 엔드포인트를 엽니다. Microsoft가 커스텀 스킴으로 리다이렉트하는 대신 “계정에서 로그아웃되었습니다” 페이지를 표시할 수 있으므로 사용자가 직접 닫아야 할 수 있습니다. 로컬 로그아웃만 필요한 경우 logoutUrl 을(를) 제거하고 prompt: 'select_account' 다음 로그인 시 전달하세요.
조건부 액세스 및 Intune
브라우저 기반 OIDC는 MFA, 패스워드리스 및 대부분의 조건부 액세스 정책을 지원합니다. 다음 두 가지 경우엔 추가 설정이 필요합니다:
- 컴플라이언트 또는 하이브리드 조인된 디바이스 요구: Entra가 디바이스 상태를 확인해야 합니다. Android에서는 Company Portal 또는 Authenticator가 설치된 경우 Custom Tabs를 통해 작동합니다. 관리되는 iOS 디바이스에서는 MDM으로 배포된 Microsoft Enterprise SSO 플러그인이 이를 제공합니다. 실제 정책을 사용하여 초기에 테스트하세요.
- Intune 앱 보호 정책 (MAM): 네이티브 앱 내부에 Intune 앱 SDK이 필요합니다. OIDC만으로는 “앱 보호 정책 요구” 권한을 충족할 수 없습니다.
문제 해결
| 오류 | 의미 | 해결 방법 |
|---|---|---|
| AADSTS50011 | Redirect URI 불일치 | 모바일 및 데스크톱 애플리케이션에 정확한 URI를 추가하세요 |
| AADSTS7000218 | 토큰 요청에 시크릿이 필요합니다 | 리다이렉트가 Web에 등록되어 있습니다. Mobile and desktop으로 이동하세요 |
| AADSTS9002326 | SPA에서만 크로스 오리진 토큰 교환 허용 | Web 빌드는 모바일 리디렉션을 사용합니다. Single-page application에 Web URL을 등록하세요 |
| AADSTS700016 | 디렉터리에서 앱을 찾을 수 없음 | authority의 tenant가 잘못되었거나, single-tenant 앱이 다음과 함께 사용됨 common |
| AADSTS50020 | 다른 tenant의 사용자 계정 | multitenant 등록을 사용하거나, 사용자를 게스트로 초대하세요 |
| AADSTS65001 | 동의가 부여되지 않음 | 관리자 동의를 부여하거나 사용자 동의를 허용하세요 |
| AADSTS53003 | Conditional Access에 의해 차단됨 | 로그인 로그를 확인하세요. 위 섹션을 참조 |
| AADSTS28000 | 하나의 요청에 여러 리소스 Scope 포함 | Graph와 API Scope를 별도의 로그인으로 요청 |
| Refresh token 없음 | offline_access 누락 |
Scope 및 권한에 추가 |
Entra 관리 센터 로그인 로그 각 시도별 정확한 실패 원인을 표시하며, logsEnabled: true 플러그인 설정에서 디바이스에 authorize URL과 token 응답을 출력합니다.
업데이트 배포
Policy changes on the Entra side are instant, but client-side fixes still need to reach devices. TypeScript changes such as scopes or error handling can ship through Capgo Live Update 사용하기네이티브 변경 사항(인텐트 필터, 새 스킴)은 스토어용 새 빌드가 필요하며, 이는 Capgo 빌드 로컬 Mac 없이 생성할 수 있습니다.