Open Thaina opened 6 years ago
I imported Newtonsoft.Json.Linq.
I tried to implement this and I'm getting errors here:
listener.Prefixes.Add($"http://localhost:{port}/");
I fixed this by changing it to this:
listener.Prefixes.Add("http://localhost:"+port+"/");
Also here:
httpListener.GetContextAsync().ContinueWith((task) => {
Unity isn't recognizing .GetContextAsync() method.... I've included:
using System.Net
I'm not sure what HttpListener class you're using? Can you please let me know how I can fix this?
@decoderzhub That code I use C# 7.2 from incremental compiler in unity 2018.2. It seem yours test project still using dotnet 3.5. Which mean it don't contain any async/await API yet
But HttpListener also contain GetContext
for pre async https://docs.microsoft.com/en-us/dotnet/api/system.net.httplistener.getcontext?view=netframework-3.5
You might need to modified related async code in the same way
Maybe just this
var context = httpListener.GetContextAsync();
string code;
if(!context.Request.Url.ParseQueryString().TryGetValue("code",out code) || string.IsNullOrEmpty(code))
{
Pending = false;
Status = GoogleSignInStatusCode.InvalidAccount;
Debug.Log("no code?");
return;
}
context.Response.StatusCode = 200;
context.Response.OutputStream.Write(Encoding.UTF8.GetBytes("Can close this page"));
context.Response.Close();
// do something similar at HttpWebRequest.CreateHttp
Thanks for the quick response! I'm downloading it now...
I tried the:
if (!context.Request.Url.ParseQueryString()...
context doesn't contain that method I'm updating from Unity 2018.1 to 2018.2 should do the trick
I am sorry there was my custom extension method around here
This is the file for it
#if UNITY_EDITOR
using System;
using System.IO;
using System.Net;
using System.Text;
using System.Linq;
using System.Threading.Tasks;
using System.Collections.Generic;
public static class EditorExt
{
public static async Task<string> PostFormUrlEncoded(this HttpWebRequest request,string data) => await request.Post("application/x-www-form-urlencoded",data);
public static async Task<string> Post(this HttpWebRequest request,string contentType,string data)
{
request.Method = "POST";
request.ContentType = contentType;
using(var stream = request.GetRequestStream())
stream.Write(Encoding.UTF8.GetBytes(data));
using(var response = await request.GetResponseAsync())
{
using(var stream = response.GetResponseStream())
return stream.ReadToEnd(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);
public static Dictionary<string,string> ParseQueryString(this Uri uri) => uri?.Query?.Split('?','&').Select((pair) => {
return pair.Split(new[] { '=' },2);
}).Where((pair) => !string.IsNullOrEmpty(pair.FirstOrDefault())).ToDictionary((pair) => pair.FirstOrDefault(),(pair) => pair.ElementAtOrDefault(1));
}
#endif
In old .net you can do...
httpListener.BeginGetContext(result => {
var context = httpListener.EndGetContext(result);
if (
!context.Request.Url.ParseQueryString().TryGetValue("code", out var code)
|| string.IsNullOrEmpty(code)
) {
Pending = false;
Status = GoogleSignInStatusCode.InvalidAccount;
Debug.Log("no code?");
return;
}
context.Response.StatusCode = 200;
context.Response.OutputStream.Write(Encoding.UTF8.GetBytes("Can close this page"));
context.Response.Close();
var request = UnityWebRequest.Post(
"https://www.googleapis.com/oauth2/v4/token",
new Dictionary<string, string> {
{"code", code},
{"client_id", configuration.WebClientId},
{"client_secret", GoogleSignInEditorDataSO.clientSecret.strict.value},
{"redirect_uri", httpListener.Prefixes.FirstOrDefault()},
{"grant_type", "authorization_code"}
}
);
// This is a way to execute some code when the web request is finished, unfortunately it is too
// complex to just paste it here. But you can devise your own way.
request.toFuture(AcceptedResponseCodes._20X, req => req.downloadHandler.text)
.onComplete(either => either.voidFold(
err => {
Log.d.error($"Failed to do google sign-in oauth request: {err.message}");
},
body => {
var jObject = JObject.Parse(body);
Result = new GoogleSignInUser {
IdToken = (string) jObject.GetValue("id_token")
};
Status = GoogleSignInStatusCode.Success;
Pending = false;
}
));
},
null
);
Thanks for posting this workaround, @Thaina! It'd be great to have this built into the plugin out of the box.
Thanks for posting this workaround, @Thaina! It'd be great to have this built into the plugin out of the box.
Justin can you show us how you did it finally?
thanks
HttpWebRequest.CreateHttp("https://www.googleapis.com/oauth2/v4/token").PostFormUrlEncoded("code=" + code + "&client_id=" + configuration.WebClientId + "&client_secret=" + GoogleSignInEditorConfig.Secret + "&redirect_uri=" + httpListener.Prefixes.FirstOrDefault() + "&grant_type=authorization_code").ContinueWith((dataTask) => {
var jobj = JObject.Parse(dataTask.Result);
Result = new GoogleSignInUser() {
IdToken = (string)jobj.GetValue("id_token")
};
Status = GoogleSignInStatusCode.Success;
Pending = false;
});
CreateHttp no longer seems to have the function PostFormUrlEncoded. I do not understand enough about http listeners to fix it, though I have been trying for hours. @Thaina , I don't want to intrude but could you help me?
Also, this project has not been updated in over a year. Still, there does not appear to be any better alternatives available. I wish Google committed more resources to this.
@phedg1 HttpWebRequest
is not related to HttpListener
. It just a REST API here. You could just use WWW
class or any REST caller you familiar with to post that data to https://www.googleapis.com/oauth2/v4/token
url to get the id_token
I edited your scripts @Thaina , I hope you don't mind. I've posted them here in case they help anyone else.
// Tweak GoogleSignIn.cs a bit
/// <summary>
/// Singleton instance of this class.
/// </summary>
/// <value>The instance.</value>
public static GoogleSignIn DefaultInstance {
get {
if (theInstance == null) {
#if UNITY_EDITOR
theInstance = new GoogleSignIn(new GoogleSignInImplEditor(Configuration));
#elif UNITY_ANDROID || UNITY_IOS
theInstance = new GoogleSignIn(new GoogleSignInImpl(Configuration));
#else
theInstance = new GoogleSignIn(null);
throw new SignInException(
GoogleSignInStatusCode.DeveloperError,
"This platform is not supported by GoogleSignIn");
#endif
}
return theInstance;
}
}
internal GoogleSignIn(ISignInImpl impl) {
this.impl = impl;
}
/// <summary>Starts the authentication process.</summary>
/// <remarks>
/// The authenication process is started and may display account picker
/// popups and consent prompts based on the state of authentication and
/// the requested elements.
/// </remarks>
public Task<GoogleSignInUser> SignIn() {
var tcs = new TaskCompletionSource<GoogleSignInUser>();
SignInHelperObject.Instance.StartCoroutine(
impl.SigningIn(true));
SignInHelperObject.Instance.StartCoroutine(
impl.SignIn().WaitForResult(tcs));
return tcs.Task;
}
/// <summary>Starts the silent authentication process.</summary>
/// <remarks>
/// The authenication process is started and will attempt to sign in without
/// displaying any UI. If this cannot be done, the developer should call
/// SignIn().
/// </remarks>
public Task<GoogleSignInUser> SignInSilently() {
var tcs = new TaskCompletionSource<GoogleSignInUser>();
SignInHelperObject.Instance.StartCoroutine(
impl.SigningIn(false));
SignInHelperObject.Instance.StartCoroutine(
impl.SignInSilently().WaitForResult(tcs));
return tcs.Task;
}
internal interface ISignInImpl {
Future<GoogleSignInUser> SignIn();
Future<GoogleSignInUser> SignInSilently();
void EnableDebugLogging(bool flag);
void SignOut();
void Disconnect();
IEnumerator SigningIn(bool signInPressed);
}
// Tweak GoogleSignInImple.cs a bit.
public IEnumerator SigningIn(bool signInPressed) {
yield return null;
}
// New file
using System.Collections.Generic;
namespace Google {
public static class GoogleSignInEditorConfig {
public static string Secret = "";
}
}
class GoogleAccessToken {
public string access_token;
public string id_token;
public string refresh_token;
public string expires_in;
public string token_type;
public string scope;
};
namespace Google.Impl {
using System;
using System.Text;
using System.Net;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
internal class GoogleSignInImplEditor : ISignInImpl, FutureAPIImpl<GoogleSignInUser> {
GoogleSignInConfiguration configuration;
public bool Pending { get; private set; }
public GoogleSignInStatusCode Status { get; private set; }
public GoogleSignInUser Result { get; private set; }
public HttpListener httpListener;
public ushort port = 49651;
private string refreshToken = "";
public GoogleSignInImplEditor(GoogleSignInConfiguration configuration) {
this.configuration = configuration;
}
public void Disconnect() {
throw new NotImplementedException();
}
public void EnableDebugLogging(bool flag) {
throw new NotImplementedException();
}
public Future<GoogleSignInUser> SignIn() {
return new Future<GoogleSignInUser>(this);
}
public Future<GoogleSignInUser> SignInSilently() {
return new Future<GoogleSignInUser>(this);
}
public void SignOut() {
throw new NotImplementedException();
}
public void BindLocalHostFirstAvailablePort() {
if (httpListener == null || !httpListener.IsListening) {
try {
httpListener = new HttpListener();
httpListener.Prefixes.Add($"http://localhost:{port}/");
httpListener.Start();
} catch {
}
}
}
public IEnumerator SigningIn(bool signInPressed) {
Pending = true;
// ATTEMPT TO GAIN AN ACCESS TOKEN USING A REFRESH TOKEN
ReadRefreshToken();
if (refreshToken != "") {
Dictionary<string, string> contentRefresh = new Dictionary<string, string>();
contentRefresh.Add("client_id", configuration.WebClientId);
contentRefresh.Add("client_secret", GoogleSignInEditorConfig.Secret);
contentRefresh.Add("refresh_token", refreshToken);
contentRefresh.Add("grant_type", "refresh_token");
UnityEngine.Networking.UnityWebRequest postRefresh = UnityEngine.Networking.UnityWebRequest.Post("https://oauth2.googleapis.com/token", contentRefresh);
postRefresh.SetRequestHeader("POST", "/oauth2/v4/token HTTP/1.1");
postRefresh.SetRequestHeader("Content-Type", "application/x-www-form-urlencoded");
yield return postRefresh.SendWebRequest();
if (postRefresh.isNetworkError || postRefresh.isHttpError) {
Pending = false;
Status = GoogleSignInStatusCode.Error;
} else {
GoogleAccessToken accessToken = UnityEngine.JsonUtility.FromJson<GoogleAccessToken>(postCode.downloadHandler.text));
Result = new GoogleSignInUser() {
IdToken = accessToken.id_token;
};
Status = GoogleSignInStatusCode.Success;
Pending = false;
signInPressed = false;
}
}
//ATTEMPT TO GAIN ACCESS USING CODE
if (signInPressed) {
BindLocalHostFirstAvailablePort();
if (httpListener.IsListening) {
// REQUEST CODE FROM GOOGLE
Application.OpenURL("https://accounts.google.com/o/oauth2/v2/auth?scope=email%20profile&response_type=code&redirect_uri=http://localhost:" + port.ToString() + "&client_id=" + configuration.WebClientId + "&prompt=consent&access_type=offline");
// WAIT TO RECEIVE CODE
bool moveOn = false;
bool valid = true;
string code = "";
httpListener.GetContextAsync().ContinueWith((task) => {
// EXTRACT CODE STRING FROM REQUEST URL
HttpListenerContext context = task.Result;
string resultsModified = context.Request.Url.ToString();
resultsModified = resultsModified.Remove(0, ("http://localhost:" + port.ToString() + "/?").Length);
System.Collections.Specialized.NameValueCollection queryResults = System.Web.HttpUtility.ParseQueryString(resultsModified);
code = queryResults["code"];
// CHECK CODE IS VALID
if (string.IsNullOrEmpty(code)) {
Pending = false;
Status = GoogleSignInStatusCode.InvalidAccount;
valid = false;
Debug.Log(context.Request.Url.ToString());
Debug.Log("No code received.");
}
// SEND RESPONSE TO BROWSER
context.Response.StatusCode = 200;
byte[] response = Encoding.UTF8.GetBytes("This page can be closed now.");
context.Response.OutputStream.Write(response, 0, response.Length);
context.Response.Close();
moveOn = true;
});
while (!moveOn) {
yield return null;
}
if (valid) {
Dictionary<string, string> contentCode = new Dictionary<string, string>();
contentCode.Add("code", code);
contentCode.Add("client_id", configuration.WebClientId);
contentCode.Add("client_secret", GoogleSignInEditorConfig.Secret);
contentCode.Add("redirect_uri", "http://localhost:" + port.ToString());
contentCode.Add("grant_type", "authorization_code");
UnityEngine.Networking.UnityWebRequest postCode = UnityEngine.Networking.UnityWebRequest.Post("https://oauth2.googleapis.com/token", contentCode);
postCode.SetRequestHeader("POST", "/oauth2/v4/token HTTP/1.1");
postCode.SetRequestHeader("Content-Type", "application/x-www-form-urlencoded");
yield return postCode.SendWebRequest();
if (postCode.isNetworkError || postCode.isHttpError) {
Pending = false;
Status = GoogleSignInStatusCode.Error;
} else {
GoogleAccessToken accessToken = UnityEngine.JsonUtility.FromJson<GoogleAccessToken>(postCode.downloadHandler.text));
refreshToken = accessToken.refresh_token;
WriteRefreshToken();
Result = new GoogleSignInUser() {
IdToken = accessToken.id_token;
};
Status = GoogleSignInStatusCode.Success;
Pending = false;
}
}
}
yield return null;
}
}
void WriteRefreshToken() {
string path = Application.persistentDataPath + "/refresh_token.txt";
StreamWriter writer = new StreamWriter(path, false);
writer.Write(refreshToken);
writer.Close();
}
void ReadRefreshToken() {
string path = Application.persistentDataPath + "/refresh_token.txt";
if (File.Exists(path)) {
StreamReader reader = new StreamReader(path);
refreshToken = reader.ReadToEnd();
reader.Close();
}
}
}
}
I prevented BindLocalHostFirstAvailablePort() from incrementing the port so that the redirect_uri would consistently match an authorized redirect uri in my project's credentials. Simplifying it helped me better understand what its function was.
I used UnityEngine.Networking.UnityWebRequest for my REST API.
Lastly, I made the process also return a refresh_token. That token is saved in the persistentDataPath so you can log in silently in the editor without being redirected to your browser every time.
app_secret means firebase API Key and app_client_id means web client id (client id type 3), isn't it? . @Thaina
@firdiar This so long I don't remember but I think it need google app ID and Secret. Not sure if it is the same as FireBase
Anyone know how to implement sign out in editor as well?
@spvn There's no "sign out" function, per se. Use this code to revoke your access and refresh tokens then tell your app it is signed out. It will require a new sign in to get a working token next time the user tries to log in.
foreach (string token in tokens) {
UnityEngine.Networking.UnityWebRequest get = UnityEngine.Networking.UnityWebRequest.Get("https://accounts.google.com/o/oauth2/revoke?token=" + token);
get.SetRequestHeader("Content-type", "application/x-www-form-urlencoded");
get.SendWebRequest();
}
I think it easy just use
HttpListener
to handle login flow from desktop browser. Please add official support into this repoI have implement quick basic
ISignInImpl
for getting idToken to be used in firebase auth and this code below work. I useNewtonsoft.Json.Linq
package and some custom extension methods which might not get along with code in this repo so I don't make a pull request. But it possible and it make developing auth smoothly