Add Authentication to Your Vanilla JavaScript App in 20 Minutes | Okta Developer

Введение

Так же как аутентификация важна для API (* Application Programming Interface – программный интерфейс приложения. Здесь и далее примеч. пер.), она является важной особенностью определенных веб-приложений – тех, у которых имеются страницы и секреты, к которым должен быть доступ только у пользователей, прошедших регистрацию и аутентификацию.

В данном руководстве в процессе изучения реализации регистрации пользователей вы создадите веб-приложение.

Управление аутентификацией Feathers

Feathers это опенсорсный микросервисный веб-фреймворк для NodeJS. На GitHub у него 11 тысяч звезд. Он дает вам возможность контроля над данными с помощью ресурсов RESTful, сокетов и гибких плагинов.

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

Идея Feathers в том, чтобы собрать различные методы аутентификации «под одной крышей», в гибкой инфраструктуре. Приступить к делу вам поможет пошаговое руководство.

Firebase Authentication (для маленьких приложений)

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

Add a new meal

To add new meals you need to bind an event listener to the form so you can grab the form values without the form submitting. When the form is submitted, you should always call event.preventDefault() which will prevent the default behavior of the form submitting.

Add api integration to your vanilla js app

When your app initializes, you want to fetch all stored meals from the server and render them. Using the request() method above, it’s a breeze. Replace init() method in public/main.js with the following code:

Adding a meal introduces a POST /meals call to store the data on the server. If this method succeeds, the code should push the new meal onto the local array and recalculate the total calories.

And finally, deleting a meal is done by calling DELETE /meals/:id on the server. The meal is removed from the local array and total calories recalculated.

Add authentication middleware

The authenticationRequired middleware will pull the token from the Authorization header and verify it. If it’s a valid token, then the request will continue to the route handlers. We add this middleware to each of our routes to secure them.

Add rest routes to the express server

Now that your frontend can add and remove meals from a local array, it’s time for you to wire up the backend to store meal data on the server. In my previous tutorial Build a Basic CRUD App with Vue.js and Node, I showed how to persist data to SQLite using Sequelize and Epilogue.

But, to keep things simple and focused on the front end, you will use a local in-memory store (an array) on the server. Storing data in memory is not for production, as every time you restart the server, the array resets.

Stop the server by pressing CTRL C and install the following dependency:

Copy the following code and paste it into index.js:

This will expose the following endpoints:

Restart your server by running node .. Next up, you’ll connect the frontend using Vanilla JS AJAX.

Configure a new okta application

Before you begin, you’ll need a free Okta developer account. Install the Okta CLI and run okta register to sign up for a new account. If you already have an account, run okta login.

Create a static file server in vanilla js

Perhaps the best part of using vanilla JavaScript is not having a lengthy bootstrap process or complicated build scripts. All you need is a static file server and a few files to get started.

Create a new directory for your app (I used vanilla-js). Open your favorite terminal and cd into the directory. Initialize your app and install a single dependency:

Create an index.js file in your project root and paste the following code into it.

This code will start an Express server on port 8080 and serve static files frompublic/ directory in your project root.

To complete the setup, create a public directory in your project root and create the following two files in it:

First, public/index.html:

And second, public/main.js:

Create nodes

You can build and render complex, dynamic objects by leveraging the DOM. Now that you are a ninja at changing colors try something more advanced: creating nodes. A “node” in this context is an HTML element (“nodes” and “elements” are frequently used to represent the same thing).

You can use the DOM to append a <div>Oh my!</div> to the <app /> element. Modify main.js to the following.

Refresh the web page. A <div/> node was not-so-magically appended to your webpage. The render() method builds a new <div />, sets the text, and appends it to the app.

Es6 ftw!

With roughly 90% browser support worldwide and numerous language improvements, ES6 is a clear choice over ES5. ES6 provides many enhancements like classes, template strings, let/const, arrow functions, default parameters, and destructuring assignment. Or, in another words, it makes JavaScript even more awesome.

If you require more comprehensive browser support, you can use babel to transpile ES6 to ES5 to support nearly 100% of browsers.

Get familiar with the dom

You are going to work directly with the DOM (Document Object Model). The DOM is an interface to change an HTML page’s structure, style, and content. Using the DOM, you can render meals, listen to form events, and update the total calories with JavaScript.

To illustrate this quickly, let’s use the DOM to change an element’s text color. In main.js, add the following code to the render() method:

Go back to your browser and refresh. Voila, red text! Mind blown! Okay, it’s not the most impressive example ever, but it is a basic example of how you can interact with HTML elements with JavaScript.

Learn more about javascript, authentication, and okta

In this tutorial, you built a dynamic SPA using JavaScript DOM APIs and added authentication using Okta’s Sign-in Widget in ~200 lines of code (including HTML). That’s pretty awesome! While I don’t suggest vanilla JavaScript for all SPAs, I hope this tutorial reignites your desire to understand more about the underlying JavaScript powering today’s large frameworks.

Manage meals

You will create several methods to manage meals within the app. Copy and paste the following code into public/main.js to get started. I will step through each code chunk below in detail.

classApp{constructor(){this.meals=[]document.getElementById('form-entry').addEventListener('submit',(event)=>{event.preventDefault()this.addMeal({id:Date.now(),// faux idtitle:document.getElementById('title').value,calories:parseInt(document.getElementById('calories').value)})})}init(){this.meals=[{id:1,title:'Breakfast Burrito',calories:150},{id:2,title:'Turkey Sandwich',calories:600},{id:3,title:'Roasted Chicken',calories:725}]this.render()}addMeal(meal){document.getElementById('meals').appendChild(this.createMealElement(meal))this.meals.push(meal)this.updateTotalCalories()}deleteMeal(id){letindex=this.meals.map(o=>o.id).indexOf(id)this.meals.splice(index,1)this.updateTotalCalories()}updateTotalCalories(){letelTotal=document.getElementById('total')elTotal.textContent=this.meals.reduce((acc,o)=>acc o.calories,0).toLocaleString()}createMealElement({id,title,calories}){letel=document.createElement('li')el.className='list-group-item d-flex justify-content-between align-items-center'el.innerHTML=`
      <div>
        <a href="#" class="remove">&times;</a>
        <span class="title">${title}</span>
      </div>
      <span class="calories badge badge-primary badge-pill">${calories}</span>
    `// when the 'x' delete link is clickedel.querySelector('a').addEventListener('click',(event)=>{event.preventDefault()this.deleteMeal(id)el.remove()})returnel}render(){letfragment=document.createDocumentFragment()for(letmealofthis.meals){fragment.appendChild(this.createMealElement(meal))}letel=document.getElementById('meals')while(el.firstChild){el.removeChild(el.firstChild)// empty the <div id="meals" />}el.appendChild(fragment)this.updateTotalCalories()}}letapp=newApp()app.init()

Remove a meal

As you can see in the markup for each meal, there is a small “x” on the left of the name. You will bind an event listener to this element to remove the meal when it’s clicked. You can attach a click event listener when creating the meal element to achieve this.

The deleteMeal() method will remove the meal from the local array and update the total calories.

Remove nodes

You can remove nodes just as easily by calling node.remove(). Add the following setTimeout call to the end of your render() method to see this in action. The newly created div should disappear after two seconds.

There are other ways you can remove nodes with the DOM like parent.removeChild(node), but for the sake of simplicity, node.remove() tends to be the easiest to understand.

Take your new vanilla javascript app for a test drive

Tab back to your browser and hit refresh. You should be able to add and delete meals!

Test out your new vanilla js app

Congrats! You have successfully built a dynamic SPA in around 100 lines of code. Not bad! Go back to your browser and hit refresh. You should initially see no meals as the server starts with an empty array. To see the app in action you can add meals or remove meals, then refresh the page to view the changes.

Okta’s Sign-in Widget is a JavaScript library that gives you a full-featured and customizable login widget that can be added to any website. With just a few lines of code, you can implement a login flow to your app.

Update total meal calories

Since the <span id=”total” /> element existed when we rendered the page, we can just update its content with a call to node.textContent().

Your project: secure authentication for vanilla javascript

You will create a calorie counter app using just JavaScript DOM APIs and a simple REST server. The goal is to create a web interface to track meals and display a running total of calories. For the backend, you will use Express to serve your static files and expose a few REST methods to manage meals.

Настройка представлений

Создайте новую папку под названием views (* представления). Внутри нее создайте две другие папки под названиями layouts и partials. Сформируйте подобную древовидную структуру в папке views. Для этого создайте необходимые файлы в их соответствующих папках.

После этого пришло время писать код.

Ошибка вторая: система сброса паролей

В деле сброса пароля есть тысячи способов всё испортить. Вот наиболее распространённые ошибки в решении этой задачи, которые мне довелось видеть:

  1. Предсказуемые токены. Токены, основанные на текущем времени — хороший пример. Токены, построенные на базе плохого генератора псевдослучайных чисел, хотя и выглядят лучше, проблему не решают.
  2. Неудачное хранилище данных. Хранение незашифрованных токенов сброса пароля в базе данных означает, что если она будет взломана, эти токены равносильны паролям, хранящимся в виде обычного текста. Создание длинных токенов с помощью криптографически стойкого генератора псевдослучайных чисел позволяет предотвратить удалённые атаки на токены сброса пароля методом грубой силы, но не защищает от локальных атак. Токены для сброса пароля следует воспринимать как учётные данные и обращаться с ними соответственно.
  3. Токены, срок действия которых не истекает. Если срок действия токенов не истекает, у атакующего есть время для того, чтобы воспользоваться временным окном сброса пароля.
  4. Отсутствие дополнительных проверок. Дополнительные вопросы при сбросе пароля — это стандарт верификации данных де-факто. Конечно, это работает как надо лишь в том случае, если разработчики выбирают хорошие вопросы. У подобных вопросов есть собственные проблемы. Тут стоит сказать и об использовании электронной почты для восстановления пароля, хотя рассуждения об этом могут показаться излишней перестраховкой. Ваш адрес электронной почты — это то, что у вас есть, а не то, что вы знаете. Он объединяет различные факторы аутентификации. Как результат, адрес почты становится ключом к любой учётной записи, которая просто отправляет на него токен сброса пароля.


Если вы со всем этим никогда не сталкивались, взгляните на

по сбросу паролей, подготовленную OWASP. Теперь, обсудив общие вопросы, перейдём к конкретике, посмотрим, что может предложить экосистема Node.

Ненадолго обратимся к npm и посмотрим, сделал ли кто-нибудь библиотеку для сброса паролей. Вот, например, пакет пятилетней давности от в целом замечательного издателя substack. Учитывая скорость развития Node, этот пакет напоминает динозавра, и если бы мне хотелось попридираться к мелочам, то я мог бы сказать, что функция Math.random()предсказуема в V8, поэтому её не следует использовать для создания токенов. Кроме того, этот пакет не использует Passport, поэтому мы идём дальше.

Stack Overflow здесь тоже особенно не помог. Как оказалось, разработчики из компании Stormpath любят писать о своём IaaS-стартапе в любом посте, хоть как-то связанным с этой темой. Их документация тоже всплывает повсюду, они также продвигают свой блог, где есть материалы по сбросу паролей.

Ладно, возвращаемся к поиску в Google. На самом деле, такое ощущение, что интересующая нас тема раскрыта в единственном материале. Возьмём первый результат, найденный по запросу express passport password reset. Тут снова встречаем нашего старого друга bcrypt, с даже меньшим коэффициентом трудоёмкости, равным 5, что значительно меньше, чем нужно в современных условиях.

Однако, это руководство выглядит довольно-таки целостным по сравнению с другими, так как оно использует crypto.randomBytes для создания по-настоящему случайных токенов, срок действия которых истекает, если они не были использованы. Однако, пункты 2 и 4 из вышеприведённого списка ошибок при сбросе пароля в этом серьёзном руководстве не учтены. Токены хранятся ненадёжно — вспоминаем первую ошибку руководств по аутентификации, связанную с хранением учётных данных.

Хорошо хотя бы то, что украденные из такой системы токены имеют ограниченный срок действия. Однако, работать с этими токенами очень весело, если у атакующего есть доступ к объектам пользователей в базе данных через BSON-инъекцию, или есть свободный доступ к Mongo из-за неправильной настройки СУБД.

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

Ошибка первая: хранилище учётных данных

Начнём с хранилища учётных данных. Запись и чтение учётных данных — это вполне обычные задачи в сфере управления аутентификацией, и традиционный способ решения этих задач заключается в использовании собственной базы данных. Passport является промежуточным программным обеспечением, которое просто сообщает нашему приложению: «этот пользователь прошёл проверку», или: «этот пользователь проверку не прошёл», требуя модуля

для работы с хранилищем паролей в локальной базе данных. Этот модуль написан тем же разработчиком, что и сам Passport.js.

Ошибка четвёртая: ограничение числа попыток аутентификации


Я не нашёл упоминаний об ограничении числа попыток аутентификации или о блокировке аккаунта ни в одном из рассмотренных руководств.

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

Помните о том, что ограничение числа попыток аутентификации способствует доступности сервиса. Так, использование bcrypt создаёт серьёзную нагрузку на процессор. Без ограничений, функции, в которых вызывают bcrypt, становятся вектором отказа в обслуживании уровня приложения, особенно при использовании высоких значений коэффициента трудоёмкости.

Хотя подходящего учебного руководства на эту тему у меня нет, есть множество вспомогательных библиотек для ограничения числа запросов, таких, как express-rate-limit, express-limiter, и express-brute. Не могу говорить об уровне безопасности этих модулей, я их даже не изучал.

Подсаливание и хеширование пароля

Не стоит сохранять пароли пользователей в незашифрованном виде. Вот что следует сделать, когда пользователь вводит пароль в незашифрованном виде при регистрации. Пароль в незашифрованном виде должен быть захеширован при помощи соли (* в криптографии – случайное число или текст, которые добавляются к данным, шифруемым с помощью пароля), которая будет сгенерирована вашим приложением (используя модуль bcryptjs). Этот захешированный пароль затем сохраняется в базу данных.

Установка приложения

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

Флаг -y сообщает npm (* менеджер пакетов для JavaScript) о необходимости использования  опций по умолчанию.

Отредактируйте часть файла package.json так, чтобы она выглядела, как моя.

Итоги: аутентификация — задача непростая

Скорее всего авторы учебных руководств будут защищать себя со словами: «Это лишь объяснение основ! Уверены, никто не будет использовать этого в продакшне!». Однако, я не могу не указать на то, что эти слова не соответствуют действительности. Это особенно справедливо, если к учебным руководствам прилагается код. Люди верят словам авторов руководств, у которых гораздо больше опыта, чем у тех, кто руководства читает.

Если вы — начинающий разработчик — не доверяйте учебным руководствам. Копипастинг кода из таких материалов, наверняка, приведёт вас, вашу компанию, и ваших клиентов, к проблемам в сфере Node.js-аутентификации. Если вам действительно нужны надёжные, готовые к использованию в продакшне, всеобъемлющие библиотеки для аутентификации, взгляните на что-то, чем вам удобно будет пользоваться, на что-то, что обладает большей стабильностью и лучше испытано временем. Например — на связку Rails/Devise.

Экосистема Node.js, несмотря на свою доступность, всё ещё таит множество опасностей для JS-разработчиков, которым нужно срочно написать веб-приложение для решения реальных задач. Если ваш опыт ограничивается фронтендом, и ничего кроме JavaScript вы не знаете, лично я уверен в том, что легче взять Ruby и встать на плечи гигантов, вместо того, чтобы быстро научиться тому, как не отстрелить себе ногу, программируя подобные решения с нуля для Node.

Если вы — автор учебного руководства, пожалуйста, обновите его, в особенности это касается шаблонного кода. Этот код попадёт в продакшн.

Build your app frontend in vanilla javascript

Now that you have a core understanding of the necessary DOM APIs, you can move on to building the app. Copy and paste the following code into public/index.html. This code will get your app to look like the screenshot at the beginning of the article.

Render stored meals

When the app first loads, it should initially render all stored meals. As I discussed above, using DocumentFragments to batch add DOM elements is the way to go.

Render lists

It’s common for web apps to render items inside a loop. Since you will be doing this in your app for rendering individual meals, I wanted to step you through a few ways you can accomplish this and showcase some hidden drawbacks to each approach.

Example 1: Concat each list element to the parent’s innerHTML property:

Although this is the easiest, it’s also pretty bad. For every loop iteration, you delete the entire parent element from the DOM and re-insert it. This might not be an issue with ten elements, but it will cause the browser to slow down to a crawl trying to render longer lists.

A better way would be to append new elements to the parent element so the browser doesn’t have to reprocess the entire parent element on each iteration.

Example 2: Use DOM API to create a new element and append it to the parent:

Using appendChild() cuts down significantly on the processing. But, there is one minor drawback to this approach when rendering larger lists: reflows. Reflow is the technical term of the web browser process that computes the page’s layout. For every loop iteration, the browser has to recalculate the position of each appended element.

A better way would be to batch all the new items into one operation, so the reflow only occurs once.

Example 3: Batch insert nodes using DocumentFragment:

We can batch our appends by creating a DocumentFragment which is calculated and tracked outside the current DOM. Since DocumentFragment works outside the current DOM, we can build the list of items then insert them into the DOM using one operation. This simple design change can significantly impact the speed at which the browser can render lists.

Using this optimal approach, your public/main.js file should now look like this.

Refresh your browser, and you should see a list of meals.

Заключение

Теперь вы знаете, как реализовать возможность  регистрации в Node-приложениях. Вы поняли важность проверки пользовательских данных и процесс ее осуществления при помощи Joi. Также вы использовали модуль bcryptjs для подсаливания и хеширования вашего пароля.

Далее вы увидите, как реализовать возможность входа в систему для зарегистрированных пользователей. Надеюсь, вам понравилось!

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

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