Closed wiverson closed 1 year ago
Thanks for your work exploring this! I’ll get to work on implementing it
Thanks! LMK if/when you have a cut and I can test it out. I will write up the instructions on how to get it working in Unity w/all the back and forth with Apple Dev. :)
@wiverson alrighty! There you are #48. Would love some feedback when you can.
Oh, and thank you very much! :)
https://PROJECT_ID.supabase.io/auth/v1
Ok, I made a bunch of progress.
For the moment, the nonce stuff is complicating things. I got it working without nonce for now, I think getting the other parts working without nonce is tricky enough for the moment. It looks like there is some choreography where if you pass in a nonce hash, the identifier key that comes back from Apple includes that nonce hash back again which can then flow back to Supabase to validate it. Unfortunately nothing about the nonce is really documented other than in code (e.g. the dart code). That said, if you omit the nonce from the request Apple omits it and the flow is easier. For the record, I'm not saying not to implement the nonce portion, just that it seems to be something that can be revisited after getting the rest fo the flow working.
I was able to get the REST API to work with the following request/body via the Rider HTTP client:
POST https://fvkoppwyozwgkiowmcrg.supabase.co/auth/v1/token?grant_type=id_token
Accept: application/json
apiKey: <my public Supabase key>
Authorization: Bearer <my public Supabase key>
{
"provider": "apple",
"id_token": "<id token that comes back from Apple native sign in>"
}
Note that I'm not passing a nonce in to Apple native sign in, so the id token doesn't include it, and Supabase seems to be fine with that.
I was able to get the method to hit the Supabase API by add in the following headers manually:
Dictionary<string, string> h = new Dictionary<string, string>();
string SUPABASE_PUBLIC_KEY =
"<my project public Supabase key>";
h.Add("apikey", SUPABASE_PUBLIC_KEY);
h.Add("Authorization", $"Bearer {SUPABASE_PUBLIC_KEY}");
return Helpers.MakeRequest<Session>(HttpMethod.Post, $"{Url}/token?grant_type=id_token", body, h);
Which doesn't make a ton of sense - I would have thought that the auth lib would add in those headers automatically when I init the client (that seems to work for RPC calls).
But at least the API was able to fetch the JSON back from Supabase with the user!
The JSON that comes back however is breaking the JSON deserialization, with the error: Exception Unable to find a constructor to use for type Supabase.Gotrue.User. A class should either have a default constructor, one constructor with arguments or a constructor marked with the JsonConstructor attribute. Path 'user.id', line 1, position 1025.
It looks like User has a default constructor, so I'm not really following what it's complaining about. Perhaps this JSON can help? I could take a stab at writing a test case that just makes sure it can parse/handle this JSON, or if you want to try...?
{
"access_token": "<long string with the token>",
"token_type": "bearer",
"expires_in": 3600,
"refresh_token": "oQ3uzBCfkSWQQJGQV8b-qg",
"user": {
"id": "794b6a65-4adf-4bda-b82f-1dfbdc374e98",
"aud": "authenticated",
"role": "authenticated",
"email": "<email address>",
"email_confirmed_at": "2023-04-25T06:31:59.43156Z",
"phone": "",
"confirmed_at": "2023-04-25T06:31:59.43156Z",
"last_sign_in_at": "2023-04-25T07:39:22.105617638Z",
"app_metadata": {
"provider": "apple",
"providers": [
"apple"
]
},
"user_metadata": {
"aud": "io.dayboard.UnityTest",
"auth_time": 1682404292,
"c_hash": "faDOUyCCRl_4pwwS7swXOQ",
"email": "<my email>",
"email_verified": "true",
"exp": 1682490692,
"iat": 1682404292,
"iss": "https://appleid.apple.com",
"nonce_supported": true,
"sub": "000538.4e496882c5d24936a71d30c3b22465f3.2149"
},
"identities": [
{
"id": "<string with my id>",
"user_id": "<another string with a different id",
"identity_data": {
"aud": "io.dayboard.UnityTest",
"auth_time": 1682404292,
"c_hash": "faDOUyCCRl_4pwwS7swXOQ",
"email": "<my email>",
"email_verified": "true",
"exp": 1682490692,
"iat": 1682404292,
"iss": "https://appleid.apple.com",
"nonce_supported": true,
"sub": "<some string>"
},
"provider": "apple",
"last_sign_in_at": "2023-04-25T06:31:59.429778Z",
"created_at": "2023-04-25T06:31:59.429817Z",
"updated_at": "2023-04-25T07:33:45.061502Z"
}
],
"created_at": "2023-04-25T06:31:59.421953Z",
"updated_at": "2023-04-25T07:39:22.10715Z"
}
}
Looks like the constructor failing is due to overly aggressive Unity code stripping. Working on a fix now.
Success! Changing the stripping level from "low" to "minimal" in the Unity build settings fixed it.
I can now log in via Unity's Sign in with Apple and then use that to log in to Supabase. I yelled a bit when it worked and startled my wife. :)
I think the only open Q in my book is why the method is failing unless I add the API key to the headers. Any guesses? Should the API key be an argument to the method and just set the headers, or is there something about the headers for this method that's failing, or...?
Here is a link to a description of how to integrate a Sign in with Apple nonce with a backend service. In theory this should also work with Supabase, but it does need to be tested. A tweaked version of this would be a nice utility to throw into gotrue-csharp - no need for every developer to reimplement this from scratch every time.
Another post clarifying the nonce flow for reference.
Thank you so much @wiverson for your work on this! Reading through these made me smile haha.
If you can make those two methods public, perhaps as just GenerateNonce() and GenerateNonceSHA256() or similar I think that would do the trick.
Would you like to update the PR branch and then I'll try it out again wiping out my now hacked up version? ;)
FYI I started writing up my notes on this, incomplete but it's a start.
https://gist.github.com/wiverson/86ec69ddf13b137306842348eaec37a2
Looking over the hacks/tweaks I made, it looks like the one thing that I'm not sure on is the proper way to ensure that the apikey header[s] are injected. I don't have any strong thoughts on how to do this WRT the supabase core vs gotrue-csharp, I just know that hacking up SignInWithIdToken to slam in my hard-coded headers is not the way to do it lol.
Alright! You should have access to Supabase.Gotrue.Helpers.GenerateNonce()
and Supabase.Gotrue.Helpers.GenerateNonceVerifier()
.
Try making setting your headers like this:
var auth = new Supabase.Gotrue.Client(new ClientOptions<Session>
{
Url = "https://PROJECT_ID.supabase.co/auth/v1",
Headers = new Dictionary<string, string>
{
{ "apikey", SUPABASE_PUBLIC_KEY }
}
})
So, for whatever reason the nonce hash function in GenerateNonceVerifier() isn't working. Here is an alternative implementation I built based on the dart version:
using System.Security.Cryptography;
using System.Text;
namespace App {
public static class NonceGenerator {
public static string GenerateSHA256NonceFromRawNonce(string rawNonce) {
SHA256Managed sha = new SHA256Managed();
byte[] utf8RawNonce = Encoding.UTF8.GetBytes(rawNonce);
byte[] hash = sha.ComputeHash(utf8RawNonce);
string result = string.Empty;
foreach (byte t in hash) {
result += t.ToString("x2");
}
return result;
}
}
}
The rest of it seems to be working great.
With Nonce:
Without Nonce:
Both of these scenarios are now working, which is most excellent.
I'm not sure what the best strategy is for the helper functions, as it might be the PKCE verify and the Apple verify are just simply two different hash strategies? Perhaps leave the original function as PKCE verify and use the above as AppleVerify?
Anyways, let me know what you decide and LMK and I'll test it.
So, I think if you can add the Apple compatible version of the nonce hash I think you could release a build that has everything needed & working. LMK if/when and I can finish writing up and maybe make a YouTube video or something to help explain how it all works. :)
Agreed! Planning on doing a real ease with these changes sometime later tonight. Will let you know. Thanks again for all your help!!
@wiverson alrighty - available in 3.1.1! Let me know if you make a tutorial/write-up and I'll gladly include it on the README!
I wound up making a video instead of trying to write it up.
Between the video and the links in the description I think it's everything someone who is experienced with Unity would need to get started. I think it would clock in at tens of thousands of words to cover everything from scratch. ¯_(ツ)_/¯
I'm working on building a Unity test/sample package that includes all of the error handling and a nice out-of-the-box UI that someone could drop into their project. If/when I wind up publishing that I'll be sure to let you know.
I think this means you can close the ticket as a pretty huge new feature added! I'm doing this for Unity but I don't see any reason this wouldn't work with Godot C# or MonoGame. :)
Let me know if you need anything else. Thank you so much for your help!
Nice! Solid video. Definitely a lot of moving parts there! I've included it on the readme here.
Available in the latest release for supabase-csharp@0.9.1 and gotrue-csharp@3.1.1 Appreciate you and your help on this!
Feature request
I would like to support native Sign In With Apple (specifically in the context of a Unity project).
Is your feature request related to a problem? Please describe.
Sign In With Apple support has recently been added to Supabase.
Per the announcement, "With supabase-flutter, this is as easy as:
A clear and concise description of what you want and what your use case is.
Describe the solution you'd like
I'd like to have the csharp library have a nice simple signInWithApple() function as well.
The flutter version calls winds up calling into the GoTrue API with this function:
https://github.com/supabase/gotrue-dart/blob/4909d8725540a5b4b0a8d87a72ed69a374dd470c/lib/src/gotrue_client.dart#L226
Provider is just a string.
The remaining details come from a native sign in solution. For Unity, for example, I have been successful bringing up the Sign in with Apple dialog and completing the flow with the Sign in with Apple Unity Plugin. Other popular packages that offer C# integration (e.g. Godot, MonoGame) would likely use other options to support bringing up the Sign In With Apple dialog.
I think that adding in the API request, combined with instructions on how to get the data from the native API would be a great option.
Describe alternatives you've considered
I have been able to get gotrue-csharp working with Unity, including email/password accounts. Apple has been increasingly aggressive about requiring developers to support Sign in with Apple, and the flow for Sign in with Apple is more seamless for many users.
Possible solution
Based on the dart code, I built out a quick and dirty version of what I think this would look like in the csharp version. I can submit a PR, but I'm not very familiar with the code base and I think it's just the one method. If someone wants to create a branch I can test it, or a PR, or...?