Make authenticated requests | Flutter

Введение

Меня, Flutter-разработчика, знакомые часто спрашивают: «

». Качают головой со словами: «А вот Петя серьёзные транспорты на Java пишет, а в Яндексе вообще плюсы в проде…». Ну что ж, пожалуй, действительно, Dart далёк от практик «

» из Java. Однако если стоит задача реализовать клиентские приложения сразу для нескольких платформ, не утонув в потоке задач по синхронизации разработчиков разных целевых ОС; создать целостный UI, узнаваемый, но специфичный для Android, iOS и веб и в целом уложиться в адекватные бюджет и сроки, — здесь Flutter не имеет конкурентов. И эти вопросы стоят вдвойне если у вас… стартап.

Итак, легенда: некий стартап решил создать новый сервис… ну, например, для

между пользователями сервиса. Цель стартапа — выпустить

за три месяца на трех платформах (плюс четвертая — сервер, конечно).

Adding authentication to a flutter app

In this section, you’ll learn how to secure a Flutter app with Auth0. You’ll take a production-ready Flutter app and add a login screen and logout functionality to it, and you’ll do it with only a fraction of the effort required to implement login and logout yourself!

You’ll be able to follow this tutorial a little more smoothly if you know the basics of Flutter, but it’s not a hard requirement. If you have experience with any modern web framework, you’ll probably be able to understand the code and learn Flutter and Dart (Flutter’s programming language) as you go.

Analyzing an example

I’ve taken an example of a JWT generated by the backend we’ll build as an example in this post. It is eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXJuYW1lIiwiaWF0IjoxNTgxOTY2MzkxLCJleHAiOjE1ODMyNjIzOTF9.IDXKR0PknG96OyVgRf7NEX1olzhhLAiwE_-v-uMbOK0.

Choosing how to sign the token

The token can be signed using either a method based on public key cryptography (for example using RSA or ECDSA) or by relying on hashing the concatenation of the secret key and the message (called a payload in JWT terms) with any hashing algorithm (usually sha256).

Configure auth0

The next step is to register MJ Coffee as an application in the Auth0 dashboard.

You’ll need an Auth0 account for this step. If you don’t already have one, you can sign up for a free account. The free tier is generous enough for many small applications.

🛠 Log in to into your Auth0 account and follow the steps below to register the application:

  • 🛠 Go to the Applications section of your dashboard:
  • 🛠 Click the Create Application button:
  • 🛠 Enter a name for your application (e.g., “MJ Coffee Flutter Application”) and select the Native application type:
  • 🛠 You’ll see the Quick Start page of your newly-registered application. Go to the Connections page…

Configure the callback url for android

🛠 To configure the Android version of the app, open the /android/app/build.gradle file.

Update the defaultConfig section of the file by adding a new item: manifestPlaceHolders and its value, [‘appAuthRedirectScheme’:’mj.coffee.app’]. The value of appAuthRedirectScheme must be in lower case letters.

🛠 You should set the value for minSdkVersion to at least 18, as it’s a requirement for the flutter_secure_storage package.

The result should look like this:

Configure the callback url for ios

The only change that you need to make in order to configure the iOS version of the app is to add a callback scheme.

🛠 To do this, open the /ios/Runner/Info.plist file. Inside the <dict> tag, add a new key, CFBundleURLTypes so that the start of the <dict> tag looks like this:



...
<dict><key>CFBundleURLTypes</key><array><dict><key>CFBundleTypeRole</key><string>Editor</string><key>CFBundleURLSchemes</key><array><string>mj.coffee.app</string></array></dict></array>
...

Creating a log-in page

The MaterialApp object we’re launching is called MyApp, but we’ll worry about that later, given that it needs to check whether we’re already logged in, and then choose whether to display a log-in page or the home page.

That’s a bit boring to worry about now, let’s build some UI and create a log-in page!

The log-in page itself will be a StatelessWidget called LoginPage:

Enabling login on the home screen

Now that you have the underlying methods for login and initial setup, it’s time to implement similar methods for the app’s screens, whose code is in the /lib/screens/ directory.

🛠 The app’s home screen is implemented in the HomeScreen class, located in /lib/screens/home.dart.

import'package:mjcoffee/services/auth_service.dart';

Now scroll past the HomeScreen class to the _HomeScreenState class. You’ll need to make some changes to this class.

🛠 The first set of changes is to the instance variables at the start of _HomeScreenState. Change them to the following:



    bool isProgressing =false;
    bool isLoggedIn =false;
    String errorMessage ='';
    String? name;

🛠 The initState() method is just below those variables. Right now, the only thing it does is call its counterpart in the superclass.

Replace the implement init action comments with a call to initAction(). The method should look like this:

voidinitState(){initAction();super.initState();}

You’ll implement initAction() shortly.

Features and bugs

Please file feature requests and bugs at the issue tracker.

Get the project, configure it, and run it

🛠 Open the repository for the MJ Coffee app and download the source from the main branch. This contains a fully functioning app that is ready for you to add Auth0 authentication/authorization and chat.

🛠 If you want to build the app for iOS, you’ll need to specify your own development team for the build process. Open the /ios/Runner.xcworkspace/ file with Xcode, select the Runner project, then the Runner target, open the Signing & Capabilities tab, and select your team in the Team drop-down menu:

🛠 Confirm that the app works by running it. Open a command-line interface, navigate to the project’s root directory, and enter flutter run.

Flutter will compile the project and run it on any mobile device connected to your computer or any mobile device emulator running on it. If it can’t find any of those, it will run a mobile device emulation in a browser window.

You will see the app’s home screen:

Integrating auth0 with flutter

Since Auth0 is a standard OAuth 2.0 authorization server, you can utilize any standard OpenID Connect SDK to authenticate against Auth0. One of them is flutter_appauth, a wrapper around the AppAuth SDK for native applications. You will need to integrate it into your application.

🛠 Open the /lib/services/auth_service.dart file and update it to import the necessary libraries as well as instantiate FlutterAppAuth and FlutterSecureStorage:

Layers of sessions

Every login requires logout! It’s more complicated than it looks since there are typically three-session layers you need to consider:

License

Licensed under the terms of the Apache 2.0 license (the license under which the OPA-Python example was released),
the full version of which can be found in the LICENSE
file included in this distribution.

Logging in

🛠 If you’ve made it this far, you’ve done well, and it’s now time to see what you’ve achieved so far. Make sure your emulators or devices are active and stop any earlier versions of this app. Once you’ve done that, run the app using this command:

flutter run -d all --dart-define=AUTH0_DOMAIN=[YOUR DOMAIN] --dart-define=AUTH0_CLIENT_ID=[YOUR CLIENT ID]

Once the app is loaded, tap on the “Login | Register” button.

On iOS, when you run the app for the first time, you will see a prompt like this:

Make authenticated requests

To fetch data from most web services, you need to provide
authorization. There are many ways to do this,
but perhaps the most common uses the Authorization HTTP header.

The http package provides a
convenient way to add headers to your requests.
Alternatively, use the HttpHeaders
class from the dart:io library.

This example builds upon the
Fetching data from the internet recipe.

import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:http/http.dart' as http;

Future<Album> fetchAlbum() async {
  final response = await http.get(
    Uri.parse('https://jsonplaceholder.typicode.com/albums/1'),
    // Send authorization headers to the backend.
    headers: {
      HttpHeaders.authorizationHeader: 'Basic your_api_token_here',
    },
  );
  final responseJson = jsonDecode(response.body);

  return Album.fromJson(responseJson);
}

class Album {
  final int userId;
  final int id;
  final String title;

  const Album({
    required this.userId,
    required this.id,
    required this.title,
  });

  factory Album.fromJson(Map<String, dynamic> json) {
    return Album(
      userId: json['userId'],
      id: json['id'],
      title: json['title'],
    );
  }
}

Prerequisites

Before getting started, you need to have the following installed on your machine:

Provide the domain and client id to the app

You will need to use the domain and client ID that you copied from the Settings page in your Flutter application. You can either store these values in constant variables in the app’s code, or you can pass these values to the app by providing them as –dart-define arguments when you run it.

Rather than store this sensitive information in your code (which is a big security risk), I suggest that you supply the app with these values as –dart-define arguments when you run it.

🛠 To do this in Terminal or PowerShell, use this command:

flutter run -d all --dart-define=AUTH0_DOMAIN={YOUR DOMAIN} --dart-define=AUTH0_CLIENT_ID={YOUR CLIENT ID}

You can optionally have your editor of choice provide these values. For example, you can have Visual Studio Code pass these additional –dart-define values by adding them to the args field of your launch configuration file (/.vscode/launch.json):

Running from docker

To run from Docker, simply specify the host and port of the OPA server through
the passed in OPA_URL environment variable:

Note that by default the Docker image enables the Dart Observatory, which binds
port 8181 within the container by default. If using –net=host, the default
Observatory port needs to be shifted out of the way. This can be done by
tweaking the DART_VM_OPTIONS, as so:

Safety first!

In other words, this is an example meant to be as easy to follow as possible and you must take the appropriate precautions when it comes to choosing or generating a private key. Since we’re talking about security precautions, you should obviously use TLS for communications between front-end and back-end in production. Also, salt the passwords before hashing them if you really want to play it safe.

Take a quick tour of the app

🛠 Tap the Login | Register button. Right now, there is no login functionality, so the app immediately takes you to the Menu screen:

The access token, refresh token, and id token

You can use the Access Token to access APIs. Clients can’t decode this token, which is all right since it only means something to the API’s authorization server.

What you’ll learn and build

While you could create a new Flutter project and implement everything you will learn in this tutorial, adding authentication to an existing production-ready app is pretty common. I’ll provide a production-ready app, MJ Coffee, which you’ll secure by adding authentication.

Авторизация сервера приложения

Для выполнения авторизации сервера приложения нужно создать объект класса ApplicationServerAuth. После этого указать
appId, secret и version. Затем мы сможем получить токен с помощью метода getToken(), который вернет Future<String>,
значением которого будет токен. Пример получения токена для сервера приложения:

Авторизироваться на с помощью dart

Мне необходимо войти в аккаунт на vk.com через мое Flutter приложение (Без создания приложения в ВК и получения access_token). Я проанализировал запросы, которые делает браузер, и выяснил, что сначала необходимо заполнить формы:

{act: login, role: al_frame, expire: , to: aW5kZXgucGhw, recaptcha: , captcha_sid: , captcha_key: , _origin: https://vk.com, ip_h: abcdefgh0123456789, lg_h: 0123456789abcdefgh, ul: , email: 79999999999, pass: 0123456789}

Значения и ключи находятся внутри html (name:value) :

<div id="index_login" class="page_block index_login">
    <form method="post" name="login" id="index_login_form" action="https://login.vk.com/?act=login">
      <input type="hidden" name="act" id="act" value="login">
      <input type="hidden" name="role" value="al_frame">
      <input type="hidden" name="expire" id="index_expire_input" value="">
      <input type="hidden" name="_origin" value="https://vk.com">
      <input type="hidden" name="ip_h" value="abcdefgh0123456789">
      <input type="hidden" name="lg_h" value="0123456789abcdefgh">
      <input type="text" class="big_text" name="email" id="index_email" value="" placeholder="Телефон или email">
      <input type="password" class="big_text" name="pass" id="index_pass" value="" placeholder="Пароль" onkeyup="toggle('index_expire', !!this.value);toggle('index_forgot', !this.value)">
      <button id="index_login_button" class="index_login_button flat_button button_big_text">Войти</button>
      <div class="forgot">
        <div class="checkbox" id="index_expire" onclick="checkbox(this);ge('index_expire_input').value=isChecked(this)?1:'';" role="checkbox" aria-checked="false" tabindex="0">Чужой компьютер</div>
        <a id="index_forgot" class="index_forgot" href="/restore" target="_top">Забыли пароль?</a>
      </div>
    </form>
  </div>

А тут находится значение для ключа to (оно всегда равно aW5kZXgucGhw):

window.handlePageParams && handlePageParams({"id":0,"loc":"","noleftmenu":1,"wrap_page":1,"width":960,"width_dec":0,"width_dec_footer":0,"top_home_link_class":"TopHomeLink TopHomeLink--logoWithText","body_class":"index_page new_header_design ","to":"aW5kZXgucGhw","counters":[],"pvbig":0,"pvdark":1});addEvent(document, 'click', onDocumentClick);

Headers я взял из запроса, который сделал браузер (скопировал все, кроме cookie).

Далее по нажатию кнопки “Войти” браузер делает POST запрос по адресу: https://login.vk.com/?act=login (который так же находится внутри html) и передает словарь вышеупомянутых параметров. В ответ приходит 302 код состояния http (т.е. redirect как я понял). Headers, полученные браузером и моим приложением почти совпадают, за исключением location.

location в браузере : https://vk.com/login.php?act=slogin&to=aW5kZXgucGhw&s=1&__q_hash=149c8d2a4784c9a019d4fb720469378e

location в Dart : https://vk.com/login?&to=&s=0&m=1&email=

ОБНОВЛЕНО

Видимо мой POST запрос неверно составлен или что-то в этом роде, раз ответы от сервера отличаются.

Вот мой код на Dart:

import 'dart:convert';
import 'package:http/http.dart' as http;
import 'dart:async';

Future<void> login() async {
  Map<String, String> headers = {
    'accept':
    'text/html,application/xhtml xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
    'accept-encoding': 'gzip, deflate, br',
    'accept-language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
    'cache-control': 'max-age=0',
    //no cookies
    'referer': 'https://vk.com',
    'sec-fetch-dest': 'document',
    'sec-fetch-mode': 'navigate',
    'sec-fetch-site': 'same-origin',
    'sec-fetch-user': '?1',
    'upgrade-insecure-requests': '1',
    'user-agent':
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.182 Safari/537.36',
  };

  String email=' 79999999999'; //email or phone
  String password='0123456789';

  http.Client client = http.Client();

  try {
    http.Response uriResponse =
    await client.get('https://vk.com/', headers: headers);

    // get params for POST request
    String html = uriResponse.body;

    String start = '<form method="POST" name="login" id="quick_login_form"',
        end = "toggle('quick_forgot', !this.value)";
    html = html.substring(html.indexOf(start)   start.length);
    html = html.substring(0, html.indexOf(end));

    start = 'action="';
    end = '"';
    String action = html.substring(html.indexOf(start)   start.length);
    html = action.substring(action.indexOf(end)   end.length);
    action = action.substring(0, action.indexOf(end));
    print(action); // https://login.vk.com/?act=login

    List<String> parts = html.split('<input type="');
    Map<String, String> forms = {};
    parts.removeAt(0);
    String name, value;
    parts.forEach((e) {
      if (e.startsWith('hidden')) {
        start = 'name="';
        end = '"';
        name = e.substring(e.indexOf(start)   start.length);
        name = name.substring(0, name.indexOf(end));
        start = 'value="';
        value = e.substring(e.indexOf(start)   start.length);
        value = value.substring(0, value.indexOf(end));
        forms[name] = value;
      } else if (e.contains('name="')) {
        start = 'name="';
        end = '"';
        name = e.substring(e.indexOf(start)   start.length);
        name = name.substring(0, name.indexOf(end));
        if (name == 'email') {
          value = email;
          forms[name] = value;
        } else if (name == 'pass') {
          value = password;
          forms[name] = value;
        }
      }
    });

    html = uriResponse.body;
    start = 'handlePageParams(';
    html = html.substring(html.indexOf(start)   start.length);
    start = '"to":"';
    html = html.substring(html.indexOf(start)   start.length);
    forms['to'] = html.substring(0, html.indexOf('"'));

    print(forms
        .toString()); // {act: login, role: al_frame, expire: , to: aW5kZXgucGhw, recaptcha: , captcha_sid: , captcha_key: , _origin: https://vk.com, ip_h: abcdefgh0123456789, lg_h: 0123456789abcdefgh, ul: , email:  79999999999, pass: 0123456789}

    //headers['content-length']='210'; error...
    headers['content-type']='application/x-www-form-urlencoded';
    headers['origin']='https://vk.com';
    headers['referer']='https://vk.com/';
    headers['sec-fetch-dest']='iframe';

    var resp = await client.post(
      action,
      headers: headers,
      body: jsonEncode(forms),
    );

    print(resp.statusCode); // 302
    print(resp.headers); // {connection: keep-alive, x-powered-by: KPHP/7.4.106227, location: https://vk.com/login?&to=&s=0&m=1&email=, cache-control: no-store, set-cookie: remixir=DELETED; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/; domain=.vk.com; secure; HttpOnly; SameSite=None,remixlang=0; expires=Sat, 26 Feb 2022 20:14:15 GMT; path=/; domain=.vk.com; secure; SameSite=None, date: Thu, 25 Feb 2021 20:39:20 GMT, content-encoding: gzip, strict-transport-security: max-age=15768000, content-length: 20, p3p: CP="IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT", content-type: text/html; charset=windows-1251, server: kittenx}

  } finally {
    client.close();
  }
}

ДОБАВЛЕНО

Реализация на Python:

import lxml.html
import requests

url='https://vk.com/'
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36',
    'Accept': 'text/html,application/xhtml xml,application/xml;q=0.9,*/*;q=0.8',
    'Accept-Language':'ru-ru,ru;q=0.8,en-us;q=0.5,en;q=0.3',
    'Accept-Encoding':'gzip, deflate',
    'Connection':'keep-alive',
    'DNT':'1'
}

session = requests.session()
data = session.get(url, headers=headers)
page = lxml.html.fromstring(data.content)

form = page.forms[0]
form.fields['email'] =  'EMAIL/PHONE'
form.fields['pass'] = 'PASSWORD'

response = session.post(form.action, data=form.form_values())
print('onLoginDone' in response.text)

Инфраструктура бекэнд

Типовым способом размещения серверного приложения является, конечно,

(виртуальный приватный сервер). Фактически — это часть физического сервера в дата центре, ресурсы которого (ядра процессора и оперативная память) разделены с помощью технологии виртуализации (о наиболее распространённых технологиях аппаратной виртуализации можно почитать здесь

). Внимание: технологии программной виртуализации (

) для нашей задачи могут не подойти из-за конфликтов с Docker и агрессивным

(зачастую, при внимательном прочтении договора аренды такого VPS, можно обнаружить, что провайдеры гарантируют «не менее 5%» (!) утилизации ядра арендуемого процессора. Это означает, что провайдер планирует продать наше ядро процессора 20 (!) раз).

Пример того, как можно получить токен в chrome packaged app

Для получения токена в Chrome Packaged App мы можем создать объект webview, у которого указать URL для получения прав.
После решения пользователя по выдаче прав приложению получить текущий url webview (который сменится) и извлечь из него
токен, либо информацию об ошибке.

Серверная авторизация

На первом шаге серверной мы должны создать объект класса ServerAuth c помощью конструктора
ServerAuth(). Затем указать ID приложения, redirect uri, набор scopes, версию api.
Полный список параметров, которые можно установить для получения токена точно такой же, как и для класса
StandaloneAuth, который описан в разделе Клиентская авторизация.

Пример получения ссылки, на по которой нужно направить пользователя при серверной авторизации:

После того, как пользователь прошел по ссылке и принял/отклонил запрос на выдачу прав приложению мы можем
попытаться получить токен, либо обработать ошибки.

Вот список методов, с помощью которых мы можем это сделать:

Заметьте, что перед тем, как попытаться получить токен, нужно указать секретный ключ приложения через сеттер secret.

Пример получения токена:

Вместо заключения

Итак, сделан первый шаг. Мы успешно раскатили на боевой сервер приложение. Во второй статье мы продолжим настройку сервера: назначим доменное имя и установим сертификат шифрования SSL. В третьей статье

Install flutter dependencies

The first step is to import the required libraries. You’ll do that by specifying three new dependencies:

You’ll install them by adding entries to the project’s /pubspec.yaml file (located in the root directory), where dependencies are specified.

🛠 Add the following lines to the /pubspec.yaml’s dependencies: section, just after the line that starts with json_annotation:

Handling the app’s initial state

The only thing missing is handling the authentication state when the app is launched. You might want to be able to silently login and retrieve a new Access Token if a Refresh Token is available.

🛠 Let’s add a new method, init(), to deal with the app’s initial state. Implement this method by adding the following to AuthService:



  Future<bool>init()async{final storedRefreshToken =await secureStorage.read(key: REFRESH_TOKEN_KEY);if(storedRefreshToken ==null){returnfalse;}try{final TokenResponse? result =await appAuth.token(TokenRequest(
          AUTH0_CLIENT_ID,
          AUTH0_REDIRECT_URI,
          issuer: AUTH0_ISSUER,
          refreshToken: storedRefreshToken,),);final String setResult =await_setLocalVariables(result);return setResult =='Success';}catch(e, s){print('error on Refresh Token: $e - stack: $s');returnfalse;}}

Conclusion

Congratulations! You have just integrated Auth0-powered login and logout into the MJ Coffee app.

In an upcoming section, you will continue to add authentication features to the app. You’ll learn more about Refresh Token rotation, managing the branding that appears in the login box, roles and adding social login via Apple and Google accounts.

Building the back-end

These are the libraries we are going to use:

Похожее:  Как обезопасить Nginx с Let’s Encrypt на Ubuntu | Блог Timeweb Cloud

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *