Мне было поручено создать веб-интерфейс для приложения Android на основе Firebase. У меня есть несколько конечных точек, которые взаимодействуют с базой данных (облачные функции). Чтобы получить доступ к этим конечным точкам, мне нужно аутентифицировать пользователя по электронной почте и паролю [1], получить accessToken [2] и авторизовать каждый запрос к конечным точкам с заголовком Authorization: Bearer {accessToken}.

Я использую php и пытаюсь обдумать, как управлять аутентифицированным пользователем в моем приложении.

TL; DR, пожалуйста, смотрите мое окончательное решение только в php. https://stackoverflow.com/a/52119600/814031

Я передаю accessToken через ajax в сеансе php, чтобы подписать запросы cURL к конечным точкам. По-видимому, нет другого пути, чем использовать firebase JS auth (не так, как я понимаю [4]).

Мой вопрос: Достаточно ли сохранить accessToken в сеансе php и сравнить его с каждой загрузкой страницы с помощью ajax-запроса POST (см. код ниже)? Что будет более надежной стратегией для обработки этого в php?

Изменить: Пользователь указал, что использование классических php-сессий с токенами JWT не очень много смысл, и я прочитал об этой теме. Что касается Firebase - это что-то рассмотреть? https://firebase.google.com/docs/auth/admin/manage- печенье

Firebase Auth обеспечивает управление файлами cookie сеансов на стороне сервера для традиционных веб-сайтов, которые используют файлы cookie сеансов. Это решение имеет несколько преимуществ по сравнению с короткоживущими токенами ID на стороне клиента, которые могут потребовать каждый раз механизм перенаправления для обновления cookie сеанса по истечении срока действия:

Вот что у меня получилось:

< Сильный > 1 . Страница входа

Как описано в примерах Firebase [3]

function initApp() {

  firebase.auth().onAuthStateChanged(function (user) {
    if (user) {
      // User is signed in.

      // obtain token, getIdToken(false) = no forced refresh
      firebase.auth().currentUser.getIdToken(false).then(function (idToken) {

        // Send token to your backend via HTTPS
        $.ajax({
          type: 'POST',
          url: '/auth/check',
          data: {'token': idToken},
          complete: function(data){
            // data = {'target' => '/redirect/to/route'}
            if(getProperty(data, 'responseJSON.target', false)){
              window.location.replace(getProperty(data, 'responseJSON.target'));
            }
          }
        });
        // ...
      }).catch(function (error) {
        console.log(error);
      });


    } else {
      // User Signed out
      $.ajax({
        type: 'POST',
        url: '/auth/logout',

        complete: function(data){
          // data = {'target' => '/redirect/to/route'}
          if(getProperty(data, 'responseJSON.target', false)){
            // don't redirect to itself
            // logout => /
            if(window.location.pathname != getProperty(data, 'responseJSON.target', false)){
              window.location.replace(getProperty(data, 'responseJSON.target'));
            }
          }
        }
      });

      // User is signed out.
    }

  });
}

window.onload = function () {
  initApp();
};

< Сильный > 2 . PHP-контроллер для обработки запросов на аутентификацию

public function auth($action)
{

  switch($action) {
    // auth/logout
    case 'logout':

      unset($_SESSION);
      // some http status header and mime type header
      echo json_encode(['target' => '/']); // / => index page
    break;

    case 'check':

      // login.
      if(! empty($_POST['token']) && empty($_SESSION['token'])){

        // What if I send some bogus data here? The call to the Endpoint later would fail anyway
        // But should it get so far?

        $_SESSION['token'] = $_POST['token'];

        // send a redirect target back to the JS
        echo json_encode(['target' => '/dashboard']);
        break;
      }


      if($_POST['token'] == $_SESSION['token']){
        // do nothing;
        break;
      }
    break;
  }
}

< Сильный > 3 . Главный контроллер

// pseudo code
class App
{
  public function __construct()
  {
    if($_SESSION['token']){
      $client = new \GuzzleHttp\Client();
      // $user now holds all custom access rights within the app.
      $this->user = $client->request(
        'GET', 
        'https://us-centralx-xyz.cloudfunctions.net/user_endpoint',
        ['headers' => 
                [
                    'Authorization' => "Bearer {$_SESSION['token']}"
                ]
            ]
        )->getBody()->getContents();
    }else{
      $this->user = null;
    }
  }

  public function dashboard(){
    if($this->user){
      var_dump($this->user);
    }else{
      unset($_SESSION);
      // redirect to '/' 
    }
  }
}

Примечание. Мне известен этот SDK https://github.com/kreait/firebase-php и я много читал о проблемах там и в постах здесь о SO, но я запутался, так как идет речь о полных правах администратора и т. д., и я действительно взаимодействую только с конечными точками, которые основаны на firebase (плюс аутентификация firebase и Firestore ) . И я до сих пор на php 5.6: - /

Спасибо за ваше время!

4
marcus 21 Авг 2018 в 20:41

3 ответа

Лучший ответ

Я должен признать, что сложность документации, примеров и различных сервисов Firebase привела меня в замешательство, и я подумал, что аутентификация в Интернете возможна только через JavaScript. Это было неправильно. По крайней мере, для моего случая, когда я просто войду в систему с помощью электронной почты и пароля , чтобы получить токен Json Web (JWT) , чтобы подписать все вызовы облачных функций Firebase. Вместо того, чтобы манипулировать странными запросами Ajax или устанавливать cookie-токен с помощью JavaScript, мне просто нужно было вызвать API REST Firebase Auth

Вот минимальный пример использования Fatfreeframework:

Форма входа

<form action="/auth" method="post">
    <input name="email">
    <input name="password">
    <input type="submit">
</form>

Маршрут

$f3->route('POST /auth', 'App->auth');

Контроллер

class App
{
    function auth()
    {
        $email = $this->f3->get('POST.email');
        $password = $this->f3->get('POST.password');

        $apiKey = 'API_KEY'; // see https://firebase.google.com/docs/web/setup

        $auth = new Auth($apiKey);
        $result = $auth->login($email,$password);

        if($result['success']){
            $this->f3->set('COOKIE.token',$result['idToken']);
            $this->f3->reroute('/dashboard');
        }else{
            $this->f3->clear('COOKIE.token');
            $this->f3->reroute('/');
        }
    }
}

< Сильный > Класс

<?php
use GuzzleHttp\Client;

class Auth
{

    protected $apiKey;

    public function __construct($apiKey){
        $this->apiKey = $apiKey;
    }

    public function login($email,$password)
    {

        $client = new Client();
        // Create a POST request using google api
        $key = $this->apiKey;
        $responsee = $client->request(
            'POST',
            'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key=' . $key,
            [
                'headers' => [
                    'content-type' => 'application/json',
                    'Accept' => 'application/json'
                ],
                'body' => json_encode([
                    'email' => $email,
                    'password' => $password,
                    'returnSecureToken' => true
                ]),
                'exceptions' => false
            ]
        );

        $body = $responsee->getBody();
        $js = json_decode($body);

        if (isset($js->error)) {
            return [
                'success' => false,
                'message' => $js->error->message
            ];
        } else {
            return [
                'success' => true,
                'localId' => $js->localId,
                'idToken' => $js->idToken,
                'email' => $js->email,
                'refreshToken' => $js->refreshToken,
                'expiresIn' => $js->expiresIn,
            ];

        }

    }

}

Кредиты

7
marcus 31 Авг 2018 в 16:34

Вы действительно не должны использовать сессии в PHP при использовании токенов. Токены должны отправляться в заголовке при каждом запросе (или файл cookie тоже работает).

Токены работают следующим образом: 1. Вы входите в систему, сервер копирует токен с некоторой закодированной информацией. 2. Вы отправляете этот токен обратно при каждом запросе.

На основании информации, закодированной в токене, сервер может получить информацию о пользователе. Обычно в нем кодируется какой-то идентификатор пользователя. Сервер знает, что это действительный токен из-за способа его кодирования.

Отправляйте токен на каждый запрос, который вам нужно сделать, затем в PHP вы можете просто передать этот токен другому API

0
Chad K 21 Авг 2018 в 17:49

Похоже, @Chad K выводит вас на правильный путь (куки и ajax - завтрак чемпионов ... :), хотя я думал поделиться своим кодом из моей рабочей системы (с некоторыми вещами о конфиденциальности, конечно!)

Ищите комментарии типа / **** для вещей, которые вам нужно настроить самостоятельно (возможно, вы захотите сделать и другие вещи из Firebase по-другому - см. Документы ...)

Страница LOGIN.php (я считаю, что проще держать это отдельно - см. Примечания, чтобы узнать, почему ....)

<script>
    /**** I picked this up somewhere off SO - kudos to them - I use it a lot!.... :) */
        function setCookie(name, value, days = 7, path = '/') {
            var expires = new Date(Date.now() + days * 864e5).toUTCString();
            document.cookie = name + '=' + encodeURIComponent(value) + '; expires=' + expires + '; path=' + path;
        }

        function getCookie(c_name) {
            if (document.cookie.length > 0) {
                c_start = document.cookie.indexOf(c_name + "=");
                if (c_start !== -1) {
                    c_start = c_start + c_name.length + 1;
                    c_end = document.cookie.indexOf(";", c_start);
                    if (c_end === -1) {
                        c_end = document.cookie.length;
                    }
                    return unescape(document.cookie.substring(c_start, c_end));
                }
            }
            return "";
        }
    </script>
    <script>
        var config = {
            apiKey: "your_key",
            authDomain: "myapp.firebaseapp.com",
            databaseURL: "https://myapp.firebaseio.com",
            projectId: "myapp",
            storageBucket: "myapp.appspot.com",
            messagingSenderId: "the_number"
        };
        firebase.initializeApp(config);
    </script>
<script src="https://cdn.firebase.com/libs/firebaseui/2.7.0/firebaseui.js"></script>
    <link type="text/css" rel="stylesheet" href="https://cdn.firebase.com/libs/firebaseui/2.7.0/firebaseui.css"/>
    <script type="text/javascript">
        /**** set this url to the 'logged in' page (mine goes to a dashboard) */ 
        var url = 'https://my.app/index.php#dashboard';
        /**** by doing this signOut first, then it is simple to send any 'logout' request in the app to 'login.php' - one page does it.... :) */
        firebase.auth().signOut().then(function () {
        }).catch(function (error) {
            console.log(error);
        });
        var signInFlow = 'popup';
        if (('standalone' in window.navigator)
            && window.navigator.standalone) {
            signInFlow = 'redirect';
        }
        var uiConfig = {
            callbacks: {
                signInSuccessWithAuthResult: function (authResult, redirectUrl) {
                    /**** here you can see the logged in user */
                    var firebaseUser = authResult.user;
                    var credential = authResult.credential;
                    var isNewUser = authResult.additionalUserInfo.isNewUser;
                    var providerId = authResult.additionalUserInfo.providerId;
                    var operationType = authResult.operationType;
                    /**** I like to force emailVerified...... */
                    if (firebaseUser.emailVerified !== true) {
                        firebase.auth().currentUser.sendEmailVerification().then(function () {
                            /**** if using this, you can set up your own usermgmt.php page for the user verifications (see firebase docs) */
                         window.location.replace("https://my.app/usermgmt.php?mode=checkEmail");
                        }).catch(function (error) {
                            console.log("an error has occurred in sending verification email " + error)
                        });
                    }
                    else {
                        var accessToken = firebaseUser.qa;
                        /**** set the Cookie (yes, I found this best, too) */
                        setCookie('firebaseRegistrationID', accessToken, 1);
                            /**** set up the AJAX call to PHP (where you will store this data for later lookup/processing....) - I use "function=....." and "return=....." to have options for all functions and what to select for the return so that ajax.php can be called for 'anything' (you can just call a special page if you like instead of this - if you use this idea, be sure to secure the ajax.php 'function' call to protect from non-authorized use!) */
                            var elements = {
                            function: "set_user_data",
                            user: JSON.stringify(firebaseUser),
                            return: 'page',
                            accessToken: accessToken
                        };
                        $.ajaxSetup({cache: false});
                        $.post("data/ajax.php", elements, function (data) {
                            /**** this calls ajax and gets the 'page' to set (this is from a feature where I store the current page the user is on, then when they log in again here, we go back to the same page - no need for cookies, etc. - only the login cookie is needed (and available for 'prying eyes' to see!) */
                            url = 'index.php#' + data;
                            var form = $('<form method="post" action="' + url + '"></form>');
                            $('body').append(form);
                            form.submit();
                        });
                    }
                    return false;
                },
                signInFailure: function (error) {
                    console.log("error - signInFailure", error);
                    return handleUIError(error);
                },
                uiShown: function () {
                    var loader = document.getElementById('loader');
                    if (loader) {
                        loader.style.display = 'none';
                    }
                }
            },
            credentialHelper: firebaseui.auth.CredentialHelper.ACCOUNT_CHOOSER_COM,
            queryParameterForWidgetMode: 'mode',
            queryParameterForSignInSuccessUrl: 'signInSuccessUrl',
            signInFlow: signInFlow,
            signInSuccessUrl: url,
            signInOptions: [
                firebase.auth.GoogleAuthProvider.PROVIDER_ID,
                //     firebase.auth.FacebookAuthProvider.PROVIDER_ID,
                //     firebase.auth.TwitterAuthProvider.PROVIDER_ID,
                {
                    provider: firebase.auth.EmailAuthProvider.PROVIDER_ID,
                    requireDisplayName: true,
                    customParameters: {
                        prompt: 'select_account'
                    }
                }
                /*      {
                        provider: firebase.auth.PhoneAuthProvider.PROVIDER_ID,
                        // Invisible reCAPTCHA with image challenge and bottom left badge.
                        recaptchaParameters: {
                          type: 'image',
                          size: 'invisible',
                          badge: 'bottomleft'
                        }
                      }
                */
            ],
            tosUrl: 'https://my.app/login.php'
        };
        var ui = new firebaseui.auth.AuthUI(firebase.auth());
        (function () {
            ui.start('#firebaseui-auth-container', uiConfig);
        })();
    </script>

Теперь, на каждой странице, которую вы хотите, чтобы пользователь видел (в моем случае все это проходит через index.php # что-то - что облегчает .... :)

 <script src="https://www.gstatic.com/firebasejs/4.12.0/firebase.js"></script>
<script>
    // Initialize Firebase - from https://github.com/firebase/firebaseui-web
    var firebaseUser;
    var config = {
        apiKey: "your_key",
        authDomain: "yourapp.firebaseapp.com",
        databaseURL: "https://yourapp.firebaseio.com",
        projectId: "yourapp",
        storageBucket: "yourapp.appspot.com",
        messagingSenderId: "the_number"
    };
    firebase.initializeApp(config);
    initFBApp = function () {
        firebase.auth().onAuthStateChanged(function (firebaseuser) {
                if (firebaseuser) {
                    /**** here, I have another ajax call that sets up some select boxes, etc. (I chose to call it here, you can call it anywhere...) */
                    haveFBuser();
                    firebaseUser = firebaseuser;
                    // User is signed in.
                    var displayName = firebaseuser.displayName;
                    var email = firebaseuser.email;
                    var emailVerified = firebaseuser.emailVerified;
                    var photoURL = firebaseuser.photoURL;
                    if (firebaseuser.photoURL.length) {
                        /**** set the profile picture (presuming you are showing it....) */
                        $(".profilepic").prop('src', firebaseuser.photoURL);
                    }
                    var phoneNumber = firebaseuser.phoneNumber;
                    var uid = firebaseuser.uid;
                    var providerData = firebaseuser.providerData;
                    var string = "";
                    firebaseuser.getIdToken().then(function (accessToken) {
                        // document.getElementById('sign-in-status').textContent = 'Signed in';
                        // document.getElementById('sign-in').textContent = 'Sign out';
                        /**** set up another ajax call.... - to store things (yes, again.... - though this time it may be due to firebase changing the token, so we need it twice...) */
                        string = JSON.stringify({
                            displayName: displayName,
                            email: email,
                            emailVerified: emailVerified,
                            phoneNumber: phoneNumber,
                            photoURL: photoURL,
                            uid: uid,
                            accessToken: accessToken,
                            providerData: providerData
                        });
                        if (accessToken !== '<?php echo $_COOKIE['firebaseRegistrationID']?>') {
                            console.log("RESETTING COOKIE with new accessToken ");
                            setCookie('firebaseRegistrationID', accessToken, 1);
                            var elements = 'function=set_user_data&user=' + string;
                            $.ajaxSetup({cache: false});
                            $.post("data/ajax.php", elements, function (data) {
                                <?php
                                /**** leave this out for now and see if anything weird happens - should be OK but you might want to use it (refreshes the page when firebase changes things.....  I found it not very user friendly as they reset at 'odd' times....)
                                /*
                            // var url = 'index.php#<?php echo(!empty($user->userNextPage) ? $user->userNextPage : 'dashboard'); ?>';
                            // var form = $('<form action="' + url + '" method="post">' + '</form>');
                            // $('body').append(form);
                            // console.log('TODO - leave this form.submit(); out for now and see if anything weird happens - should be OK');
                            // form.submit();
                            */
                                ?>
                            });
                        }
                    });
                } else {
                    console.log("firebase user CHANGED");
                    document.location.href = "../login.php";
                }
            }, function (error) {
                console.log(error);
            }
        );
    };
    window.addEventListener('load', function () {
        initFBApp();
    });
</script>

Надеюсь это поможет. Это из моей рабочей системы, которая включает в себя некоторые дополнительные функции, которые я добавил туда по пути, но в основном это напрямую из firebase, так что вы должны быть в состоянии следовать достаточно хорошо.

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

1
CFP Support 21 Авг 2018 в 21:06
51954047