Перейти к основному содержимому

Усиленная аутентификация Java с помощью веб-токенов JSON (JWT)

· 27 мин. чтения

Готовитесь к созданию безопасной аутентификации в своем Java-приложении или боретесь с ней? Не уверены в преимуществах использования токенов (и, в частности, веб-токенов JSON) или в том, как их следует развертывать? Я рад ответить на эти и другие вопросы для вас в этом уроке!

Прежде чем мы углубимся в веб-токены JSON ( JWT ) и библиотеку JJWT (созданную техническим директором Stormpath Лесом Хэзлвудом и поддерживаемую сообществом участников ), давайте рассмотрим некоторые основы.

1. Аутентификация против аутентификации по токену

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

Аутентификация с помощью токенов была разработана для решения проблем, с которыми идентификаторы сеансов на стороне сервера не справились и не могли справиться. Как и при традиционной аутентификации, пользователи предоставляют проверяемые учетные данные, но теперь им выдается набор токенов вместо идентификатора сеанса. Исходными учетными данными могут быть стандартная пара имени пользователя и пароля, ключи API или даже токены из другого сервиса. (Примером этого является функция аутентификации ключа API Stormpath.)

1.1. Почему жетоны?

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

Токены предлагают широкий спектр приложений, в том числе: схемы защиты от подделки межсайтовых запросов ( CSRF ), взаимодействия OAuth 2.0 , идентификаторы сеансов и (в файлах cookie) в качестве представлений аутентификации. В большинстве случаев стандарты не определяют конкретный формат токенов. Вот пример типичного токена Spring Security CSRF в HTML-форме:

<input name="_csrf" type="hidden" 
value="f3f42ea9-3104-4d13-84c0-7bcb68202f16"/>

Если вы попытаетесь опубликовать эту форму без правильного токена CSRF, вы получите ответ об ошибке, и в этом полезность токенов. Приведенный выше пример является «тупым» токеном. Это означает, что из самого токена нельзя извлечь никакого внутреннего смысла. Это также то, где JWT имеют большое значение.

2. Что входит в JWT?

JWT (произносится как «jots») — это URL-безопасные, закодированные, криптографически подписанные (иногда зашифрованные) строки, которые можно использовать в качестве токенов в различных приложениях. Вот пример использования JWT в качестве токена CSRF:

<input name="_csrf" type="hidden" 
value="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJlNjc4ZjIzMzQ3ZTM0MTBkYjdlNjg3Njc4MjNiMmQ3MCIsImlhdCI6MTQ2NjYzMzMxNywibmJmIjoxNDY2NjMzMzE3LCJleHAiOjE0NjY2MzY5MTd9.rgx_o8VQGuDa2AqCHSgVOD5G68Ld_YYM7N7THmvLIKc"/>

В этом случае вы можете видеть, что токен намного длиннее, чем в нашем предыдущем примере. Как мы видели ранее, если форма отправлена без токена, вы получите ответ об ошибке.

Итак, почему JWT?

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

Давайте посмотрим на анатомию JWT, чтобы лучше понять, как мы выжимаем из него все это добро. Возможно, вы заметили, что есть три отдельных раздела, разделенных точками (

.

):

   | Заголовок    | эйJhbGciOiJIUzI1NiJ9   | 
| Полезная нагрузка | eyJqdGkiOiJlNjc4ZjIzMzQ3ZTM0MTBkYjdlNjg3Njc4MjNiMmQ3MCIsImlhdC

I6MTQ2NjYzMzMxNywibmJmIjoxNDY2NjMzMzE3LCJleHAiOjE0NjY2MzY5MTd9 | | Подпись | rgx_o8VQGuDa2AqCHSgVOD5G68Ld_YYM7N7THmvLIKc |

Каждый раздел имеет URL-кодировку base64 . Это гарантирует, что его можно безопасно использовать в URL-адресе (подробнее об этом позже). Рассмотрим подробнее каждый раздел в отдельности.

2.1. Заголовок

Если вы используете base64 для декодирования заголовка, вы получите следующую строку JSON:

{"alg":"HS256"}

Это показывает, что JWT был подписан с помощью HMAC с использованием SHA-256 .

2.2. Полезная нагрузка

Если вы декодируете полезную нагрузку, вы получите следующую строку JSON (отформатированную для ясности):

{
"jti": "e678f23347e3410db7e68767823b2d70",
"iat": 1466633317,
"nbf": 1466633317,
"exp": 1466636917
}

Как видите, в полезной нагрузке есть несколько ключей со значениями. Эти ключи называются «утверждениями», и в спецификации JWT семь из них указаны как «зарегистрированные» утверждения. Они есть:

   | исс    | Эмитент   | 
| суб | Предмет |
| ауд | Аудитория |
| опыт | Срок действия |
| нбф | Не раньше, чем |
| иат | Выпущено в |
| йти | JWT-идентификатор |

При создании JWT вы можете вводить любые пользовательские утверждения по своему желанию. В приведенном выше списке просто представлены утверждения, зарезервированные как в используемом ключе, так и в ожидаемом типе. Наш CSRF имеет идентификатор JWT, время «Выпущено в», время «Не раньше» и время истечения срока действия. Время истечения ровно на одну минуту позже выданного времени.

2.3. Подпись

Наконец, раздел подписи создается путем объединения заголовка и полезной нагрузки (с . между ними) и прохождения их через указанный алгоритм (в данном случае HMAC с использованием SHA-256) вместе с известным секретом. Обратите внимание, что секрет всегда представляет собой массив байтов и должен иметь длину, подходящую для используемого алгоритма. Ниже я использую случайную строку в кодировке base64 (для удобочитаемости), которая преобразуется в массив байтов.

В псевдокоде это выглядит так:

computeHMACSHA256(
header + "." + payload,
base64DecodeToByteArray("4pE8z3PBoHjnV1AhvGk+e8h2p+ShZpOnpr8cwHmMh1w=")
)

Если вы знаете секрет, вы можете сгенерировать подпись самостоятельно и сравнить свой результат с разделом подписи JWT, чтобы убедиться, что он не был подделан. Технически JWT с криптографической подписью называется JWS . JWT также могут быть зашифрованы и тогда будут называться JWE . (На практике термин JWT используется для описания JWE и JWS.)

Это возвращает нас к преимуществам использования JWT в качестве токена CSRF. Мы можем проверить подпись и использовать информацию, закодированную в JWT, чтобы подтвердить ее достоверность. Таким образом, не только строковое представление JWT должно соответствовать тому, что хранится на стороне сервера, мы можем гарантировать, что срок его действия не истек, просто проверив утверждение exp . Это избавляет сервер от необходимости поддерживать дополнительное состояние.

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

3. Настройте учебник JJWT

JJWT ( https://github.com/jwtk/jjwt ) — это библиотека Java, обеспечивающая сквозное создание и проверку веб-токена JSON. Навсегда бесплатный и с открытым исходным кодом (лицензия Apache, версия 2.0), он был разработан с ориентированным на сборщика интерфейсом, скрывающим большую часть его сложности.

Основные операции при использовании JJWT включают создание и анализ JWT. Далее мы рассмотрим эти операции, затем перейдем к некоторым расширенным функциям JJWT и, наконец, мы увидим JWT в действии в качестве токенов CSRF в приложении Spring Security, Spring Boot.

Код, продемонстрированный в следующих разделах, можно найти здесь . Примечание. В проекте с самого начала используется Spring Boot, так как с ним легко взаимодействовать с API, который он предоставляет.

Одна из замечательных особенностей Spring Boot — простота создания и запуска приложения . Чтобы запустить приложение JJWT Fun, просто сделайте следующее:

mvn clean spring-boot:run

В этом примере приложения представлено десять конечных точек (я использую httpie для взаимодействия с приложением. Его можно найти здесь ).

http localhost:8080
Available commands (assumes httpie - https://github.com/jkbrzt/httpie):

http http://localhost:8080/
This usage message

http http://localhost:8080/static-builder
build JWT from hardcoded claims

http POST http://localhost:8080/dynamic-builder-general claim-1=value-1 ... [claim-n=value-n]
build JWT from passed in claims (using general claims map)

http POST http://localhost:8080/dynamic-builder-specific claim-1=value-1 ... [claim-n=value-n]
build JWT from passed in claims (using specific claims methods)

http POST http://localhost:8080/dynamic-builder-compress claim-1=value-1 ... [claim-n=value-n]
build DEFLATE compressed JWT from passed in claims

http http://localhost:8080/parser?jwt=<jwt>
Parse passed in JWT

http http://localhost:8080/parser-enforce?jwt=<jwt>
Parse passed in JWT enforcing the 'iss' registered claim and the 'hasMotorcycle' custom claim

http http://localhost:8080/get-secrets
Show the signing keys currently in use.

http http://localhost:8080/refresh-secrets
Generate new signing keys and show them.

http POST http://localhost:8080/set-secrets
HS256=base64-encoded-value HS384=base64-encoded-value HS512=base64-encoded-value
Explicitly set secrets to use in the application.

В следующих разделах мы рассмотрим каждую из этих конечных точек и код JJWT, содержащийся в обработчиках.

4. Создание JWT с JJWT

Из-за гибкого интерфейса JJWT создание JWT в основном состоит из трех шагов:

  1. Определение внутренних утверждений токена, таких как эмитент, тема, срок действия и идентификатор.
  2. Криптографическая подпись JWT (что делает его JWS).
  3. Сжатие JWT в строку, безопасную для URL, в соответствии с правилами компактной сериализации JWT .

Окончательный JWT будет состоящей из трех частей строкой в кодировке base64, подписанной с помощью указанного алгоритма подписи и с использованием предоставленного ключа. После этого токен готов к передаче другой стороне.

Вот пример JJWT в действии:

String jws = Jwts.builder()
.setIssuer("Stormpath")
.setSubject("msilverman")
.claim("name", "Micah Silverman")
.claim("scope", "admins")
// Fri Jun 24 2016 15:33:42 GMT-0400 (EDT)
.setIssuedAt(Date.from(Instant.ofEpochSecond(1466796822L)))
// Sat Jun 24 2116 15:33:42 GMT-0400 (EDT)
.setExpiration(Date.from(Instant.ofEpochSecond(4622470422L)))
.signWith(
SignatureAlgorithm.HS256,
TextCodec.BASE64.decode("Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E=")
)
.compact();

Это очень похоже на код в методе StaticJWTController.fixedBuilder проекта кода.

На этом этапе стоит поговорить о нескольких антишаблонах, связанных с JWT и подписью. Если вы когда-либо видели примеры JWT раньше, вы, вероятно, сталкивались с одним из этих сценариев антишаблона подписи:

.signWith(
SignatureAlgorithm.HS256,
"secret".getBytes("UTF-8")
)
.signWith(
SignatureAlgorithm.HS256,
"Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E=".getBytes("UTF-8")
)
.signWith(
SignatureAlgorithm.HS512,
TextCodec.BASE64.decode("Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E=")
)

Любой из алгоритмов подписи типа HS принимает массив байтов. Людям удобно читать, чтобы взять строку и преобразовать ее в массив байтов.

Антипаттерн 1 выше демонстрирует это. Это проблематично, потому что секрет ослабляется из-за того, что он такой короткий, и это не массив байтов в его исходной форме. Итак, чтобы сохранить читабельность, мы можем закодировать массив байтов в base64.

Однако анти-шаблон 2 выше берет строку в кодировке base64 и преобразует ее непосредственно в массив байтов. Что нужно сделать, так это декодировать строку base64 обратно в исходный массив байтов.

Номер 3 выше демонстрирует это. Итак, почему это тоже анти-шаблон? В данном случае это тонкая причина. Обратите внимание, что алгоритм подписи — HS512. Массив байтов не является максимальной длиной, которую может поддерживать HS512 , что делает его более слабым секретом, чем это возможно для этого алгоритма.

Пример кода включает класс SecretService , который обеспечивает использование секретов надлежащей надежности для данного алгоритма. Во время запуска приложения для каждого из алгоритмов HS создается новый набор секретов. Существуют конечные точки для обновления секретов, а также для явной установки секретов.

Если у вас есть проект, работающий, как описано выше, выполните следующее, чтобы приведенные ниже примеры JWT соответствовали ответам вашего проекта.

http POST localhost:8080/set-secrets \
HS256="Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E=" \
HS384="VW96zL+tYlrJLNCQ0j6QPTp+d1q75n/Wa8LVvpWyG8pPZOP6AA5X7XOIlI90sDwx" \
HS512="cd+Pr1js+w2qfT2BoCD+tPcYp9LbjpmhSMEJqUob1mcxZ7+Wmik4AYdjX+DlDjmE4yporzQ9tm7v3z/j+QbdYg=="

Теперь вы можете перейти к конечной точке /static-builder :

http http://localhost:8080/static-builder

Это создает JWT, который выглядит следующим образом:

eyJhbGciOiJIUzI1NiJ9.
eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIiwibmFtZSI6Ik1pY2FoIFNpbHZlcm1hbiIsInNjb3BlIjoiYWRtaW5zIiwiaWF0IjoxNDY2Nzk2ODIyLCJleHAiOjQ2MjI0NzA0MjJ9.
kP0i_RvTAmI8mgpIkDFhRX3XthSdP-eqqFKGcU92ZIQ

Теперь нажмите:

http http://localhost:8080/parser?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIiwibmFtZSI6Ik1pY2FoIFNpbHZlcm1hbiIsInNjb3BlIjoiYWRtaW5zIiwiaWF0IjoxNDY2Nzk2ODIyLCJleHAiOjQ2MjI0NzA0MjJ9.kP0i_RvTAmI8mgpIkDFhRX3XthSdP-eqqFKGcU92ZIQ

В ответе есть все утверждения, которые мы включили при создании JWT.

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
...
{
"jws": {
"body": {
"exp": 4622470422,
"iat": 1466796822,
"iss": "Stormpath",
"name": "Micah Silverman",
"scope": "admins",
"sub": "msilverman"
},
"header": {
"alg": "HS256"
},
"signature": "kP0i_RvTAmI8mgpIkDFhRX3XthSdP-eqqFKGcU92ZIQ"
},
"status": "SUCCESS"
}

Это операция синтаксического анализа, которой мы займемся в следующем разделе.

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

http -v POST localhost:8080/dynamic-builder-general iss=Stormpath sub=msilverman hasMotorcycle:=true

Примечание . Существует небольшая разница между утверждением hasMotorcycle и другими утверждениями. httpie предполагает, что параметры JSON по умолчанию являются строками. Чтобы отправить необработанный JSON с помощью httpie, вы используете форму := , а не = . Без этого он отправил бы «hasMotorcycle»: «true» , а это не то, что нам нужно.

Вот результат:

POST /dynamic-builder-general HTTP/1.1
Accept: application/json
...
{
"hasMotorcycle": true,
"iss": "Stormpath",
"sub": "msilverman"
}

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
...
{
"jwt":
"eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIiwiaGFzTW90b3JjeWNsZSI6dHJ1ZX0.OnyDs-zoL3-rw1GaSl_KzZzHK9GoiNocu-YwZ_nQNZU",
"status": "SUCCESS"
}

Давайте посмотрим на код, поддерживающий эту конечную точку:

@RequestMapping(value = "/dynamic-builder-general", method = POST)
public JwtResponse dynamicBuilderGeneric(@RequestBody Map<String, Object> claims)
throws UnsupportedEncodingException {
String jws = Jwts.builder()
.setClaims(claims)
.signWith(
SignatureAlgorithm.HS256,
secretService.getHS256SecretBytes()
)
.compact();
return new JwtResponse(jws);
}

Строка 2 гарантирует, что входящий JSON автоматически преобразуется в Java Map<String, Object>, что очень удобно для JJWT, поскольку метод в строке 5 просто берет эту карту и сразу устанавливает все утверждения.

Каким бы кратким ни был этот код, нам нужно что-то более конкретное, чтобы убедиться, что передаваемые утверждения действительны. Использование метода .setClaims(Map<String, Object> Claims) удобно, когда вы уже знаете, что утверждения, представленные на карте, действительны. Именно здесь безопасность типов Java проявляется в библиотеке JJWT.

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

Давайте перейдем к другой конечной точке в нашем примере и посмотрим, что произойдет:

http -v POST localhost:8080/dynamic-builder-specific iss=Stormpath sub:=5 hasMotorcycle:=true

Обратите внимание, что мы передали целое число 5 для утверждения «sub». Вот результат:

POST /dynamic-builder-specific HTTP/1.1
Accept: application/json
...
{
"hasMotorcycle": true,
"iss": "Stormpath",
"sub": 5
}

HTTP/1.1 400 Bad Request
Connection: close
Content-Type: application/json;charset=UTF-8
...
{
"exceptionType": "java.lang.ClassCastException",
"message": "java.lang.Integer cannot be cast to java.lang.String",
"status": "ERROR"
}

Теперь мы получаем ответ об ошибке, потому что код обеспечивает тип зарегистрированных утверждений. В этом случае sub должен быть строкой. Вот код, поддерживающий эту конечную точку:

@RequestMapping(value = "/dynamic-builder-specific", method = POST)
public JwtResponse dynamicBuilderSpecific(@RequestBody Map<String, Object> claims)
throws UnsupportedEncodingException {
JwtBuilder builder = Jwts.builder();

claims.forEach((key, value) -> {
switch (key) {
case "iss":
builder.setIssuer((String) value);
break;
case "sub":
builder.setSubject((String) value);
break;
case "aud":
builder.setAudience((String) value);
break;
case "exp":
builder.setExpiration(Date.from(
Instant.ofEpochSecond(Long.parseLong(value.toString()))
));
break;
case "nbf":
builder.setNotBefore(Date.from(
Instant.ofEpochSecond(Long.parseLong(value.toString()))
));
break;
case "iat":
builder.setIssuedAt(Date.from(
Instant.ofEpochSecond(Long.parseLong(value.toString()))
));
break;
case "jti":
builder.setId((String) value);
break;
default:
builder.claim(key, value);
}
});

builder.signWith(SignatureAlgorithm.HS256, secretService.getHS256SecretBytes());

return new JwtResponse(builder.compact());
}

Как и прежде, метод принимает Map<String, Object> утверждений в качестве своего параметра. Однако на этот раз мы вызываем конкретный метод для каждого из зарегистрированных утверждений, который обеспечивает тип.

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

private void ensureType(String registeredClaim, Object value, Class expectedType) {
boolean isCorrectType =
expectedType.isInstance(value) ||
expectedType == Long.class && value instanceof Integer;

if (!isCorrectType) {
String msg = "Expected type: " + expectedType.getCanonicalName() +
" for registered claim: '" + registeredClaim + "', but got value: " +
value + " of type: " + value.getClass().getCanonicalName();
throw new JwtException(msg);
}
}

Строка 3 проверяет, что переданное значение имеет ожидаемый тип. Если нет, генерируется JwtException с конкретной ошибкой. Давайте посмотрим на это в действии, выполнив тот же вызов, что и ранее:

http -v POST localhost:8080/dynamic-builder-specific iss=Stormpath sub:=5 hasMotorcycle:=true
POST /dynamic-builder-specific HTTP/1.1
Accept: application/json
...
User-Agent: HTTPie/0.9.3

{
"hasMotorcycle": true,
"iss": "Stormpath",
"sub": 5
}

HTTP/1.1 400 Bad Request
Connection: close
Content-Type: application/json;charset=UTF-8
...
{
"exceptionType": "io.jsonwebtoken.JwtException",
"message":
"Expected type: java.lang.String for registered claim: 'sub', but got value: 5 of type: java.lang.Integer",
"status": "ERROR"
}

Теперь у нас есть очень конкретное сообщение об ошибке, говорящее нам, что подзаявка ошибочна.

Вернемся к этой ошибке в нашем коде. Проблема не имеет ничего общего с библиотекой JJWT. Проблема в том, что встроенный в Spring Boot преобразователь объектов JSON в Java слишком умен для нашего же блага.

Если есть метод, который принимает объект Java, сопоставитель JSON автоматически преобразует переданное число, меньшее или равное 2 147 483 647, в целое число Java . Точно так же он автоматически преобразует переданное число, превышающее 2 147 483 647, в Java Long . Для утверждений iat , nbf и exp JWT мы хотим, чтобы наш тест sureType прошел, независимо от того, является ли сопоставленный объект целым или длинным. Вот почему у нас есть дополнительное предложение для определения того, является ли переданное значение правильным типом:

boolean isCorrectType =
expectedType.isInstance(value) ||
expectedType == Long.class && value instanceof Integer;

Если мы ожидаем Long, но значение является экземпляром Integer, мы все равно говорим, что это правильный тип. Теперь, когда мы понимаем, что происходит с этой проверкой, мы можем интегрировать ее в наш метод dynamicBuilderSpecific :

@RequestMapping(value = "/dynamic-builder-specific", method = POST)
public JwtResponse dynamicBuilderSpecific(@RequestBody Map<String, Object> claims)
throws UnsupportedEncodingException {
JwtBuilder builder = Jwts.builder();

claims.forEach((key, value) -> {
switch (key) {
case "iss":
ensureType(key, value, String.class);
builder.setIssuer((String) value);
break;
case "sub":
ensureType(key, value, String.class);
builder.setSubject((String) value);
break;
case "aud":
ensureType(key, value, String.class);
builder.setAudience((String) value);
break;
case "exp":
ensureType(key, value, Long.class);
builder.setExpiration(Date.from(
Instant.ofEpochSecond(Long.parseLong(value.toString()))
));
break;
case "nbf":
ensureType(key, value, Long.class);
builder.setNotBefore(Date.from(
Instant.ofEpochSecond(Long.parseLong(value.toString()))
));
break;
case "iat":
ensureType(key, value, Long.class);
builder.setIssuedAt(Date.from(
Instant.ofEpochSecond(Long.parseLong(value.toString()))
));
break;
case "jti":
ensureType(key, value, String.class);
builder.setId((String) value);
break;
default:
builder.claim(key, value);
}
});

builder.signWith(SignatureAlgorithm.HS256, secretService.getHS256SecretBytes());

return new JwtResponse(builder.compact());
}

Примечание . Во всех примерах кода в этом разделе JWT подписаны с помощью HMAC с использованием алгоритма SHA-256. Это сделано для того, чтобы примеры были простыми. Библиотека JJWT поддерживает 12 различных алгоритмов подписи, которые вы можете использовать в своем собственном коде.

5. Разбор JWT с помощью JJWT

Ранее мы видели, что в нашем примере кода есть конечная точка для синтаксического анализа JWT. Достижение этой конечной точки:

http http://localhost:8080/parser?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIiwibmFtZSI6Ik1pY2FoIFNpbHZlcm1hbiIsInNjb3BlIjoiYWRtaW5zIiwiaWF0IjoxNDY2Nzk2ODIyLCJleHAiOjQ2MjI0NzA0MjJ9.kP0i_RvTAmI8mgpIkDFhRX3XthSdP-eqqFKGcU92ZIQ

производит этот ответ:

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
...
{
"claims": {
"body": {
"exp": 4622470422,
"iat": 1466796822,
"iss": "Stormpath",
"name": "Micah Silverman",
"scope": "admins",
"sub": "msilverman"
},
"header": {
"alg": "HS256"
},
"signature": "kP0i_RvTAmI8mgpIkDFhRX3XthSdP-eqqFKGcU92ZIQ"
},
"status": "SUCCESS"
}

Метод парсера класса StaticJWTController выглядит следующим образом:

@RequestMapping(value = "/parser", method = GET)
public JwtResponse parser(@RequestParam String jwt) throws UnsupportedEncodingException {
Jws<Claims> jws = Jwts.parser()
.setSigningKeyResolver(secretService.getSigningKeyResolver())
.parseClaimsJws(jwt);

return new JwtResponse(jws);
}

Строка 4 указывает, что мы ожидаем, что входящая строка будет подписанным JWT (JWS). И мы используем тот же секрет, который использовался для подписи JWT при его разборе. Строка 5 анализирует утверждения от JWT. Внутри он проверяет подпись и выдает исключение, если подпись недействительна.

Обратите внимание, что в этом случае мы передаем SigningKeyResolver , а не сам ключ. Это один из самых мощных аспектов JJWT. Заголовок JWT указывает алгоритм, используемый для его подписи. Однако нам нужно проверить JWT, прежде чем доверять ему. Казалось бы, ловушка 22. Посмотрим на метод SecretService.getSigningKeyResolver :

private SigningKeyResolver signingKeyResolver = new SigningKeyResolverAdapter() {
@Override
public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) {
return TextCodec.BASE64.decode(secrets.get(header.getAlgorithm()));
}
};

Используя доступ к JwsHeader , я могу проверить алгоритм и вернуть правильный массив байтов для секрета, который использовался для подписи JWT. Теперь JJWT проверит, что JWT не был подделан с использованием этого массива байтов в качестве ключа.

Если я удалю последний символ, переданный в JWT (который является частью подписи), это ответ:

HTTP/1.1 400 Bad Request
Connection: close
Content-Type: application/json;charset=UTF-8
Date: Mon, 27 Jun 2016 13:19:08 GMT
Server: Apache-Coyote/1.1
Transfer-Encoding: chunked

{
"exceptionType": "io.jsonwebtoken.SignatureException",
"message":
"JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.",
"status": "ERROR"
}

6. JWT на практике: токены Spring Security CSRF

Хотя в центре внимания этого поста не Spring Security, мы собираемся немного углубиться в него, чтобы продемонстрировать реальное использование библиотеки JJWT.

Подделка межсайтовых запросов — это уязвимость системы безопасности, при которой вредоносный веб-сайт обманом заставляет вас отправлять запросы на веб-сайт, с которым вы установили доверие. Одним из распространенных способов решения этой проблемы является реализация шаблона токена синхронизатора . Этот подход вставляет токен в веб-форму, и сервер приложений проверяет входящий токен в своем репозитории, чтобы подтвердить его правильность. Если токен отсутствует или недействителен, сервер ответит ошибкой.

Spring Security имеет встроенный шаблон токена синхронизатора. Еще лучше, если вы используете шаблоны Spring Boot и Thymeleaf , токен синхронизатора вставляется автоматически.

По умолчанию токен, который использует Spring Security, является «тупым» токеном. Это просто набор букв и цифр. Этот подход просто прекрасен, и он работает. В этом разделе мы улучшаем базовую функциональность, используя JWT в качестве токена. В дополнение к проверке того, что отправленный токен является ожидаемым, мы проверяем JWT, чтобы дополнительно доказать, что токен не был подделан, и убедиться, что срок его действия не истек.

Для начала мы собираемся настроить Spring Security с помощью конфигурации Java. По умолчанию для всех путей требуется аутентификация, а для всех конечных точек POST требуются токены CSRF. Мы собираемся немного ослабить это, чтобы то, что мы создали до сих пор, все еще работало.

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

private String[] ignoreCsrfAntMatchers = {
"/dynamic-builder-compress",
"/dynamic-builder-general",
"/dynamic-builder-specific",
"/set-secrets"
};

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.ignoringAntMatchers(ignoreCsrfAntMatchers)
.and().authorizeRequests()
.antMatchers("/**")
.permitAll();
}
}

Мы делаем две вещи здесь. Во-первых, мы говорим, что токены CSRF не требуются при отправке на наши конечные точки REST API (строка 15). Во-вторых, мы говорим, что доступ без аутентификации должен быть разрешен для всех путей (строки 17-18).

Давайте подтвердим, что Spring Security работает так, как мы ожидаем. Запустите приложение и нажмите этот URL в своем браузере:

http://localhost:8080/jwt-csrf-form

Вот шаблон Thymeleaf для этого представления:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<!--/*/ <th:block th:include="fragments/head :: head"/> /*/-->
</head>
<body>
<div class="container-fluid">
<div class="row">
<div class="box col-md-6 col-md-offset-3">
<p/>
<form method="post" th:action="@{/jwt-csrf-form}">
<input type="submit" class="btn btn-primary" value="Click Me!"/>
</form>
</div>
</div>
</div>
</body>
</html>

Это очень простая форма, которая будет отправлять POST в ту же конечную точку при отправке. Обратите внимание, что в форме нет явной ссылки на токены CSRF. Если вы просмотрите источник, вы увидите что-то вроде:

<input type="hidden" name="_csrf" value="5f375db2-4f40-4e72-9907-a290507cb25e" />

Это все подтверждение того, что вам нужно знать, что Spring Security работает и что шаблоны Thymeleaf автоматически вставляют токен CSRF.

Чтобы сделать значение JWT, мы включим пользовательский CsrfTokenRepository . Вот как меняется наша конфигурация Spring Security:

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
CsrfTokenRepository jwtCsrfTokenRepository;

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.csrfTokenRepository(jwtCsrfTokenRepository)
.ignoringAntMatchers(ignoreCsrfAntMatchers)
.and().authorizeRequests()
.antMatchers("/**")
.permitAll();
}
}

Чтобы подключить это, нам нужна конфигурация, которая предоставляет bean-компонент, возвращающий пользовательский репозиторий токенов. Вот конфигурация:

@Configuration
public class CSRFConfig {

@Autowired
SecretService secretService;

@Bean
@ConditionalOnMissingBean
public CsrfTokenRepository jwtCsrfTokenRepository() {
return new JWTCsrfTokenRepository(secretService.getHS256SecretBytes());
}
}

И вот наш пользовательский репозиторий (важные биты):

public class JWTCsrfTokenRepository implements CsrfTokenRepository {

private static final Logger log = LoggerFactory.getLogger(JWTCsrfTokenRepository.class);
private byte[] secret;

public JWTCsrfTokenRepository(byte[] secret) {
this.secret = secret;
}

@Override
public CsrfToken generateToken(HttpServletRequest request) {
String id = UUID.randomUUID().toString().replace("-", "");

Date now = new Date();
Date exp = new Date(System.currentTimeMillis() + (1000*30)); // 30 seconds

String token;
try {
token = Jwts.builder()
.setId(id)
.setIssuedAt(now)
.setNotBefore(now)
.setExpiration(exp)
.signWith(SignatureAlgorithm.HS256, secret)
.compact();
} catch (UnsupportedEncodingException e) {
log.error("Unable to create CSRf JWT: {}", e.getMessage(), e);
token = id;
}

return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", token);
}

@Override
public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
...
}

@Override
public CsrfToken loadToken(HttpServletRequest request) {
...
}
}

Метод generateToken создает JWT, срок действия которого истекает через 30 секунд после его создания. С этой сантехникой мы можем снова запустить приложение и посмотреть на источник /jwt-csrf-form .

Теперь скрытое поле выглядит так:

<input type="hidden" name="_csrf" 
value="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxZjIyMDdiNTVjOWM0MjU0YjZlMjY4MjQwYjIwNzZkMSIsImlhdCI6MTQ2NzA3MDQwMCwibmJmIjoxNDY3MDcwNDAwLCJleHAiOjE0NjcwNzA0MzB9.2kYLO0iMWUheAncXAzm0UdQC1xUC5I6RI_ShJ_74e5o" />

Ура! Теперь наш токен CSRF — это JWT. Это было не так уж сложно.

However, this is only half the puzzle. By default, Spring Security simply saves the CSRF token and confirms that the token submitted in a web form matches the one that's saved. We want to extend the functionality to validate the JWT and make sure it hasn't expired. To do that, we'll add in a filter. Here's what our Spring Security configuration looks like now:

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

...

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterAfter(new JwtCsrfValidatorFilter(), CsrfFilter.class)
.csrf()
.csrfTokenRepository(jwtCsrfTokenRepository)
.ignoringAntMatchers(ignoreCsrfAntMatchers)
.and().authorizeRequests()
.antMatchers("/**")
.permitAll();
}

...
}

On line 9, we've added in a filter and we are placing it in the filter chain after the default CsrfFilter . So, by the time our filter is hit, the JWT token (as a whole) will have already been confirmed to be the correct value saved by Spring Security.

Here's the JwtCsrfValidatorFilter (it's private as it's an inner class of our Spring Security configuration):

private class JwtCsrfValidatorFilter extends OncePerRequestFilter {

@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// NOTE: A real implementation should have a nonce cache so the token cannot be reused
CsrfToken token = (CsrfToken) request.getAttribute("_csrf");

if (
// only care if it's a POST
"POST".equals(request.getMethod()) &&
// ignore if the request path is in our list
Arrays.binarySearch(ignoreCsrfAntMatchers, request.getServletPath()) < 0 &&
// make sure we have a token
token != null
) {
// CsrfFilter already made sure the token matched.
// Here, we'll make sure it's not expired
try {
Jwts.parser()
.setSigningKey(secret.getBytes("UTF-8"))
.parseClaimsJws(token.getToken());
} catch (JwtException e) {
// most likely an ExpiredJwtException, but this will handle any
request.setAttribute("exception", e);
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
RequestDispatcher dispatcher = request.getRequestDispatcher("expired-jwt");
dispatcher.forward(request, response);
}
}

filterChain.doFilter(request, response);
}
}

Take a look at line 23 on. We are parsing the JWT as before. In this case, if an Exception is thrown, the request is forwarded to the expired-jwt template. If the JWT validates, then processing continues as normal.

This closes the loop on overriding the default Spring Security CSRF token behavior with a JWT token repository and validator.

If you fire up the app, browse to /jwt-csrf-form , wait a little more than 30 seconds and click the button, you will see something like this:

./2fcb8a77b0a77eab7bff53c8bea38f6a.png

7. JJWT Extended Features

We'll close out our JJWT journey with a word on some of the features that extend beyond the specification.

7.1. Enforce Claims

As part of the parsing process, JJWT allows you to specify required claims and values those claims should have. This is very handy if there is certain information in your JWTs that must be present in order for you to consider them valid. It avoids a lot of branching logic to manually validate claims. Here's the method that serves the /parser-enforce endpoint of our sample project.

@RequestMapping(value = "/parser-enforce", method = GET)
public JwtResponse parserEnforce(@RequestParam String jwt)
throws UnsupportedEncodingException {
Jws<Claims> jws = Jwts.parser()
.requireIssuer("Stormpath")
.require("hasMotorcycle", true)
.setSigningKeyResolver(secretService.getSigningKeyResolver())
.parseClaimsJws(jwt);

return new JwtResponse(jws);
}

Lines 5 and 6 show you the syntax for registered claims as well as custom claims. In this example, the JWT will be considered invalid if the iss claim is not present or does not have the value: Stormpath. It will also be invalid if the custom hasMotorcycle claim is not present or does not have the value: true.

Let's first create a JWT that follows the happy path:

http -v POST localhost:8080/dynamic-builder-specific \
iss=Stormpath hasMotorcycle:=true sub=msilverman
POST /dynamic-builder-specific HTTP/1.1
Accept: application/json
...
{
"hasMotorcycle": true,
"iss": "Stormpath",
"sub": "msilverman"
}

HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Type: application/json;charset=UTF-8
...
{
"jwt":
"eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJoYXNNb3RvcmN5Y2xlIjp0cnVlLCJzdWIiOiJtc2lsdmVybWFuIn0.qrH-U6TLSVlHkZdYuqPRDtgKNr1RilFYQJtJbcgwhR0",
"status": "SUCCESS"
}

Now, let's validate that JWT:

http -v localhost:8080/parser-enforce?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJoYXNNb3RvcmN5Y2xlIjp0cnVlLCJzdWIiOiJtc2lsdmVybWFuIn0.qrH-U6TLSVlHkZdYuqPRDtgKNr1RilFYQJtJbcgwhR0
GET /parser-enforce?jwt=http 
-v localhost:8080/parser-enforce?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJoYXNNb3RvcmN5Y2xlIjp0cnVlLCJzdWIiOiJtc2lsdmVybWFuIn0.qrH-U6TLSVlHkZdYuqPRDtgKNr1RilFYQJtJbcgwhR0 HTTP/1.1
Accept: */*
...
HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Type: application/json;charset=UTF-8
...
{
"jws": {
"body": {
"hasMotorcycle": true,
"iss": "Stormpath",
"sub": "msilverman"
},
"header": {
"alg": "HS256"
},
"signature": "qrH-U6TLSVlHkZdYuqPRDtgKNr1RilFYQJtJbcgwhR0"
},
"status": "SUCCESS"
}

So far, so good. Now, this time, let's leave the hasMotorcycle out:

http -v POST localhost:8080/dynamic-builder-specific iss=Stormpath sub=msilverman

This time, if we try to validate the JWT:

http -v localhost:8080/parser-enforce?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIn0.YMONlFM1tNgttUYukDRsi9gKIocxdGAOLaJBymaQAWc

we get:

GET /parser-enforce?jwt=http -v localhost:8080/parser-enforce?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIn0.YMONlFM1tNgttUYukDRsi9gKIocxdGAOLaJBymaQAWc HTTP/1.1
Accept: */*
...
HTTP/1.1 400 Bad Request
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Connection: close
Content-Type: application/json;charset=UTF-8
...
{
"exceptionType": "io.jsonwebtoken.MissingClaimException",
"message":
"Expected hasMotorcycle claim to be: true, but was not present in the JWT claims.",
"status": "ERROR"
}

This indicates that our hasMotorcycle claim was expected, but was missing.

Let's do one more example:

http -v POST localhost:8080/dynamic-builder-specific iss=Stormpath hasMotorcycle:=false sub=msilverman

This time, the required claim is present, but it has the wrong value. Let's see the output of:

http -v localhost:8080/parser-enforce?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJoYXNNb3RvcmN5Y2xlIjpmYWxzZSwic3ViIjoibXNpbHZlcm1hbiJ9.8LBq2f0eINB34AzhVEgsln_KDo-IyeM8kc-dTzSCr0c
GET /parser-enforce?jwt=http 
-v localhost:8080/parser-enforce?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJoYXNNb3RvcmN5Y2xlIjpmYWxzZSwic3ViIjoibXNpbHZlcm1hbiJ9.8LBq2f0eINB34AzhVEgsln_KDo-IyeM8kc-dTzSCr0c HTTP/1.1
Accept: */*
...
HTTP/1.1 400 Bad Request
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Connection: close
Content-Type: application/json;charset=UTF-8
...
{
"exceptionType": "io.jsonwebtoken.IncorrectClaimException",
"message": "Expected hasMotorcycle claim to be: true, but was: false.",
"status": "ERROR"
}

This indicates that our hasMotorcycle claim was present, but had a value that was not expected.

MissingClaimException and IncorrectClaimException are your friends when enforcing claims in your JWTs and a feature that only the JJWT library has.

7.2. JWT Compression

If you have a lot of claims on a JWT, it can get big – so big, that it might not fit in a GET url in some browsers.

Let's a make a big JWT:

http -v POST localhost:8080/dynamic-builder-specific \
iss=Stormpath hasMotorcycle:=true sub=msilverman the=quick brown=fox jumped=over lazy=dog \
somewhere=over rainbow=way up=high and=the dreams=you dreamed=of

Here's the JWT that produces:

eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJoYXNNb3RvcmN5Y2xlIjp0cnVlLCJzdWIiOiJtc2lsdmVybWFuIiwidGhlIjoicXVpY2siLCJicm93biI6ImZveCIsImp1bXBlZCI6Im92ZXIiLCJsYXp5IjoiZG9nIiwic29tZXdoZXJlIjoib3ZlciIsInJhaW5ib3ciOiJ3YXkiLCJ1cCI6ImhpZ2giLCJhbmQiOiJ0aGUiLCJkcmVhbXMiOiJ5b3UiLCJkcmVhbWVkIjoib2YifQ.AHNJxSTiDw_bWNXcuh-LtPLvSjJqwDvOOUcmkk7CyZA

That sucker's big! Now, let's hit a slightly different endpoint with the same claims:

http -v POST localhost:8080/dynamic-builder-compress \
iss=Stormpath hasMotorcycle:=true sub=msilverman the=quick brown=fox jumped=over lazy=dog \
somewhere=over rainbow=way up=high and=the dreams=you dreamed=of

This time, we get:

eyJhbGciOiJIUzI1NiIsImNhbGciOiJERUYifQ.eNpEzkESwjAIBdC7sO4JegdXnoC2tIk2oZLEGB3v7s84jjse_AFe5FOikc5ZLRycHQ3kOJ0Untu8C43ZigyUyoRYSH6_iwWOyGWHKd2Kn6_QZFojvOoDupRwyAIq4vDOzwYtugFJg1QnJv-5sY-TVjQqN7gcKJ3f-j8c-6J-baDFhEN_uGn58XtnpfcHAAD__w.3_wc-2skFBbInk0YAQ96yGWwr8r1xVdbHn-uGPTFuFE

62 characters shorter! Here's the code for the method used to generate the JWT:

@RequestMapping(value = "/dynamic-builder-compress", method = POST)
public JwtResponse dynamicBuildercompress(@RequestBody Map<String, Object> claims)
throws UnsupportedEncodingException {
String jws = Jwts.builder()
.setClaims(claims)
.compressWith(CompressionCodecs.DEFLATE)
.signWith(
SignatureAlgorithm.HS256,
secretService.getHS256SecretBytes()
)
.compact();
return new JwtResponse(jws);
}

Notice on line 6 we are specifying a compression algorithm to use. That's all there is to it.

What about parsing compressed JWTs? The JJWT library automatically detects the compression and uses the same algorithm to decompress:

GET /parser?jwt=eyJhbGciOiJIUzI1NiIsImNhbGciOiJERUYifQ.eNpEzkESwjAIBdC7sO4JegdXnoC2tIk2oZLEGB3v7s84jjse_AFe5FOikc5ZLRycHQ3kOJ0Untu8C43ZigyUyoRYSH6_iwWOyGWHKd2Kn6_QZFojvOoDupRwyAIq4vDOzwYtugFJg1QnJv-5sY-TVjQqN7gcKJ3f-j8c-6J-baDFhEN_uGn58XtnpfcHAAD__w.3_wc-2skFBbInk0YAQ96yGWwr8r1xVdbHn-uGPTFuFE HTTP/1.1
Accept: */*
...
HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Type: application/json;charset=UTF-8
...
{
"claims": {
"body": {
"and": "the",
"brown": "fox",
"dreamed": "of",
"dreams": "you",
"hasMotorcycle": true,
"iss": "Stormpath",
"jumped": "over",
"lazy": "dog",
"rainbow": "way",
"somewhere": "over",
"sub": "msilverman",
"the": "quick",
"up": "high"
},
"header": {
"alg": "HS256",
"calg": "DEF"
},
"signature": "3_wc-2skFBbInk0YAQ96yGWwr8r1xVdbHn-uGPTFuFE"
},
"status": "SUCCESS"
}

Notice the calg claim in the header. This was automatically encoded into the JWT and it provides the hint to the parser about what algorithm to use for decompression.

NOTE: The JWE specification does support compression. In an upcoming release of the JJWT library, we will support JWE and compressed JWEs. We will continue to support compression in other types of JWTs, even though it is not specified.

8. Token Tools for Java Devs

While the core focus of this article was not Spring Boot or Spring Security, using those two technologies made it easy to demonstrate all the features discussed in this article. You should be able to build in fire up the server and start playing with the various endpoints we've discussed. Just hit:

http http://localhost:8080

Stormpath is also excited to bring a number of open source developer tools to the Java community. These include:

8.1. JJWT (What We've Been Talking About)

JJWT is an easy to use tool for developers to create and verify JWTs in Java . Like many libraries Stormpath supports, JJWT is completely free and open source (Apache License, Version 2.0), so everyone can see what it does and how it does it. Do not hesitate to report any issues, suggest improvements, and even submit some code!

8.2. jsonwebtoken.io and java.jsonwebtoken.io

jsonwebtoken.io is a developer tool we created to make it easy to decode JWTs. Simply paste an existing JWT into the appropriate field to decode its header, payload, and signature. jsonwebtoken.io is powered by nJWT , the cleanest free and open source (Apache License, Version 2.0) JWT library for Node.js developers. You can also see code generated for a variety of languages at this website. The website itself is open-source and can be found here .

java.jsonwebtoken.io is specifically for the JJWT library. You can alter the headers and payload in the upper right box, see the JWT generated by JJWT in the upper left box, and see a sample of the builder and parser Java code in the lower boxes. The website itself is open source and can be found here .

8.3. JWT Inspector

The new kid on the block, JWT Inspector is an open source Chrome extension that allows developers to inspect and debug JWTs directly in-browser. The JWT Inspector will discover JWTs on your site (in cookies, local/session storage, and headers) and make them easily accessible through your navigation bar and DevTools panel.

9. JWT This Down!

JWTs add some intelligence to ordinary tokens. The ability to cryptographically sign and verify, build in expiration times and encode other information into JWTs sets the stage for truly stateless session management. This has a big impact on the ability to scale applications.

At Stormpath, we use JWTs for OAuth2 tokens, CSRF tokens and assertions between microservices, among other usages.

Once you start using JWTs, you may never go back to the dumb tokens of the past. Have any questions? Hit me up at @afitnerd on twitter.