Xamarin Form에서 SNS 인증하기

Xamarin Form에서 SNS 인증하기

과거에 잠깐 회사에서 개발할 때 Tstory에 작성한 글이다. BLEX 블로그를 겸하면서 옮겨서 다시 정리하고자한다. 참고로 해당 글은 해당 깃허브 소스를 참고해 현재 버전에 맞게 수정한 것에 불과하다.

Blog
https://m.blog.naver.com/PostView.nhn?blogId=goldrushing&logNo=221482308506&proxyReferer=https:%2F%2Fwww.google.com%2F
Github
https://github.com/imagef5/Xamarin.SNS.Login.Sample

Xamarin.From

MS 측 .NET Framework를 이용해 크로스 플랫폼 앱을 개발하는 오픈소스 플랫폼이다.
각 플랫폼의 Native UI를 C#을 통해 제어하는 형태로 거의 네이티브에 유사한 속도를 자랑한다.
당시 주 언어가 C#이였기에 크로스플랫폼 개발 중 도입해 진행했었다.
최고의 장점은 MS에서 관리하기에 VIsual Studio를 이용해 개발할 수 있다는게 장점이고 C#의 간결한 문법도 장점에 속한다. 하지만 사용자 층이 적고 한국어로 된 자료는 없다시피하다. 그래서인지 SNS에 관련된 오픈소스 특히 Kakao Auth 로그인은 관련 정보가 하나 뿐이였고 그마저도 4년은 지난 내용이였다. 이번에 진행하게 된 기회삼아 Xamarin의 SNS 인증 관련 코드를 최신화 해보고자했다.
부족한 실력이라 많은 설명은 없으나 도움이 되길 기원한다.
❗ 참고로 IOS쪽에서는 추가 설정이 필요하다. IOS쪽은 필자가 맞은 파트가 아니라서 정확히 모르겠다.

Nuget Pakagae Version Info

C# Nuget

  • Com.Airbnb.Xamarin.Forms.Lottie 4.0.8
  • NETStandardLibary 2.0.3
  • Newtonsoft.Json 12.0.3
  • Prism.Unity.Forms 8.0.0.1909
  • Xam.Plugins.Settings 3.1.1
  • Xamarin.Auth 1.7.0
  • Xamarin.Auth.XamarinnForms 1.7.0
  • Xamarin.Essentials 1.6.1
  • Xamarin.Forms 5.0.0.1931

Android Nuget

  • Com.Airbnb.Android.Lottie 3.5.0
  • Android API Level 29

Folder Tree

더 많은 폴더가 있지만 수정이나 추가할 폴더만 표시해보았다.

├── C# NameSpace (사용자가 정하는 것)
│   ├── Extensions
│       ├── ISettingExtensions.cs
│   ├── Models
└── Services/Authentication

폴더 별 소스코드

폴더 별로 소스코드를 작성한 것을 공유합니다.

Extensions Folder

Edtension 관련 파일이 들어가는 폴더입니다. 아래처럼 같이 솔루션 하위 폴더로 C# 네임스페이스에 만들고 정의합시다.

ISettingExtensions.cs

using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Plugin.Settings.Abstractions;

namespace [YourProjectName].Extensions
{
    public static class ISettingsExtensions
    {
        public static T GetValueOrDefault<T>(this ISettings settings, string key, T @default) where T : class
        {
            string serialized = settings.GetValueOrDefault(key, string.Empty);
            T result = @default;

            try
            {
                result = JsonConvert.DeserializeObject<T>(serialized);
            }
            catch (Exception ex)
            {
                System.Diagnostics.Debug.WriteLine($"Error deserializing settings value: {ex}");
            }

            return result;
        }

        public static bool AddOrUpdateValue<T>(this ISettings settings, string key, T obj) where T : class
        {
            try
            {
                JsonSerializerSettings serializeSettings = GetSerializerSettings();
                string serialized = JsonConvert.SerializeObject(obj, serializeSettings);

                return settings.AddOrUpdateValue(key, serialized);
            }
            catch (Exception ex)
            {
                System.Diagnostics.Debug.WriteLine($"Error serializing settings value: {ex}");
            }

            return false;
        }

        private static JsonSerializerSettings GetSerializerSettings()
        {
            return new JsonSerializerSettings
            {
                ContractResolver = new CamelCasePropertyNamesContractResolver()
            };
        }

    }
}
Models Folder

Models 폴더 입니다.
아래처럼 같이 솔루션 하위 폴더로 C# 네임스페이스에 만들고 정의합시다.

🔥 참고 블로그에는 Kakao 외에도 구현되어 있으나 개발 당시 카카오 외에는 필요치않아서 제거했다.

User.cs

using System;

namespace [YourProjectName].Models
{
    public class User
    {
        public string Id { get; set; }
        public string Token { get; set; }
        public string RefreshToken { get; set; }
        public DateTime ExpiresIn { get; set; }
        public string Name { get; set; }
        public string NickName { get; set; }
        public string LastName { get; set; }
        public string Email { get; set; }
        public string Gender { get; set; }
        public string Birthday { get; set; }
        public string PictureUrl { get; set; }
        public bool LoggedInWithSNSAccount { get; set; }
        public SNSProvider Provider { get; set; }
    }
}

User 클래스의 경우 유저 인증에 필요한 모델들을 정의하는데,
SNSProvider의 경우 카카오 인증만 구현할 땐 필요없는 모델이지만, 혹시나 다른 인증들을 구현할 기회가 생길까봐 유지했습니다.

SNSProvider

using System;
using System.Collections.Generic;
using System.Text;

namespace [YourProjectName].Models
{
    public enum SNSProvider
    {
        None = -1,
        Kakao,
        //카카오 외의 다른 sns로그인 이용시 추가
        //Line,
        //Facebook,
        //Naver,
    }
}

OAuth2ProviderFactory

namespace [YourProjectName].Models.Providers
{
    public class OAuth2ProviderFactory
    {
        public static OAuth2Base CreateProvider(SNSProvider provider)
        {
            //OAuth2Base oAuth2 = null;
            OAuth2Base oAuth2 = KakaoOAuth2.Instance; ;
            
            /*
            switch (provider)
            {
                case SNSProvider.Kakao:
                    oAuth2 = KakaoOAuth2.Instance;
                    break;
                case SNSProvider.Line:
                    oAuth2 = LineOAuth2.Instance;
                    break;
                case SNSProvider.Facebook:
                    oAuth2 = FacebookOAuth2.Instance;
                    break;
                case SNSProvider.Naver:
                    oAuth2 = NaverOAuth2.Instance;
                    break;
            }
            */

            return oAuth2;
        }
    }
}

OAuth2ProviderFactory는 OAuth2 인증을 진행할 Provider의 인스턴스를 생성해주는 코드였으나, KaKao외에는 사용하지 않아서 주석처리했다. 혹시 필요하시면 주석을 제거하면 사용하는게 좋을듯 싶다.

OAuth2Base.cs

using System;
using System.Threading.Tasks;
using Xamarin.Auth;

namespace [YourProjectName].Models.Providers
{
    public abstract class OAuth2Base
    {
        public string ProviderName { get; set; }
        public string Description { get; set; }
        public SNSProvider Provider { get; protected set; }
        public string ClientId { get; set; }
        public string ClientSecret { get; set; }
        public string Scope { get; set; }
        public Uri AuthorizationUri { get; set; }
        public Uri RedirectUri { get; set; }
        public Uri RequestTokenUri { get; set; }
        public Uri UserInfoUri { get; set; }
        public bool IsUsingNativeUI { get; set; } = false;

        public abstract Task<User> GetUserInfoAsync(Account account);
        public abstract Task<(bool IsRefresh, User User)> RefreshTokenAsync(User user);
    }
}

OAuth2를 구현할 때 Auth 표준에서 필요한 필드들의 정의를 강제하기 위해 추상메서드로 구현했습니다.

KakaoUser.cs

 using Newtonsoft.Json;

namespace [YourProjectName].Models.Providers
{
    [JsonObject]
    public class KakaoUser
    {
        [JsonProperty("id")]
        public string Id { get; set; }

        [JsonProperty("kaccount_email")]
        public string Email { get; set; }

        [JsonProperty("kaccount_email_verified")]
        public bool VerifiedEmail { get; set; }

        public Properties Properties { get; set; }
    }

    [JsonObject]
    public class Properties
    {
        [JsonProperty("nickname")]
        public string NickName { get; set; }

        [JsonProperty("thumbnail_image")]
        public string Thumbnail { get; set; }

        [JsonProperty("profile_image")]
        public string ProfileImage { get; set; }
    }
}

xamairn으로 SNS 인증을 하기위해서는 REST API로 구현해야됩니다.
따라서 Json 형태로 받는 값들을 처리하기 위한 코드입니다.

KakaoOAuth2.cs

using System;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Xamarin.Auth;

namespace [YourProjectName].Models.Providers
{
    public class KakaoOAuth2 : OAuth2Base
    {
        private static readonly Lazy<KakaoOAuth2> lazy = new Lazy<KakaoOAuth2>(() => new KakaoOAuth2());

        public static KakaoOAuth2 Instance
        {
            get
            {
                return lazy.Value;
            }
        }

        private KakaoOAuth2()
        {
            Initialize();
        }

        void Initialize()
        {
            ProviderName = "Kakao";
            Description = "Kakao Login Provider";
            Provider = SNSProvider.Kakao;
            ClientId = "{YourKakao REST API App Key}"; //REST API App Key 입력
            ClientSecret = null; 
            Scope = null; //원하는 값 형태만 지정 
            AuthorizationUri = new Uri("https://kauth.kakao.com/oauth/authorize");
            RequestTokenUri = new Uri("https://kauth.kakao.com/oauth/token");
            RedirectUri = new Uri("https://www.naver.com/oauth");//oauth 경로가 있는 url (주로 자신의 블로그), 네이버는 임시로 설정했습니다.
            UserInfoUri = new Uri("https://kapi.kakao.com/v2/user/me"); // V1과 V2는 다른 서버입니다.
        }

        #region Implement Abstract Method
        public override async Task<User> GetUserInfoAsync(Account account)
        {
            User user = null;
            string token = account.Properties["access_token"];
            string refreshToke = account.Properties["refresh_token"];
            int.TryParse(account.Properties["expires_in"], out int expriesIn);

            var request = new OAuth2Request("GET", UserInfoUri, null, account); 
            var response = await request.GetResponseAsync();
            if (response != null && response.StatusCode == HttpStatusCode.OK)
            {
                string userJson = await response.GetResponseTextAsync();
                var kakaoUser = JsonConvert.DeserializeObject<KakaoUser>(userJson);
                user = new User
                {
                    Id = kakaoUser.Id,
                    Token = token,
                    RefreshToken = refreshToke,
                    Name = kakaoUser.Properties.NickName,
                    Email = kakaoUser.Email,
                    ExpiresIn = DateTime.UtcNow.Add(new TimeSpan(expriesIn)),
                    PictureUrl = kakaoUser.Properties.ProfileImage,
                    Provider = SNSProvider.Kakao,
                    LoggedInWithSNSAccount = true,
                };
            }
            return user;
        }

        public override async Task<(bool IsRefresh, User User)> RefreshTokenAsync(User user)
        {
            bool refreshSuccess = false;
            if (user == null)
            {
                return (refreshSuccess, user);
            }

            Dictionary<string, string> dictionary = new Dictionary<string, string> { { "grant_type", "refresh_token" }, { "refresh_token", user.RefreshToken }, { "client_id", ClientId } };
            var request = new Request("POST", RequestTokenUri, dictionary, null);
            var response = await request.GetResponseAsync();
            if (response != null && response.StatusCode == HttpStatusCode.OK)
            {
                string tokenString = await response.GetResponseTextAsync();
                JObject jwtDynamic = JsonConvert.DeserializeObject<JObject>(tokenString);
                var accessToken = jwtDynamic.Value<string>("access_token");
                var refreshToken = jwtDynamic.Value<string>("refresh_token");
                var expiresIn = jwtDynamic.Value<int>("expires_in");


                user.Token = accessToken;
                user.RefreshToken = refreshToken;
                user.ExpiresIn = DateTime.UtcNow.Add(new TimeSpan(0, 0, expiresIn));

                refreshSuccess = true;
            }

            return (refreshSuccess, user);
        }
        #endregion
    }
}

Initialize() 메서드에서 ClientId는 설정해주셔야됩니다.
카카오 Devleoer에 방문하셔서 REST API key를 받아 작성해주시면 됩니다.

Kakao Developers
https://developers.kakao.com/

Services/Authentication Folder

IAuthenticationService.cs

using System.Threading.Tasks;
using [YourProjectName].Models;

namespace [YourProjectName].Services.Authentication
{
    public interface IAuthenticationService
    {
        bool IsAuthenticated { get; }

        User AuthenticatedUser { get; }

        Task<bool> LoginAsync(string email, string password);

        Task LoginWithSNSAsync(SNSProvider provider);

        Task<bool> UserIsAuthenticatedAndValidAsync();

        Task LogoutAsync();
    }
}

먼저 Auth interface를 정의해줍니다. __ Authentication.cs__

using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Xamarin.Auth;
using Xamarin.Forms;
using [YourProjectName].Models;
using [YourProjectName].Models.Providers;

namespace [YourProjectName].Services.Authentication
{
    public class AuthenticationService : IAuthenticationService
    {
        OAuth2Base oAuth2;

        public bool IsAuthenticated => AppSettings.User != null;

        public User AuthenticatedUser => AppSettings.User;

        public Task<bool> LoginAsync(string email, string password)
        {
            var user = new User
            {
                Email = email,
                Name = email,
                LastName = string.Empty,
                PictureUrl = "",
                Token = email,
                LoggedInWithSNSAccount = false,
                Provider = SNSProvider.None
            };

            AppSettings.User = user;

            return Task.FromResult(true);
        }

        public Task LoginWithSNSAsync(SNSProvider provider)
        {
            try
            {
                oAuth2 = OAuth2ProviderFactory.CreateProvider(provider);
                var authenticator = new OAuth2Authenticator(
                    oAuth2.ClientId,
                    oAuth2.ClientSecret,
                    oAuth2.Scope,
                    oAuth2.AuthorizationUri,
                    oAuth2.RedirectUri,
                    oAuth2.RequestTokenUri,
                    null,
                    oAuth2.IsUsingNativeUI);

                authenticator.Completed += async (s, e) =>
                {
                    if (e.IsAuthenticated)
                    {
                        var user = await oAuth2.GetUserInfoAsync(e.Account);

                        AppSettings.User = user;
                        MessagingCenter.Send(user, MessengerKey.AuthenticationRequested, true);
                        Debug.WriteLine("Authentication Success");
                    }
                };
                authenticator.Error += (s, e) =>
                {
                    Debug.WriteLine("Authentication error: " + e.Message);
                };

                var presenter = new Xamarin.Auth.Presenters.OAuthLoginPresenter();
                presenter.Login(authenticator);

            }
            catch (Exception ex)
            {
                Debug.WriteLine("Login Error : " + ex.Message);
                return Task.FromResult(false);
            }
            return Task.FromResult(true);
        }

        public async Task<bool> UserIsAuthenticatedAndValidAsync()
        {
            if (!IsAuthenticated)
            {
                return false;
            }
            else if (!AuthenticatedUser.LoggedInWithSNSAccount)
            {
                return true;
            }
            else
            {
                bool refreshSucceded = false;
                oAuth2 = OAuth2ProviderFactory.CreateProvider(AuthenticatedUser.Provider);
                try
                {
                    var utcNow = DateTime.UtcNow.AddMinutes(30);
                    if (AuthenticatedUser.ExpiresIn < utcNow)
                    {
                        var ret = await oAuth2.RefreshTokenAsync(AuthenticatedUser);
                        if (ret.IsRefresh)
                        {
                            AppSettings.User = ret.User;
                        }
                        else
                        {
                            AppSettings.RemoveUserData();
                        }

                        refreshSucceded = ret.IsRefresh;
                    }
                    else
                    {
                        refreshSucceded = true;
                    }
                }
                catch (Exception ex)
                {
                    System.Diagnostics.Debug.WriteLine($"Error with refresh attempt: {ex}");
                }

                return refreshSucceded;
            }
        }

        public Task LogoutAsync()
        {
            AppSettings.RemoveUserData();
            return Task.FromResult(true);
        }
    }
}

실제로 Auth 요청이 이루어지는 코드입니다.


이전 코드 (참고에있는 이전 개발자분 깃헙 참고)에서 변화된 부분들은 다 작성했다. 이후 메인페이지 등을 정의하면된다. 필자의 경우 profile을 가져올 필요가 없는 프로젝트 이기에 원 개발자 분 코드에서 사용하는 FFImage를 사용하지 않았다.


etc

추가로 안드로이드 인터넷 사용권한을 부여해야된다.

방법은 안드로이드 네임스페이스에 마우스 오른쪽 - > 속성 -> Android 매니페스트 -> 필수권한 -> INTERNET 항목을 선택하면된다.

Android MainActivity.cs
Android 인터넷 사용권한

.

.

❗ 메인 Oncreate 메서드에서 아래 코드를 추가하지 않는다면 화면이 로딩 중에서 넘어가지 않는 오류가 발생한다. 유의하도록 하자.
global::Xamarin.Auth.Presenters.XamarinAndroid.AuthenticationConfiguration.Init(this,savedInstanceState)


BLEX를 이틀 째 사용중으로 당분간은 BLEX위주로 작성하면서 장단점을 잴 예정이다. 필자는 다크모드를 자주 사용하는데, 일반모드에서는 코드블록이 크게 거슬리지 않으나 다크모드의 경우 색감이 보기에 불편하지 않을까싶다.

이 글이 도움이 되었나요?

신고하기
0분 전
작성된 댓글이 없습니다. 첫 댓글을 달아보세요!
    댓글을 작성하려면 로그인이 필요합니다.