diff --git a/GoogleSignIn/Impl/GoogleSignInImpl.cs b/GoogleSignIn/Impl/GoogleSignInImpl.cs index b135b972..66454deb 100644 --- a/GoogleSignIn/Impl/GoogleSignInImpl.cs +++ b/GoogleSignIn/Impl/GoogleSignInImpl.cs @@ -187,19 +187,26 @@ static void GoogleSignIn_Signout(HandleRef self) internal static IntPtr GoogleSignIn_Result(HandleRef self) => googleIdTokenCredential.GetRawObject(); internal static int GoogleSignIn_Status(HandleRef self) => GoogleSignInHelper.CallStatic("getStatus"); - + internal static string GoogleSignIn_GetServerAuthCode(HandleRef self) => authorizationResult?.Call("getServerAuthCode"); internal static string GoogleSignIn_GetUserId(HandleRef self) { try { - var idTokenPart = googleIdTokenCredential?.Call("getIdToken")?.Split('.')?.ElementAtOrDefault(1); - if(!(idTokenPart?.Length is int length && length > 1)) + string idToken = googleIdTokenCredential?.Call("getIdToken"); + string idTokenPart = idToken?.Split('.')?.ElementAtOrDefault(1); + if(!(idTokenPart?.Length > 1)) return null; - string fill = new string('=',(4 - (idTokenPart.Length % 4)) % 4); - var jobj = Newtonsoft.Json.Linq.JObject.Parse(Encoding.UTF8.GetString(Convert.FromBase64String(idTokenPart + fill))); + // Replace URL-safe characters and fix padding + idTokenPart = idTokenPart.Replace('-', '+').Replace('_', '/'); + int mod = idTokenPart.Length % 4; + if(mod > 0) + idTokenPart += new string('=',4 - mod); + var idTokenFromBase64 = Convert.FromBase64String(idTokenPart); + var idToken = Encoding.UTF8.GetString(idTokenFromBase64); + var jobj = Newtonsoft.Json.Linq.JObject.Parse(idToken); return jobj?["sub"]?.ToString(); } catch(Exception e) @@ -293,7 +300,7 @@ internal static extern UIntPtr GoogleSignIn_GetServerAuthCode( [DllImport(DllName)] internal static extern UIntPtr GoogleSignIn_GetUserId(HandleRef self, [In, Out] byte[] bytes, UIntPtr len); - + internal static string GoogleSignIn_GetServerAuthCode(HandleRef self) => OutParamsToString((out_string, out_size) => GoogleSignIn_GetServerAuthCode(self, out_string, out_size)); diff --git a/GoogleSignIn/Impl/GoogleSignInImplEditor.cs b/GoogleSignIn/Impl/GoogleSignInImplEditor.cs index 34ae913d..eb08c2a8 100644 --- a/GoogleSignIn/Impl/GoogleSignInImplEditor.cs +++ b/GoogleSignIn/Impl/GoogleSignInImplEditor.cs @@ -1,204 +1,211 @@ -#if UNITY_EDITOR || UNITY_STANDALONE -using System; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -using System.Net; -using System.Net.NetworkInformation; - -using UnityEngine; - -using Newtonsoft.Json.Linq; - -namespace Google.Impl -{ - internal class GoogleSignInImplEditor : ISignInImpl, FutureAPIImpl - { - GoogleSignInConfiguration configuration; - - public bool Pending { get; private set; } - - public GoogleSignInStatusCode Status { get; private set; } - - public GoogleSignInUser Result { get; private set; } - - public GoogleSignInImplEditor(GoogleSignInConfiguration configuration) - { - this.configuration = configuration; - } - - public void Disconnect() - { - throw new NotImplementedException(); - } - - public void EnableDebugLogging(bool flag) - { - throw new NotImplementedException(); - } - - public Future SignIn() - { - SigningIn(); - return new Future(this); - } - - public Future SignInSilently() - { - SigningIn(); - return new Future(this); - } - - public void SignOut() - { - Debug.Log("No need on editor?"); - } - - static HttpListener BindLocalHostFirstAvailablePort() - { - ushort minPort = 49215; -#if UNITY_EDITOR_WIN - var listeners = IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpListeners(); - return Enumerable.Range(minPort, ushort.MaxValue - minPort).Where((i) => !listeners.Any((x) => x.Port == i)).Select((port) => { -#elif UNITY_EDITOR_OSX - return Enumerable.Range(minPort, ushort.MaxValue - minPort).Select((port) => { -#else - return Enumerable.Range(0,10).Select((i) => UnityEngine.Random.Range(minPort,ushort.MaxValue)).Select((port) => { -#endif - try - { - var listener = new HttpListener(); - listener.Prefixes.Add($"http://localhost:{port}/"); - listener.Start(); - return listener; - } - catch(System.Exception e) - { - Debug.LogException(e); - return null; - } - }).FirstOrDefault((listener) => listener != null); - } - - void SigningIn() - { - Pending = true; - var httpListener = BindLocalHostFirstAvailablePort(); - try - { - var openURL = "https://accounts.google.com/o/oauth2/v2/auth?" + Uri.EscapeUriString("scope=openid email profile&response_type=code&redirect_uri=" + httpListener.Prefixes.FirstOrDefault() + "&client_id=" + configuration.WebClientId); - Debug.Log(openURL); - Application.OpenURL(openURL); - } - catch(Exception e) - { - Debug.LogException(e); - throw; - } - - var taskScheduler = TaskScheduler.FromCurrentSynchronizationContext(); - httpListener.GetContextAsync().ContinueWith(async(task) => { - try - { - Debug.Log(task); - var context = task.Result; - var queryString = context.Request.Url.Query; - var queryDictionary = System.Web.HttpUtility.ParseQueryString(queryString); - if(queryDictionary == null || queryDictionary.Get("code") is not string code || string.IsNullOrEmpty(code)) - { - Status = GoogleSignInStatusCode.INVALID_ACCOUNT; - - context.Response.StatusCode = 404; - context.Response.OutputStream.Write(Encoding.UTF8.GetBytes("Cannot get code")); - context.Response.Close(); - return; - } - - context.Response.StatusCode = 200; - context.Response.OutputStream.Write(Encoding.UTF8.GetBytes("Can close this page")); - context.Response.Close(); - - var jobj = await HttpWebRequest.CreateHttp("https://www.googleapis.com/oauth2/v4/token").Post("application/x-www-form-urlencoded","code=" + code + "&client_id=" + configuration.WebClientId + "&client_secret=" + configuration.ClientSecret + "&redirect_uri=" + httpListener.Prefixes.FirstOrDefault() + "&grant_type=authorization_code").ContinueWith((task) => { - return JObject.Parse(task.Result); - },taskScheduler); - - var accessToken = (string)jobj.GetValue("access_token"); - var expiresIn = (int)jobj.GetValue("expires_in"); - var scope = (string)jobj.GetValue("scope"); - var tokenType = (string)jobj.GetValue("token_type"); - - var user = new GoogleSignInUser(); - if(configuration.RequestAuthCode) - user.AuthCode = code; - - if(configuration.RequestIdToken) - user.IdToken = (string)jobj.GetValue("id_token"); - - var request = HttpWebRequest.CreateHttp("https://openidconnect.googleapis.com/v1/userinfo"); - request.Method = "GET"; - request.Headers.Add("Authorization", "Bearer " + accessToken); - - var data = await request.GetResponseAsStringAsync().ContinueWith((task) => task.Result,taskScheduler); - var userInfo = JObject.Parse(data); - user.UserId = (string)userInfo.GetValue("sub"); - user.DisplayName = (string)userInfo.GetValue("name"); - - if(configuration.RequestEmail) - user.Email = (string)userInfo.GetValue("email"); - - if(configuration.RequestProfile) - { - user.GivenName = (string)userInfo.GetValue("given_name"); - user.FamilyName = (string)userInfo.GetValue("family_name"); - user.ImageUrl = Uri.TryCreate((string)userInfo.GetValue("picture"),UriKind.Absolute,out var url) ? url : null; - } - - Result = user; - - Status = GoogleSignInStatusCode.SUCCESS; - } - catch(Exception e) - { - Status = GoogleSignInStatusCode.ERROR; - Debug.LogException(e); - throw; - } - finally - { - Pending = false; - } - },taskScheduler); - } - } - - public static class EditorExt - { - public static Task Post(this HttpWebRequest request,string contentType,string data,Encoding encoding = null) - { - if(encoding == null) - encoding = Encoding.UTF8; - - request.Method = "POST"; - request.ContentType = contentType; - using(var stream = request.GetRequestStream()) - stream.Write(encoding.GetBytes(data)); - - return request.GetResponseAsStringAsync(encoding); - } - - public static async Task GetResponseAsStringAsync(this HttpWebRequest request,Encoding encoding = null) - { - using(var response = await request.GetResponseAsync()) - { - using(var stream = response.GetResponseStream()) - return stream.ReadToEnd(encoding ?? Encoding.UTF8); - } - } - - public static string ReadToEnd(this Stream stream,Encoding encoding = null) => new StreamReader(stream,encoding ?? Encoding.UTF8).ReadToEnd(); - public static void Write(this Stream stream,byte[] data) => stream.Write(data,0,data.Length); - } -} +#if UNITY_EDITOR || UNITY_STANDALONE +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using System.Net; +using System.Net.NetworkInformation; + +using UnityEngine; + +using Newtonsoft.Json.Linq; + +namespace Google.Impl +{ + internal class GoogleSignInImplEditor : ISignInImpl, FutureAPIImpl + { + GoogleSignInConfiguration configuration; + + public bool Pending { get; private set; } + + public GoogleSignInStatusCode Status { get; private set; } + + public GoogleSignInUser Result { get; private set; } + + public GoogleSignInImplEditor(GoogleSignInConfiguration configuration) + { + this.configuration = configuration; + } + + public void Disconnect() + { + throw new NotImplementedException(); + } + + public void EnableDebugLogging(bool flag) + { + throw new NotImplementedException(); + } + + public Future SignIn() + { + SigningIn(); + return new Future(this); + } + + public Future SignInSilently() + { + SigningIn(); + return new Future(this); + } + + public void SignOut() + { + Debug.Log("No need on editor?"); + } + + static HttpListener BindLocalHostFirstAvailablePort() + { + ushort minPort = 49215; +#if UNITY_EDITOR_WIN + var listeners = IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpListeners(); + return Enumerable.Range(minPort, ushort.MaxValue - minPort).Where((i) => !listeners.Any((x) => x.Port == i)).Select((port) => { +#elif UNITY_EDITOR_OSX + return Enumerable.Range(minPort, ushort.MaxValue - minPort).Select((port) => { +#else + return Enumerable.Range(0,10).Select((i) => UnityEngine.Random.Range(minPort,ushort.MaxValue)).Select((port) => { +#endif + try + { + var listener = new HttpListener(); + listener.Prefixes.Add($"http://localhost:{port}/"); + listener.Start(); + return listener; + } + catch(System.Exception e) + { + Debug.LogException(e); + return null; + } + }).FirstOrDefault((listener) => listener != null); + } + + void SigningIn() + { + Pending = true; + var httpListener = BindLocalHostFirstAvailablePort(); + try + { + var openURL = "https://accounts.google.com/o/oauth2/v2/auth?" + Uri.EscapeUriString("scope=openid email profile&response_type=code&redirect_uri=" + httpListener.Prefixes.FirstOrDefault() + "&client_id=" + configuration.WebClientId); + Debug.Log(openURL); + Application.OpenURL(openURL); + } + catch(Exception e) + { + Debug.LogException(e); + throw; + } + + var taskScheduler = TaskScheduler.FromCurrentSynchronizationContext(); + httpListener.GetContextAsync().ContinueWith(async(task) => { + try + { + Debug.Log(task); + var context = task.Result; + var queryString = context.Request.Url.Query; + var queryDictionary = System.Web.HttpUtility.ParseQueryString(queryString); + if(queryDictionary == null || queryDictionary.Get("code") is not string code || string.IsNullOrEmpty(code)) + { + Status = GoogleSignInStatusCode.INVALID_ACCOUNT; + + context.Response.StatusCode = 404; + context.Response.OutputStream.Write(Encoding.UTF8.GetBytes("Cannot get code")); + context.Response.Close(); + return; + } + + context.Response.StatusCode = 200; + context.Response.OutputStream.Write(Encoding.UTF8.GetBytes("Can close this page")); + context.Response.Close(); + + var jobj = await HttpWebRequest.CreateHttp("https://www.googleapis.com/oauth2/v4/token").Post("application/x-www-form-urlencoded","code=" + code + "&client_id=" + configuration.WebClientId + "&client_secret=" + configuration.ClientSecret + "&redirect_uri=" + httpListener.Prefixes.FirstOrDefault() + "&grant_type=authorization_code").ContinueWith((task) => { + return JObject.Parse(task.Result); + },taskScheduler); + + var accessToken = (string)jobj.GetValue("access_token"); + var expiresIn = (int)jobj.GetValue("expires_in"); + var scope = (string)jobj.GetValue("scope"); + var tokenType = (string)jobj.GetValue("token_type"); + + var user = new GoogleSignInUser(); + if(configuration.RequestAuthCode) + user.AuthCode = code; + + if(configuration.RequestIdToken) + user.IdToken = (string)jobj.GetValue("id_token"); + + var request = HttpWebRequest.CreateHttp("https://openidconnect.googleapis.com/v1/userinfo"); + request.Method = "GET"; + request.Headers.Add("Authorization", "Bearer " + accessToken); + + var data = await request.GetResponseAsStringAsync().ContinueWith((task) => task.Result,taskScheduler); + var userInfo = JObject.Parse(data); + user.UserId = (string)userInfo.GetValue("sub"); + user.DisplayName = (string)userInfo.GetValue("name"); + + if(configuration.RequestEmail) + user.Email = (string)userInfo.GetValue("email"); + + if(configuration.RequestProfile) + { + user.GivenName = (string)userInfo.GetValue("given_name"); + user.FamilyName = (string)userInfo.GetValue("family_name"); + user.ImageUrl = Uri.TryCreate((string)userInfo.GetValue("picture"),UriKind.Absolute,out var url) ? url : null; + } + + Result = user; + + Status = GoogleSignInStatusCode.SUCCESS; + } + catch(Exception e) + { + Status = GoogleSignInStatusCode.ERROR; + + Debug.LogException(e); + if(e is AggregateException ae) + { + foreach(var inner in ae.InnerExceptions) + Debug.LogException(inner); + } + + throw; + } + finally + { + Pending = false; + } + },taskScheduler); + } + } + + public static class EditorExt + { + public static Task Post(this HttpWebRequest request,string contentType,string data,Encoding encoding = null) + { + if(encoding == null) + encoding = Encoding.UTF8; + + request.Method = "POST"; + request.ContentType = contentType; + using(var stream = request.GetRequestStream()) + stream.Write(encoding.GetBytes(data)); + + return request.GetResponseAsStringAsync(encoding); + } + + public static async Task GetResponseAsStringAsync(this HttpWebRequest request,Encoding encoding = null) + { + using(var response = await request.GetResponseAsync()) + { + using(var stream = response.GetResponseStream()) + return stream.ReadToEnd(encoding ?? Encoding.UTF8); + } + } + + public static string ReadToEnd(this Stream stream,Encoding encoding = null) => new StreamReader(stream,encoding ?? Encoding.UTF8).ReadToEnd(); + public static void Write(this Stream stream,byte[] data) => stream.Write(data,0,data.Length); + } +} #endif \ No newline at end of file diff --git a/Plugins/Android/src/main/java/com/google/googlesignin/GoogleSignInHelper.java b/Plugins/Android/src/main/java/com/google/googlesignin/GoogleSignInHelper.java index 35acfca8..d543c2e6 100644 --- a/Plugins/Android/src/main/java/com/google/googlesignin/GoogleSignInHelper.java +++ b/Plugins/Android/src/main/java/com/google/googlesignin/GoogleSignInHelper.java @@ -1,292 +1,295 @@ -/* - * Copyright 2017 Google Inc. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.googlesignin; - -import android.os.CancellationSignal; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.credentials.ClearCredentialStateRequest; -import androidx.credentials.Credential; -import androidx.credentials.CredentialManager; -import androidx.credentials.CredentialManagerCallback; -import androidx.credentials.GetCredentialRequest; -import androidx.credentials.GetCredentialResponse; -import androidx.credentials.exceptions.ClearCredentialException; -import androidx.credentials.exceptions.GetCredentialException; - -import com.google.android.gms.auth.api.identity.AuthorizationRequest; -import com.google.android.gms.auth.api.identity.AuthorizationResult; -import com.google.android.gms.auth.api.identity.Identity; -import com.google.android.gms.common.Scopes; -import com.google.android.gms.common.api.CommonStatusCodes; -import com.google.android.gms.common.api.Scope; -import com.google.android.gms.common.util.Strings; -import com.google.android.gms.tasks.OnCompleteListener; -import com.google.android.gms.tasks.OnSuccessListener; -import com.google.android.gms.tasks.SuccessContinuation; -import com.google.android.gms.tasks.Task; -import com.google.android.gms.tasks.TaskCompletionSource; -import com.google.android.gms.tasks.TaskExecutors; -import com.google.android.libraries.identity.googleid.GetGoogleIdOption; -import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption; -import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential; -import com.unity3d.player.UnityPlayer; - -import org.jetbrains.annotations.NotNull; - -import java.util.ArrayList; -import java.util.List; -import java.util.function.Function; - -/** - * Helper class used by the native C++ code to interact with Google Sign-in API. - * The general flow is - * Call configure, then one of signIn or signInSilently. - */ -public class GoogleSignInHelper { - - // Set to true to get more debug logging. - public static boolean loggingEnabled = false; - - /** - * Enables verbose logging - */ - public static void enableDebugLogging(boolean flag) { - loggingEnabled = flag; - } - - private static CancellationSignal cancellationSignal; - private static Task task; - private static Function> signInFunction; - public static boolean isPending() { - return task != null && !task.isComplete() && !task.isCanceled(); - } - - public static int getStatus() { - if(signInFunction == null) - return CommonStatusCodes.DEVELOPER_ERROR; - - if(task == null) - return CommonStatusCodes.SIGN_IN_REQUIRED; - - if(task.isCanceled()) - return CommonStatusCodes.CANCELED; - - if(task.isSuccessful()) - return CommonStatusCodes.SUCCESS; - - Exception e = task.getException(); - if(e != null) - return CommonStatusCodes.INTERNAL_ERROR; - - return CommonStatusCodes.ERROR; - } - - /** - * Sets the configuration of the sign-in api that should be used. - * - * @param useGamesConfig - true if the GAMES_CONFIG should be used when - * signing-in. - * @param webClientId - the web client id of the backend server - * associated with this application. - * @param requestAuthCode - true if a server auth code is needed. This also - * requires the web - * client id to be set. - * @param forceRefreshToken - true to force a refresh token when using the - * server auth code. - * @param requestEmail - true if email address of the user is requested. - * @param requestIdToken - true if an id token for the user is requested. - * @param hideUiPopups - true if the popups during sign-in from the Games - * API should be hidden. - * This only has affect if useGamesConfig is true. - * @param defaultAccountName - the account name to attempt to default to when - * signing in. - * @param additionalScopes - additional API scopes to request when - * authenticating. - * @param requestHandle - the handle to this request, created by the native - * C++ code, this is used - * to correlate the response with the request. - */ - public static void configure( - boolean useGamesConfig, - String webClientId, - boolean requestAuthCode, - boolean forceRefreshToken, - boolean requestEmail, - boolean requestIdToken, - boolean hideUiPopups, - String defaultAccountName, - String[] additionalScopes, - IListener requestHandle) { - logDebug("TokenFragment.configure called"); - - signInFunction = new Function>() { - @Override - public Task apply(@NonNull Boolean silent) { - if(isPending()) { - TaskCompletionSource source = new TaskCompletionSource<>(); - source.trySetException(new Exception("Last task still pending")); - return source.getTask(); - } - - cancellationSignal = new CancellationSignal(); - - GetCredentialRequest.Builder getCredentialRequestBuilder = new GetCredentialRequest.Builder() - .setPreferImmediatelyAvailableCredentials(hideUiPopups); - - if(silent) { - GetGoogleIdOption.Builder getGoogleIdOptionBuilder = new GetGoogleIdOption.Builder() - .setFilterByAuthorizedAccounts(hideUiPopups) - .setAutoSelectEnabled(hideUiPopups); - - if(defaultAccountName != null) - getGoogleIdOptionBuilder.setNonce(defaultAccountName); - - if(!Strings.isEmptyOrWhitespace(webClientId)) - getGoogleIdOptionBuilder.setServerClientId(webClientId); - - getCredentialRequestBuilder.addCredentialOption(getGoogleIdOptionBuilder.build()); - } - else { - GetSignInWithGoogleOption.Builder getSignInWithGoogleOptionBuilder = new GetSignInWithGoogleOption.Builder(webClientId); - getCredentialRequestBuilder.addCredentialOption(getSignInWithGoogleOptionBuilder.build()); - } - - TaskCompletionSource source = new TaskCompletionSource<>(); - - CredentialManager.create(UnityPlayer.currentActivity).getCredentialAsync(UnityPlayer.currentActivity, - getCredentialRequestBuilder.build(), - cancellationSignal, - TaskExecutors.MAIN_THREAD, - new CredentialManagerCallback() { - @Override - public void onResult(GetCredentialResponse getCredentialResponse) { - source.trySetResult(getCredentialResponse); - } - - @Override - public void onError(@NotNull GetCredentialException e) { - source.trySetException(e); - } - }); - - return source.getTask().onSuccessTask(new SuccessContinuation() { - @NonNull - @Override - public Task then(GetCredentialResponse getCredentialResponse) throws Exception { - try { - Credential credential = getCredentialResponse.getCredential(); - Log.i(TAG, "credential.getType() : " + credential.getType()); - - GoogleIdTokenCredential googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.getData()); - requestHandle.onAuthenticated(googleIdTokenCredential); - } - catch (Exception e) { - throw e; - } - - AuthorizationRequest.Builder authorizationRequestBuilder = new AuthorizationRequest.Builder(); - if (requestAuthCode && !Strings.isEmptyOrWhitespace(webClientId)) - authorizationRequestBuilder.requestOfflineAccess(webClientId, forceRefreshToken); - - int additionalCount = additionalScopes != null ? additionalScopes.length : 0; - List scopes = new ArrayList<>(2 + additionalCount); - scopes.add(new Scope(Scopes.PROFILE)); - if (requestEmail) - scopes.add(new Scope(Scopes.EMAIL)); - if (additionalCount > 0) { - for (String scope : additionalScopes) { - scopes.add(new Scope(scope)); - } - } - - if (!scopes.isEmpty()) - authorizationRequestBuilder.setRequestedScopes(scopes); - - return Identity.getAuthorizationClient(UnityPlayer.currentActivity).authorize(authorizationRequestBuilder.build()); - } - }).addOnFailureListener(requestHandle).addOnCanceledListener(requestHandle).addOnSuccessListener(new OnSuccessListener() { - @Override - public void onSuccess(AuthorizationResult authorizationResult) { - requestHandle.onAuthorized(authorizationResult); - } - }).addOnCompleteListener(new OnCompleteListener() { - @Override - public void onComplete(@NonNull Task _unused) { - cancellationSignal = null; - } - }); - } - }; - } - - public static Task signIn() { - task = signInFunction.apply(false); - return task; - } - - public static Task signInSilently() { - task = signInFunction.apply(true); - return task; - } - - public static void cancel() { - if(isPending() && cancellationSignal != null){ - cancellationSignal.cancel(); - cancellationSignal = null; - } - - task = null; - } - - public static void signOut() { - cancel(); - - CredentialManager.create(UnityPlayer.currentActivity).clearCredentialStateAsync(new ClearCredentialStateRequest(), - new CancellationSignal(), - TaskExecutors.MAIN_THREAD, - new CredentialManagerCallback() { - @Override - public void onResult(Void unused) { - logInfo("signOut"); - } - - @Override - public void onError(@NonNull ClearCredentialException e) { - logError(e.getMessage()); - } - }); - } - - static final String TAG = GoogleSignInHelper.class.getSimpleName(); - - public static void logInfo(String msg) { - if (loggingEnabled) { - Log.i(TAG, msg); - } - } - - public static void logError(String msg) { - Log.e(TAG, msg); - } - - public static void logDebug(String msg) { - if (loggingEnabled) { - Log.d(TAG, msg); - } - } -} +/* + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.googlesignin; + +import android.os.CancellationSignal; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.credentials.ClearCredentialStateRequest; +import androidx.credentials.Credential; +import androidx.credentials.CredentialManager; +import androidx.credentials.CredentialManagerCallback; +import androidx.credentials.GetCredentialRequest; +import androidx.credentials.GetCredentialResponse; +import androidx.credentials.exceptions.ClearCredentialException; +import androidx.credentials.exceptions.GetCredentialException; + +import com.google.android.gms.auth.api.identity.AuthorizationRequest; +import com.google.android.gms.auth.api.identity.AuthorizationResult; +import com.google.android.gms.auth.api.identity.Identity; +import com.google.android.gms.common.Scopes; +import com.google.android.gms.common.api.CommonStatusCodes; +import com.google.android.gms.common.api.Scope; +import com.google.android.gms.common.util.Strings; +import com.google.android.gms.tasks.OnCompleteListener; +import com.google.android.gms.tasks.OnSuccessListener; +import com.google.android.gms.tasks.SuccessContinuation; +import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.TaskCompletionSource; +import com.google.android.gms.tasks.TaskExecutors; +import com.google.android.libraries.identity.googleid.GetGoogleIdOption; +import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption; +import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential; +import com.unity3d.player.UnityPlayer; + +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +/** + * Helper class used by the native C++ code to interact with Google Sign-in API. + * The general flow is + * Call configure, then one of signIn or signInSilently. + */ +public class GoogleSignInHelper { + + // Set to true to get more debug logging. + public static boolean loggingEnabled = false; + + /** + * Enables verbose logging + */ + public static void enableDebugLogging(boolean flag) { + loggingEnabled = flag; + } + + private static CancellationSignal cancellationSignal; + private static Task task; + private static Function> signInFunction; + public static boolean isPending() { + return task != null && !task.isComplete() && !task.isCanceled(); + } + + public static int getStatus() { + if(signInFunction == null) + return CommonStatusCodes.DEVELOPER_ERROR; + + if(task == null) + return CommonStatusCodes.SIGN_IN_REQUIRED; + + if(task.isCanceled()) + return CommonStatusCodes.CANCELED; + + if(task.isSuccessful()) + return CommonStatusCodes.SUCCESS; + + Exception e = task.getException(); + if(e != null) + { + logError("onFailure with INTERNAL_ERROR : " + e.getClass().toString() + " " + e.getMessage()); + return CommonStatusCodes.INTERNAL_ERROR; + } + + return CommonStatusCodes.ERROR; + } + + /** + * Sets the configuration of the sign-in api that should be used. + * + * @param useGamesConfig - true if the GAMES_CONFIG should be used when + * signing-in. + * @param webClientId - the web client id of the backend server + * associated with this application. + * @param requestAuthCode - true if a server auth code is needed. This also + * requires the web + * client id to be set. + * @param forceRefreshToken - true to force a refresh token when using the + * server auth code. + * @param requestEmail - true if email address of the user is requested. + * @param requestIdToken - true if an id token for the user is requested. + * @param hideUiPopups - true if the popups during sign-in from the Games + * API should be hidden. + * This only has affect if useGamesConfig is true. + * @param defaultAccountName - the account name to attempt to default to when + * signing in. + * @param additionalScopes - additional API scopes to request when + * authenticating. + * @param requestHandle - the handle to this request, created by the native + * C++ code, this is used + * to correlate the response with the request. + */ + public static void configure( + boolean useGamesConfig, + String webClientId, + boolean requestAuthCode, + boolean forceRefreshToken, + boolean requestEmail, + boolean requestIdToken, + boolean hideUiPopups, + String defaultAccountName, + String[] additionalScopes, + IListener requestHandle) { + logDebug("TokenFragment.configure called"); + + signInFunction = new Function>() { + @Override + public Task apply(@NonNull Boolean silent) { + if(isPending()) { + TaskCompletionSource source = new TaskCompletionSource<>(); + source.trySetException(new Exception("Last task still pending")); + return source.getTask(); + } + + cancellationSignal = new CancellationSignal(); + + GetCredentialRequest.Builder getCredentialRequestBuilder = new GetCredentialRequest.Builder() + .setPreferImmediatelyAvailableCredentials(hideUiPopups); + + if(silent) { + GetGoogleIdOption.Builder getGoogleIdOptionBuilder = new GetGoogleIdOption.Builder() + .setFilterByAuthorizedAccounts(hideUiPopups) + .setAutoSelectEnabled(hideUiPopups); + + if(defaultAccountName != null) + getGoogleIdOptionBuilder.setNonce(defaultAccountName); + + if(!Strings.isEmptyOrWhitespace(webClientId)) + getGoogleIdOptionBuilder.setServerClientId(webClientId); + + getCredentialRequestBuilder.addCredentialOption(getGoogleIdOptionBuilder.build()); + } + else { + GetSignInWithGoogleOption.Builder getSignInWithGoogleOptionBuilder = new GetSignInWithGoogleOption.Builder(webClientId); + getCredentialRequestBuilder.addCredentialOption(getSignInWithGoogleOptionBuilder.build()); + } + + TaskCompletionSource source = new TaskCompletionSource<>(); + + CredentialManager.create(UnityPlayer.currentActivity).getCredentialAsync(UnityPlayer.currentActivity, + getCredentialRequestBuilder.build(), + cancellationSignal, + TaskExecutors.MAIN_THREAD, + new CredentialManagerCallback() { + @Override + public void onResult(GetCredentialResponse getCredentialResponse) { + source.trySetResult(getCredentialResponse); + } + + @Override + public void onError(@NotNull GetCredentialException e) { + source.trySetException(e); + } + }); + + return source.getTask().onSuccessTask(new SuccessContinuation() { + @NonNull + @Override + public Task then(GetCredentialResponse getCredentialResponse) throws Exception { + try { + Credential credential = getCredentialResponse.getCredential(); + Log.i(TAG, "credential.getType() : " + credential.getType()); + + GoogleIdTokenCredential googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.getData()); + requestHandle.onAuthenticated(googleIdTokenCredential); + } + catch (Exception e) { + throw e; + } + + AuthorizationRequest.Builder authorizationRequestBuilder = new AuthorizationRequest.Builder(); + if (requestAuthCode && !Strings.isEmptyOrWhitespace(webClientId)) + authorizationRequestBuilder.requestOfflineAccess(webClientId, forceRefreshToken); + + int additionalCount = additionalScopes != null ? additionalScopes.length : 0; + List scopes = new ArrayList<>(2 + additionalCount); + scopes.add(new Scope(Scopes.PROFILE)); + if (requestEmail) + scopes.add(new Scope(Scopes.EMAIL)); + if (additionalCount > 0) { + for (String scope : additionalScopes) { + scopes.add(new Scope(scope)); + } + } + + if (!scopes.isEmpty()) + authorizationRequestBuilder.setRequestedScopes(scopes); + + return Identity.getAuthorizationClient(UnityPlayer.currentActivity).authorize(authorizationRequestBuilder.build()); + } + }).addOnFailureListener(requestHandle).addOnCanceledListener(requestHandle).addOnSuccessListener(new OnSuccessListener() { + @Override + public void onSuccess(AuthorizationResult authorizationResult) { + requestHandle.onAuthorized(authorizationResult); + } + }).addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task _unused) { + cancellationSignal = null; + } + }); + } + }; + } + + public static Task signIn() { + task = signInFunction.apply(false); + return task; + } + + public static Task signInSilently() { + task = signInFunction.apply(true); + return task; + } + + public static void cancel() { + if(isPending() && cancellationSignal != null){ + cancellationSignal.cancel(); + cancellationSignal = null; + } + + task = null; + } + + public static void signOut() { + cancel(); + + CredentialManager.create(UnityPlayer.currentActivity).clearCredentialStateAsync(new ClearCredentialStateRequest(), + new CancellationSignal(), + TaskExecutors.MAIN_THREAD, + new CredentialManagerCallback() { + @Override + public void onResult(Void unused) { + logInfo("signOut"); + } + + @Override + public void onError(@NonNull ClearCredentialException e) { + logError(e.getMessage()); + } + }); + } + + static final String TAG = GoogleSignInHelper.class.getSimpleName(); + + public static void logInfo(String msg) { + if (loggingEnabled) { + Log.i(TAG, msg); + } + } + + public static void logError(String msg) { + Log.e(TAG, msg); + } + + public static void logDebug(String msg) { + if (loggingEnabled) { + Log.d(TAG, msg); + } + } +}