2단계. 로그인
사용자가 별도의 회원가입 없이 Apple 계정으로 앱에 로그인하도록 구현합니다. 시작하기 전에 1단계. 연동 환경 구성을 마치세요.
Apple 로그인은 앱이 실행되는 OS에 따라 자격 증명을 얻는 방법과 로그인 흐름이 다릅니다.
| OS | 자격 증명 획득 방법 | 동작 흐름 |
|---|---|---|
| iOS, macOS | Apple 로그인 Add-on | Direct Token 흐름 |
| Android, Windows | 웹 로그인 세션 | Authorization Code 흐름 |
iOS와 macOS에서는 Add-on이 Hive Axyl 인증 서버에서 바로 검증할 수 있는 identityToken을 반환하므로 교환 단계 없이 로그인합니다. 다른 OS에서는 웹 로그인 세션이 Apple 인가 코드만 반환합니다. 외부 인증 코드 교환으로 인가 코드를 교환한 뒤 로그인합니다.
1. Apple 자격 증명 획득
앱이 실행되는 OS에 맞는 방법으로 Apple 자격 증명을 획득합니다.
1.1. iOS와 macOS
iOS와 macOS에서는 Apple 로그인 Add-on이 OS의 Apple 로그인 창을 띄우고, 인증을 마친 사용자의 자격 증명을 돌려줍니다.
nonce 준비
nonce는 Apple identityToken이 현재 로그인 요청에 맞게 발급되었는지 확인하는 일회성 난수입니다. 이 값을 사용하면 다른 요청에서 발급한 토큰을 가로채 재사용하는 공격을 막을 수 있습니다.
Add-on에는 난수 자체가 아니라 그 SHA256 해시값을 소문자 16진 문자열로 전달합니다. Add-on은 전달받은 값을 Apple 인증 요청에 그대로 실어 보낼 뿐, 값을 스스로 만들거나 검증하지 않습니다. 로그인할 때마다 난수를 새로 생성하세요.
using System;
using System.Security.Cryptography;
// 암호학적으로 안전한 32바이트 난수를 URL-safe base64(패딩 제거)로 인코딩합니다.
static string CreateNonce()
{
var bytes = new byte[32];
using (var rng = RandomNumberGenerator.Create()) rng.GetBytes(bytes);
return Convert.ToBase64String(bytes).TrimEnd('=').Replace('+', '-').Replace('/', '_');
}
해시로 변환할 때는 유저네임 로그인에서 정의한 Sha256Hex() 헬퍼를 사용합니다.
로그인 Add-on 호출
LoginAsync
IAppleSignInPlugin.LoginAsync()를 호출해 Apple 로그인 창을 띄우고 자격 증명을 받습니다.
| 필드명 | 타입 | 필수 여부 | 설명 |
|---|---|---|---|
NonceHash | string | Required | 앱이 생성한 난수의 SHA256 해시값. 소문자 16진 문자열로 넣습니다. |
RequestedScopes | IReadOnlyList<RequestedScope> | Required | Apple에 요청할 사용자 정보. Email과 FullName을 지정합니다. 기본값은 빈 목록이며, 빈 목록이면 사용자 식별자만 돌려받습니다. |
RequestedScopes에 Unspecified를 넣거나 NonceHash를 비워 두면 ArgumentException이 발생합니다.
using Hive.Axyl.Auth.Addon.Apple;
using Hive.Axyl.Core;
using UnityEngine;
// Add-on은 iOS와 macOS 빌드에서만 등록됩니다.
if (!HiveCore.TryResolve<IAppleSignInPlugin>(out var apple))
{
// iOS나 macOS가 아니거나 Add-on이 등록되지 않음 → 웹 로그인 세션으로 분기
return;
}
string rawNonce = CreateNonce();
var loginResult = await apple.LoginAsync(new AppleSignInServiceLoginRequest {
NonceHash = Sha256Hex(rawNonce),
RequestedScopes = new[] { RequestedScope.Email, RequestedScope.FullName },
});
string appleProviderToken; // 로그인 요청의 ProviderToken
string appleProviderUserId; // 로그인 요청의 ProviderUserId
switch (loginResult)
{
case AppleSignInServiceLoginResult.Success success:
appleProviderToken = success.Data.IdentityToken;
appleProviderUserId = success.Data.UserIdentifier;
// 이메일과 이름은 최초 로그인에서만 값이 채워집니다.
SaveProfileIfPresent(
success.Data.Email,
success.Data.UserName.GivenName,
success.Data.UserName.FamilyName);
break;
case AppleSignInServiceLoginResult.UserCanceled:
// 사용자가 Apple 로그인 창을 닫음 → 로그인 화면 유지
return;
case AppleSignInServiceLoginResult.Failure failure:
HiveError err = failure.Problem;
Debug.LogError($"[{err.Code}] {err.Message} (trace: {err.TraceId})");
return;
// 안전망: 처리하지 않은 결과 및 알 수 없는 신규 결과(UnknownOutcome)
default:
Debug.LogWarning($"처리되지 않은 결과: {loginResult.GetType().Name}");
return;
}
SaveProfileIfPresent()는 값이 비어 있지 않을 때만 저장하도록 앱이 직접 구현하는 코드입니다.
응답 데이터
성공 시 AppleSignInServiceLoginResult.Success의 Data에 Apple이 돌려준 자격 증명이 담깁니다.
| 필드명 | 타입 | 필수 여부 | 설명 |
|---|---|---|---|
Data.UserIdentifier | string | Required | Apple이 발급한 사용자 식별자. 로그인 요청의 ProviderUserId로 사용합니다. |
Data.IdentityToken | string | Required | Apple이 발급한 인증 토큰. 로그인 요청의 ProviderToken으로 사용합니다. |
Data.AuthorizationCode | string | Required | Apple의 인가 코드. 이 흐름에서는 사용하지 않습니다. Apple이 값을 주지 않으면 빈 문자열입니다. |
Data.Email | string | Required | 사용자의 이메일 주소. 최초 로그인에서만 값이 채워지고 이후에는 빈 문자열입니다. 사용자가 이메일 가리기를 선택하면 Apple이 만든 대체 주소가 담깁니다. |
Data.UserName | AppleUserName | Required | 사용자의 이름. GivenName, FamilyName, MiddleName으로 나뉩니다. 최초 로그인에서만 값이 채워지고 이후에는 모두 빈 문자열입니다. |
Data.RealUserStatus | RealUserStatus | Required | Apple이 판단한 실제 사용자 여부. LikelyReal, Unknown, Unsupported, Unspecified 중 하나입니다. |
Warning
이메일과 이름은 사용자가 이 앱에 처음 로그인할 때 한 번만 전달됩니다. 이후 로그인에서는 다시 받을 수 없으므로, 앱에서 이 정보를 사용한다면 최초 로그인 시점에 값을 저장해 두세요.
응답 상태
| 응답 케이스 | 설명 | 앱 클라이언트 대응 |
|---|---|---|
Success | 자격 증명 획득 성공 | 외부 인증 제공자 로그인 진행 |
UserCanceled | 사용자가 Apple 로그인 창을 닫은 경우 | 로그인 화면 유지 |
UnknownOutcome | 이 SDK 버전이 알지 못하는 신규 결과 | 로깅 후 보수적으로 처리 |
Failure | 공통 Failure입니다. 앱이 취소한 경우(Cancelled)도 여기로 분기하며 원인은 Failure.Problem.Code에 담깁니다. 공통 오류 처리를 참조하세요. | 공통 오류 처리 기준에 따라 처리 |
사용자가 로그인 화면을 벗어나는 등 앱이 먼저 인증을 중단해야 할 때는 CancelCurrentSession()을 호출합니다. 대기 중이던 LoginAsync()는 Failure로 끝나고 Failure.Problem.Code에 Cancelled가 담깁니다.
1.2. iOS와 macOS 외 OS
Android와 Windows에서는 웹 로그인 세션으로 Apple 로그인 페이지를 열고 인가 코드를 받습니다. Apple은 인증 결과를 등록된 HTTPS 주소로만 보내므로, 인증 결과는 Hive Axyl 중계 주소를 거쳐 앱으로 돌아옵니다.
앱은 아래 순서로 Apple 자격 증명을 받습니다.
- 난수를 새로 만들어 보관하고, 이 난수로
state값을 구성합니다. - Hive Axyl 중계 주소를
redirect_uri로 사용하는 Apple 인가 URL을 만듭니다. - 앱 콜백 주소를
OpenRequest.RedirectUri에 넣어 웹 로그인 세션을 엽니다. - 콜백의
state를 확인한 뒤 인가 코드를 꺼냅니다. - 인가 코드를 외부 인증 코드 교환으로 보내 자격 증명을 받습니다.
state 구성
state는 결과를 전달할 곳을 Hive Axyl 중계 주소에 알리는 접두사와 앱이 만든 난수를 이어 붙인 값입니다. Hive Axyl 중계 주소는 접두사를 떼어 내고 난수만 앱으로 전달하므로, 앱은 이 난수를 보관해 두었다가 콜백의 state와 비교합니다. 이 난수는 앱이 시작하지 않은 인증 결과를 걸러 내는 값이므로 로그인을 시도할 때마다 새로 만드세요.
- Android:
{앱 콜백 주소}|{난수}. 예:{appId}://oauth-callback|{난수} - Windows:
{포트}:{난수}.{포트}는AllocateLoopbackRedirectUriAsync()로 예약한 로컬 주소의 포트 번호
앱 콜백 주소와 난수에는 구분자인 | 문자를 넣지 마세요. 앱 콜백 주소는 중계 주소와 앱 콜백 주소에서 정한 값이고, 난수는 nonce 준비에서 정의한 CreateNonce()로 만듭니다. 이 헬퍼가 만드는 값에는 | 문자가 들어가지 않습니다.
인가 URL 구성
Apple 인가 엔드포인트 https://appleid.apple.com/auth/authorize에 아래 파라미터를 붙여 인가 URL을 만듭니다. 각 값은 URL 인코딩해서 넣으세요.
response_type:codeclient_id: Apple 로그인 연동에서 준비한 Service IDredirect_uri: Hive Axyl 중계 주소https://core-api.hiveaxyl.com/auth/v1/provider/callback. Service ID의 Return URL로 등록한 값과 같아야 합니다.response_mode:form_postscope: 이름과 이메일을 요청할 때만name emailstate: state 구성에서 만든 값
Apple 인가 URL에는 PKCE code_challenge를 넣지 않고, 교환 요청에도 CodeVerifier를 지정하지 않습니다.
웹 로그인 세션 열기
만든 인가 URL을 웹 로그인 세션의 OpenAsync()로 엽니다. OpenRequest.RedirectUri에는 중계 주소가 아닌 앱 콜백 주소를 넣으세요. Android에서는 {appId}://oauth-callback, Windows에서는 예약한 로컬 주소가 앱 콜백 주소입니다.
콜백 확인
Hive Axyl 중계 주소는 Apple 응답의 code 또는 error와 접두사를 뗀 state를 쿼리 파라미터로 붙여 앱 콜백 주소로 전달합니다. error에는 Apple이 보낸 값이 그대로 담깁니다.
콜백을 받으면 state부터 확인하세요. 콜백의 state가 보관한 난수와 다르면 이번 로그인에서 시작한 응답이 아니므로 로그인을 중단합니다. state가 같더라도 error가 있거나 code가 없으면 로그인을 중단합니다. error가 user_cancelled_authorize이면 사용자가 Apple 로그인을 취소한 경우이고, 그 밖의 error는 로그인에 실패한 경우입니다.
외부 인증 코드 교환
콜백의 code를 외부 인증 코드 교환으로 보내 로그인에 사용할 자격 증명으로 바꿉니다. 교환 요청에는 아래 값을 넣습니다.
ProviderId:Provider.SigninAppleProviderCode: 콜백의codeRedirectUri: 인가 URL의redirect_uri에 넣은 중계 주소.OpenRequest.RedirectUri에 넣은 앱 콜백 주소와는 다른 값CodeVerifier: 지정하지 않음
교환이 실패하면 로그인을 중단하세요. 응답 케이스별 처리는 외부 인증 코드 교환을 참조하세요.
using System;
using Hive.Axyl.Auth;
using Hive.Axyl.Auth.Addon.WebAuth;
using Hive.Axyl.Core;
if (!HiveCore.TryResolve<IExternalUserAgent>(out var webAuth))
{
return;
}
IAuthService auth = HiveCore.Resolve<IAuthService>();
const string RelayUrl = "https://core-api.hiveaxyl.com/auth/v1/provider/callback";
// 로그인을 시도할 때마다 새로 만들어 보관합니다.
string realState = CreateNonce();
// Android 예시입니다. Windows에서는 appCallback에 예약한 로컬 주소를,
// state에 $"{포트}:{realState}"를 사용합니다.
string appCallback = "{appId}://oauth-callback";
string state = $"{appCallback}|{realState}";
string authorizationUrl = "https://appleid.apple.com/auth/authorize"
+ "?response_type=code"
+ "&client_id=" + Uri.EscapeDataString("{appleServiceId}")
+ "&redirect_uri=" + Uri.EscapeDataString(RelayUrl)
+ "&response_mode=form_post"
+ "&scope=" + Uri.EscapeDataString("name email")
+ "&state=" + Uri.EscapeDataString(state);
var sessionResult = await webAuth.OpenAsync(new OpenRequest {
Url = authorizationUrl,
RedirectUri = appCallback, // 중계 주소가 아니라 앱 콜백 주소
});
if (sessionResult is not ExternalUserAgentServiceOpenResult.Success session)
{
// UserCanceled와 Failure 처리는 [웹 로그인 세션](web-auth-session.md) 참조
return;
}
var callback = session.Data.Parameters;
// state를 가장 먼저 확인합니다.
if (!callback.TryGetValue("state", out var returnedState) || returnedState != realState)
{
// 이번 로그인에서 시작한 응답이 아님 → 로그인 중단
return;
}
if (callback.TryGetValue("error", out var appleError))
{
// "user_cancelled_authorize"이면 사용자가 취소함 → 로그인 화면 유지
// 그 밖의 값이면 로그인 실패 → 로그인 중단
return;
}
if (!callback.TryGetValue("code", out var appleCode))
{
// 인가 코드가 없음 → 로그인 중단
return;
}
// 콜백에서 꺼낸 인가 코드를 로그인에 사용할 자격 증명으로 교환합니다.
var exchange = await auth.ExchangeProviderTokenAsync(new ProviderTokenRequest {
ProviderId = Provider.SigninApple,
ProviderCode = appleCode,
RedirectUri = RelayUrl, // 인가 URL의 redirect_uri와 같은 값
});
if (exchange is not AuthExchangeProviderTokenResult.Success exchanged)
{
// 교환 실패 → 로그인 중단
return;
}
string appleProviderToken = exchanged.Data.ProviderToken; // 로그인 요청의 ProviderToken
string appleProviderUserId = exchanged.Data.ProviderUserId; // 로그인 요청의 ProviderUserId
2. 외부 인증 제공자 로그인
앞 단계에서 얻은 appleProviderUserId와 appleProviderToken으로 외부 인증 제공자 로그인을 호출합니다. ProviderId에는 Provider.SigninApple을 지정합니다.
using Hive.Axyl.Auth;
using Hive.Axyl.Core;
IAuthService auth = HiveCore.Resolve<IAuthService>();
var (codeVerifier, codeChallenge) = CreatePkce();
var result = await auth.LoginProviderAsync(new ProviderLoginRequest {
ProviderId = Provider.SigninApple,
ProviderUserId = appleProviderUserId,
ProviderToken = appleProviderToken,
DeviceKey = deviceKey,
ClientId = "{clientId}",
CodeChallenge = codeChallenge,
CodeChallengeMethod = CodeChallengeMethod.S256,
});
if (result is AuthLoginProviderResult.Success success)
{
// 로그인 성공 → 토큰을 발급하고 세션을 활성화합니다.
await StartSessionAsync(success.Data.AuthorizationCode, codeVerifier, success.Data.PlayerId);
}
// 그 밖의 응답 케이스와 전체 호출 파라미터는 [외부 인증 제공자 로그인](provider-login.md) 참조
Note
여기에서 생성하는 PKCE 값은 Hive Axyl 인증 서버가 발급한 인가 코드를 토큰으로 교환할 때 사용합니다. Apple이 요구하는 값이 아니므로 Apple 인가 URL에는 넣지 마세요.
CreatePkce()는 게스트 계정 생성에서 정의한 헬퍼이고, StartSessionAsync()의 정의와 세션 활성화 절차는 토큰 발급과 세션 활성화를 참조하세요.