Решения для JWT и JWE в React Native и Node Backend
Самым сложным было найти метод, который работает как в RN, так и в Node, потому что я не могу просто использовать любую библиотеку Node в RN, так что крипто и Пакеты jwt, которые я устанавливаю в приложении и на сервере, должны работать вместе.
Я передаю все вызовы API через HTTPS, так что часть шифрования в любом случае, вероятно, избыточна, как упомянул Флорент.
Метод № 1 чище, но я предпочитаю метод № 2, потому что он дает мне больше контроля над тем, что зашифровано и как создается JWT. Это позволяет мне проверить JWT до Я пытаюсь расшифровать полезную нагрузку.
Метод # 1
Создать JWE для одновременного шифрования токена и полезной нагрузки.
Плюсы:
- Соответствует принятым стандартам шифрования / безопасности.
- Гораздо меньше кода.
- Не нужно отслеживать секретных паролей, потому что мы используем только пару открытых / закрытых ключей, известных только серверу, и приложение, созданное при входе в систему.
Минусы:
- Я не вижу способа настроить JWT. При создании простого JWT я мог установить явное время истечения 60 с или меньше. Я не вижу способа сделать это с JWE.
- Ошибки, возникающие при расшифровке JWE, не так полезны. Это будет либо работать, либо потерпеть неудачу. При дешифровке JWT вы получаете хорошие сообщения об ошибках, если что-то не работает или недействительно.
Реагируйте собственный код приложения
import {JWK, JWE} from 'react-native-jose';
/**
* Create JWE encrypted web token
*
* @param payload
* @returns {Promise<string>}
*/
async function createJWEToken(payload = {}) {
// This is the Public Key created at login. It is stored in the App.
// I'm hard-coding the key here just for convenience but normally it
// would be kept in a Keychain, a flat file on the mobile device, or
// in React state to refer to before making the API call.
const publicKey = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApl9FLYsLnP10T98mT70e
qdAeHA8qDU5rmY8YFFlcOcy2q1dijpgfop8WyHu1ULufJJXm0PV20/J9BD2HqTAK
DZ+/qTv4glDJjyIlo/PIhehQJqSrdIim4fjuwkax9FOCuFQ9nesv32hZ6rbFjETe
QSxUPjNzsYGOuULWSR3cI8FuV9InlSZQ7q6dEunLPRf/rZujxiAxGzY8zrMehjM5
LNdl7qDEOsc109Yy3HBbOwUdJyyTg/GRPwklLogw9kkldz5+wMvwOT38IlkO2rCr
qJpqqt1KmxdOQNbeGwNzZiGiuYIdiQWjilq5a5K9e75z+Uivx+G3LfTxSAnebPlE
LwIDAQAB
-----END PUBLIC KEY-----`;
try {
const makeKey = pem => JWK.asKey(pem, 'pem');
const key = await makeKey(publicKey);
// This returns the encrypted JWE string
return await JWE.createEncrypt({
zip: true,
format: 'compact',
}, key).update(JSON.stringify(payload)).final();
} catch (err) {
throw new Error(err.message);
}
}
Node Backend
const keygen = require('generate-rsa-keypair');
const {JWK, JWE} = require('node-jose');
/**
* Create private/public keys for JWE encrypt/decrypt
*
* @returns {Promise<object>}
*
*/
async function createKeys() {
// When user logs in, create a standard RSA key-pair.
// The public key is returned to the user when he logs in.
// The private key stays on the server to decrypt the message with each API call.
// Keys are destroyed when the user logs out.
const keys = keygen();
const publicKey = keys.public;
const privateKey = keys.private;
return {
publicKey,
privateKey
};
}
/**
* Decrypt JWE Web Token
*
* @param input
* @returns {Promise<object>}
*/
async function decryptJWEToken(input) {
// This is the Private Key kept on the server. This was
// the key created along with the Public Key after login.
// The public key was sent to the App and the Private Key
// stays on the server.
// I'm hard-coding the key here just for convenience but
// normally it would be held in a database to
// refer during the API call.
const privateKey = `-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEApl9FLYsLnP10T98mT70eqdAeHA8qDU5rmY8YFFlcOcy2q1di
jpgfop8WyHu1ULufJJXm0PV20/J9BD2HqTAKDZ+/qTv4glDJjyIlo/PIhehQJqSr
dIim4fjuwkax9FOCuFQ9nesv32hZ6rbFjETeQSxUPjNzsYGOuULWSR3cI8FuV9In
lSZQ7q6dEunLPRf/rZujxiAxGzY8zrMehjM5LNdl7qDEOsc109Yy3HBbOwUdJyyT
g/GRPwklLogw9kkldz5+wMvwOT38IlkO2rCrqJpqqt1KmxdOQNbeGwNzZiGiuYId
iQWjilq5a5K9e75z+Uivx+G3LfTxSAnebPlELwIDAQABAoIBAQCmJ2FkMYhAmhOO
LRMK8ZntB876QN7DeT0WmAT5VaE4jE0mY1gnhp+Zfn53bKzQ2v/9vsNMjsjEtVjL
YlPY0QRJRPBZqG3wX5RcoUKsMaxip3dckHo3IL5h0YVJeucAVmKnimIbE6W03Xdn
ZG94PdMljYr4r9PsQ7JxLOHrFaoj/c7Dc7rd6M5cNtmcozqZsz6zVtqO1PGaNa4p
5mAj9UHtumIb49e3tHxr//JUwZv2Gqik0RKkjkrnUmFpHX4N+f81RLDnKsY4+wyI
bM5Gwq/2t8suZbwfHNFufytaRnRFjk+P6crPIpcfe05Xc+Y+Wq4yL62VY3wSS13C
EeUZ2FXpAoGBANPtw8De96TXsxdHcbmameWv4uepHUrYKq+7H+pJEGIfJf/1wsJ0
Gc6w2AE69WJVvCtTzP9XZmfiIze2sMR/ynhbUl9wOzakFpEh0+AmJUG+lUHOy4k2
Mdmu6GmeIM9azz6EXyfXuSZ39LHowS0Es1xaWRuu5kta73B5efz/hz2tAoGBAMj4
QR87z14tF6dPG+/OVM/hh9H5laKMaKCbesoXjvcRVkvi7sW8MbfxVlaRCpLbsSOs
cvAkc4oPY+iQt8fJWSJ1nwGJ0g7iuObLJh9w6P5C3udCGLcvqNbmQ9r+edy1IDBr
t7pdrFKiPFvaEEqYl06gVSsPCg041N6bRTJ1nEzLAoGAajSOVDqo6lA6bOEd6gDD
PSr+0E+c4WQhSD3Dibqh3jpz5aj4uFBMmptfNIaicGw8x43QfuoC5O6b7ZC9V0wf
YF+LkU6CLijfMk48iuky5Jao3/jNYW7qXofb6woWsTN2BoN52FKwc8nLs9jL7k6b
wB166Hem636f3cLS0moQEWUCgYABWjJN/IALuS/0j0K33WKSt4jLb+uC2YEGu6Ua
4Qe0P+idwBwtNnP7MeOL15QDovjRLaLkXMpuPmZEtVyXOpKf+bylLQE92ma2Ht3V
zlOzCk4nrjkuWmK/d3MzcQzu4EUkLkVhOqojMDZJw/DiH569B7UrAgHmTuCX0uGn
UkVH+wKBgQCJ+z527LXiV1l9C0wQ6q8lrq7iVE1dqeCY1sOFLmg/NlYooO1t5oYM
bNDYOkFMzHTOeTUwbuEbCO5CEAj4psfcorTQijMVy3gSDJUuf+gKMzVubzzmfQkV
syUSjC+swH6T0SiEFYlU1FTqTGKsOM68huorD/HEX64Bt9mMBFiVyA==
-----END RSA PRIVATE KEY-----`;
try {
const makeKey = pem => JWK.asKey(pem, 'pem');
const key = await makeKey(privateKey);
// This returns the decrypted data
return await JWE.createDecrypt(key).decrypt(input);
} catch (err) {
throw new Error(err.message);
}
}
Метод # 2
Создайте JWT и зашифруйте полезную нагрузку самостоятельно.
Ниже JS показывает, как я создаю JWT и шифрую данные в React Native, а затем проверяю токен и расшифровать данные в бэкэнде Node.
Когда пользователь войдет в систему, я передам обратно секретный ключ для JWT и отдельный секретный ключ для шифрования (при необходимости), который действует только в течение сеанса. После того, как они выйдут из системы, ключи будут уничтожены.
Я также сохраняю время истечения JWT до 60 секунд, чтобы быть в безопасности.
Плюсы:
Вы имеете больший контроль над JWT с точки зрения установки времени истечения.
Минусы:
Может быть медленнее, потому что вам нужно создать JWT и зашифруйте полезную нагрузку отдельными шагами.
React Native App
import jwt from 'react-native-pure-jwt';
import CryptoJS from 'react-native-crypto-js';
/**
* Create JWT Signature from payload for API call
*
* @param payload {object|string} - payload to add to JWT
* @param encrypt {boolean} - encrypt the payload
*
*/
async function createJWT(payload, encrypt = false) {
try {
const signature = await jwt.sign({
// REQUIRED: Payload
// Any data you want to pass in the JWT
// Two options:
// 1. Send the payload without encryption.
// No need to stringify payload. Just send it as-is.
// 2. Send the payload as an encrypted string.
// Use the CryptoJS library with a 'another-secret-key' that the App and the server share.
// Payload needs to be stringified.
data: encrypt ? CryptoJS.AES.encrypt(JSON.stringify(payload), 'another-secret-key').toString() : payload,
// REQUIRED: Expires Time
// Milliseconds since 1970 when the token can no longer be decoded.
// Here, I've set the value to however many seconds I want the token to last.
// The clock starts ticking from the moment this token was created. Afer the time has expired,
// the token can't be decoded by anyone.
exp: new Date().getTime() + 60 * 1000,
// OPTIONAL: Issuer Claim
// String that identifies whoever issued this JWT.
// The JWT is coming from my App so I'll use the App name here.
iss: 'APP ID',
// OPTIONAL: Subject Claim
// String that uniquely identifies the subject of the JWT.
// The I'll probably use the Username or User ID of the person using the App.
sub: 'UNIQUE USER ID',
},
// REQUIRED: Secret Key
// This is the string that is used to encode/decode the JWT.
// Here, I'll probably use something that's unique to the user's account on the App such
// as a password or strong token that's stored in the user's Keychain and that's
// also in a database the server can retrieve.
'my-secret-key',
{
// REQUIRED: Algorithm
// What algorithm is being used to encode the JWT
// The supported algorithms by react-native-pure-jwt are HS256, HS384, HS512.
// The higher the number, the better the security but it will also take longer to encode/decode the token.
// I would prefer to use RS256 or RS512 but those algorithms aren't supported
// by the react-native-pure-jwt package as far as I can tell.
alg: 'HS256',
},
);
return signature;
} catch (err) {
throw new Error(err.message);
}
}
Узел Backend
const jwt = require('jsonwebtoken');
const CryptoJS = require('crypto-js');
/**
* Decode JWT Signature from API call
*
* @param token {string}
* @param decrypt {boolean} - decrypt the payload
*
*/
async function decodeJWT(token, decrypt = false) {
// Use the same options to decode the JWT that were used to encode the JWT.
const options = {
issuer: 'APP ID',
subject: 'UNIQUE USER ID',
};
// Use the same Secret Key that was used to encode the JWT.
// This should be a password or some hidden value that is only
// known by the App and the Server such as the user's password or previously
// agreed upon strong token shared after login and discarded after logout.
const secretJWTKey = 'my-secret-key';
// If decrypting an encrypted payload, you'll also need the encryption secret
// which should ideally not be the same as the secretJWTKey.
// It can also be some hidden value that is only known by the App and the Server such
// an agreed upon strong token shared after login and discarded after logout.
const secretDecryptKey = 'another-secret-key';
return new Promise((resolve, reject) => {
try {
// First check if the token is valid. Otherwise, it will throw an error.
const verified = jwt.verify(token, secretJWTKey, options);
let data = verified.data;
if (decrypt) {
// If the payload is encrypted, unwind it into an object or string.
const bytes = CryptoJS.AES.decrypt(data, secretDecryptKey);
data = JSON.parse(bytes.toString(CryptoJS.enc.Utf8));
}
resolve(data);
} catch (err) {
// If the token is not valid or the decryption failed.
reject(err.message);
}
});
}
Пример
async function test() {
// Create the JWT and encrypt the payload (optional) in React Native
const tokenSignature = await createJWT({
name: 'Marc',
password: 'P@55w0rd',
cc: 'Visa',
cc_num: '4400-0000-0000-0000'
phone: '704-000-0000'
info: {
birthdate: '1970-00-00',
ssn: '000-00-0000',
},
}, true).catch(err => {
console.log('Error creating signature', err);
});
// Use the JWT signature to make the https api call.
// On the server...
// Decode the JWT and decrypt the payload (optional)
const data = await decodeJWT(tokenSignature, true).catch(err => {
console.log('Error retrieving data', err);
});
// Use the data to update the database, etc.
console.log({data});
}