zino-hofmann / graphql-flutter

A GraphQL client for Flutter, bringing all the features from a modern GraphQL client to one easy to use package.
https://zino-hofmann.github.io/graphql-flutter
MIT License
3.24k stars 613 forks source link

Better cookie-based authentication support #592

Open rajihawa opened 4 years ago

rajihawa commented 4 years ago

how do I add the credentials: 'include' to the httpLink ?

kateile commented 4 years ago

I am asking my self similar question. in react apollo we can do something like

const link = createHttpLink({
  uri: '/graphql',
  credentials: 'same-origin' //or credentials: 'include'
});

const client = new ApolloClient({
  cache: new InMemoryCache(),
  link,
});

I don't know how to do this in this library. @mainawycliffe how do we do this?

kateile commented 4 years ago

@rajihawa have you found solution?

rajihawa commented 4 years ago

No, but I found a "workaround", I don't know how efficient it is though. using the requests package.

Future<String> getCookie() async {
  Map<String, String> cookie = await Requests.getStoredCookies(
      Requests.getHostname(uri));
  try {
    return cookie.keys.first + "=" + cookie.values.first;
  } catch (e) {
    print(e);
    return '';
  }
}

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final HttpLink httpLink = HttpLink(
      uri: uri,
      headers: {"cookie": await getCookie()});
  ValueNotifier<GraphQLClient> client =
      ValueNotifier(GraphQLClient(link: httpLink, cache: InMemoryCache()));

  return runApp(MyApp(
    client: client,
  ));
}
kateile commented 4 years ago

@rajihawa thanks for response. Can you also help me how you save cookies to requests library?

rajihawa commented 4 years ago

@kateile you send a request to an api like that for example

Requests.post("https://api.com/login",
        bodyEncoding: RequestBodyEncoding.JSON,
        body: {
          'email': email,
          'password': password
        }) 

and the api is suppose to store the cookie as HttpOnly. and using the methods in the earlier comment I fetched the cookies.

kateile commented 4 years ago

@rajihawa nice trick. But the server I am working with expose only GraphQL api. I think my workaround would be to use requests to send post to graphQL endpoint. Thanks for help

rajihawa commented 4 years ago

Yes that's how you would do it if your api only expose graphql endpoint, But still this workaround is not that efficient and we nned the solution that flutter_graphql should provide. do you know anyone from the devs that can help us ?

andrewescutia commented 4 years ago

I too have recently run into needing this option available.

webdev7x6 commented 4 years ago

Same

rajihawa commented 4 years ago

is this package still being maintained ??

rajihawa commented 4 years ago

I have made a new workaround that will make sure the cookies are being sent with every graphql request. by making a custom httpClient and injecting the cookies with every request


GraphqlConfig graphqlConfig = GraphqlConfig();

Future<String> getCookie() async {
  Map<String, String> cookie = await Requests.getStoredCookies(
      Requests.getHostname("http://10.0.0.8:4000/auth/login"));
  try {
    return cookie.keys.first + "=" + cookie.values.first;
  } catch (e) {
    print(e);
    return '';
  }
}

class ClientWithCookies extends IOClient {
  @override
  Future<StreamedResponse> send(BaseRequest request) async {
    String cookie = await getCookie();
    String getCookieString(String _) => cookie;
    request.headers.update('cookie', getCookieString);
    return super.send(request).then((response) {
      return response;
    });
  }
}

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final HttpLink httpLink = HttpLink(
      uri: 'http://10.0.0.8:4000/graphql',
      httpClient: ClientWithCookies(),
      headers: {"cookie": await getCookie()});
  ValueNotifier<GraphQLClient> client =
      ValueNotifier(GraphQLClient(link: httpLink, cache: InMemoryCache()));

  return runApp(MyApp(
    client: client,
  ));
} 

just a note: I handle authentication outside of graphql endpoint hope this will help

kateile commented 4 years ago

@rajihawa I have multiple cookies and try to extract them by using

Future<String> getCookie() async {
  try {
    final hostname = Requests.getHostname(graphqlEndpoint);
    final Map<String, String> map = await Requests.getStoredCookies(hostname);
    final List<String> array = [];

    map.forEach((key, value) => array.add(key + "=" + value));

    return array.join('; ');
  } catch (e) {
    return '';
  }
}
  final cookie = await getCookie();
  print('Cookie: $cookie');
  final HttpLink _httpLink = HttpLink(uri: graphqlEndpoint, headers: {'Cookie': cookie});

But I still get 401. Do I send cookies in correct way? If yes I think I need to set credentials: 'include' but don't know how!

micimize commented 4 years ago

It seems we really should have first-class support for cookies.

My first inclination is a custom link, but that might be awkward

In #531 @chynamyerz suggests adding it as a flag to HttpLink:

final HttpLink httpLink = HttpLink(
  uri: 'https://api.github.com/graphql',
  credentials: 'include' // As it's seen from the apollo-link-http package in React.
);
rajihawa commented 4 years ago

This is the current workaround I'm using for anyone who is looking for one

// main.dart
import ...

Future<String> getCookie() async {
  Map<String, String> cookie =
      await Requests.getStoredCookies(Requests.getHostname(k_Backend_Url()));
  if (cookie.isEmpty) {
    return '';
  } else {
    return cookie.keys.first + "=" + cookie.values.first + "";
  }
}

class ClientWithCookies extends IOClient {
  @override
  Future<IOStreamedResponse> send(BaseRequest request) async {
    String cookie = await getCookie();
    String getCookieString(String _) => cookie;
    request.headers.update('cookie', getCookieString);
    return super.send(request).then((response) {
      return response;
    });
  }
}

Future<ValueNotifier<GraphQLClient>> createClient() async {
  final HttpLink httpLink = HttpLink(
      uri: k_Backend_Url() + "/graphql",
      httpClient: ClientWithCookies(),
      headers: {"cookie": await getCookie()});
  return ValueNotifier(GraphQLClient(link: httpLink, cache: InMemoryCache()));
}

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await PushNotificationService().initialise();
  ValueNotifier<GraphQLClient> client = await createClient();
  bool isAuthed = await UserProvider().autoLogin(client);

  runApp(MyApp(isAuthed));
}

class MyApp extends StatelessWidget {
  final bool isAuthed;
  MyApp(this.isAuthed);

  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(
          create: (_) => UserProvider(),
        ),
      ],
      child: MyMaterialApp(isAuthed),
    );
  }
}

class MyMaterialApp extends StatelessWidget {
  final bool _isAuthed;
  MyMaterialApp(this._isAuthed);

  @override
  Widget build(BuildContext context) {
    Provider.of<UserProvider>(context).info;
    return FutureBuilder(
      future: createClient(),
      builder: (_ctx, AsyncSnapshot<ValueNotifier<GraphQLClient>> e) =>
          e.hasData
              ? GraphQLProvider(
                  client: e.data,
                  child: MaterialApp(
                    title: 'My App',
                    home: _isAuthed ? HomeScreen() : LoginScreen(),
                    routes: {
                      k_LoginScreen_Route: (_) => LoginScreen(),
                      k_HomeScreen_Route: (_) => HomeScreen()
                    },
                  ),
                )
              : MaterialApp(
                  title: 'My App',
                  home: LoginScreen(),
                ),
    );
  }
}
joshuadeguzman commented 4 years ago

Hey @rajihawa, this is actually helpful. However, you may have forgotten to set the cookies.

... (update cookie headers)

return super.send(request).then((response) {
   // Retrieve cookies from request headers here after updating it 
   return response;
});
kateile commented 4 years ago

v4 have option for using other links. I use gql_dio_link and with dio you can use

  final dio = Dio();
  final cookieJar = CookieJar(); //PersistCookieJar(); //todo.
  dio.interceptors.add(CookieManager(cookieJar));

  final Link _dioLink = DioLink(
    graphqlEndpoint,
    client: dio,
  );

No need for using request package. Lets play with and wait for stable version

joshuadeguzman commented 4 years ago

v4 have option for using other links. I use gql_dio_link and with dio you can use

  final dio = Dio();
  final cookieJar = CookieJar(); //PersistCookieJar(); //todo.
  dio.interceptors.add(CookieManager(cookieJar));

  final Link _dioLink = DioLink(
    graphqlEndpoint,
    client: dio,
  );

No need for using request package. Lets play with and wait for stable version

Thanks @kateile!

altech7 commented 3 years ago

Thanks @kateile, your solution works well!

But I got this error during mutation call for upload file due to DioLink :

flutter: *** DioError ***:
flutter: uri: http://localhost:4000/graphql
flutter: DioError [DioErrorType.DEFAULT]: Converting object to an encodable object failed: Instance of 'MultipartFile'
#0      _JsonStringifier.writeObject (dart:convert/json.dart:687:7)
#1      _JsonStringifier.writeMap (dart:convert/json.dart:768:7)
#2      _JsonStringifier.writeJsonValue (dart:convert/json.dart:723:21)
#3      _JsonStringifier.writeObject (dart:convert/json.dart:678:9)
#4      _JsonStringifier.writeMap (dart:convert/json.dart:768:7)
#5      _JsonStringifier.writeJsonValue (dart:convert/json.dart:723:21)
#6      _JsonStringifier.writeObject (dart:convert/json.dart:678:9)
#7      _JsonStringStringifier.printOn (dart:convert/json.dart:876:17)
#8      _JsonStringStringifier.stringify (dart:convert/json.dart:861:5)
#9      JsonEncoder.convert (dart:convert/json.dart:261:30)
#10     JsonCodec.encode (dart:convert/json.dart:171:45)
#11     DefaultTransformer.transformRequest (package:dio/src/transformer.dart:62:21)
#12     DioMixin._transformData (package:dio/src/dio.dart:1014:39)
<…>

Here is my graphql client :

  final Link link = DioLink(
      '${DotEnv().env['BASE_URL']}/graphql',
      client: httpService.dio,
    );
    client = GraphQLClient(
      cache: GraphQLCache(),
      link: link,
    );

Mutation :

const String uploadFile = """
  mutation uploadRequestedFile(\$requestedFileId: ID!, \$file: Upload!) {
    uploadRequestedFile(requestedFileId: \$requestedFileId, file: \$file) {
      id
      documentUrl
    }
  }
""";
    final QueryResult result = await graphqlService.client.mutate(
      MutationOptions(
        document: gql(uploadFile),
        variables: {
          'file': await imagePickerService.multipartFileFrom(document.file),
          'requestedFileId': document.id,
        },
      ),
    );

This problem occurs when serializing the body http. Therefore i checked HttpLink source code, and here is how httpLink handles this case :

    final httpBody = _encodeAttempter(
      request,
      (Map body) => json.encode(
        body,
        toEncodable: (dynamic object) =>
            (object is http.MultipartFile) ? null : object.toJson(),
      ),
    )(body);
emme1444 commented 3 years ago

Are there any updates on this? Just curious.

Has anyone started with this? I have an idea for an API.

micimize commented 3 years ago

@emme1444 there have been no updates, no

megataps commented 3 years ago

v4 have option for using other links. I use gql_dio_link and with dio you can use

  final dio = Dio();
  final cookieJar = CookieJar(); //PersistCookieJar(); //todo.
  dio.interceptors.add(CookieManager(cookieJar));

  final Link _dioLink = DioLink(
    graphqlEndpoint,
    client: dio,
  );

No need for using request package. Lets play with and wait for stable version

Thanks @kateile . It saved my day 👍

dan-gandolfo commented 3 years ago

Hi everyone !

The next solution is perfect for mobile solution, bet have you another workaround for a web app?

final dio = Dio();
  final cookieJar = CookieJar(); //PersistCookieJar(); //todo.
  dio.interceptors.add(CookieManager(cookieJar));

  final Link _dioLink = DioLink(
    graphqlEndpoint,
    client: dio,
  );

Because in the web case the Cookie manager does not catch cookies...

Thanks for your help!

Daniel

fralways commented 2 years ago

session cookies are default approach on web. we need this fixed for web.

emicklei commented 1 year ago

In the context of Flutter Web, I found a solution to pass Browser cookies along GraphQL requests without retrieving them first. It uses the Browser client

import 'package:http/browser_client.dart';

Insert a prepared Http client into the httpLink


  var myClient = BrowserClient();
  myClient.withCredentials =true;
  final httpLink = HttpLink(endpoint +"/graphql",
    httpClient: myClient,
    // ...
  );
Debox-Mehank commented 1 year ago

v4 have option for using other links. I use gql_dio_link and with dio you can use

  final dio = Dio();
  final cookieJar = CookieJar(); //PersistCookieJar(); //todo.
  dio.interceptors.add(CookieManager(cookieJar));

  final Link _dioLink = DioLink(
    graphqlEndpoint,
    client: dio,
  );

No need for using request package. Lets play with and wait for stable version

Hey @kateile, not working for me can you help me?

naimulemon commented 1 year ago

Hi everyone !

The next solution is perfect for mobile solution, bet have you another workaround for a web app?

final dio = Dio();
  final cookieJar = CookieJar(); //PersistCookieJar(); //todo.
  dio.interceptors.add(CookieManager(cookieJar));

  final Link _dioLink = DioLink(
    graphqlEndpoint,
    client: dio,
  );

Because in the web case the Cookie manager does not catch cookies...

Thanks for your help!

Daniel

It doesn't work for me can you please help me . Can you please give me full demo ?

vamshinenu commented 1 year ago

is this been resolved?

ziyadshaikh013 commented 1 year ago

v4 have option for using other links. I use gql_dio_link and with dio you can use

  final dio = Dio();
  final cookieJar = CookieJar(); //PersistCookieJar(); //todo.
  dio.interceptors.add(CookieManager(cookieJar));

  final Link _dioLink = DioLink(
    graphqlEndpoint,
    client: dio,
  );

No need for using request package. Lets play with and wait for stable version

This works as expected, but is there any way to persist cookies between reload. PersistCookieJar() just doesn't work?

TeirikangasNorre commented 1 year ago

Would love to see an official solution for this.