ios

import Foundation
import AuthenticationServices
import UIKit

// 전역 세션 참조 (취소/유지용)
var currentAuthSession: ASWebAuthenticationSession?

@_cdecl("ASWP_StartAuth")
public func ASWP_StartAuth(
    _ authUrl: UnsafePointer<CChar>?,
    _ callbackUrlScheme: UnsafePointer<CChar>?
) {
    // 1) 파라미터 체크
    guard let authUrl = authUrl,
          let schemePtr = callbackUrlScheme else {
        return
    }

    let urlString = String(cString: authUrl)
    let schemeString = String(cString: schemePtr)

    guard let url = URL(string: urlString) else {
        NSLog("[SwiftUnityPlugin] Invalid auth URL: \(urlString)")
        return
    }

    DispatchQueue.main.async {
        // 2) ASWebAuthenticationSession 생성
        currentAuthSession = ASWebAuthenticationSession(
            url: url,
            callbackURLScheme: schemeString,
            completionHandler: { callbackURL, error in
                // ✅ 여기서는 Unity로 아무 것도 보내지 않고 그냥 처리만 함

                if let error = error {
                    NSLog("[SwiftUnityPlugin] Auth error: \(error.localizedDescription)")
                    return
                }

                if let callbackURL = callbackURL {
                    NSLog("[SwiftUnityPlugin] Auth success, redirect: \(callbackURL.absoluteString)")
                    DispatchQueue.main.async {
                                   UIApplication.shared.open(callbackURL, options: [:], completionHandler: nil)
                               }
                } else {
                    NSLog("[SwiftUnityPlugin] Auth finished with no callback URL")
                }
            }
        )

        if #available(iOS 13.0, *) {
            currentAuthSession?.presentationContextProvider = AuthWindowProvider.shared
        }

        _ = currentAuthSession?.start()
    }
}

// iOS 13+: 어떤 윈도우에 세션을 띄울지 지정
@available(iOS 13.0, *)
class AuthWindowProvider: NSObject, ASWebAuthenticationPresentationContextProviding {
    static let shared = AuthWindowProvider()

    func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
        // 가장 먼저 뜨는 윈도우를 사용 (필요하면 커스텀 처리 가능)
        return UIApplication.shared.windows.first ?? UIWindow()
    }
}

android

package com.ologgames.plugin

import android.app.Activity
import android.net.Uri
import android.widget.Toast
import androidx.browser.customtabs.CustomTabsIntent
import com.unity3d.player.UnityPlayer


import android.util.Log
import androidx.credentials.CredentialManager
import androidx.credentials.GetCredentialRequest
import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption
import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential
import kotlinx.coroutines.*
object UnityPlugin {
    // UnityPlayer.currentActivity는 항상 최신 Activity를 반환합니다.
    private val context: Activity
        get() = UnityPlayer.currentActivity



    /** Unity → Android 호출용: Chrome Custom Tab 열기 */
    @JvmStatic
    fun openCustomTab(url: String) {
        context.runOnUiThread {
            try {
                val builder = CustomTabsIntent.Builder()
                val customTabsIntent = builder.build()
                customTabsIntent.launchUrl(context, Uri.parse(url))
            } catch (e: Exception) {
                Toast.makeText(context, "Custom Tab 실행 실패: ${e.message}", Toast.LENGTH_LONG).show()
            }
        }
    }

}

server

using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;


public class OAuth
{


    Dictionary<string, string> stateCache = new Dictionary<string, string>();   // state → code_verifier


    string TEAM_ID;
    string KEY_ID;
    string SERVICE_ID;
    string P8_PATH;

    WebApplication _app;
    JWTCreator _jWTCreator;
    public OAuth(WebApplication app, JWTCreator jWTCreator)
    {
        TEAM_ID = GetEnv("TEAM_ID");
        KEY_ID = GetEnv("KEY_ID");
        SERVICE_ID = GetEnv("SERVICE_ID");
        P8_PATH = GetEnv("P8_PATH");

        _app = app;
        _jWTCreator = jWTCreator;

        Setup("android", "https://goraddy.ologgames.com/oauthresults");

        Setup("ios", "goraddy://oauthresults");

    }



    string GetEnv(string key)
    {
        var v = Environment.GetEnvironmentVariable(key);
     
        return v;
    }

    void Setup(string platform,string scheme)
    {
        //
        string appleRedirect = $"https://goraddy.ologgames.com/{platform}/oauth2/apple-complete";
        string googleRedirect = $"https://goraddy.ologgames.com/{platform}/oauth2/google-complete";


        _app.MapGet($"{platform}/oauth2/start-apple-login", () =>
        {
            System.Console.WriteLine($"{platform}/oauth2/start-apple-login");
            string state = WebEncoders.Base64UrlEncode(RandomNumberGenerator.GetBytes(16));
            string verifier = WebEncoders.Base64UrlEncode(RandomNumberGenerator.GetBytes(32));
            string challenge = WebEncoders.Base64UrlEncode(
            SHA256.HashData(Encoding.ASCII.GetBytes(verifier)));

            stateCache[state] = verifier;          // 메모리 저장

            var qp = new QueryBuilder
            {
            { "response_type", "code" },
            { "response_mode", "form_post" },
            { "client_id",     SERVICE_ID },
            { "redirect_uri",  appleRedirect },
            { "scope",         "name email" },
            { "state",         state },
            { "code_challenge", challenge },
            { "code_challenge_method", "S256" }
        };
            string authUrl = $"https://appleid.apple.com/auth/authorize{qp.ToQueryString()}";
            return Results.Json(new { authUrl });
        });



        _app.MapMethods($"/{platform}/oauth2/apple-complete", new[] { "GET", "POST" }, async (HttpContext ctx, AppDbContext db) =>
        {
            System.Console.WriteLine($"/{platform}/oauth2/apple-complete");
            string code = "";
            string state = "";

            if (ctx.Request.Method == "POST")
            {
                var form = await ctx.Request.ReadFormAsync();
                code = form["code"];
                state = form["state"];
            }
            else
            {
                code = ctx.Request.Query["code"];
                state = ctx.Request.Query["state"];
            }

            if (string.IsNullOrEmpty(code) || string.IsNullOrEmpty(state))
                return Results.BadRequest("missing code/state");

            if (!stateCache.TryGetValue(state, out string verifier))
                return Results.BadRequest("bad state");

            stateCache.Remove(state);             // 1회용

            var dict = new Dictionary<string, string>
            {
                ["grant_type"] = "authorization_code",
                ["code"] = code,
                ["client_id"] = SERVICE_ID,
                ["client_secret"] = AppleClientSecretCache.Get(TEAM_ID, KEY_ID, SERVICE_ID, P8_PATH),
                ["redirect_uri"] = appleRedirect,
                ["code_verifier"] = verifier
            };

            using var http = new HttpClient();
            var resp = await http.PostAsync(
                "https://appleid.apple.com/auth/token",
                new FormUrlEncodedContent(dict));

            if (!resp.IsSuccessStatusCode)
                return Results.Problem(await resp.Content.ReadAsStringAsync(),
                                       statusCode: (int)resp.StatusCode);

            var json = JsonDocument.Parse(await resp.Content.ReadAsStringAsync()).RootElement;
            string idToken = json.GetProperty("id_token").GetString();

            var claims = new JwtSecurityTokenHandler().ReadJwtToken(idToken).Claims;
            string appleSub = claims.First(c => c.Type == "sub").Value;

            var userData = await db.Users.Where(u => u.AppleId == appleSub).FirstOrDefaultAsync();
            string jwt;
            if (userData == null)
            {
                var user = new User(); // 아무 속성도 설정하지 않음
                user.AppleId = appleSub;
                db.Users.Add(user);

                await db.SaveChangesAsync(); // 여기서 guest.Id 생성됨
                jwt = _jWTCreator.CreateToken(user.UserDataId.ToString());
            }
            else
            {
                jwt = _jWTCreator.CreateToken(userData.UserDataId.ToString());
            }

            string redirectUrl = $"{scheme}#code={jwt}";
            ctx.Response.Redirect(redirectUrl);
            return Results.Empty;
        });



        _app.MapGet($"{platform}/oauth2/start-google-login", (HttpContext context) =>
{
    System.Console.WriteLine($"{platform}/oauth2/start-google-login");
    var clientId = "";
    var scope = "email profile"; // 자동 URL 인코딩됨
    var state = Guid.NewGuid().ToString();

    // ✅ state 쿠키에 저장 (검증용)
    context.Response.Cookies.Append("oauth_state", state, new CookieOptions
    {
        HttpOnly = true,
        Secure = true,
        SameSite = SameSiteMode.Lax,
        MaxAge = TimeSpan.FromMinutes(10)
    });

    // ✅ query string 생성
    var query = new QueryString()
        .Add("client_id", clientId)
        .Add("redirect_uri", googleRedirect)
        .Add("response_type", "code")
        .Add("scope", scope)
        .Add("state", state)
        .Add("response_mode", "query");

    var authUrl = $"https://accounts.google.com/o/oauth2/v2/auth{query}";

    System.Console.WriteLine(authUrl);
    return Results.Json(new { authUrl });
});

        _app.MapGet($"/{platform}/oauth2/google-complete", async (HttpContext context, AppDbContext db) =>
        {
            System.Console.WriteLine($"/{platform}/oauth2/google-complete");
            var query = context.Request.Query;

         

            string code = query["code"];
            if (string.IsNullOrEmpty(code))
            {
                context.Response.Redirect($"https://goraddy.ologgames.com");
                //await context.Response.WriteAsync("Missing authorization code");
                //return;
            }

            Console.WriteLine($"[OAuth Redirect] code = {code}");

            // 1. Google access_token 요청
            using var http = new HttpClient();

            var tokenRequest = new HttpRequestMessage(HttpMethod.Post, "https://oauth2.googleapis.com/token");
            tokenRequest.Content = new FormUrlEncodedContent(new Dictionary<string, string>
            {
        { "code", code },
        { "client_id", "" },
        { "client_secret", "G" },
        { "redirect_uri", googleRedirect },
        { "grant_type", "authorization_code" },
        { "response_mode", "query" },
            });

            var tokenResponse = await http.SendAsync(tokenRequest);
            if (!tokenResponse.IsSuccessStatusCode)
            {
                await context.Response.WriteAsync("Failed to exchange code");
                //return;
            }

            var tokenJson = await tokenResponse.Content.ReadAsStringAsync();
            var tokenData = JsonDocument.Parse(tokenJson).RootElement;

            string accessToken = tokenData.GetProperty("access_token").GetString();
            string idToken = tokenData.GetProperty("id_token").GetString(); // Optional

            Console.WriteLine($"[OAuth] access_token = {accessToken}");

            // 2. 사용자 정보 요청
            var userInfoRequest = new HttpRequestMessage(HttpMethod.Get, "https://www.googleapis.com/oauth2/v3/userinfo");
            userInfoRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

            var userInfoResponse = await http.SendAsync(userInfoRequest);
            if (!userInfoResponse.IsSuccessStatusCode)
            {
                await context.Response.WriteAsync("Failed to fetch user info");
                //return;
            }

            var userJson = await userInfoResponse.Content.ReadAsStringAsync();
            var user = JsonDocument.Parse(userJson).RootElement;

            string email = user.GetProperty("email").GetString();
            string sub = user.GetProperty("sub").GetString(); // Google의 유니크한 사용자 ID
            string name = user.GetProperty("name").GetString();
            //string state = user.GetProperty("state").GetString();




            // 3. uid 생성 또는 내부 유저 DB 처리
            var userData = await db.Users.Where(u => u.GoogleId == sub).FirstOrDefaultAsync();
            string jwt;
            if (userData == null)
            {
                var newuser = new User(); // 아무 속성도 설정하지 않음
                newuser.GoogleId = sub;
                db.Users.Add(newuser);

                await db.SaveChangesAsync(); // 여기서 guest.Id 생성됨
                jwt = _jWTCreator.CreateToken(newuser.UserDataId.ToString());
            }
            else
            {
                jwt = _jWTCreator.CreateToken(userData.UserDataId.ToString());
            }

            // 4. 앱으로 딥링크 전달

            string redirectUrl = $"{scheme}#code={jwt}";

            context.Response.Redirect(redirectUrl);
            return Results.Empty;
        });
    }





}

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다