supabase-community / gotrue-swift

A Swift client library for GoTrue.
MIT License
35 stars 31 forks source link

Support Sign in With Apple #4

Closed catlan closed 1 year ago

catlan commented 3 years ago

Feature request

Following on the thread in https://github.com/supabase/supabase/discussions/1882 and https://github.com/supabase/supabase/discussions/2805 I looked into adding support for AuthenticationServices and in this regard ASAuthorizationAppleIDCredential.

Is your feature request related to a problem? Please describe.

When GoTrue signs users in using SIWA they're redirected to the OAuth apple site where they sign in and eventually GoTrue received the above parameters, just through the OAuth callback flow. If this flow were to be used on iOS, it would be a very clunky UX, and could be grounds for an iOS app to be rejected from review.

Describe the solution you'd like

I started work on this here: https://github.com/catlan/gotrue-swift/commit/bac99073ad56ce6fa3257f33a22a628a135e0e9a

The problem is that the response I get: "Invalid token: signing method RS256 is invalid"

I'm not too familiar with either Sign in With Apple nor supabase. Any clues on what is required to make this work?

thecoolwinter commented 3 years ago

In a normal SIWA token exchange, the app sends the access token and identity token to the server. Then, the server validates the identity token and stores the access token for later use. Then, instead of using the Apple JWT, you're supposed to sign and send back a custom JWT and access token for your own server. Apple's JWTs only last 24 hours, so it's built to be like that. Then if the server needs to update or request user info it uses the access token to ask Apple for that info. The SIWA JWT is only used for the original user sign in.

Right now I think the GoTrue server would need to be modified to accept and validate the JWT from the AuthenticationServices framework and send back a GoTrue JWT.

I was also looking at the /callback endpoint and wondering if we could send the access token and identity token to that endpoint and if it would work.

catlan commented 3 years ago

So do we need to move the issue to server component?

thecoolwinter commented 3 years ago

Yeah I think we do, once they add an endpoint we can add it to the swift repo. We should also test using the existing /callback endpoint to see if that does in fact work

thecoolwinter commented 3 years ago

Actually scratch that, we'd need support for sending a nonce with the tokens, so the /callback endpoint won't work.

Sadly I don't have enough experience in Go to make sense of the server library

I'll make an issue there and reference this.

johndpope commented 2 years ago

https://github.com/supabase/gotrue/pull/189 It seems this callback with nonce has been delivered - I'm looking to port existing java architecture over to supabase - but without this - it's a bit of a hinderance.

wmorgue commented 1 year ago

Any updates?

mergesort commented 1 year ago

I had the same question. I'm starting on an app that supports Sign In With Apple and only now discovered that SIWA isn't supported. Not meaning to pressure y'all but is there an estimate on when SIWA could work?

johndpope commented 1 year ago

I’ve done apple stuff before a few times - perhaps we could compare notes and make a self hosted nodejs apple sign in / express passport with JavaScript that updates the auth.users table instead of being blocked here. It’s a bit crap but need to ship the app. Note - a lot of the javascript is specific to browser stuff / my approach is using nodejs below and it does not have a user / session.

The critical distinction in flows - native login on server can use grant_type authorization_code (this is different to web callback url / access_tokens that webflow uses)

SERVER - nodejs on ec2 - (ideally this would be supported on supabase)

    const params = {
        grant_type: 'authorization_code', // refresh_token authorization_code
        code: req.body.code,
        redirect_uri: config.apple.redirectURI,
        client_id: config.apple.clientID,
        client_secret: clientSecret,
        // refresh_token:req.body.id_token
    }
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        data: qs.stringify(params),
        url: 'https://appleid.apple.com/auth/token'

-> here we get back an authorization.code - on swift app (see sample repo below)

   if let code = appleCredential.authorizationCode {
            let authCode = String(data: code, encoding: .utf8)!
            loginWithApple( authCode) // send this code to our nodejs -> create the user / or login / return supabase jwt.
        }

in essense some psuedocode

STEP 1: from iphone - we will send the apple authorization code we get back to our hosted ec2 server running this code / p8 file

curl -X POST -d 'code=cf2add9a5a15842d4b06683fa89152446.0.ntrx.OHzVN63UPWqSjEr-oBsU6g' http://0.0.0.0:80/login/apple
{"message":"error","error":{"error":"invalid_grant","error_description":"The code has expired or has been revoked."}}%

A) if no account exists on system (google / firebase / facebook / apple / snapchat) - create a new one


const returnExistingSupabaseJWTorCreateAccount = async (jwtClaims) => {

    let user =  await findExistingUserByEmail(jwtClaims.email);
    {
        const { data: response, error } = await supabase.auth.admin.listUsers();
        // console.log("listUsers response:", response.users);
        for await (let u of response.users) {
            if (u.id == user.id) {
                console.log("we found existing user in supabase:", u);
            }
        }
    }

    if (user == null) {
        console.log("🌱 creating user");
        const { data: newUser, error } = await supabase.auth.admin.createUser({
            email: jwtClaims.email,
            email_confirm: true // missing provide / identities guff
        })
        console.log("newUser:", newUser);

    } else {
        console.log("🌱 we found a gotrue user:",user);
        // create an access token - this needs more work - or port to golang 
        let claims = {
            "StandardClaims": {
                "sub": user.id,
                "aud": "",
                "exp": Math.floor(Date.now() / 1000),
            },
            "Email": user.Email,
            "AppMetaData": user.AppMetaData,
            "UserMetaData": user.UserMetaData,
        }
        console.log("✅ claims:", claims);
        const jwt = sign(claims, config.supabase.jwtSecret); // needs testing
        console.log("jwt:", jwt);
        return jwt;
    }

}

additional notes - https://github.com/supabase/gotrue/issues/451

UPDATE: so instead of using the official javascript gotrue libraries - we're can use postgres to get to auth.users (auth schema is not public)

I've made sample project ( follow along here) https://github.com/johndpope/Sign-in-with-Apple-for-supabase

This needs more work - blocked on this - how to craft the access token / jwt for the user found. It would be better if this logic lived in gotrue - https://github.com/supabase/gotrue/issues/807

https://github.com/netlify/gotrue-js/issues/114

Once authorization code is verified - we can dig up the the apple jwt claims (though we may need to pass apple email for email)

const jwtClaims = { iss: 'https://appleid.apple.com',
              aud: 'app.test.ios', 
              exp: 1579483805,
              iat: 1579483205,
              sub: '000317.c7d501c4f43c4a40ac3f79e122336fcf.0952',
              at_hash: 'G413OYB2Ai7UY5GtiuG68A',
              email: 'da6evzzywz@privaterelay.appleid.com',
              email_verified: 'true',
              is_private_email: 'true',
              auth_time: 1579483204 }

here we get a response from apple (jwtClaims) - and return a jwt(supabase) referening auth.user

Screenshot 2022-11-14 at 8 51 32 pm Screenshot 2022-11-14 at 8 50 35 pm

code in my sample repo uses the supabase.admin.createuser - but I can drop into postgres -> auth.users and manually create - though I'm not clear on what to put for password.

https://github.com/supabase/supabase/discussions/5248


INSERT INTO
  auth.users (
    id,
    instance_id,
    ROLE,
    aud,
    email,
    raw_app_meta_data,
    raw_user_meta_data,
    is_super_admin,
    encrypted_password,
    created_at,
    updated_at,
    last_sign_in_at,
    email_confirmed_at,
    confirmation_sent_at,
    confirmation_token,
    recovery_token,
    email_change_token_new,
    email_change
  )
VALUES
  (
    gen_random_uuid(),
    '00000000-0000-0000-0000-000000000000',
    'authenticated',
    'authenticated',
    'dev@email.io',
    '{"provider":"email","providers":["email"]}',
    '{}',
    FALSE,
    crypt('Pa55word!', gen_salt('bf')),
    NOW(),
    NOW(),
    NOW(),
    NOW(),
    NOW(),
    '',
    '',
    '',
    ''
  );

INSERT INTO
  auth.identities (
    id,
    provider,
    user_id,
    identity_data,
    last_sign_in_at,
    created_at,
    updated_at
  )
VALUES
  (
    (
      SELECT
        id
      FROM
        auth.users
      WHERE
        email = 'dev@email.io'
    ),
    'email',
    (
      SELECT
        id
      FROM
        auth.users
      WHERE
        email = 'dev@email.io'
    ),
    json_build_object(
      'sub',
      (
        SELECT
          id
        FROM
          auth.users
        WHERE
          email = 'dev@email.io'
      )
    ),
    NOW(),
    NOW(),
    NOW()
  );

N.B. - I have the webflow sign in working using client_secret.rb in my repo ( this is not what I want)

Screenshot 2022-11-15 at 1 13 53 am

this is apple's latest sample sign in code - can download here https://developer.apple.com/documentation/authenticationservices/implementing_user_authentication_with_sign_in_with_apple

Screenshot 2022-11-15 at 1 35 04 am Screenshot 2022-11-15 at 1 37 24 am Screenshot 2022-11-15 at 1 36 55 am

UPDATE: so critically - we get back authorization.code in the response from apple on successful logins.

Screenshot 2022-11-15 at 1 45 14 am

we can use then post this to the nodejs server - and get the supabase jwt.

Screenshot 2022-11-15 at 2 18 57 am

Problem I have is this isn't a legitimate jwt. I've pushed. all the code + included apple sample sign in - I need some backend help to get this across the line. https://github.com/johndpope/Sign-in-with-Apple-for-supabase specifically - this jwt is not complete - it's been hacked in the server.js

Screenshot 2022-11-15 at 2 23 07 am

NOTEs - when using the client_secret - webflow - the identity is correctly stored using the gotrue flows.

I need to either get gotrue to support this authorization flow upstream - or we need craft this to insert

Screenshot 2022-11-15 at 2 31 38 am

// I need to 
let identityData = {"iss":"https://appleid.apple.com/auth/keys","sub":"000265.c7232b56014b4437adec51370b3d7004.1422","name":"John Pope","email":"yp4yscqsft@privaterelay.appleid.com","full_name":"John Pope","provider_id":"000265.c7232b56014b4437adec51370b3d7004.1422","email_verified":true}

    const res2 = await pool.query('INSERT INTO auth.identities ( id, provider, user_id, identity_data, last_sign_in_at, created_at, updated_at ) VALUES ( $1::UUID, \'email\', $1::UUID, json_build_object( \'sub\', $1::UUID ), NOW(), NOW(), NOW() );', [result.id]);
matthewmorek commented 1 year ago

Any update on having this fixed to work with "Sign in with Apple" workflow, or is this still a partially server-side issue that needs a broader look?

wweevv-johndpope commented 1 year ago

I'm wondering if this is a more complete fit - https://next-auth.js.org/adapters/supabase = still need the nodejs server - but using this adapter - maybe more elegant than hacking with postgres injection....

arguiot commented 1 year ago

Hello any updates? This issue has been opened in 2021... any ETA on when this would be fixed?

ajsharp commented 1 year ago

Bumping this. Any updates from the supabase team? cc @thecoolwinter

johndpope commented 1 year ago

@kangmingtay - is there anyway we can progress this?

kangmingtay commented 1 year ago

Hi @johndpope, apologies for the late reply, this should be possible via the signInWithIdToken method

You'll need to add your IOS Bundle ID in the dashboard's auth settings under the apple provider -> "Services ID". We're looking to add the "IOS Bundle ID" as a field in the dashboard as well so that we can support both mobile & web logins through apple.

As this is a pretty long thread, i just wanted to make sure i'm understanding the issue correctly:

We want to be able to use native apple authentication with swift for SIWA. Using something like Apple Authentication Services would be ideal for the iOS App Store reviews and also provides a nice native experience for the user rather than redirecting them to a web browser if you use signInWithOAuth. There needs to be some way to pass in the id token returned from SIWA and a nonce to gotrue to complete the authentication flow with Supabase.

johndpope commented 1 year ago

at first glance - it doesn't seem adequate - we have the official sign in via apple code in swift provided by apple - it's the orange screen shots - juice app above.

I want this app to sign into supabase - using swift - natively.

the successful authorization code we get back from apple looks like this 'code=cf2add9a5a15842d4b06683fa89152446.0.ntrx.OHzVN63UPWqSjEr-oBsU6g'

it's not a json web token. I can see it's possible to perhaps craft this to fit your suggestion - but...

export type SignInWithIdTokenCredentials = {
  /**
   * Only Apple and Google ID tokens are supported for use from within iOS or Android applications.
   */
  provider: 'google' | 'apple'
  /** ID token issued by Apple or Google. */
  token: string
  /** If the ID token contains a `nonce`, then the hash of this value is compared to the value in the ID token. */
  nonce?: string
  options?: {
    /** Verification token received when the user completes the captcha on the site. */
    captchaToken?: string
  }
}

when I hit the backend in javascript / passport / node for my approach - it runs some extra validation against apple servers to verify code

this is apple / nodejs passport sample node https://github.com/johndpope/Sign-in-with-Apple-for-supabase/blob/8da249ef1311353e5501930a7519a61bb5808e8e/server.js#L11

when I search supbase codebase for response_type=code OR https://appleid.apple.com/auth/authorize there's no hits. meaning this functionality doesn't exist?

this is the heart of how the backend - gotrue should go outbound with above authorization code - and either create user / or login and return the jwt. please help - I want this fixed. If you can take the above sample apple code - and hack to use the token approach - I'm also call with that.


// BACKEND - APPLE LOGIN 
// https://developer.apple.com/documentation/signinwithapplerestapi/generate_and_validate_tokens
//  const url = new URL(`https://appleid.apple.com/auth/authorize?scope=name%20email&client_id=${appleid.client_id}&redirect_uri=${redirectUri}&response_type=code%20id_token&response_mode=form_post`)

const getAuthorizationUrl = () => {
    const url = new URL('https://appleid.apple.com/auth/authorize');
    url.searchParams.append('response_type', 'code id_token');
    url.searchParams.append('response_mode', 'form_post');
    url.searchParams.append('client_id', config.apple.clientID);
    url.searchParams.append('redirect_uri', config.apple.redirectURI);
    url.searchParams.append('scope', 'name,email');
    return url.toString();
};

UPDATE it seems no one has ever used this call signInWithIdToken in swift? presumably it's not going to work? this function lives in js library - and we need to be hitting a golang / gotrue end point instead? this is why where asking for native support - not through webpage with sign in via apple (which works).

Screenshot 2023-04-04 at 2 12 02 pm
kangmingtay commented 1 year ago

Hi @johndpope, i think you'll need to complete the authorization code flow with swift natively first. You should get back an id token as mentioned in the link you provided: https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens

This id token can then be used in signInWithIdToken which checks that the id token & nonce are valid before returning the gotrue tokens. What you're doing here is essentially using apple's authentication services framework to complete the user authentication natively and then syncing the user with Supabase auth so you get back a gotrue access token JWT which you can then use for subsequent requests to Supabase (e.g. storage / postgrest / realtime / functions)

it seems no one has ever used this call signInWithIdToken in swift? presumably it's not going to work? this function lives in js library - and we need to be hitting a golang / gotrue end point instead? this is why where asking for native support - not through webpage with sign in via apple (which works).

Yeah just confirmed that that's unfortunately the case :/ the swift library for gotrue is community maintained and as such, it seems to have fallen behind the JS and Flutter client libs. You're welcome to make a contribution to the library to add the signInWithIdToken method which should resemble closely to the JS implementation.

Also, just looping in @maail here who's helping us out with the swift community library in case he has the bandwidth to pick this up :)

johndpope commented 1 year ago

ok - this looks very doable


  /// - Tag: did_complete_authorization
    func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
        switch authorization.credential {
        case let appleIDCredential as ASAuthorizationAppleIDCredential:

            if let token = appleIDCredential.identityToken{
                print("token:",token);
               // TODO -  call the signInWithIdToken endpoint
            }
        //    if let authCode = appleIDCredential.authorizationCode{
          //      print("authorizationCode:",authCode);
            //    if let authCodeStr = String(data:authCode,encoding: .utf8){
              //      loginWithApple(authCodeStr);
             //   }
   //         }

UPDATE

working https://github.com/johndpope/Sign-in-with-Apple-for-supabase/blob/master/ImplementingUserAuthenticationWithSignInWithApple/Juice/LoginViewController.swift

UPDATE 2 - good news - it's working ✅

Screenshot 2023-04-04 at 10 37 13 pm Screenshot 2023-04-04 at 10 41 55 pm

@catlan - you can close this ticket. @grsouza / @maail - it would be good to have this apple signin code in this repo to demonstrate sign in + assigning access token to auth credentials for the supabase client.

grdsdev commented 1 year ago

signInWithIdToken was added in https://github.com/supabase-community/gotrue-swift/pull/49 will close this issue then.

Feel free to open a new issue if needed.