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;
});
}
}