Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

馃悰 Bug Report: React Native - createOAuth2Session throws error #10

Open
2 tasks done
KartikBazzad opened this issue Oct 3, 2022 · 7 comments
Open
2 tasks done
Assignees
Labels
bug Something isn't working

Comments

@KartikBazzad
Copy link

KartikBazzad commented Oct 3, 2022

馃憻 Reproduction steps

Start a new React Native project.
Enable OAuth.

import React from 'react';
import {Button, Text, View} from 'react-native';
import {Client, Account} from 'appwrite';

const client = new Client()
  .setEndpoint('https://endpoint/v1')
  .setProject('12343455667');

const App = () => {
  function handleUserLogin() {
    const account = new Account(client);
    const session = account.createOAuth2Session('google');
    return session;
  }

  return (
    <View>
      <Text>Hello world</Text>
      <Button title="Login" onPress={handleUserLogin} />
    </View>
  );
};

馃憤 Expected behavior

Should have opened a window for selecting an account from the OAuth client like google

馃憥 Actual Behavior

Throwed Error

TypeError: Cannot set property href of [object WorkerLocation] which has only a getter

image

馃幉 Appwrite version

Different version (specify in environment)

馃捇 Operating system

Windows

馃П Your Environment

I am using Appwrite v:1.0.1.500
React Native 0.70.1
React 18.1.0

馃憖 Have you spent some time to check if this issue has been raised before?

  • I checked and didn't find similar issue

馃彚 Have you read the Code of Conduct?

@KartikBazzad KartikBazzad added the bug Something isn't working label Oct 3, 2022
@Alwinseb01
Copy link

I would like to work on this issue please

@stnguyen90 stnguyen90 changed the title 馃悰 Bug Report: React Native - TypeError: Cannot set property href of [object WorkerLocation] which has only a getter 馃悰 Bug Report: React Native - createOAuth2Session throws error Nov 5, 2022
@stnguyen90
Copy link
Contributor

This is where the error occurs:

https://github.com/appwrite/sdk-for-web/blob/98d0c5a212e7fd7a11d9f11625e1a4b7347e5cca/src/services/account.ts#L674

Looks like href is read only in react native

@khanghk
Copy link

khanghk commented Nov 11, 2022

I try but don't show login form of google. Please check. Thanks

@DiegoBM
Copy link

DiegoBM commented Dec 10, 2022

React-native can't just open a web url. The Appwrite SDK was designed for web browsers, which make redirections, and for that reason certain parts like OAuth authentication won't work out of the box in react-native. There are a few ways around this though:

  1. As far as I know, the recommended way to do OAuth authentication in mobile is by triggering the phone's external browser to do the authentication and go back to your app with the token. There are libraries that do this such as "react-native-app-auth". but bear in mind that, as of today, it will require some configuration on your app, although you can find all the steps in their github page.
  2. You can roll out your own OAuth implementation by adding a WebView into your app and capturing the redirections. This is not recommended anymore though, due to potential security implications, and certain providers like Facebook have started to try to prevent developers from using this method (although at the moment can still be overridden)
  3. If you choose to go with option 2. I created long ago a react-native component that implements the OAuth flow over a WebView and should work with the latest Appwrite version as of December 2022. You can find it here "react-native-appwrite-oauth".

Hope this helps

Note: this might be relevant for issue #1177797467

@ghost
Copy link

ghost commented Feb 7, 2023

I was looking for a solution to avoid closed common other backend services.
I think AppWrite is doing that well and it would be so useful if AppWrite supports oauth with reactive-native in the future.

@eldadfux
Copy link
Member

We're working a new dedicated React Native SDK. Moving this issue to the new repository

@eldadfux eldadfux transferred this issue from appwrite/sdk-for-web Mar 30, 2024
@winterdouglas
Copy link

winterdouglas commented May 2, 2024

Hey, nice that you're putting some efforts on a dedicated react-native sdk! :)

Turns out that I've been trying appwrite in a RN app and this was the first thing that I came across.

I managed to workaround the issue for now by overriding the Account and doing my own implementation based on the other appwrite mobile sdks.

I've used expo-web-browser for the auth session (react-native-inappbrowser-reborn is another option), along with @react-native-cookies/cookies to set the needed cookie.

If anybody's interested on it until it's implemented in the sdk, here are my snippets:

Use at your own risk!

/lib/appwrite/account.ts

import * as WebBrowser from "expo-web-browser";

import { Account, AppwriteException, OAuthProvider } from "appwrite";

import { handleIncomingCookie } from "./handleIncomingCookie";
import { buildQueryParams } from "./uri";

export class OauthAwareAccount extends Account {
  /**
   * @deprecated Temporaryly use {@link createOAuthSession} instead, can't use this because of its return type (not a promise)
   * @param provider
   * @param success
   * @param failure
   * @param scopes
   * @returns
   */
  override createOAuth2Session(
    provider: OAuthProvider,
    success?: string | undefined,
    failure?: string | undefined,
    scopes?: string[] | undefined,
  ): void | URL {
    return super.createOAuth2Session(provider, success, failure, scopes);
  }

  async createOAuthSession(
    provider: OAuthProvider,
    success?: string | undefined,
    failure?: string | undefined,
    scopes?: string[] | undefined,
  ): Promise<void | URL> {
    if (!provider) {
      throw new AppwriteException('Missing required parameter: "provider"');
    }

    const { endpoint, project } = this.client.config;
    const apiPath = `/account/sessions/oauth2/${provider}`;

    const payload: Record<string, string | string[] | undefined> = {
      success,
      failure,
      scopes,
      project,
    };

    const queryParams = buildQueryParams(payload);
    const authUrl = `${endpoint}${apiPath}${queryParams ? `?${queryParams}` : ""}`;
    const callbackUrl = `appwrite-callback-${project}`;

    const browserResult = await WebBrowser.openAuthSessionAsync(
      authUrl,
      callbackUrl,
    );

    if (browserResult.type !== "success") {
      return;
    }

    const url = browserResult.url;

    if (!(await handleIncomingCookie(url, endpoint))) {
      return;
    }

    return new URL(url);
  }
}

/lib/appwrite/handleIncomingCookie.ts

import CookieManager, { Cookie } from "@react-native-cookies/cookies";
import { AppwriteException } from "appwrite";

import { parseQueryParams } from "./uri";

export const handleIncomingCookie = async (url: string, endpoint: string) => {
  if (!url.includes("appwrite-callback")) {
    return false;
  }

  const queryParams = parseQueryParams(url);

  if (!queryParams.key || !queryParams.secret || !queryParams.domain) {
    throw new AppwriteException(
      "Invalid OAuth2 Response. Key, Secret and Domain not available.",
      500,
    );
  }

  const domainUrl = new URL(endpoint);

  const cookie: Cookie = {
    name: queryParams.key,
    value: queryParams.secret,
    path: queryParams.path,
    expires: queryParams.expires,
    secure: "secure" in queryParams,
    httpOnly: "httpOnly" in queryParams,
    domain: domainUrl.hostname,
  };

  return CookieManager.set(domainUrl.toString(), cookie);
};

/lib/appwrite/uri.ts

The sdk has some utilities for this already, like flatten in service.ts, I didn't use that because I based my implementation on Flutter's for this. I wasn't sure if I should encodeURIComponent or not, for example. Seems to work like this. Additionally, I didn't implement the recursive flatten, I do that with a simple map, which should be fine too.

export const buildQueryParams = (
  params: Record<string, string | string[] | undefined>,
) =>
  Object.keys(params).reduce((acc, currentKey) => {
    const currentValueForKey = params[currentKey];

    if (currentValueForKey === undefined) {
      return acc;
    }

    if (Array.isArray(currentValueForKey)) {
      const arrayQuery = currentValueForKey
        .map(
          (value) =>
            `${encodeURIComponent(`${currentKey}[]`)}=${encodeURIComponent(value)}`,
        )
        .join("&");
      return `${acc}&${arrayQuery}`;
    }

    return `${acc}&${encodeURIComponent(currentKey)}=${encodeURIComponent(currentValueForKey)}`;
  }, "");

export const parseQueryParams = (url: string) => {
  const queryParams = url.includes("?") ? url.split("?")[1] : url;

  if (!queryParams) {
    return {};
  }

  return queryParams.split("&").reduce(
    (acc, curr) => {
      const [key, value] = curr.split("=");
      return { ...acc, [key as string]: value };
    },
    {} as Record<string, string | undefined>,
  );
};

To use the code above simply instantiate a new OauthAwareAccount(client) rather than the Account from the sdk in your appwrite client configuration.

@eldadfux depending on what you envision for this on the react native sdk, I can PR something, just let me know.

Hope this helps in some way!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

8 participants