Apple에서 로그인 사용 Xamarin.Forms

Download Sample 샘플 다운로드

Apple로 로그인은 타사 인증 서비스를 사용하는 iOS 13의 모든 새 애플리케이션에 대한 것입니다. iOS와 Android 간의 구현 세부 정보는 매우 다릅니다. 이 가이드에서는 오늘 이 작업을 수행하는 방법을 안내합니다 Xamarin.Forms.

이 가이드 및 샘플에서는 특정 플랫폼 서비스를 사용하여 Apple로 로그인을 처리합니다.

  • OpenID/OpenAuth를 사용하여 Azure Functions와 통신하는 일반 웹 서비스를 사용하는 Android
  • iOS는 iOS 13에서 인증을 위해 네이티브 API를 사용하고 iOS 12 이하의 일반 웹 서비스로 돌아갑니다.

샘플 Apple 로그인 흐름

이 샘플은 앱에서 작동하도록 Apple 로그인을 가져오기 위한 의견 있는 구현을 Xamarin.Forms 제공합니다.

두 가지 Azure Functions를 사용하여 인증 흐름을 지원합니다.

  1. applesignin_auth - Apple 로그인 권한 부여 URL을 생성하고 해당 URL로 리디렉션합니다. 모바일 앱 대신 서버 쪽에서 이 작업을 수행하므로 Apple 서버가 콜백을 state 보낼 때 캐시하고 유효성을 검사할 수 있습니다.
  2. applesignin_callback - Apple의 POST 콜백을 처리하고 액세스 토큰 및 ID 토큰에 대한 권한 부여 코드를 안전하게 교환합니다. 마지막으로 URL 조각의 토큰을 다시 전달하여 앱의 URI 체계로 다시 리디렉션합니다.

모바일 앱은 선택한 사용자 지정 URI 체계(이 경우 xamarinformsapplesignin://)를 처리하도록 자체 등록하므로 함수가 applesignin_callback 토큰을 다시 릴레이할 수 있습니다.

사용자가 인증을 시작하면 다음 단계가 수행됩니다.

  1. 모바일 앱은 a noncestate 값을 생성하고 Azure 함수에 applesignin_auth 전달합니다.
  2. Azure 함수는 applesignin_auth 제공된 권한 부여 URL을 사용하여 Apple 로그인 권한 부여 URL을 state 생성하고 nonce모바일 앱 브라우저를 해당 URL로 리디렉션합니다.
  3. 사용자는 Apple 서버에서 호스트되는 Apple 로그인 권한 부여 페이지에서 자격 증명을 안전하게 입력합니다.
  4. Apple의 서버에서 Apple 로그인 흐름이 완료되면 Apple은 Azure 함수로 리디렉션됩니다 redirect_uriapplesignin_callback .
  5. 함수로 전송된 Apple의 applesignin_callback 요청은 올바른 state 요청이 반환되고 ID 토큰 클레임이 유효한지 확인하기 위해 유효성을 검사합니다.
  6. Azure 함수는 applesignin_callback Apple에서 게시한 내용을 액세스 토큰, 새로 고침 토큰 및 ID 토큰(사용자 ID, 이름 및 전자 메일에 대한 클레임 포함)으로 교환 code 합니다.
  7. Azure 함수는 applesignin_callback 마지막으로 토큰(xamarinformsapplesignin://예: xamarinformsapplesignin://#access_token=...&refresh_token=...&id_token=...)과 함께 URI 조각을 추가하는 앱의 URI 체계()로 다시 리디렉션됩니다.
  8. 모바일 앱은 URI 조각을 분석하여 AppleAccount 수신된 클레임이 nonce 흐름 시작 시 생성된 클레임과 일치하는지 nonce 유효성을 검사합니다.
  9. 이제 모바일 앱이 인증되었습니다!

Azure 기능

이 샘플에서는 Azure Functions를 사용합니다. 또는 ASP.NET Core Controller 또는 유사한 웹 서버 솔루션이 동일한 기능을 제공할 수 있습니다.

구성

Azure Functions를 사용하는 경우 몇 가지 앱 설정을 구성해야 합니다.

  • APPLE_SIGNIN_KEY_ID - 이것은 이전에서 온 것입니다 KeyId .
  • APPLE_SIGNIN_TEAM_ID- 일반적으로 멤버 자격 프로필에 있는 팀 ID입니다.
  • APPLE_SIGNIN_SERVER_ID: 이전 버전 ServerId 입니다. 번들 ID가 아니라 사용자가 만든 서비스 ID식별자입니다.
  • APPLE_SIGNIN_APP_CALLBACK_URI - 앱으로 다시 리디렉션하려는 사용자 지정 URI 체계입니다. 이 샘플 xamarinformsapplesignin:// 에서는 사용됩니다.
  • APPLE_SIGNIN_REDIRECT_URI- Apple 로그인 구성 섹션에서 서비스 ID를 만들 때 설정하는 리디렉션 URL입니다. 테스트하려면 다음과 같이 표시될 수 있습니다. http://local.test:7071/api/applesignin_callback
  • APPLE_SIGNIN_P8_KEY- 모든 \n 줄 바꿈이 .p8 제거되어 하나의 긴 문자열이 되도록 파일의 텍스트 내용

보안 고려 사항

P8 키를 애플리케이션 코드 내에 저장하지 마세요 . 애플리케이션 코드는 쉽게 다운로드하고 디스어셈블할 수 있습니다.

또한 인증 흐름을 호스트하고 URL 탐색 이벤트를 가로채 권한 부여 코드를 가져오는 데 사용하는 WebView 것이 잘못된 사례로 간주됩니다. 현재 토큰 교환을 처리하기 위해 서버에서 일부 코드를 호스팅하지 않고 iOS13 이상의 디바이스가 아닌 디바이스에서 Apple로 로그인을 처리하는 완전히 안전한 방법은 없습니다. Apple이 서버에 POST 콜백을 실행할 때 상태를 캐시하고 유효성을 검사할 수 있도록 서버에서 권한 부여 URL 생성 코드를 호스팅하는 것이 좋습니다.

플랫폼 간 로그인 서비스

DependencyService를 Xamarin.Forms 사용하여 iOS에서 플랫폼 서비스를 사용하는 별도의 인증 서비스 및 공유 인터페이스를 기반으로 Android 및 기타 비 iOS 플랫폼에 대한 일반 웹 서비스를 만들 수 있습니다.

public interface IAppleSignInService
{
    bool Callback(string url);

    Task<AppleAccount> SignInAsync();
}

iOS에서는 네이티브 API가 사용됩니다.

public class AppleSignInServiceiOS : IAppleSignInService
{
#if __IOS__13
    AuthManager authManager;
#endif

    bool Is13 => UIDevice.CurrentDevice.CheckSystemVersion(13, 0);
    WebAppleSignInService webSignInService;

    public AppleSignInServiceiOS()
    {
        if (!Is13)
            webSignInService = new WebAppleSignInService();
    }

    public async Task<AppleAccount> SignInAsync()
    {
        // Fallback to web for older iOS versions
        if (!Is13)
            return await webSignInService.SignInAsync();

        AppleAccount appleAccount = default;

#if __IOS__13
        var provider = new ASAuthorizationAppleIdProvider();
        var req = provider.CreateRequest();

        authManager = new AuthManager(UIApplication.SharedApplication.KeyWindow);

        req.RequestedScopes = new[] { ASAuthorizationScope.FullName, ASAuthorizationScope.Email };
        var controller = new ASAuthorizationController(new[] { req });

        controller.Delegate = authManager;
        controller.PresentationContextProvider = authManager;

        controller.PerformRequests();

        var creds = await authManager.Credentials;

        if (creds == null)
            return null;

        appleAccount = new AppleAccount();
        appleAccount.IdToken = JwtToken.Decode(new NSString(creds.IdentityToken, NSStringEncoding.UTF8).ToString());
        appleAccount.Email = creds.Email;
        appleAccount.UserId = creds.User;
        appleAccount.Name = NSPersonNameComponentsFormatter.GetLocalizedString(creds.FullName, NSPersonNameComponentsFormatterStyle.Default, NSPersonNameComponentsFormatterOptions.Phonetic);
        appleAccount.RealUserStatus = creds.RealUserStatus.ToString();
#endif

        return appleAccount;
    }

    public bool Callback(string url) => true;
}

#if __IOS__13
class AuthManager : NSObject, IASAuthorizationControllerDelegate, IASAuthorizationControllerPresentationContextProviding
{
    public Task<ASAuthorizationAppleIdCredential> Credentials
        => tcsCredential?.Task;

    TaskCompletionSource<ASAuthorizationAppleIdCredential> tcsCredential;

    UIWindow presentingAnchor;

    public AuthManager(UIWindow presentingWindow)
    {
        tcsCredential = new TaskCompletionSource<ASAuthorizationAppleIdCredential>();
        presentingAnchor = presentingWindow;
    }

    public UIWindow GetPresentationAnchor(ASAuthorizationController controller)
        => presentingAnchor;

    [Export("authorizationController:didCompleteWithAuthorization:")]
    public void DidComplete(ASAuthorizationController controller, ASAuthorization authorization)
    {
        var creds = authorization.GetCredential<ASAuthorizationAppleIdCredential>();
        tcsCredential?.TrySetResult(creds);
    }

    [Export("authorizationController:didCompleteWithError:")]
    public void DidComplete(ASAuthorizationController controller, NSError error)
        => tcsCredential?.TrySetException(new Exception(error.LocalizedDescription));
}
#endif

컴파일 플래그 __IOS__13 는 일반 웹 서비스로 대체되는 레거시 버전뿐만 아니라 iOS 13에 대한 지원을 제공하는 데 사용됩니다.

Android에서는 Azure Functions를 사용하는 일반 웹 서비스가 사용됩니다.

public class WebAppleSignInService : IAppleSignInService
{
    // IMPORTANT: This is what you register each native platform's url handler to be
    public const string CallbackUriScheme = "xamarinformsapplesignin";
    public const string InitialAuthUrl = "http://local.test:7071/api/applesignin_auth";

    string currentState;
    string currentNonce;

    TaskCompletionSource<AppleAccount> tcsAccount = null;

    public bool Callback(string url)
    {
        // Only handle the url with our callback uri scheme
        if (!url.StartsWith(CallbackUriScheme + "://"))
            return false;

        // Ensure we have a task waiting
        if (tcsAccount != null && !tcsAccount.Task.IsCompleted)
        {
            try
            {
                // Parse the account from the url the app opened with
                var account = AppleAccount.FromUrl(url);

                // IMPORTANT: Validate the nonce returned is the same as our originating request!!
                if (!account.IdToken.Nonce.Equals(currentNonce))
                    tcsAccount.TrySetException(new InvalidOperationException("Invalid or non-matching nonce returned"));

                // Set our account result
                tcsAccount.TrySetResult(account);
            }
            catch (Exception ex)
            {
                tcsAccount.TrySetException(ex);
            }
        }

        tcsAccount.TrySetResult(null);
        return false;
    }

    public async Task<AppleAccount> SignInAsync()
    {
        tcsAccount = new TaskCompletionSource<AppleAccount>();

        // Generate state and nonce which the server will use to initial the auth
        // with Apple.  The nonce should flow all the way back to us when our function
        // redirects to our app
        currentState = Util.GenerateState();
        currentNonce = Util.GenerateNonce();

        // Start the auth request on our function (which will redirect to apple)
        // inside a browser (either SFSafariViewController, Chrome Custom Tabs, or native browser)
        await Xamarin.Essentials.Browser.OpenAsync($"{InitialAuthUrl}?&state={currentState}&nonce={currentNonce}",
            Xamarin.Essentials.BrowserLaunchMode.SystemPreferred);

        return await tcsAccount.Task;
    }
}

요약

이 문서에서는 애플리케이션에서 사용하기 Xamarin.Forms 위해 Apple에서 로그인을 설정하는 데 필요한 단계를 설명했습니다.