From 7b7457e07472b36085a54080ce679671c1b1651d Mon Sep 17 00:00:00 2001 From: seasonsolution Date: Wed, 10 Mar 2021 14:54:08 +0000 Subject: [PATCH 01/15] Implement OAuth method Inclusion of routines for OAuth, discussed in https://github.com/parse-community/parse-server/issues/7248 --- package-lock.json | 3 +- package.json | 1 + src/Auth.js | 92 ++++++++++++++++++++ src/Options/Definitions.js | 16 ++++ src/Options/index.js | 10 +++ src/Routers/UsersRouter.js | 166 ++++++++++++++++++++++++++++++++++++- 6 files changed, 283 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index eef6319cec..f6d0cf3f8a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4028,8 +4028,7 @@ "crypto-js": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.0.0.tgz", - "integrity": "sha512-bzHZN8Pn+gS7DQA6n+iUmBfl0hO5DJq++QP3U6uTucDtk/0iGpXd/Gg7CGR0p8tJhofJyaKoWBuJI4eAO00BBg==", - "optional": true + "integrity": "sha512-bzHZN8Pn+gS7DQA6n+iUmBfl0hO5DJq++QP3U6uTucDtk/0iGpXd/Gg7CGR0p8tJhofJyaKoWBuJI4eAO00BBg==" }, "css-select": { "version": "1.2.0", diff --git a/package.json b/package.json index 456cb47484..ab98485b53 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "body-parser": "1.19.0", "commander": "5.1.0", "cors": "2.8.5", + "crypto-js": "4.0.0", "deepcopy": "2.1.0", "express": "4.17.1", "follow-redirects": "1.13.2", diff --git a/src/Auth.js b/src/Auth.js index 2d63e785be..a89d8dc7e6 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -1,6 +1,9 @@ +const CryptoJS = require('crypto-js'); const cryptoUtils = require('./cryptoUtils'); +const jwt = require('jsonwebtoken'); const RestQuery = require('./RestQuery'); const Parse = require('parse/node'); +const SHA256 = require('crypto-js/sha256'); // An Auth object tells you who is requesting something and whether // the master key was used. @@ -27,6 +30,76 @@ function Auth({ this.rolePromise = null; } +// A helper to convert data to base64 encoded to URL +function base64url(source) { + // Encode in classical base64 + var encodedSource = CryptoJS.enc.Base64.stringify(source); + + // Remove padding equal characters + encodedSource = encodedSource.replace(/=+$/, ''); + + // Replace characters according to base64url specifications + encodedSource = encodedSource.replace(/\+/g, '-'); + encodedSource = encodedSource.replace(/\//g, '_'); + + return encodedSource; +} + +// A helper to generate a random hash +const generateRefreshToken = function () { + return SHA256(CryptoJS.lib.WordArray.random(256)).toString(); +}; + +// Function to create a token JWT to authentication +const createJWT = function (sessionToken, oauthKey, oauthTTL = 1800) { + // Header + const header = { + alg: 'HS256', + typ: 'JWT', + }; + + const stringifiedHeader = CryptoJS.enc.Utf8.parse(JSON.stringify(header)); + const encodedHeader = base64url(stringifiedHeader); + + const timestamp = Math.floor(new Date().getTime() / 1000); + const expiration = timestamp + oauthTTL; + + // Payload + const data = { + sub: sessionToken, + iat: timestamp, + exp: expiration, + }; + + const stringifiedData = CryptoJS.enc.Utf8.parse(JSON.stringify(data)); + const encodedData = base64url(stringifiedData); + + const token = encodedHeader + '.' + encodedData; + + // Signature + let signature = CryptoJS.HmacSHA256(token, oauthKey); + signature = base64url(signature); + + return { + accessToken: token + '.' + signature, + expires_in: expiration, + }; +}; + +// Valid if token is valid +const validJWT = function (token, secret) { + try { + return jwt.verify(token, secret); + } catch (err) { + return false; + } +}; + +// Parse a JWT informations +const decodeJWT = function (token) { + return jwt.decode(token); +}; + // Whether this auth could possibly modify the given user id. // It still could be forbidden via ACLs even if this returns true. Auth.prototype.isUnauthenticated = function () { @@ -63,6 +136,15 @@ const getAuthForSessionToken = async function ({ }) { cacheController = cacheController || (config && config.cacheController); if (cacheController) { + // Check if you use OAuth to retrieve the sessionToken from within the JWT + if (config.oauth === true) { + if (validJWT(sessionToken, config.oauthKey) === false) { + throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); + } + const decoded = decodeJWT(sessionToken); + sessionToken = decoded.sub; + } + const userJSON = await cacheController.user.get(sessionToken); if (userJSON) { const cachedUser = Parse.Object.fromJSON(userJSON); @@ -321,6 +403,12 @@ const createSession = function ( sessionData.installationId = installationId; } + // Check if you use OAuth to retrieve the sessionToken from within the JWT + // Generate a random hash + if (config.oauth === true) { + sessionData.refreshToken = generateRefreshToken(); + } + Object.assign(sessionData, additionalSessionData); // We need to import RestWrite at this point for the cyclic dependency it has to it const RestWrite = require('./RestWrite'); @@ -339,5 +427,9 @@ module.exports = { readOnly, getAuthForSessionToken, getAuthForLegacySessionToken, + generateRefreshToken, createSession, + createJWT, + validJWT, + decodeJWT, }; diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index e06d1c8fc3..120d3fefd0 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -353,6 +353,22 @@ module.exports.ParseServerOptions = { env: 'PARSE_SERVER_REST_API_KEY', help: 'Key for REST calls', }, + oauth: { + env: 'PARSE_SERVER_OAUTH', + help: 'Sets whether to use the OAuth protocol', + action: parsers.booleanParser, + default: false, + }, + oauthKey: { + env: 'PARSE_SERVER_OAUTH_KEY', + help: 'Key for OAuth protocol', + }, + oauthTTL: { + env: 'PARSE_SERVER_REST_API_KEY', + help: 'The JSON Web Token (JWT) expiration TTL', + action: parsers.numberParser('oauthTTL'), + default: 1800, + }, revokeSessionOnPasswordReset: { env: 'PARSE_SERVER_REVOKE_SESSION_ON_PASSWORD_RESET', help: diff --git a/src/Options/index.js b/src/Options/index.js index 1114e4fe0f..9693361387 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -83,6 +83,16 @@ export interface ParseServerOptions { /* Key for REST calls :ENV: PARSE_SERVER_REST_API_KEY */ restAPIKey: ?string; + /* Enable (or disable) the addition of OAuth 2.0 + :ENV: PARSE_SERVER_OAUTH + :DEFAULT: false */ + oauth: ?boolean; + /* Key for OAuth 2.0 + :ENV: PARSE_SERVER_OAUTH_KEY */ + oauthKey: ?string; + /* The TTL for Access Token + :DEFAULT: 1800 - 30 minutes */ + oauthTTL: ?number; /* Read-only key, which has the same capabilities as MasterKey without writes */ readOnlyMasterKey: ?string; /* Key sent with outgoing webhook calls */ diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index 7843cf4674..be53fabb46 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -139,11 +139,136 @@ export class UsersRouter extends ClassesRouter { }); } + handleCreate(req) { + return rest + .create( + req.config, + req.auth, + this.className(req), + req.body, + req.info.clientSDK, + req.info.context + ) + .then(response => { + response.response.oauth = req.config.oauth === true ? true : false; + if (req.config.oauth === true) { + const token = Auth.createJWT( + response.response.sessionToken, + req.config.oauthKey, + req.config.oauthTTL + ); + response.response.accessToken = token.accessToken; + response.response.expires_in = token.expires_in; + delete response.response.sessionToken; + } + return response; + }); + } + + handleRefresh(req) { + const payload = req.body; + const { client, code } = payload; + + if (!client || !code) { + throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); + } + + // Consulta + const refreshToken = code; + return rest + .find( + req.config, + Auth.master(req.config), + '_Session', + { refreshToken }, + { include: 'user' }, + req.info.clientSDK, + req.info.context + ) + .then(response => { + if (!response.results || response.results.length == 0 || !response.results[0].user) { + throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); + } else { + // Retorno + const data = response.results[0]; + // Novo Code Refresh + const newCode = Auth.generateRefreshToken(); + const sessionId = data.objectId; + const token = Auth.createJWT(data.sessionToken, req.config.oauthKey, req.config.oauthTTL); + + // Atualizar o novo code + req.config.database.update( + '_Session', + { objectId: sessionId }, + { refreshToken: newCode } + ); + + return { + response: { + client: client, + accesstoken: token.accessToken, + refreshToken: newCode, + expires_in: token.expires_in, + }, + }; + } + }); + } + + handleRevoke(req) { + const payload = req.body; + const { code } = payload; + const success = { response: {} }; + + if (!code) { + throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); + } + + return rest + .find( + req.config, + Auth.master(req.config), + '_Session', + { refreshToken: code }, + undefined, + req.info.clientSDK, + req.info.context + ) + .then(records => { + if (records.results && records.results.length) { + return rest + .del( + req.config, + Auth.master(req.config), + '_Session', + records.results[0].objectId, + req.info.context + ) + .then(() => { + this._runAfterLogoutTrigger(req, records.results[0]); + return Promise.resolve(success); + }); + } + return Promise.resolve(success); + }); + } + handleMe(req) { if (!req.info || !req.info.sessionToken) { throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); } - const sessionToken = req.info.sessionToken; + let sessionToken = req.info.sessionToken; + const originalToken = req.info.sessionToken; + + // Check if you use OAuth to retrieve the sessionToken from within the JWT + if (req.config.oauth === true) { + if (Auth.validJWT(sessionToken, req.config.oauthKey) === false) { + throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); + } + const decoded = Auth.decodeJWT(sessionToken); + sessionToken = decoded.sub; + } + return rest .find( req.config, @@ -160,7 +285,14 @@ export class UsersRouter extends ClassesRouter { } else { const user = response.results[0].user; // Send token back on the login, because SDKs expect that. - user.sessionToken = sessionToken; + user.oauth = req.config.oauth === true ? true : false; + if (req.config.oauth === true) { + const decoded = Auth.decodeJWT(originalToken); + user.accessToken = originalToken; + user.expires_in = decoded.exp; + } else { + user.sessionToken = sessionToken; + } // Remove hidden properties. UsersRouter.removeHiddenProperties(user); @@ -227,7 +359,23 @@ export class UsersRouter extends ClassesRouter { installationId: req.info.installationId, }); - user.sessionToken = sessionData.sessionToken; + // Check if you use OAuth to generate a JWT to return + user.oauth = req.config.oauth === true ? true : false; + if (req.config.oauth === true) { + var signedToken = Auth.createJWT( + sessionData.sessionToken, + req.config.oauthKey, + req.config.oauthTTL + ); + + user.accessToken = signedToken.accessToken; + user.refreshToken = sessionData.refreshToken; + user.expires_in = signedToken.expires_in; + + delete user.sessionToken; + } else { + user.sessionToken = sessionData.sessionToken; + } await createSession(); @@ -259,6 +407,12 @@ export class UsersRouter extends ClassesRouter { handleLogOut(req) { const success = { response: {} }; if (req.info && req.info.sessionToken) { + // Check if you use OAuth to retrieve the sessionToken from within the JWT + if (req.config.oauth === true) { + const decoded = Auth.decodeJWT(req.info.sessionToken); + req.info.sessionToken = decoded.sub; + } + return rest .find( req.config, @@ -402,6 +556,12 @@ export class UsersRouter extends ClassesRouter { this.route('GET', '/users/me', req => { return this.handleMe(req); }); + this.route('POST', '/users/refresh', req => { + return this.handleRefresh(req); + }); + this.route('POST', '/users/revoke', req => { + return this.handleRevoke(req); + }); this.route('GET', '/users/:objectId', req => { return this.handleGet(req); }); From 8c8ff9f80f7961133a6825f61c40c2f8eb7778bf Mon Sep 17 00:00:00 2001 From: seasonsolution Date: Wed, 10 Mar 2021 15:12:03 +0000 Subject: [PATCH 02/15] Revert "Implement OAuth method" This reverts commit 7b7457e07472b36085a54080ce679671c1b1651d. --- package-lock.json | 3 +- package.json | 1 - src/Auth.js | 92 -------------------- src/Options/Definitions.js | 16 ---- src/Options/index.js | 10 --- src/Routers/UsersRouter.js | 166 +------------------------------------ 6 files changed, 5 insertions(+), 283 deletions(-) diff --git a/package-lock.json b/package-lock.json index f6d0cf3f8a..eef6319cec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4028,7 +4028,8 @@ "crypto-js": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.0.0.tgz", - "integrity": "sha512-bzHZN8Pn+gS7DQA6n+iUmBfl0hO5DJq++QP3U6uTucDtk/0iGpXd/Gg7CGR0p8tJhofJyaKoWBuJI4eAO00BBg==" + "integrity": "sha512-bzHZN8Pn+gS7DQA6n+iUmBfl0hO5DJq++QP3U6uTucDtk/0iGpXd/Gg7CGR0p8tJhofJyaKoWBuJI4eAO00BBg==", + "optional": true }, "css-select": { "version": "1.2.0", diff --git a/package.json b/package.json index ab98485b53..456cb47484 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,6 @@ "body-parser": "1.19.0", "commander": "5.1.0", "cors": "2.8.5", - "crypto-js": "4.0.0", "deepcopy": "2.1.0", "express": "4.17.1", "follow-redirects": "1.13.2", diff --git a/src/Auth.js b/src/Auth.js index a89d8dc7e6..2d63e785be 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -1,9 +1,6 @@ -const CryptoJS = require('crypto-js'); const cryptoUtils = require('./cryptoUtils'); -const jwt = require('jsonwebtoken'); const RestQuery = require('./RestQuery'); const Parse = require('parse/node'); -const SHA256 = require('crypto-js/sha256'); // An Auth object tells you who is requesting something and whether // the master key was used. @@ -30,76 +27,6 @@ function Auth({ this.rolePromise = null; } -// A helper to convert data to base64 encoded to URL -function base64url(source) { - // Encode in classical base64 - var encodedSource = CryptoJS.enc.Base64.stringify(source); - - // Remove padding equal characters - encodedSource = encodedSource.replace(/=+$/, ''); - - // Replace characters according to base64url specifications - encodedSource = encodedSource.replace(/\+/g, '-'); - encodedSource = encodedSource.replace(/\//g, '_'); - - return encodedSource; -} - -// A helper to generate a random hash -const generateRefreshToken = function () { - return SHA256(CryptoJS.lib.WordArray.random(256)).toString(); -}; - -// Function to create a token JWT to authentication -const createJWT = function (sessionToken, oauthKey, oauthTTL = 1800) { - // Header - const header = { - alg: 'HS256', - typ: 'JWT', - }; - - const stringifiedHeader = CryptoJS.enc.Utf8.parse(JSON.stringify(header)); - const encodedHeader = base64url(stringifiedHeader); - - const timestamp = Math.floor(new Date().getTime() / 1000); - const expiration = timestamp + oauthTTL; - - // Payload - const data = { - sub: sessionToken, - iat: timestamp, - exp: expiration, - }; - - const stringifiedData = CryptoJS.enc.Utf8.parse(JSON.stringify(data)); - const encodedData = base64url(stringifiedData); - - const token = encodedHeader + '.' + encodedData; - - // Signature - let signature = CryptoJS.HmacSHA256(token, oauthKey); - signature = base64url(signature); - - return { - accessToken: token + '.' + signature, - expires_in: expiration, - }; -}; - -// Valid if token is valid -const validJWT = function (token, secret) { - try { - return jwt.verify(token, secret); - } catch (err) { - return false; - } -}; - -// Parse a JWT informations -const decodeJWT = function (token) { - return jwt.decode(token); -}; - // Whether this auth could possibly modify the given user id. // It still could be forbidden via ACLs even if this returns true. Auth.prototype.isUnauthenticated = function () { @@ -136,15 +63,6 @@ const getAuthForSessionToken = async function ({ }) { cacheController = cacheController || (config && config.cacheController); if (cacheController) { - // Check if you use OAuth to retrieve the sessionToken from within the JWT - if (config.oauth === true) { - if (validJWT(sessionToken, config.oauthKey) === false) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); - } - const decoded = decodeJWT(sessionToken); - sessionToken = decoded.sub; - } - const userJSON = await cacheController.user.get(sessionToken); if (userJSON) { const cachedUser = Parse.Object.fromJSON(userJSON); @@ -403,12 +321,6 @@ const createSession = function ( sessionData.installationId = installationId; } - // Check if you use OAuth to retrieve the sessionToken from within the JWT - // Generate a random hash - if (config.oauth === true) { - sessionData.refreshToken = generateRefreshToken(); - } - Object.assign(sessionData, additionalSessionData); // We need to import RestWrite at this point for the cyclic dependency it has to it const RestWrite = require('./RestWrite'); @@ -427,9 +339,5 @@ module.exports = { readOnly, getAuthForSessionToken, getAuthForLegacySessionToken, - generateRefreshToken, createSession, - createJWT, - validJWT, - decodeJWT, }; diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 120d3fefd0..e06d1c8fc3 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -353,22 +353,6 @@ module.exports.ParseServerOptions = { env: 'PARSE_SERVER_REST_API_KEY', help: 'Key for REST calls', }, - oauth: { - env: 'PARSE_SERVER_OAUTH', - help: 'Sets whether to use the OAuth protocol', - action: parsers.booleanParser, - default: false, - }, - oauthKey: { - env: 'PARSE_SERVER_OAUTH_KEY', - help: 'Key for OAuth protocol', - }, - oauthTTL: { - env: 'PARSE_SERVER_REST_API_KEY', - help: 'The JSON Web Token (JWT) expiration TTL', - action: parsers.numberParser('oauthTTL'), - default: 1800, - }, revokeSessionOnPasswordReset: { env: 'PARSE_SERVER_REVOKE_SESSION_ON_PASSWORD_RESET', help: diff --git a/src/Options/index.js b/src/Options/index.js index 9693361387..1114e4fe0f 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -83,16 +83,6 @@ export interface ParseServerOptions { /* Key for REST calls :ENV: PARSE_SERVER_REST_API_KEY */ restAPIKey: ?string; - /* Enable (or disable) the addition of OAuth 2.0 - :ENV: PARSE_SERVER_OAUTH - :DEFAULT: false */ - oauth: ?boolean; - /* Key for OAuth 2.0 - :ENV: PARSE_SERVER_OAUTH_KEY */ - oauthKey: ?string; - /* The TTL for Access Token - :DEFAULT: 1800 - 30 minutes */ - oauthTTL: ?number; /* Read-only key, which has the same capabilities as MasterKey without writes */ readOnlyMasterKey: ?string; /* Key sent with outgoing webhook calls */ diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index be53fabb46..7843cf4674 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -139,136 +139,11 @@ export class UsersRouter extends ClassesRouter { }); } - handleCreate(req) { - return rest - .create( - req.config, - req.auth, - this.className(req), - req.body, - req.info.clientSDK, - req.info.context - ) - .then(response => { - response.response.oauth = req.config.oauth === true ? true : false; - if (req.config.oauth === true) { - const token = Auth.createJWT( - response.response.sessionToken, - req.config.oauthKey, - req.config.oauthTTL - ); - response.response.accessToken = token.accessToken; - response.response.expires_in = token.expires_in; - delete response.response.sessionToken; - } - return response; - }); - } - - handleRefresh(req) { - const payload = req.body; - const { client, code } = payload; - - if (!client || !code) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); - } - - // Consulta - const refreshToken = code; - return rest - .find( - req.config, - Auth.master(req.config), - '_Session', - { refreshToken }, - { include: 'user' }, - req.info.clientSDK, - req.info.context - ) - .then(response => { - if (!response.results || response.results.length == 0 || !response.results[0].user) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); - } else { - // Retorno - const data = response.results[0]; - // Novo Code Refresh - const newCode = Auth.generateRefreshToken(); - const sessionId = data.objectId; - const token = Auth.createJWT(data.sessionToken, req.config.oauthKey, req.config.oauthTTL); - - // Atualizar o novo code - req.config.database.update( - '_Session', - { objectId: sessionId }, - { refreshToken: newCode } - ); - - return { - response: { - client: client, - accesstoken: token.accessToken, - refreshToken: newCode, - expires_in: token.expires_in, - }, - }; - } - }); - } - - handleRevoke(req) { - const payload = req.body; - const { code } = payload; - const success = { response: {} }; - - if (!code) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); - } - - return rest - .find( - req.config, - Auth.master(req.config), - '_Session', - { refreshToken: code }, - undefined, - req.info.clientSDK, - req.info.context - ) - .then(records => { - if (records.results && records.results.length) { - return rest - .del( - req.config, - Auth.master(req.config), - '_Session', - records.results[0].objectId, - req.info.context - ) - .then(() => { - this._runAfterLogoutTrigger(req, records.results[0]); - return Promise.resolve(success); - }); - } - return Promise.resolve(success); - }); - } - handleMe(req) { if (!req.info || !req.info.sessionToken) { throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); } - let sessionToken = req.info.sessionToken; - const originalToken = req.info.sessionToken; - - // Check if you use OAuth to retrieve the sessionToken from within the JWT - if (req.config.oauth === true) { - if (Auth.validJWT(sessionToken, req.config.oauthKey) === false) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); - } - const decoded = Auth.decodeJWT(sessionToken); - sessionToken = decoded.sub; - } - + const sessionToken = req.info.sessionToken; return rest .find( req.config, @@ -285,14 +160,7 @@ export class UsersRouter extends ClassesRouter { } else { const user = response.results[0].user; // Send token back on the login, because SDKs expect that. - user.oauth = req.config.oauth === true ? true : false; - if (req.config.oauth === true) { - const decoded = Auth.decodeJWT(originalToken); - user.accessToken = originalToken; - user.expires_in = decoded.exp; - } else { - user.sessionToken = sessionToken; - } + user.sessionToken = sessionToken; // Remove hidden properties. UsersRouter.removeHiddenProperties(user); @@ -359,23 +227,7 @@ export class UsersRouter extends ClassesRouter { installationId: req.info.installationId, }); - // Check if you use OAuth to generate a JWT to return - user.oauth = req.config.oauth === true ? true : false; - if (req.config.oauth === true) { - var signedToken = Auth.createJWT( - sessionData.sessionToken, - req.config.oauthKey, - req.config.oauthTTL - ); - - user.accessToken = signedToken.accessToken; - user.refreshToken = sessionData.refreshToken; - user.expires_in = signedToken.expires_in; - - delete user.sessionToken; - } else { - user.sessionToken = sessionData.sessionToken; - } + user.sessionToken = sessionData.sessionToken; await createSession(); @@ -407,12 +259,6 @@ export class UsersRouter extends ClassesRouter { handleLogOut(req) { const success = { response: {} }; if (req.info && req.info.sessionToken) { - // Check if you use OAuth to retrieve the sessionToken from within the JWT - if (req.config.oauth === true) { - const decoded = Auth.decodeJWT(req.info.sessionToken); - req.info.sessionToken = decoded.sub; - } - return rest .find( req.config, @@ -556,12 +402,6 @@ export class UsersRouter extends ClassesRouter { this.route('GET', '/users/me', req => { return this.handleMe(req); }); - this.route('POST', '/users/refresh', req => { - return this.handleRefresh(req); - }); - this.route('POST', '/users/revoke', req => { - return this.handleRevoke(req); - }); this.route('GET', '/users/:objectId', req => { return this.handleGet(req); }); From a60739862051f5bb30e25ee453520ca1e18748d2 Mon Sep 17 00:00:00 2001 From: seasonsolution Date: Wed, 10 Mar 2021 15:13:16 +0000 Subject: [PATCH 03/15] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 271c2703f4..1c57b892ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ [Full Changelog](https://github.com/parse-community/parse-server/compare/4.5.0...master) __BREAKING CHANGES:__ +- NEW: Added a OAuth method to authentication. [#7248](https://github.com/parse-community/parse-server/issues/7248). - NEW: Added file upload restriction. File upload is now only allowed for authenticated users by default for improved security. To allow file upload also for Anonymous Users or Public, set the `fileUpload` parameter in the [Parse Server Options](https://parseplatform.org/parse-server/api/master/ParseServerOptions.html). [#7071](https://github.com/parse-community/parse-server/pull/7071). Thanks to [dblythy](https://github.com/dblythy), [Manuel Trezza](https://github.com/mtrezza). ___ - NEW (EXPERIMENTAL): Added new page router with placeholder rendering and localization of custom and feature pages such as password reset and email verification. **Caution, this is an experimental feature that may not be appropriate for production.** [#6891](https://github.com/parse-community/parse-server/issues/6891). Thanks to [Manuel Trezza](https://github.com/mtrezza). From 7e59d29a2ba1bbb0060eea1ef54182560eeb5ddc Mon Sep 17 00:00:00 2001 From: seasonsolution Date: Wed, 10 Mar 2021 15:15:35 +0000 Subject: [PATCH 04/15] Revert "Revert "Implement OAuth method"" This reverts commit 8c8ff9f80f7961133a6825f61c40c2f8eb7778bf. --- package-lock.json | 3 +- package.json | 1 + src/Auth.js | 92 ++++++++++++++++++++ src/Options/Definitions.js | 16 ++++ src/Options/index.js | 10 +++ src/Routers/UsersRouter.js | 166 ++++++++++++++++++++++++++++++++++++- 6 files changed, 283 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index eef6319cec..f6d0cf3f8a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4028,8 +4028,7 @@ "crypto-js": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.0.0.tgz", - "integrity": "sha512-bzHZN8Pn+gS7DQA6n+iUmBfl0hO5DJq++QP3U6uTucDtk/0iGpXd/Gg7CGR0p8tJhofJyaKoWBuJI4eAO00BBg==", - "optional": true + "integrity": "sha512-bzHZN8Pn+gS7DQA6n+iUmBfl0hO5DJq++QP3U6uTucDtk/0iGpXd/Gg7CGR0p8tJhofJyaKoWBuJI4eAO00BBg==" }, "css-select": { "version": "1.2.0", diff --git a/package.json b/package.json index 456cb47484..ab98485b53 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "body-parser": "1.19.0", "commander": "5.1.0", "cors": "2.8.5", + "crypto-js": "4.0.0", "deepcopy": "2.1.0", "express": "4.17.1", "follow-redirects": "1.13.2", diff --git a/src/Auth.js b/src/Auth.js index 2d63e785be..a89d8dc7e6 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -1,6 +1,9 @@ +const CryptoJS = require('crypto-js'); const cryptoUtils = require('./cryptoUtils'); +const jwt = require('jsonwebtoken'); const RestQuery = require('./RestQuery'); const Parse = require('parse/node'); +const SHA256 = require('crypto-js/sha256'); // An Auth object tells you who is requesting something and whether // the master key was used. @@ -27,6 +30,76 @@ function Auth({ this.rolePromise = null; } +// A helper to convert data to base64 encoded to URL +function base64url(source) { + // Encode in classical base64 + var encodedSource = CryptoJS.enc.Base64.stringify(source); + + // Remove padding equal characters + encodedSource = encodedSource.replace(/=+$/, ''); + + // Replace characters according to base64url specifications + encodedSource = encodedSource.replace(/\+/g, '-'); + encodedSource = encodedSource.replace(/\//g, '_'); + + return encodedSource; +} + +// A helper to generate a random hash +const generateRefreshToken = function () { + return SHA256(CryptoJS.lib.WordArray.random(256)).toString(); +}; + +// Function to create a token JWT to authentication +const createJWT = function (sessionToken, oauthKey, oauthTTL = 1800) { + // Header + const header = { + alg: 'HS256', + typ: 'JWT', + }; + + const stringifiedHeader = CryptoJS.enc.Utf8.parse(JSON.stringify(header)); + const encodedHeader = base64url(stringifiedHeader); + + const timestamp = Math.floor(new Date().getTime() / 1000); + const expiration = timestamp + oauthTTL; + + // Payload + const data = { + sub: sessionToken, + iat: timestamp, + exp: expiration, + }; + + const stringifiedData = CryptoJS.enc.Utf8.parse(JSON.stringify(data)); + const encodedData = base64url(stringifiedData); + + const token = encodedHeader + '.' + encodedData; + + // Signature + let signature = CryptoJS.HmacSHA256(token, oauthKey); + signature = base64url(signature); + + return { + accessToken: token + '.' + signature, + expires_in: expiration, + }; +}; + +// Valid if token is valid +const validJWT = function (token, secret) { + try { + return jwt.verify(token, secret); + } catch (err) { + return false; + } +}; + +// Parse a JWT informations +const decodeJWT = function (token) { + return jwt.decode(token); +}; + // Whether this auth could possibly modify the given user id. // It still could be forbidden via ACLs even if this returns true. Auth.prototype.isUnauthenticated = function () { @@ -63,6 +136,15 @@ const getAuthForSessionToken = async function ({ }) { cacheController = cacheController || (config && config.cacheController); if (cacheController) { + // Check if you use OAuth to retrieve the sessionToken from within the JWT + if (config.oauth === true) { + if (validJWT(sessionToken, config.oauthKey) === false) { + throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); + } + const decoded = decodeJWT(sessionToken); + sessionToken = decoded.sub; + } + const userJSON = await cacheController.user.get(sessionToken); if (userJSON) { const cachedUser = Parse.Object.fromJSON(userJSON); @@ -321,6 +403,12 @@ const createSession = function ( sessionData.installationId = installationId; } + // Check if you use OAuth to retrieve the sessionToken from within the JWT + // Generate a random hash + if (config.oauth === true) { + sessionData.refreshToken = generateRefreshToken(); + } + Object.assign(sessionData, additionalSessionData); // We need to import RestWrite at this point for the cyclic dependency it has to it const RestWrite = require('./RestWrite'); @@ -339,5 +427,9 @@ module.exports = { readOnly, getAuthForSessionToken, getAuthForLegacySessionToken, + generateRefreshToken, createSession, + createJWT, + validJWT, + decodeJWT, }; diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index e06d1c8fc3..120d3fefd0 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -353,6 +353,22 @@ module.exports.ParseServerOptions = { env: 'PARSE_SERVER_REST_API_KEY', help: 'Key for REST calls', }, + oauth: { + env: 'PARSE_SERVER_OAUTH', + help: 'Sets whether to use the OAuth protocol', + action: parsers.booleanParser, + default: false, + }, + oauthKey: { + env: 'PARSE_SERVER_OAUTH_KEY', + help: 'Key for OAuth protocol', + }, + oauthTTL: { + env: 'PARSE_SERVER_REST_API_KEY', + help: 'The JSON Web Token (JWT) expiration TTL', + action: parsers.numberParser('oauthTTL'), + default: 1800, + }, revokeSessionOnPasswordReset: { env: 'PARSE_SERVER_REVOKE_SESSION_ON_PASSWORD_RESET', help: diff --git a/src/Options/index.js b/src/Options/index.js index 1114e4fe0f..9693361387 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -83,6 +83,16 @@ export interface ParseServerOptions { /* Key for REST calls :ENV: PARSE_SERVER_REST_API_KEY */ restAPIKey: ?string; + /* Enable (or disable) the addition of OAuth 2.0 + :ENV: PARSE_SERVER_OAUTH + :DEFAULT: false */ + oauth: ?boolean; + /* Key for OAuth 2.0 + :ENV: PARSE_SERVER_OAUTH_KEY */ + oauthKey: ?string; + /* The TTL for Access Token + :DEFAULT: 1800 - 30 minutes */ + oauthTTL: ?number; /* Read-only key, which has the same capabilities as MasterKey without writes */ readOnlyMasterKey: ?string; /* Key sent with outgoing webhook calls */ diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index 7843cf4674..be53fabb46 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -139,11 +139,136 @@ export class UsersRouter extends ClassesRouter { }); } + handleCreate(req) { + return rest + .create( + req.config, + req.auth, + this.className(req), + req.body, + req.info.clientSDK, + req.info.context + ) + .then(response => { + response.response.oauth = req.config.oauth === true ? true : false; + if (req.config.oauth === true) { + const token = Auth.createJWT( + response.response.sessionToken, + req.config.oauthKey, + req.config.oauthTTL + ); + response.response.accessToken = token.accessToken; + response.response.expires_in = token.expires_in; + delete response.response.sessionToken; + } + return response; + }); + } + + handleRefresh(req) { + const payload = req.body; + const { client, code } = payload; + + if (!client || !code) { + throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); + } + + // Consulta + const refreshToken = code; + return rest + .find( + req.config, + Auth.master(req.config), + '_Session', + { refreshToken }, + { include: 'user' }, + req.info.clientSDK, + req.info.context + ) + .then(response => { + if (!response.results || response.results.length == 0 || !response.results[0].user) { + throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); + } else { + // Retorno + const data = response.results[0]; + // Novo Code Refresh + const newCode = Auth.generateRefreshToken(); + const sessionId = data.objectId; + const token = Auth.createJWT(data.sessionToken, req.config.oauthKey, req.config.oauthTTL); + + // Atualizar o novo code + req.config.database.update( + '_Session', + { objectId: sessionId }, + { refreshToken: newCode } + ); + + return { + response: { + client: client, + accesstoken: token.accessToken, + refreshToken: newCode, + expires_in: token.expires_in, + }, + }; + } + }); + } + + handleRevoke(req) { + const payload = req.body; + const { code } = payload; + const success = { response: {} }; + + if (!code) { + throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); + } + + return rest + .find( + req.config, + Auth.master(req.config), + '_Session', + { refreshToken: code }, + undefined, + req.info.clientSDK, + req.info.context + ) + .then(records => { + if (records.results && records.results.length) { + return rest + .del( + req.config, + Auth.master(req.config), + '_Session', + records.results[0].objectId, + req.info.context + ) + .then(() => { + this._runAfterLogoutTrigger(req, records.results[0]); + return Promise.resolve(success); + }); + } + return Promise.resolve(success); + }); + } + handleMe(req) { if (!req.info || !req.info.sessionToken) { throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); } - const sessionToken = req.info.sessionToken; + let sessionToken = req.info.sessionToken; + const originalToken = req.info.sessionToken; + + // Check if you use OAuth to retrieve the sessionToken from within the JWT + if (req.config.oauth === true) { + if (Auth.validJWT(sessionToken, req.config.oauthKey) === false) { + throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); + } + const decoded = Auth.decodeJWT(sessionToken); + sessionToken = decoded.sub; + } + return rest .find( req.config, @@ -160,7 +285,14 @@ export class UsersRouter extends ClassesRouter { } else { const user = response.results[0].user; // Send token back on the login, because SDKs expect that. - user.sessionToken = sessionToken; + user.oauth = req.config.oauth === true ? true : false; + if (req.config.oauth === true) { + const decoded = Auth.decodeJWT(originalToken); + user.accessToken = originalToken; + user.expires_in = decoded.exp; + } else { + user.sessionToken = sessionToken; + } // Remove hidden properties. UsersRouter.removeHiddenProperties(user); @@ -227,7 +359,23 @@ export class UsersRouter extends ClassesRouter { installationId: req.info.installationId, }); - user.sessionToken = sessionData.sessionToken; + // Check if you use OAuth to generate a JWT to return + user.oauth = req.config.oauth === true ? true : false; + if (req.config.oauth === true) { + var signedToken = Auth.createJWT( + sessionData.sessionToken, + req.config.oauthKey, + req.config.oauthTTL + ); + + user.accessToken = signedToken.accessToken; + user.refreshToken = sessionData.refreshToken; + user.expires_in = signedToken.expires_in; + + delete user.sessionToken; + } else { + user.sessionToken = sessionData.sessionToken; + } await createSession(); @@ -259,6 +407,12 @@ export class UsersRouter extends ClassesRouter { handleLogOut(req) { const success = { response: {} }; if (req.info && req.info.sessionToken) { + // Check if you use OAuth to retrieve the sessionToken from within the JWT + if (req.config.oauth === true) { + const decoded = Auth.decodeJWT(req.info.sessionToken); + req.info.sessionToken = decoded.sub; + } + return rest .find( req.config, @@ -402,6 +556,12 @@ export class UsersRouter extends ClassesRouter { this.route('GET', '/users/me', req => { return this.handleMe(req); }); + this.route('POST', '/users/refresh', req => { + return this.handleRefresh(req); + }); + this.route('POST', '/users/revoke', req => { + return this.handleRevoke(req); + }); this.route('GET', '/users/:objectId', req => { return this.handleGet(req); }); From 25e5ec4b462c229b732bbb2a6364acb0c50fcf1d Mon Sep 17 00:00:00 2001 From: seasonsolution Date: Wed, 10 Mar 2021 19:47:54 +0000 Subject: [PATCH 05/15] Fix definitions params Co-Authored-By: Manuel <5673677+mtrezza@users.noreply.github.com> Co-Authored-By: Diamond Lewis --- src/Auth.js | 6 +++--- src/Options/Definitions.js | 6 +++--- src/Options/index.js | 5 +++-- src/Routers/UsersRouter.js | 20 ++++++++++---------- 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/Auth.js b/src/Auth.js index a89d8dc7e6..74b387589e 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -51,7 +51,7 @@ const generateRefreshToken = function () { }; // Function to create a token JWT to authentication -const createJWT = function (sessionToken, oauthKey, oauthTTL = 1800) { +const createJWT = function (sessionToken, oauthKey, oauthTTL) { // Header const header = { alg: 'HS256', @@ -137,7 +137,7 @@ const getAuthForSessionToken = async function ({ cacheController = cacheController || (config && config.cacheController); if (cacheController) { // Check if you use OAuth to retrieve the sessionToken from within the JWT - if (config.oauth === true) { + if (config.oauth20 === true) { if (validJWT(sessionToken, config.oauthKey) === false) { throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); } @@ -405,7 +405,7 @@ const createSession = function ( // Check if you use OAuth to retrieve the sessionToken from within the JWT // Generate a random hash - if (config.oauth === true) { + if (config.oauth20 === true) { sessionData.refreshToken = generateRefreshToken(); } diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 120d3fefd0..624068128a 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -353,8 +353,8 @@ module.exports.ParseServerOptions = { env: 'PARSE_SERVER_REST_API_KEY', help: 'Key for REST calls', }, - oauth: { - env: 'PARSE_SERVER_OAUTH', + oauth20: { + env: 'PARSE_SERVER_OAUTH_20', help: 'Sets whether to use the OAuth protocol', action: parsers.booleanParser, default: false, @@ -364,7 +364,7 @@ module.exports.ParseServerOptions = { help: 'Key for OAuth protocol', }, oauthTTL: { - env: 'PARSE_SERVER_REST_API_KEY', + env: 'PARSE_SERVER_OAUTH_TTL', help: 'The JSON Web Token (JWT) expiration TTL', action: parsers.numberParser('oauthTTL'), default: 1800, diff --git a/src/Options/index.js b/src/Options/index.js index 9693361387..927a3d20cc 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -84,13 +84,14 @@ export interface ParseServerOptions { :ENV: PARSE_SERVER_REST_API_KEY */ restAPIKey: ?string; /* Enable (or disable) the addition of OAuth 2.0 - :ENV: PARSE_SERVER_OAUTH + :ENV: PARSE_SERVER_OAUTH_20 :DEFAULT: false */ - oauth: ?boolean; + oauth20: ?boolean; /* Key for OAuth 2.0 :ENV: PARSE_SERVER_OAUTH_KEY */ oauthKey: ?string; /* The TTL for Access Token + :ENV: PARSE_SERVER_OAUTH_TTL :DEFAULT: 1800 - 30 minutes */ oauthTTL: ?number; /* Read-only key, which has the same capabilities as MasterKey without writes */ diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index be53fabb46..a6fcbfc6ab 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -150,8 +150,7 @@ export class UsersRouter extends ClassesRouter { req.info.context ) .then(response => { - response.response.oauth = req.config.oauth === true ? true : false; - if (req.config.oauth === true) { + if (req.config.oauth20 === true) { const token = Auth.createJWT( response.response.sessionToken, req.config.oauthKey, @@ -170,7 +169,7 @@ export class UsersRouter extends ClassesRouter { const { client, code } = payload; if (!client || !code) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); + throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid update token or ClientID'); } // Consulta @@ -187,7 +186,10 @@ export class UsersRouter extends ClassesRouter { ) .then(response => { if (!response.results || response.results.length == 0 || !response.results[0].user) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); + throw new Parse.Error( + Parse.Error.INVALID_SESSION_TOKEN, + 'Invalid update token or ClientID' + ); } else { // Retorno const data = response.results[0]; @@ -261,7 +263,7 @@ export class UsersRouter extends ClassesRouter { const originalToken = req.info.sessionToken; // Check if you use OAuth to retrieve the sessionToken from within the JWT - if (req.config.oauth === true) { + if (req.config.oauth20 === true) { if (Auth.validJWT(sessionToken, req.config.oauthKey) === false) { throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); } @@ -285,8 +287,7 @@ export class UsersRouter extends ClassesRouter { } else { const user = response.results[0].user; // Send token back on the login, because SDKs expect that. - user.oauth = req.config.oauth === true ? true : false; - if (req.config.oauth === true) { + if (req.config.oauth20 === true) { const decoded = Auth.decodeJWT(originalToken); user.accessToken = originalToken; user.expires_in = decoded.exp; @@ -360,8 +361,7 @@ export class UsersRouter extends ClassesRouter { }); // Check if you use OAuth to generate a JWT to return - user.oauth = req.config.oauth === true ? true : false; - if (req.config.oauth === true) { + if (req.config.oauth20 === true) { var signedToken = Auth.createJWT( sessionData.sessionToken, req.config.oauthKey, @@ -408,7 +408,7 @@ export class UsersRouter extends ClassesRouter { const success = { response: {} }; if (req.info && req.info.sessionToken) { // Check if you use OAuth to retrieve the sessionToken from within the JWT - if (req.config.oauth === true) { + if (req.config.oauth20 === true) { const decoded = Auth.decodeJWT(req.info.sessionToken); req.info.sessionToken = decoded.sub; } From 4c2caad070217a21be7309a93559748a2bd696b6 Mon Sep 17 00:00:00 2001 From: seasonsolution Date: Thu, 11 Mar 2021 14:06:14 +0000 Subject: [PATCH 06/15] Remove comments and fix signup Removes unnecessary code comments and corrects the signup method to return the refreshToken. --- CHANGELOG.md | 2 +- src/Auth.js | 11 ----------- src/Routers/UsersRouter.js | 40 +++++++++++++++++++++++++------------- 3 files changed, 27 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f6134dd78..95b2bb6b68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -86,7 +86,7 @@ Jump directly to a version: __BREAKING CHANGES:__ -- NEW: Added a OAuth method to authentication. [#7248](https://github.com/parse-community/parse-server/issues/7248). +- NEW: Added a OAuth 2.0 method to authentication. [#7248](https://github.com/parse-community/parse-server/issues/7248). - NEW: Added file upload restriction. File upload is now only allowed for authenticated users by default for improved security. To allow file upload also for Anonymous Users or Public, set the `fileUpload` parameter in the [Parse Server Options](https://parseplatform.org/parse-server/api/master/ParseServerOptions.html). [#7071](https://github.com/parse-community/parse-server/pull/7071). Thanks to [dblythy](https://github.com/dblythy), [Manuel Trezza](https://github.com/mtrezza). ___ ## Unreleased (Master Branch) diff --git a/src/Auth.js b/src/Auth.js index 74b387589e..183d4e0223 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -30,29 +30,22 @@ function Auth({ this.rolePromise = null; } -// A helper to convert data to base64 encoded to URL function base64url(source) { - // Encode in classical base64 var encodedSource = CryptoJS.enc.Base64.stringify(source); - // Remove padding equal characters encodedSource = encodedSource.replace(/=+$/, ''); - // Replace characters according to base64url specifications encodedSource = encodedSource.replace(/\+/g, '-'); encodedSource = encodedSource.replace(/\//g, '_'); return encodedSource; } -// A helper to generate a random hash const generateRefreshToken = function () { return SHA256(CryptoJS.lib.WordArray.random(256)).toString(); }; -// Function to create a token JWT to authentication const createJWT = function (sessionToken, oauthKey, oauthTTL) { - // Header const header = { alg: 'HS256', typ: 'JWT', @@ -64,7 +57,6 @@ const createJWT = function (sessionToken, oauthKey, oauthTTL) { const timestamp = Math.floor(new Date().getTime() / 1000); const expiration = timestamp + oauthTTL; - // Payload const data = { sub: sessionToken, iat: timestamp, @@ -76,7 +68,6 @@ const createJWT = function (sessionToken, oauthKey, oauthTTL) { const token = encodedHeader + '.' + encodedData; - // Signature let signature = CryptoJS.HmacSHA256(token, oauthKey); signature = base64url(signature); @@ -86,7 +77,6 @@ const createJWT = function (sessionToken, oauthKey, oauthTTL) { }; }; -// Valid if token is valid const validJWT = function (token, secret) { try { return jwt.verify(token, secret); @@ -95,7 +85,6 @@ const validJWT = function (token, secret) { } }; -// Parse a JWT informations const decodeJWT = function (token) { return jwt.decode(token); }; diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index a6fcbfc6ab..ef27e66c3a 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -149,18 +149,34 @@ export class UsersRouter extends ClassesRouter { req.info.clientSDK, req.info.context ) - .then(response => { + .then(res => { if (req.config.oauth20 === true) { - const token = Auth.createJWT( - response.response.sessionToken, - req.config.oauthKey, - req.config.oauthTTL - ); - response.response.accessToken = token.accessToken; - response.response.expires_in = token.expires_in; - delete response.response.sessionToken; + const sessionToken = res.response.sessionToken; + return rest + .find( + req.config, + Auth.master(req.config), + '_Session', + { sessionToken }, + { include: 'user' }, + req.info.clientSDK, + req.info.context + ) + .then(result => { + const user = result.results[0]; + const token = Auth.createJWT( + res.response.sessionToken, + req.config.oauthKey, + req.config.oauthTTL + ); + res.response.accessToken = token.accessToken; + res.response.refreshToken = user.refreshToken; + res.response.expires_in = token.expires_in; + delete res.response.sessionToken; + return res; + }); } - return response; + return res; }); } @@ -172,7 +188,6 @@ export class UsersRouter extends ClassesRouter { throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid update token or ClientID'); } - // Consulta const refreshToken = code; return rest .find( @@ -191,14 +206,11 @@ export class UsersRouter extends ClassesRouter { 'Invalid update token or ClientID' ); } else { - // Retorno const data = response.results[0]; - // Novo Code Refresh const newCode = Auth.generateRefreshToken(); const sessionId = data.objectId; const token = Auth.createJWT(data.sessionToken, req.config.oauthKey, req.config.oauthTTL); - // Atualizar o novo code req.config.database.update( '_Session', { objectId: sessionId }, From 05741920c0e68d6efdf676f344d3ba0e7df09111 Mon Sep 17 00:00:00 2001 From: seasonsolution Date: Fri, 12 Mar 2021 11:58:48 +0000 Subject: [PATCH 07/15] Test codecov 1 --- spec/helper.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/helper.js b/spec/helper.js index dc28ecdc76..85a8de1681 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -84,6 +84,8 @@ const defaultConfiguration = { dotNetKey: 'windows', clientKey: 'client', restAPIKey: 'rest', + oauth20: true, + oauthKey: 'test', webhookKey: 'hook', masterKey: 'test', readOnlyMasterKey: 'read-only-test', From 2f71ecb68fc7094341f1462791236207fb954ce1 Mon Sep 17 00:00:00 2001 From: seasonsolution Date: Sat, 13 Mar 2021 16:34:40 +0000 Subject: [PATCH 08/15] Change name of variables Change name of variables, suggestions from @cbaker6 --- src/Routers/UsersRouter.js | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index ef27e66c3a..45690a2605 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -171,7 +171,7 @@ export class UsersRouter extends ClassesRouter { ); res.response.accessToken = token.accessToken; res.response.refreshToken = user.refreshToken; - res.response.expires_in = token.expires_in; + res.response.expiresIn = token.expires_in; delete res.response.sessionToken; return res; }); @@ -182,13 +182,12 @@ export class UsersRouter extends ClassesRouter { handleRefresh(req) { const payload = req.body; - const { client, code } = payload; + const { refreshToken } = payload; - if (!client || !code) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid update token or ClientID'); + if (!refreshToken) { + throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid update token'); } - const refreshToken = code; return rest .find( req.config, @@ -219,10 +218,9 @@ export class UsersRouter extends ClassesRouter { return { response: { - client: client, accesstoken: token.accessToken, refreshToken: newCode, - expires_in: token.expires_in, + expiresIn: token.expires_in, }, }; } @@ -231,10 +229,10 @@ export class UsersRouter extends ClassesRouter { handleRevoke(req) { const payload = req.body; - const { code } = payload; + const { refreshToken } = payload; const success = { response: {} }; - if (!code) { + if (!refreshToken) { throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); } @@ -243,7 +241,7 @@ export class UsersRouter extends ClassesRouter { req.config, Auth.master(req.config), '_Session', - { refreshToken: code }, + { refreshToken: refreshToken }, undefined, req.info.clientSDK, req.info.context @@ -302,7 +300,7 @@ export class UsersRouter extends ClassesRouter { if (req.config.oauth20 === true) { const decoded = Auth.decodeJWT(originalToken); user.accessToken = originalToken; - user.expires_in = decoded.exp; + user.expiresIn = decoded.exp; } else { user.sessionToken = sessionToken; } @@ -382,7 +380,7 @@ export class UsersRouter extends ClassesRouter { user.accessToken = signedToken.accessToken; user.refreshToken = sessionData.refreshToken; - user.expires_in = signedToken.expires_in; + user.expiresIn = signedToken.expires_in; delete user.sessionToken; } else { @@ -571,7 +569,7 @@ export class UsersRouter extends ClassesRouter { this.route('POST', '/users/refresh', req => { return this.handleRefresh(req); }); - this.route('POST', '/users/revoke', req => { + this.route('POST', '/revoke', req => { return this.handleRevoke(req); }); this.route('GET', '/users/:objectId', req => { From de9e398e357a832d611f9206b06a587e70f7121f Mon Sep 17 00:00:00 2001 From: seasonsolution Date: Sat, 13 Mar 2021 17:04:46 +0000 Subject: [PATCH 09/15] Update helper.js --- spec/helper.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/spec/helper.js b/spec/helper.js index 85a8de1681..dc28ecdc76 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -84,8 +84,6 @@ const defaultConfiguration = { dotNetKey: 'windows', clientKey: 'client', restAPIKey: 'rest', - oauth20: true, - oauthKey: 'test', webhookKey: 'hook', masterKey: 'test', readOnlyMasterKey: 'read-only-test', From 6d8efae4b2db7baba047e094fdc52856f81ebd68 Mon Sep 17 00:00:00 2001 From: seasonsolution Date: Sat, 13 Mar 2021 17:33:23 +0000 Subject: [PATCH 10/15] Update UsersRouter.js --- src/Routers/UsersRouter.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index 45690a2605..dd59acdf9c 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -171,7 +171,7 @@ export class UsersRouter extends ClassesRouter { ); res.response.accessToken = token.accessToken; res.response.refreshToken = user.refreshToken; - res.response.expiresIn = token.expires_in; + res.response.expiresAt = token.expires_in; delete res.response.sessionToken; return res; }); @@ -220,7 +220,7 @@ export class UsersRouter extends ClassesRouter { response: { accesstoken: token.accessToken, refreshToken: newCode, - expiresIn: token.expires_in, + expiresAt: token.expires_in, }, }; } @@ -300,7 +300,7 @@ export class UsersRouter extends ClassesRouter { if (req.config.oauth20 === true) { const decoded = Auth.decodeJWT(originalToken); user.accessToken = originalToken; - user.expiresIn = decoded.exp; + user.expiresAt = decoded.exp; } else { user.sessionToken = sessionToken; } @@ -380,7 +380,7 @@ export class UsersRouter extends ClassesRouter { user.accessToken = signedToken.accessToken; user.refreshToken = sessionData.refreshToken; - user.expiresIn = signedToken.expires_in; + user.expiresAt = signedToken.expires_in; delete user.sessionToken; } else { From 9b804550aea201a711c3e2806da2284057a8466c Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Mon, 15 Mar 2021 15:13:44 -0500 Subject: [PATCH 11/15] initial tests --- spec/Auth.spec.js | 64 +++++++------ spec/ParseUser.spec.js | 140 ++++++++++++++++++++++++++++ src/Auth.js | 26 ++---- src/Routers/UsersRouter.js | 186 +++++++++++++++---------------------- 4 files changed, 257 insertions(+), 159 deletions(-) diff --git a/spec/Auth.spec.js b/spec/Auth.spec.js index 5ed6bfe941..7bc27f9afb 100644 --- a/spec/Auth.spec.js +++ b/spec/Auth.spec.js @@ -1,7 +1,13 @@ 'use strict'; describe('Auth', () => { - const { Auth, getAuthForSessionToken } = require('../lib/Auth.js'); + const { + Auth, + getAuthForSessionToken, + createJWT, + validJWT, + decodeJWT, + } = require('../lib/Auth.js'); const Config = require('../lib/Config'); describe('getUserRoles', () => { let auth; @@ -123,35 +129,6 @@ describe('Auth', () => { expect(userAuth.user.id).toBe(user.id); }); - it('should load auth without a config', async () => { - const user = new Parse.User(); - await user.signUp({ - username: 'hello', - password: 'password', - }); - expect(user.getSessionToken()).not.toBeUndefined(); - const userAuth = await getAuthForSessionToken({ - sessionToken: user.getSessionToken(), - }); - expect(userAuth.user instanceof Parse.User).toBe(true); - expect(userAuth.user.id).toBe(user.id); - }); - - it('should load auth with a config', async () => { - const user = new Parse.User(); - await user.signUp({ - username: 'hello', - password: 'password', - }); - expect(user.getSessionToken()).not.toBeUndefined(); - const userAuth = await getAuthForSessionToken({ - sessionToken: user.getSessionToken(), - config: Config.get('test'), - }); - expect(userAuth.user instanceof Parse.User).toBe(true); - expect(userAuth.user.id).toBe(user.id); - }); - describe('getRolesForUser', () => { const rolesNumber = 100; @@ -241,4 +218,31 @@ describe('Auth', () => { expect(cloudRoles2.length).toBe(rolesNumber); }); }); + + describe('OAuth2.0 JWT', () => { + it('should handle jwt', async () => { + const oauthKey = 'jwt-secret'; + const oauthTTL = 100; + const user = new Parse.User(); + await user.signUp({ + username: 'jwt-test', + password: 'jwt-password', + }); + const sessionToken = user.getSessionToken(); + + const jwt = createJWT(sessionToken, oauthKey, oauthTTL); + expect(jwt.accessToken).toBeDefined(); + expect(jwt.expires_in).toBeDefined(); + + const isValid = validJWT('invalid', oauthKey); + expect(isValid).toBe(false); + + const result = validJWT(jwt.accessToken, oauthKey); + expect(result.sub).toBe(sessionToken); + expect(result.exp).toBe(jwt.expires_in); + + const decoded = decodeJWT(jwt.accessToken); + expect(result).toEqual(decoded); + }); + }); }); diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index a44926caa4..2181d4c2e6 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -3925,6 +3925,146 @@ describe('Parse.User testing', () => { } }); + it('user signup with JWT', async () => { + const oauthKey = 'jwt-secret'; + const oauthTTL = 100; + await reconfigureServer({ + oauth20: true, + oauthKey, + oauthTTL, + }); + let response = await request({ + method: 'POST', + url: 'http://localhost:8378/1/users', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + 'Content-Type': 'application/json', + }, + body: { + username: 'jwt-test', + password: 'jwt-password', + }, + }); + const { accessToken, refreshToken, expiresAt, sessionToken } = response.data; + expect(accessToken).toBeDefined(); + expect(refreshToken).toBeDefined(); + expect(expiresAt).toBeDefined(); + expect(sessionToken).toBeUndefined(); + + response = await request({ + method: 'POST', + url: 'http://localhost:8378/1/users/refresh', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + 'Content-Type': 'application/json', + }, + body: { + refreshToken, + }, + }); + const jwt = response.data; + expect(jwt.accessToken).toBe(accessToken); + expect(jwt.expiresAt).toBe(expiresAt); + expect(jwt.refreshToken).not.toBe(refreshToken); + + const query = new Parse.Query('_Session'); + query.equalTo('refreshToken', jwt.refreshToken); + let session = await query.first({ useMasterKey: true }); + expect(session).toBeDefined(); + + await request({ + method: 'POST', + url: 'http://localhost:8378/1/users/revoke', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + 'Content-Type': 'application/json', + }, + body: { + refreshToken: jwt.refreshToken, + }, + }); + session = await query.first({ useMasterKey: true }); + expect(session).toBeUndefined(); + + try { + await request({ + method: 'POST', + url: 'http://localhost:8378/1/users/refresh', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + 'Content-Type': 'application/json', + }, + body: { + refreshToken: jwt.refreshToken, + }, + }); + fail(); + } catch (response) { + const { code, error } = response.data; + expect(code).toBe(Parse.Error.INVALID_SESSION_TOKEN); + expect(error).toBe('Invalid refresh token'); + } + + response = await request({ + method: 'POST', + url: 'http://localhost:8378/1/users/revoke', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + 'Content-Type': 'application/json', + }, + body: { + refreshToken: jwt.refreshToken, + }, + }); + expect(response.data).toEqual({}); + }); + + it('handle JWT errors', async () => { + try { + await request({ + method: 'POST', + url: 'http://localhost:8378/1/users/refresh', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + 'Content-Type': 'application/json', + }, + body: { + refreshToken: null, + }, + }); + fail(); + } catch (response) { + const { code, error } = response.data; + expect(code).toBe(Parse.Error.INVALID_SESSION_TOKEN); + expect(error).toBe('Invalid refresh token'); + } + try { + await request({ + method: 'POST', + url: 'http://localhost:8378/1/users/revoke', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + 'Content-Type': 'application/json', + }, + body: { + refreshToken: null, + }, + }); + fail(); + } catch (response) { + const { code, error } = response.data; + expect(code).toBe(Parse.Error.INVALID_SESSION_TOKEN); + expect(error).toBe('Invalid refresh token'); + } + }); + describe('issue #4897', () => { it_only_db('mongo')('should be able to login with a legacy user (no ACL)', async () => { // This issue is a side effect of the locked users and legacy users which don't have ACL's diff --git a/src/Auth.js b/src/Auth.js index 183d4e0223..4ea483cc92 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -30,42 +30,34 @@ function Auth({ this.rolePromise = null; } -function base64url(source) { - var encodedSource = CryptoJS.enc.Base64.stringify(source); - +const base64url = source => { + let encodedSource = CryptoJS.enc.Base64.stringify(source); encodedSource = encodedSource.replace(/=+$/, ''); - encodedSource = encodedSource.replace(/\+/g, '-'); encodedSource = encodedSource.replace(/\//g, '_'); - return encodedSource; -} +}; -const generateRefreshToken = function () { +const generateRefreshToken = () => { return SHA256(CryptoJS.lib.WordArray.random(256)).toString(); }; -const createJWT = function (sessionToken, oauthKey, oauthTTL) { +const createJWT = (sessionToken, oauthKey, oauthTTL) => { const header = { alg: 'HS256', typ: 'JWT', }; - const stringifiedHeader = CryptoJS.enc.Utf8.parse(JSON.stringify(header)); const encodedHeader = base64url(stringifiedHeader); - const timestamp = Math.floor(new Date().getTime() / 1000); const expiration = timestamp + oauthTTL; - const data = { sub: sessionToken, iat: timestamp, exp: expiration, }; - const stringifiedData = CryptoJS.enc.Utf8.parse(JSON.stringify(data)); const encodedData = base64url(stringifiedData); - const token = encodedHeader + '.' + encodedData; let signature = CryptoJS.HmacSHA256(token, oauthKey); @@ -77,7 +69,7 @@ const createJWT = function (sessionToken, oauthKey, oauthTTL) { }; }; -const validJWT = function (token, secret) { +const validJWT = (token, secret) => { try { return jwt.verify(token, secret); } catch (err) { @@ -85,7 +77,7 @@ const validJWT = function (token, secret) { } }; -const decodeJWT = function (token) { +const decodeJWT = token => { return jwt.decode(token); }; @@ -125,7 +117,6 @@ const getAuthForSessionToken = async function ({ }) { cacheController = cacheController || (config && config.cacheController); if (cacheController) { - // Check if you use OAuth to retrieve the sessionToken from within the JWT if (config.oauth20 === true) { if (validJWT(sessionToken, config.oauthKey) === false) { throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); @@ -133,7 +124,6 @@ const getAuthForSessionToken = async function ({ const decoded = decodeJWT(sessionToken); sessionToken = decoded.sub; } - const userJSON = await cacheController.user.get(sessionToken); if (userJSON) { const cachedUser = Parse.Object.fromJSON(userJSON); @@ -392,8 +382,6 @@ const createSession = function ( sessionData.installationId = installationId; } - // Check if you use OAuth to retrieve the sessionToken from within the JWT - // Generate a random hash if (config.oauth20 === true) { sessionData.refreshToken = generateRefreshToken(); } diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index dd59acdf9c..b333ad6b8b 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -139,130 +139,97 @@ export class UsersRouter extends ClassesRouter { }); } - handleCreate(req) { - return rest - .create( + async handleCreate(req) { + const res = await rest.create( + req.config, + req.auth, + this.className(req), + req.body, + req.info.clientSDK, + req.info.context + ); + if (req.config.oauth20 === true) { + const sessionToken = res.response.sessionToken; + const result = await rest.find( req.config, - req.auth, - this.className(req), - req.body, + Auth.master(req.config), + '_Session', + { sessionToken }, + { include: 'user' }, req.info.clientSDK, req.info.context - ) - .then(res => { - if (req.config.oauth20 === true) { - const sessionToken = res.response.sessionToken; - return rest - .find( - req.config, - Auth.master(req.config), - '_Session', - { sessionToken }, - { include: 'user' }, - req.info.clientSDK, - req.info.context - ) - .then(result => { - const user = result.results[0]; - const token = Auth.createJWT( - res.response.sessionToken, - req.config.oauthKey, - req.config.oauthTTL - ); - res.response.accessToken = token.accessToken; - res.response.refreshToken = user.refreshToken; - res.response.expiresAt = token.expires_in; - delete res.response.sessionToken; - return res; - }); - } - return res; - }); + ); + const user = result.results[0]; + const token = Auth.createJWT(sessionToken, req.config.oauthKey, req.config.oauthTTL); + res.response.accessToken = token.accessToken; + res.response.refreshToken = user.refreshToken; + res.response.expiresAt = token.expires_in; + delete res.response.sessionToken; + } + return res; } - handleRefresh(req) { - const payload = req.body; - const { refreshToken } = payload; - + async handleRefresh(req) { + const { refreshToken } = req.body; if (!refreshToken) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid update token'); + throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid refresh token'); } - - return rest - .find( - req.config, - Auth.master(req.config), + const res = await rest.find( + req.config, + Auth.master(req.config), + '_Session', + { refreshToken }, + { include: 'user' }, + req.info.clientSDK, + req.info.context + ); + if (!res.results || res.results.length == 0 || !res.results[0].user) { + throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid refresh token'); + } else { + const data = res.results[0]; + const newCode = Auth.generateRefreshToken(); + const sessionId = data.objectId; + const token = Auth.createJWT(data.sessionToken, req.config.oauthKey, req.config.oauthTTL); + await req.config.database.update( '_Session', - { refreshToken }, - { include: 'user' }, - req.info.clientSDK, - req.info.context - ) - .then(response => { - if (!response.results || response.results.length == 0 || !response.results[0].user) { - throw new Parse.Error( - Parse.Error.INVALID_SESSION_TOKEN, - 'Invalid update token or ClientID' - ); - } else { - const data = response.results[0]; - const newCode = Auth.generateRefreshToken(); - const sessionId = data.objectId; - const token = Auth.createJWT(data.sessionToken, req.config.oauthKey, req.config.oauthTTL); - - req.config.database.update( - '_Session', - { objectId: sessionId }, - { refreshToken: newCode } - ); - - return { - response: { - accesstoken: token.accessToken, - refreshToken: newCode, - expiresAt: token.expires_in, - }, - }; - } - }); + { objectId: sessionId }, + { refreshToken: newCode } + ); + return { + response: { + accessToken: token.accessToken, + refreshToken: newCode, + expiresAt: token.expires_in, + }, + }; + } } - handleRevoke(req) { - const payload = req.body; - const { refreshToken } = payload; - const success = { response: {} }; - + async handleRevoke(req) { + const { refreshToken } = req.body; if (!refreshToken) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); + throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid refresh token'); } - - return rest - .find( + const res = await rest.find( + req.config, + Auth.master(req.config), + '_Session', + { refreshToken: refreshToken }, + undefined, + req.info.clientSDK, + req.info.context + ); + if (res.results && res.results.length) { + await rest.del( req.config, Auth.master(req.config), '_Session', - { refreshToken: refreshToken }, - undefined, - req.info.clientSDK, + res.results[0].objectId, req.info.context - ) - .then(records => { - if (records.results && records.results.length) { - return rest - .del( - req.config, - Auth.master(req.config), - '_Session', - records.results[0].objectId, - req.info.context - ) - .then(() => { - this._runAfterLogoutTrigger(req, records.results[0]); - return Promise.resolve(success); - }); - } - return Promise.resolve(success); - }); + ); + this._runAfterLogoutTrigger(req, res.results[0]); + } + return { response: {} }; } handleMe(req) { @@ -296,7 +263,6 @@ export class UsersRouter extends ClassesRouter { throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); } else { const user = response.results[0].user; - // Send token back on the login, because SDKs expect that. if (req.config.oauth20 === true) { const decoded = Auth.decodeJWT(originalToken); user.accessToken = originalToken; @@ -569,7 +535,7 @@ export class UsersRouter extends ClassesRouter { this.route('POST', '/users/refresh', req => { return this.handleRefresh(req); }); - this.route('POST', '/revoke', req => { + this.route('POST', '/users/revoke', req => { return this.handleRevoke(req); }); this.route('GET', '/users/:objectId', req => { From 87419fb871780d81cd2eb246dd86dba891609a8d Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Tue, 16 Mar 2021 16:14:47 -0400 Subject: [PATCH 12/15] send expiresAt response as date --- spec/Auth.spec.js | 1 - spec/ParseUser.spec.js | 2 +- src/Auth.js | 6 ++++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/spec/Auth.spec.js b/spec/Auth.spec.js index 7bc27f9afb..a97ec65680 100644 --- a/spec/Auth.spec.js +++ b/spec/Auth.spec.js @@ -239,7 +239,6 @@ describe('Auth', () => { const result = validJWT(jwt.accessToken, oauthKey); expect(result.sub).toBe(sessionToken); - expect(result.exp).toBe(jwt.expires_in); const decoded = decodeJWT(jwt.accessToken); expect(result).toEqual(decoded); diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index 2e2630c623..40ffadb567 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -3970,7 +3970,7 @@ describe('Parse.User testing', () => { }); const jwt = response.data; expect(jwt.accessToken).toBe(accessToken); - expect(jwt.expiresAt).toBe(expiresAt); + expect(jwt.expiresAt).toBeDefined(); expect(jwt.refreshToken).not.toBe(refreshToken); const query = new Parse.Query('_Session'); diff --git a/src/Auth.js b/src/Auth.js index 4ea483cc92..88e607bd98 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -49,7 +49,8 @@ const createJWT = (sessionToken, oauthKey, oauthTTL) => { }; const stringifiedHeader = CryptoJS.enc.Utf8.parse(JSON.stringify(header)); const encodedHeader = base64url(stringifiedHeader); - const timestamp = Math.floor(new Date().getTime() / 1000); + var currentTime = new Date(); + const timestamp = Math.floor(currentTime.getTime() / 1000); const expiration = timestamp + oauthTTL; const data = { sub: sessionToken, @@ -62,10 +63,11 @@ const createJWT = (sessionToken, oauthKey, oauthTTL) => { let signature = CryptoJS.HmacSHA256(token, oauthKey); signature = base64url(signature); + currentTime.setSeconds(currentTime.getSeconds() + 1800); return { accessToken: token + '.' + signature, - expires_in: expiration, + expires_in: { __type: 'Date', iso: currentTime.toISOString() }, }; }; From 77c5cc4767bf2d16a943f9f6df76145489525903 Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Tue, 16 Mar 2021 16:19:45 -0400 Subject: [PATCH 13/15] update testcase --- spec/Auth.spec.js | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/Auth.spec.js b/spec/Auth.spec.js index a97ec65680..dd6d20ffdd 100644 --- a/spec/Auth.spec.js +++ b/spec/Auth.spec.js @@ -239,6 +239,7 @@ describe('Auth', () => { const result = validJWT(jwt.accessToken, oauthKey); expect(result.sub).toBe(sessionToken); + expect(result.exp).toBeDefined(); const decoded = decodeJWT(jwt.accessToken); expect(result).toEqual(decoded); From 557e92f0bb75e01a65bb5d7407948a8ce9ef70dd Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Tue, 16 Mar 2021 16:25:11 -0400 Subject: [PATCH 14/15] fix time declaration --- src/Auth.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Auth.js b/src/Auth.js index 88e607bd98..103b8c5129 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -49,7 +49,7 @@ const createJWT = (sessionToken, oauthKey, oauthTTL) => { }; const stringifiedHeader = CryptoJS.enc.Utf8.parse(JSON.stringify(header)); const encodedHeader = base64url(stringifiedHeader); - var currentTime = new Date(); + const currentTime = new Date(); const timestamp = Math.floor(currentTime.getTime() / 1000); const expiration = timestamp + oauthTTL; const data = { From 4a0d22ab3ede2189f2d067192492e02cde15e6f8 Mon Sep 17 00:00:00 2001 From: seasonsolution Date: Sun, 21 Mar 2021 18:27:46 +0000 Subject: [PATCH 15/15] Fix expiresAt param Put oauthTTL instead of the fixed value and put the same return in /users/me --- src/Auth.js | 2 +- src/Routers/UsersRouter.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Auth.js b/src/Auth.js index 103b8c5129..34f3eb9796 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -63,7 +63,7 @@ const createJWT = (sessionToken, oauthKey, oauthTTL) => { let signature = CryptoJS.HmacSHA256(token, oauthKey); signature = base64url(signature); - currentTime.setSeconds(currentTime.getSeconds() + 1800); + currentTime.setSeconds(currentTime.getSeconds() + oauthTTL); return { accessToken: token + '.' + signature, diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index b333ad6b8b..ea51a8a495 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -265,8 +265,9 @@ export class UsersRouter extends ClassesRouter { const user = response.results[0].user; if (req.config.oauth20 === true) { const decoded = Auth.decodeJWT(originalToken); + const expiresDate = new Date(decoded.exp * 1000); user.accessToken = originalToken; - user.expiresAt = decoded.exp; + user.expiresAt = { __type: 'Date', iso: expiresDate.toISOString() }; } else { user.sessionToken = sessionToken; }