Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ Jump directly to a version:
| | [2.0.8](#208) |
</details>

__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).
___
## Unreleased (Master Branch)
[Full Changelog](https://github.com/parse-community/parse-server/compare/4.5.0...master)
Expand Down
3 changes: 1 addition & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
92 changes: 92 additions & 0 deletions src/Auth.js
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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) {
// 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 () {
Expand Down Expand Up @@ -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.oauth20 === 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);
Expand Down Expand Up @@ -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.oauth20 === 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');
Expand All @@ -339,5 +427,9 @@ module.exports = {
readOnly,
getAuthForSessionToken,
getAuthForLegacySessionToken,
generateRefreshToken,
createSession,
createJWT,
validJWT,
decodeJWT,
};
16 changes: 16 additions & 0 deletions src/Options/Definitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,22 @@ module.exports.ParseServerOptions = {
env: 'PARSE_SERVER_REST_API_KEY',
help: 'Key for REST calls',
},
oauth20: {
env: 'PARSE_SERVER_OAUTH_20',
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_OAUTH_TTL',
help: 'The JSON Web Token (JWT) expiration TTL',
action: parsers.numberParser('oauthTTL'),
default: 1800,
},
revokeSessionOnPasswordReset: {
env: 'PARSE_SERVER_REVOKE_SESSION_ON_PASSWORD_RESET',
help:
Expand Down
11 changes: 11 additions & 0 deletions src/Options/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,17 @@ 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_20
:DEFAULT: false */
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 */
readOnlyMasterKey: ?string;
/* Key sent with outgoing webhook calls */
Expand Down
166 changes: 163 additions & 3 deletions src/Routers/UsersRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,138 @@ 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 => {
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;
}
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 update token or ClientID');
}

// 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 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 },
{ 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.oauth20 === 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,
Expand All @@ -160,7 +287,13 @@ 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;
if (req.config.oauth20 === true) {
const decoded = Auth.decodeJWT(originalToken);
user.accessToken = originalToken;
user.expires_in = decoded.exp;
} else {
user.sessionToken = sessionToken;
}

// Remove hidden properties.
UsersRouter.removeHiddenProperties(user);
Expand Down Expand Up @@ -227,7 +360,22 @@ export class UsersRouter extends ClassesRouter {
installationId: req.info.installationId,
});

user.sessionToken = sessionData.sessionToken;
// Check if you use OAuth to generate a JWT to return
if (req.config.oauth20 === 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();

Expand Down Expand Up @@ -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.oauth20 === true) {
const decoded = Auth.decodeJWT(req.info.sessionToken);
req.info.sessionToken = decoded.sub;
}

return rest
.find(
req.config,
Expand Down Expand Up @@ -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);
});
Expand Down