I'm trying to verify a user's email by calling the verifyAttribute method.
I am able to authenticate and get a JWT response - and with that I can do the following:
const { email, phone, accessToken } = verifyUserAttributesDto;
const getUser = await CognitoService.getUser(accessToken);
const attributes = getUser.UserAttributes.reduce(
(acc, { Name, Value }) => ({ ...acc, [Name]: Value }),
{},
);
console.log(attributes);
........
{
sub: '5f04a73b-...',
email_verified: 'false',
phone_number_verified: 'false',
phone_number: '+12222222222',
given_name: 'First',
family_name: 'Last',
email: 'example#email.com'
}
So I know I'm able to access the user.
Later I do:
const cognitoUser = new AmazonCognitoIdentity.CognitoUser({
Username: attributes.email,
Pool: this.userPool,
});
...
CognitoUser {
username: 'example#email.com',
pool: CognitoUserPool {
...
I believe I have an instance of a CognitoUser here, but maybe I don't; maybe that's the problem.
If I do:
return new Promise(function (resolve, reject) {
return cognitoUser.verifyAttribute(attribute, accessToken, {
onSuccess: (success) => {
console.log(success);
resolve(success);
},
onFailure: (err) => {
console.log(err);
reject(err);
},
});
});
The response I get back is:
ERROR [ExceptionsHandler] User is not authenticated
I have verified in the AWS console that the confirmation status for the user is confirmed and the account status is enabled.
If I've got a valid JWT, and am able to "get" the user, why am I getting that error?
It's painfully obvious I'm not sure what I'm doing. I got this working due to:
I wasn't providing the verification code
I was using the wrong identity provider (cognito vs. cognito user)
There are two methods needed to verify:
getUserAttributeVerificationCode
verifyUserAttribute
async getUserAttributeVerificationCode(
attribute: string,
accessTokenDto: AccessTokenDto,
) {
const { accessToken } = accessTokenDto;
const cognito = new AWS.CognitoIdentityServiceProvider();
try {
return await cognito
.getUserAttributeVerificationCode({
AccessToken: accessToken,
AttributeName: attribute,
})
.promise();
} catch (err) {
throw new BadRequestException(err.message);
}
}
async verifyUserAttribute(verifyUserAttributesDto: VerifyUserAttributesDto) {
const { email, phone, accessToken, verificationCode } =
verifyUserAttributesDto;
const cognito = new AWS.CognitoIdentityServiceProvider();
try {
if (email || phone) {
const attribute = email ? 'email' : 'phone';
return await cognito
.verifyUserAttribute({
AccessToken: accessToken,
AttributeName: attribute,
Code: verificationCode,
})
.promise();
}
} catch (err) {
throw new BadRequestException(err.message);
}
}
You'll first need to call getUserAttributeVerificationCode with a valid JWT. You'll then get sent a verification code (email/sms).
With that, you'll call verifyUserAttribute with all of the proper attributes.
Related
I've created an email authentication system, however there appears to be an issue with how I jwt.verify this token.
I believe there's an issue with my : process.env.PASS_SEC, which is just my Mongo.DB password secret. Is this correct?
I can confirm if I do a res.sent(req.params.token), my token comes through fine, for example in this.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjYyZjc0MWU3ZjBkZjZkY2IyZjM0ZDc3ZSIsImlhdCI6MTY2MDM3MTQzMSwiZXhwIjoxNjYwNjMwNjMxfQ.vFtdRzEH2_52Hdhxs84bk7RPdIRDIoZ6Rcd-zZoBhus
As such, I believe it's the SECRET is being passed incorrectly.
My current functioning code is:
router.post("/register", async (req, res, EMAIL_SECRET) => {
const newUser = new User({
fullname: req.body.fullname,
email: req.body.email,
position: req.body.position,
username: req.body.fullname,
password: CryptoJS.AES.encrypt(
req.body.password,
process.env.PASS_SEC
).toString(),
});
const accessToken = jwt.sign(
{
id: newUser._id,
},
process.env.JWT_SEC,
{
expiresIn:"3d"
},
);
const url = `http://localhost:5000/api/auth/confirmation/${accessToken}`;
const mailOptions = {
from: 'nathandrewphone#gmail.com',
to: req.body.email,
subject: 'Confirm Email',
html: `Please click this email to confirm your email: ${url}`
};
transporter.sendMail(mailOptions, function(error, info){
if (error) {
console.log(error);
} else {
console.log('Email sent: ' + info.response);
}
});
try {
const savedUser = await newUser.save();
res.status(201).json(savedUser);
} catch (err) {
res.status(500).json(err);
}
});
Which sends a code fine, however it does not appear to be correct, how would you create an EMAIL_SECRET?
This is how I wish to validate the email.
//User Email Auth Login
//Not yet functioning
router.get('/confirmation/:token', async (req, res) => {
try {
//verify the token with the secret
const { _id: { _id } } = jwt.verify(req.params.token, process.env.PASS_SEC);
await models.User.update({ confirmed: true }, { where: { _id } });
} catch (e) {
res.send('This isnt working');
}
});
However, I cannot get to verify, whats wrong with secret
You signed your token with process.env.JWT_SEC, you should verify it using the same key:
const { _id } = jwt.verify(req.params.token, process.env.JWT_SEC);
Also, you should be able to update your User with findByIdAndUpdate:
await User.findByIdAndUpdate(_id, { confirmed: true });
I am trying to get the user information by resolving the auth. And I have an array of phone numbers and I am passing them to the auth function to get the user and I am able to do it but when the result appears the code doesn't exit. And the user who doesn't have auth but I have phone number, I want to return a dummy object. But I am unable to exist. Here are my attempts at auth code and the phoneNumber array is the array of phone numbers.
const getAuth = async phoneNumber => {
try {
return await auth.getUserByPhoneNumber(phoneNumber);
} catch (error) {
if (error === 'auth/user-not-found') {
console.log('this number is not auth');
}
}
};
const getAuth = async phoneNumber => {
try {
return authGrowthfile.getUserByPhoneNumber(phoneNumber);
} catch (error) {
if (error) {
return {
phoneNumber,
uid: null,
email: '',
displayName: '',
emailVerified: false,
disabled: false,
};
}
}
};
I am calling this function like this:
const phoneNumberArray = ['+918888','+91798299']
const userRecord = await Promise.all(phoneNumberArray.map(getAuth))
You are doing fine, you have done all things right, just a simple typo.
const userRecord = await Promise.all(phoneNumberArray.map(phoneNumber => getAuth(phoneNumber)))
Working on an app, and I want security from the start, so I've created a private/public key pair, and I'm setting up passport-jwt like this: (key is the public part of the keypair)
(passport, key) => {
const opts = {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: key
};
passport.use(
new JwtStrategy(opts, (payload, done) => {
log.info({message: 'verifying the token', payload});
User.findById(payload.id)
.then(user => {
if (user) {
return done(null, {
id: user._id,
name: user.userName,
email: user.emailAddress
});
}
log.info(payload);
return done(null, false);
})
.catch(err => {
log.error(err)
return done('Unauthorized', false, payload);
});
})
);
};
and when the user logs in, I'm signing the token with the private key like this:
router.post('/login', (req, res) => {
const email = req.body.email;
const password = req.body.password;
User.findOne({ email }).then(user => {
if (!user) {
errors.email = 'No Account Found';
return res.status(404).json(errors);
}
bcrypt.compare(password, user.password).then(isMatch => {
if (isMatch) {
const payload = {
id: user._id,
name: user.userName,
email: user.emailAddress
};
log.info(payload);
jwt.sign(payload, private, { expiresIn: 30000000 }, (err, token) => {
if (err)
res.status(500).json({ error: 'Error signing token', raw: err });
// const refresh = uuid.v4();
res.json({ success: true, token: `Bearer ${token}` });
});
} else {
errors.password = 'Password is incorrect';
res.status(400).json(errors);
}
});
});
});
I think there might be something that I'm missing, but I'm unsure what it could be.
Also I've been generating the keys inside the app on initialization as well, using the following code.
const ensureKeys = () => {
return new Promise((resolve, reject) => {
ensureFolder('./keys').then(() => {
/**
* Ensure that both the private and public keys
* are created, and if not create them both.
* Never generate just a single key.
*/
try {
if (
!fs.existsSync('./keys/private.key') &&
!fs.existsSync('./keys/public.key')
) {
log.info('Keys do not exist. Creating them.');
diffHell.generateKeys('base64');
const public = diffHell.getPublicKey('base64');
const private = diffHell.getPrivateKey('base64');
fs.writeFileSync('./keys/public.key', public);
fs.writeFileSync('./keys/private.key', private);
log.info('keys created and being served to the app.');
resolve({ private, public });
} else {
log.info('keys are already generated. Loading from key files.');
const public = fs.readFileSync('./keys/public.key');
const private = fs.readFileSync('./keys/private.key');
log.info('keys loaded from files. Serving to the rest of the app.');
resolve({ private, public });
}
} catch (e) {
log.error('issue loading or generating keys. Sorry.', e);
reject(e);
}
});
});
};
Okay, so the issue was two-fold. First, I was generating the keys incorrectly for passport. According to the documentation for passport-jwt, documentation, keys must be encoded in PEM format, and according to this post on Medium, there needs to be some more configuration for passport and JWT.
The final solution included the use of the keypair library which is available on npm.
Here are the modifications used to make the working resulting code.
const keypair = require('keypair');
const ensureKeys = () => {
return new Promise((resolve, reject) => {
ensureFolder('./keys').then(() => {
/**
* Ensure that both the private and public keys
* are created, and if not create them both.
* Never generate just a single key.
*/
try {
if (
!fs.existsSync('./keys/private.key') &&
!fs.existsSync('./keys/public.key')
) {
log.info('Keys do not exist. Creating them.');
const pair = keypair();
fs.writeFileSync('./keys/public.key', pair.public);
fs.writeFileSync('./keys/private.key', pair.private);
log.info('keys created and being served to the app.');
resolve({ private: pair.private,public: pair.public });
} else {
log.info('keys are already generated. Loading from key files.');
const public = fs.readFileSync('./keys/public.key', 'utf8');
const private = fs.readFileSync('./keys/private.key', 'utf8');
log.info('keys loaded from files. Serving to the rest of the app.');
resolve({ private, public });
}
} catch (e) {
log.error('issue loading or generating keys. Sorry.', e);
reject(e);
}
});
});
};
Keys are signed with the private key, which is never to be shared.
router.post('/login', (req, res) => {
const { errors, isValid } = require('../validation/user').loginUser(
req.body
);
if (!isValid) {
return res.status(400).json(errors);
}
const email = req.body.email;
const password = req.body.password;
User.findOne({ email }).then(user => {
if (!user) {
errors.email = 'No Account Found';
return res.status(404).json(errors);
}
bcrypt.compare(password, user.password).then(isMatch => {
if (isMatch) {
const payload = {
id: user._id,
name: user.userName,
email: user.emailAddress
};
log.info(payload);
jwt.sign(payload, private, {
expiresIn: 30000000,
subject: user.emailAddress,
algorithm: 'RS256'
}, (err, token) => {
if (err)
res.status(500).json({ error: 'Error signing token', raw: err });
res.json({ success: true, token: `Bearer ${token}` });
});
} else {
errors.password = 'Password is incorrect';
res.status(400).json(errors);
}
});
});
And the verification function:
const opts = {
jwtFromRequest: ExtractJwt.fromAuthHeaderWithScheme('Bearer'),
secretOrKey: key,
algorithm: ["RS256"]
};
passport.use(
new JwtStrategy(opts, (payload, done) => {
log.info({message: 'verifying the token', payload});
User.findById(payload.id)
.then(user => {
if (user) {
return done(null, {
id: user._id,
name: user.userName,
email: user.emailAddress
});
}
log.info(payload);
return done(null, false);
})
.catch(err => {
log.error(err)
return done('Unauthorized', false, payload);
});
})
);
I hope this helps anyone looking to use asymmetric keys in the future.
Nice one, this helped me, thanks.
If it helps anyone else, I didn't have to use the library - found a link that explained how to convert the public key to PEM format, which seemed to work (private key is already in the correct format)
ssh-keygen -f id_rsa.pub -m 'PEM' -e > id_rsa.pem
My question
I am trying to create User with adminCreateUser function, but I am not receiving temporary password on my mail id.
var RegisterUser = exports.RegisterUser = function (data) {
var params = {
UserPoolId: __MY_POOL_ID__,
Username: data.username,
DesiredDeliveryMediums: [
'EMAIL'
],
ForceAliasCreation: false,
MessageAction: 'SUPPRESS',
TemporaryPassword: 'tempPassword1',
UserAttributes: [
{
Name: 'email',
Value: data.email
},
{
Name: 'name',
Value: data.name
}
]
};
return new Promise((res, rej) => {
_CISP.adminCreateUser(params, function (err, data) {
if (err) {
rej(err)
}
else {
res(data)
}
});
})
}
Case 1: When I am trying to creating a user from the Cognito AWS Console, then I am receiving the temporary password. (It is working)
Case 2: When I am trying to create a user using Cognito Admin API, can not get password. (Not working) Any idea?
Remove
MessageAction: 'SUPPRESS'
That option prevents messages from sending: https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_AdminCreateUser.html#CognitoUserPools-AdminCreateUser-request-MessageAction
I have a javascript project where I use the aws-sdk. No I want to use amazon-cognito-identity-js. On the page it says:
Note that the Amazon Cognito AWS SDK for JavaScript is just a slimmed down version
of the AWS Javascript SDK namespaced as AWSCognito instead of AWS. It
references only the Amazon Cognito Identity service.
and indeed, I can for example create CognitoIdentityServiceProvider with:
CognitoIdentityServiceProvider = new AWS.CognitoIdentityServiceProvider();
But how do I do thinks like authenticate a user? According to the amazon-cognito-identity-js documentation:
authenticationDetails = new CognitoIdentityServiceProvider.AuthenticationDetails({Userame: ..., Password: ...});
cognitoUser.authenticateUser(authenticationDetails, ...)
But the CognitoIdentityServiceProvider object does not have a AuthenticationDetails property.
Do I have to do something different when I use the aws-sdk instead of amazon-cognito-identity-js?
Or is my assumption wrong, and I need both, the aws-sdk and amazon-cognito-identity-js?
No, you don't. You can login using aws-sdk like this:
const cognito = new aws.CognitoIdentityServiceProvider({ region });
cognito.adminInitiateAuth({
AuthFlow: 'ADMIN_NO_SRP_AUTH',
ClientId: clientId,
UserPoolId: poolId,
AuthParameters: {
USERNAME: email,
PASSWORD: password,
},
});
For userName password authentication, it needs to have ADMIN_NO_SRP_AUTH.
I also added function to handle err and success. For more details about response and token sent in data, you can check was reference.
const cognito = new AWS.CognitoIdentityServiceProvider();
cognito.adminInitiateAuth({
AuthFlow: 'ADMIN_NO_SRP_AUTH',
ClientId: 'clientId',
UserPoolId: 'poolId'
AuthParameters: {
USERNAME: 'userName',
PASSWORD: 'password',
},
}, function(err, data) {
if (err) {
console.log(err, err.stack);
}
else {
console.log(data);
}
});
for any coming in the future
the following is what i did for register and login using
private cognito = new AWS.CognitoIdentityServiceProvider();
register:
public async register(event) {
try {
if(!this.validator.isObjectNotEmpty(event.body)){
throw new Error("you have to include body to insert into DB !!");
}
const usermodel = event.body;
if (this.validator.isEmailNotValid(usermodel)) {
throw new Error("Not Valid username or password");
}
const email = usermodel.email.toLowerCase();
const password = usermodel.password;
const createUserParams = {
UserPoolId: Cognito_User_Pool, // From Cognito dashboard 'Pool Id'
Username: email,
MessageAction: "SUPPRESS", // Do not send welcome email
TemporaryPassword: password,
};
const user = await this.cognito.adminCreateUser(createUserParams).promise();
this.logger.info("created user: ",user);
const initiateAuthParams = {
AuthFlow: "ADMIN_USER_PASSWORD_AUTH",
ClientId: Cognito_User_Pool_Client,
UserPoolId: Cognito_User_Pool,
AuthParameters: {
USERNAME: email,
PASSWORD: password
}
};
const challengesResponse = await this.cognito.adminInitiateAuth(initiateAuthParams).promise();
this.logger.info("challengesResponse: ",challengesResponse);
const userPasswordParams = {
Password: password,
UserPoolId: Cognito_User_Pool,
Username: email,
Permanent: true
};
return this.cognito.adminSetUserPassword(userPasswordParams).promise();
} catch (error) {
console.log("error: ", error);
throw error;
}
}
login
public async login(event) {
try {
this.logger.info("login - event: ",JSON.stringify(event));
this.logger.info("register - Cognito_User_Pool: ",Cognito_User_Pool);
this.logger.info("register - Cognito_User_Pool_Client: ",Cognito_User_Pool_Client);
if(!this.validator.isObjectNotEmpty(event.body)){
throw new Error("you have to include body to insert into DB !!");
}
const usermodel = event.body;
if (this.validator.isEmailNotValid(usermodel)) {
throw new Error("Not Valid username or password");
}
const email = usermodel.email.toLowerCase();
const password = usermodel.password;
const initiateAuthParams = {
AuthFlow: "ADMIN_USER_PASSWORD_AUTH",
ClientId: Cognito_User_Pool_Client,
UserPoolId: Cognito_User_Pool,
AuthParameters: {
USERNAME: email,
PASSWORD: password
}
};
return await this.cognito.adminInitiateAuth(initiateAuthParams).promise();
} catch (error) {
console.log("error: ", error);
throw error;
}
}
the login function return response like this:
"tokenObj": {
"ChallengeParameters": {},
"AuthenticationResult": {
"AccessToken": "eyJraWQiOiI1ZFN5R0tEUFE2WnhTbzJ4N01QTXpxaVRPNXZoK3p5NkJaOFZhRzV1M0Z3PSIsImFsZyI6IlJTMjU2In0.eyJvcmlnaW5fanRpIjoiMGYzY2RmZTItZTM3Zi00NGRiLWJiMWQtMDIzYjFiZTdlZGVjIiwic3ViIjoiZWJjN2M4YzctZjllYS00MTMxLTgxNDQtNjdkM2ZmYzg1ZTdlIiwiZXZlbnRfaWQiOiJhYWJhNzgyZi1iZTVhLTQxMjItYjljYS0yNjg5NTY2ZDVkMTgiLCJ0b2tlbl91c2UiOiJhY2Nlc3MiLCJzY29wZSI6ImF3cy5jb2duaXRvLnNpZ25pbi51c2VyLmFkbWluIiwiYXV0aF90aW1lIjoxNjM4MTI0NTY3LCJpc3MiOiJodHRwczpcL1wvY29nbml0by1pZHAudXMtd2VzdC0yLmFtYXpvbmF3cy5jb21cL3VzLXdlc3QtMl9vVmFnb3hFd0wiLCJleHAiOjE2MzgxMjgxNjcsImlhdCI6MTYzODEyNDU2NywianRpIjoiZjE0MTBjMGEtOWM4ZS00YjMzLWJkMjEtNmQ1ZDIwYzJiNDU5IiwiY2xpZW50X2lkIjoiNnVvc2dxNzNtYjFlYmk5c3BnbDV0N282Z2QiLCJ1c2VybmFtZSI6ImViYzdjOGM3LWY5ZWEtNDEzMS04MTQ0LTY3ZDNmZmM4NWU3ZSJ9.4vswc6-ei6SFlbN0k_C8PAtmEK294oFfylYZW3hhmHqMT49d_JWdDPR8XgA7n5cLqtLJsiG4PVh9SyxdirpUTmC0yra2GZejNWP8eVHG9-JUq_xXQ81a8AYEb1KZJBAv0j2F1ZLuCFaiFvbOBFEGSlJXp4e2bLz-NWlSbEAUbxZV5BKrRmZEeXJUxw_DO5R30SG1D2qcOn_1mnbdfTH-W0ZMuuMJsHWQBsO7QEtyMI62XG35WRDijzzMr6ekbLyIQ7J77dYcsmTdR6S5nTNWE1yrGYtkdxk2x35nYob06PAWdnWL5OK2G1aiJ97BMvM65VtjX8VQZbAoalIII-PbCA",
"ExpiresIn": 3600,
"TokenType": "Bearer",
"RefreshToken": "eyJjdHkiOiJKV1QiLCJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiUlNBLU9BRVAifQ.FKN-KM415xH-TioTkKjmoy_NV1AssBq9GlkweHAK7TuYzMeI_qMfeL00Qv6MiOPFMJ6UG7cLKfJjkdyI1vK09p9lk5_wor3FoCX1Dvt3DK3SKzeyqW6ULQL1IQmNunPhpC3sARom0q74YxSaQKdF_6PgDjNnf0-aI1XGkZYZUw69BZGQDVobTx8nR_Lf6srF622s0JuhaClDfvIgqjZun7AIFlxgNUKL6uuMgmSrRJm7GvAqAnTQpmQD2sdz6D3fWeoVja8LRpc_QMbZPhjZmOoRrQJ3VA-m0M0TsNMvQSw6px-WS-hMUH4RFeFasrbmoiUKIU8-MXltyPVltoy0qg.dHBgjs1NAOAROzD6.vi7DzrqzLkuHGuuGY1vNZaYmdDp0-EliwxTtEOaMboVlMCm9KZv0tU06ovQ9Z2CbplqJdEaAu-YZSG3Oeagqj5JJ2-739C0yvA0J_qE2Ehn5gCUMuiPPOjakIczOcI8wcRItsTGqt1GFmyq4DSnX-pjc9GG2QWpgmcsoDyxodIv1wTu7YN15cGK0YN_xSKolcCn04gCzqXCDkQyUqX-Sr7QqD2o-aQC1ynmKz1tJGxRM3I6rzbcRZkNvUK02wSIUMeY05hJNAdKBItIE0t45Sk5_EV3mzLgebC39Q6z-kRKNQwhyQKrcXg71aRJGjD3HH61VEoops4wcgRCmNnI7u-9JjJic_SDU_Fw-9W1L8BxZZKPsJrjOvJroDFc1xX2njgeivo6ZpRWTg3nQphMTqJFSYHnWcEmw3EW1uUhN7e1J2bdGuebW40Opf5zMXo2V4-OBI3UMxTOVZEPit4g3yLDHQDAfPZllPPRf5H8Kf1Qe54fEq77sKb32I5PH7t-7xQ79iE5KX6BZjsLjwHMAlGNtS7WSE-U0yAsjUFFH8pA7eYR5ysz3o9L7jJsNCyNb06YU_QX5mozGlspLMNs4qpUcWkFGhSNG6qTRMREpzRfELU82Dc-LirJ1425zPQNsQV1CGGtlDoXVA-RCIgnMG1hslfpIllKm_IH-5yyPMfH2UFdr7jdRjt9crFyFu9ktGkHjnAqH_ZUcueL0q_vrIqmkGcBcrQQRY9jCSC4VNeGhKe9ecJciAHUCktkZuut5yI0De0hXANoaoxeIucJlox57nxHnQTD0Q0LEuXvfqG9M9sNdB4GGrt1rYR58ZWa8zc5udoKDMxvrb6GEeDI1co2mLEt3XghjcJBJqB8nqwofXZDxiDSXQ53R9L5U8wDlxhxZxPMDKFL6wu4gqDjw4m60R8mOvfOI2Gz_yelqfk5CxD6Ts_HVbv9TRbsV0adQ4S8n5Cktf5miWUtFCOktruS2_fKfB6TF1yzisgxQpJFD7B1qdTcBu0N82KgHrljoBlVKiwGdalL52H-cJkNkagZIBQYXvlvkBrbsSNAp1CeUiqXbddHku47RNjXSPUMN1aoHdEL14_7zbaPYBMxgEsmqjP67B45QKmTyDDsOkf8aSdAWYFKvj3c_4Cn_LczLU-YcuzE4UZvd6BQK70MiKBIsd3DU4rdKTDjAsN73mJ_U-_8r34jocny4rzbO_NostPjgqUugoO2bGhfr_PUgXb7sm_wrKlvjFnUcOLpQSBxlZL_z25e8vZokRn4QiNa83_VG6xyp77-c_DQqLC3RdwXOyRmCEXkM2D10cBNQpkAzCBMyP9FO3mJZcQ.aAJlWM76hqlRprKY8P_Qmw",
"IdToken": "eyJraWQiOiJoWmpBMXl1d2JjT1lyNks5dnhQc2ZKVDhXSHRrYm51WmZlTURlK0dBbWR3PSIsImFsZyI6IlJTMjU2In0.eyJvcmlnaW5fanRpIjoiMGYzY2RmZTItZTM3Zi00NGRiLWJiMWQtMDIzYjFiZTdlZGVjIiwic3ViIjoiZWJjN2M4YzctZjllYS00MTMxLTgxNDQtNjdkM2ZmYzg1ZTdlIiwiYXVkIjoiNnVvc2dxNzNtYjFlYmk5c3BnbDV0N282Z2QiLCJldmVudF9pZCI6ImFhYmE3ODJmLWJlNWEtNDEyMi1iOWNhLTI2ODk1NjZkNWQxOCIsInRva2VuX3VzZSI6ImlkIiwiYXV0aF90aW1lIjoxNjM4MTI0NTY3LCJpc3MiOiJodHRwczpcL1wvY29nbml0by1pZHAudXMtd2VzdC0yLmFtYXpvbmF3cy5jb21cL3VzLXdlc3QtMl9vVmFnb3hFd0wiLCJjb2duaXRvOnVzZXJuYW1lIjoiZWJjN2M4YzctZjllYS00MTMxLTgxNDQtNjdkM2ZmYzg1ZTdlIiwiZXhwIjoxNjM4MTI0ODY3LCJpYXQiOjE2MzgxMjQ1NjcsImp0aSI6ImRmMzVlZWQ1LWZjNGItNDBlZS1hYWEwLTYxNTViZWNiNjIyNyIsImVtYWlsIjoidGVzdDJAdGVzdC5jb20ifQ.L9nNSlcQakRfVpAo-bf9jnXFrhov8iD9j3R2UI7x5eUT42OBrvSdEk3Y2JtFKQt1FpxBFLqtWCRyY0Wxlcq3MzZE04zIFBC02WmV24vF1QS9TxpBYAm8NkyNoW-7Lqe7CIWhK_DHMxSRpzo87txmwpID6xwd9JOlZG04L76hkXr0_2JgM2KuvB0lKL0v4heHK8e2ht_VuKBPNpVvMIbIA2xN22LRhivfG8EMeHmF3jhhQYggi1Pczb2WEviPUqebmLhdvjJpHC9OMVhvN4GMcr0JVv-GepxSCrNSFoXPDNZaznJbDJW0Zt3xY9bWek_l62v1Wm0a1h7V7nxw8a0Csw"
}
}
}