capacitor-community / apple-sign-in

Sign in with Apple Support
MIT License
135 stars 58 forks source link

Sign in with apple on the web not redirecting #68

Closed Lzolcsi closed 2 years ago

Lzolcsi commented 2 years ago

Hi folks, I'm not sure if this is a bug, probably more like a settings issue, however I can't proceed with using this plugin on the web. (on a device it works fine). So this is how I start the flow:

SignInWithApple.authorize({
    clientId: environment.appId+'.webapp',
    redirectURI: 'https://api.xxxx.hu/signInWithAppleCallback',
    scopes: 'email name',
    state: '12345',
    //nonce: 'nonce',
  }).then(appleUser => {
    this.sendOauthConnectRequest(OauthProvider.apple, appleUser.response.identityToken).subscribe();
  }).catch(() => {
    this.oauthLoading[oauthProvider] = false;
    return;
  });

clientId is what I registered with apple and the redirect url is OK as well. When I start this call a new window pops up where I am able to log in with my apple ID, but after finishing it, it won't redirect me to the specified URL. As a matter of fact, nothing happens. No errors in the console either. I can see that there is a successful request to "https://appleid.apple.com/appleauth/auth/oauth/authorize", and in the response I can see all the correct authentication data, simply the redirection is not happening. If I close the login window after that, the callback for the error handler is correctly called, thus notifying me, that the login was not successful (which is what is. expected, since the user closed the window). So any idea on why the redirection is not happening? Thanks a lot!

ghonche commented 2 years ago

Hi @Lzolcsi I'm facing the same issue. any solutions for this?

Lzolcsi commented 2 years ago

Hi @ghonche ,

at the moment I am going with the following solution:

async appleTokenReceived(event) {
    if(
      typeof event.data !== 'object' ||
      !event.data.hasOwnProperty('appleToken') ||
      !event.data.appleToken
    ) {
      return;
    }
    this.appleSignInAbortController.abort();
    const loadingMessage = await firstValueFrom(this.translate.get('LOADING'));
    this.errorDialog.showLoading(loadingMessage);
    const deviceId = await Device.getId();
    let postData: any = {
      token: event.data.id_token,
      device_name: deviceId.uuid
    };
    if(event.data.hasOwnProperty('name')) {
      postData.name = event.data.name;
    }
    this.http
      .post(environment.apiUrl+'/auth/loginWithApple', postData)
      .subscribe((response: any) => {
        localStorage.setItem('apiToken', response.token);
        this.authService.refreshCurrentUser().then((user: UserModel) => {
          this.errorDialog.hideLoading();
          //REDIRECT user to the main page
        });
      });
  }

  async doAppleLogin() {

    if(Capacitor.getPlatform() === 'web') {
      this.appleSignInAbortController = new AbortController();
      window.open(
        'https://appleid.apple.com/auth/authorize?' +
        'client_id='+environment.appId+'.webapp&' +
        'redirect_uri='+environment.apiUrl+'/auth/signInWithAppleCallback&' +
        'response_type=code id_token&' +
        'scope=name email&' +
        'response_mode=form_post',
        '_blank'
      );
      window.addEventListener('message', e => {this.appleTokenReceived(e)}, {signal: this.appleSignInAbortController.signal});
      return;
    }
    //TODO Make sure this still works on native platform (and possibly android, maybe...)
    const options: SignInWithAppleOptions = {
      clientId: environment.appId+'.webapp',
      redirectURI: '',
      scopes: 'email name',
      state: '12345',
      //nonce: 'nonce',
    };

    const loadingMessage = await firstValueFrom(this.translate.get('LOADING'));
    const deviceId = await Device.getId();
    SignInWithApple.authorize(options)
        .then((result: SignInWithAppleResponse) => {
          this.errorDialog.showLoading(loadingMessage);
          this.http
              .post(environment.apiUrl+'/auth/loginWithApple', {
                token: result.response.identityToken,
                device_name: deviceId.uuid
              })
              .subscribe((response: any) => {
                this.errorDialog.hideLoading();
                localStorage.setItem('apiToken', response.token);
                this.authService.refreshCurrentUser().then((user: UserModel) => {
                  this.errorDialog.hideLoading();
                  this.appComponent.navigateAfterActivated(user, ['purchases']);
                });
              });
        })
        .catch(error => {
          //console.log(error);
        });
  }

This mostly works, at the moment I'm having some trouble figuring out why it doesn't work in dev environment when the domain isn't localhost. But hopefully this is a good starting point for you. The main trick that solves the window not closing issue is manually opening apples sign in page, but with "response_mode=form_post". This way you can listen to its response message.

Lzolcsi commented 2 years ago

Unfortunately it seems like this only works on localhost at the moment, the message is not posted back when the domain is something else. I'm having two ideas at the moment: 1: I'll try to use apples own JS framework for the sign in process: https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/configuring_your_webpage_for_sign_in_with_apple This might have the same issue, but we'll see. Also, I don't want to include Apples JS file for the native apps, so the build process needs to be adjusted for this. 2: The Apple pop up window should post back the credentials in the background to the backend with the state variable, so theoretically if the backend stores the received credentials and the JS client asks the backend with the state variable periodically, it might solve the issue. But this seems to much of a hassle at the moment.

Lzolcsi commented 2 years ago

Dang, it was so long ago, that I forgot that "localhost" was referenced in my backend as well, that's why only localhost was working. So the catch is that the callback URL should answer with something like this:

        $data = [
            'appleToken' => true,
            'code' => $request -> get('code'),
            'id_token' => $request -> get('id_token')
        ];
        if($request -> has('user')) {
            $user = json_decode($request -> get('user'));
            $data['name'] = $user -> name -> lastName.' '.$user -> name -> firstName;
        }
        //TODO Origin should be working on live server as well
        return response('<html><body><script>
                if (window.opener) {
                    window.opener.postMessage('.json_encode($data).', \'http://localhost:8100/\');
                    window.close();
                }
        </script></body></html>', 200);

the point is: $data should contain the info that you want to pass back to your Ionic app, and obviously localhost should be whatever origin the request really comes from. So if you add that to the mix, it should work.

In the meantime I was able to make it work with response_mode=web_message as well.

Lzolcsi commented 2 years ago

Here is a gist for a service provider that works both on ios and web as well: https://gist.github.com/Lzolcsi/9c275953c63c94f8240d59d04e54507d

I'm closing this because with this everything is working fine for me now.

viking2917 commented 2 years ago

I have the same situation; works fine on a device, will not work on the web.

I'm going to try your workaround, but shouldn't this issue remain open? The plugin does not seem to be working correctly on the web?

Has anyone had the recommended code work for them on a website? The documentation suggests it should work 'as is'. ( I have confirmed I haven't made the usual mistakes, my domain is listed without the 'https://' on it, and my redirect_url is https (in fact, it's firebase, so like this: 'https://my-firebase-domain/__/auth/handler')

viking2917 commented 2 years ago

For what it's worth, since I'm using Firebase, I just re-coded the web Apple Signin following the Firebase instructions. (e.g. https://firebase.google.com/docs/auth/web/apple#web-version-9), using Javascript and avoiding this plugin. It works fine, although I'd rather not have an extra set of more-or-less duplicate code to maintain.

The main drawback is that I have two different apps with the same backend signin, which doesn't work quite as well with Firebase. Apple Signin uses the Services ID to determine logo and messaging to the user (e.g. "Use your Apple ID to Sign in to App ABC". With this plugin, one can have multiple Apple Service IDs, one for each app, and thus get the correct messaging (what happens for my apps on devices). With Firebase, only one Service ID seems to be allowed, so my second app will show a potentially confusing message mentioning the wrong app....so it would be cool if this plugin could work on the web. (Of course it's possible I'm doing something wrong, but cannot figure what it might be).