Skip to content

Improved Auth Spec & challenge endpoint #1276

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: alpha
Choose a base branch
from
Open
Changes from all 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
42 changes: 20 additions & 22 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -5,32 +5,30 @@ on:
- master
pull_request:
branches:
- '**'
- "**"
jobs:
build:
runs-on: ubuntu-18.04
timeout-minutes: 30
env:
env:
MONGODB_VERSION: 3.6.9
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: '10.14'
- name: Cache Node.js modules
uses: actions/cache@v2
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: "10.14"
- name: Cache Node.js modules
uses: actions/cache@v2
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- run: npm ci
- run: npm install -g mongodb-runner
- run: mongodb-runner start
- run: npm run lint
- run: npm test -- --maxWorkers=4
- run: npm run integration
env:
CI: true
- run: bash <(curl -s https://codecov.io/bash)
- run: npm ci
- run: npm run lint
- run: npm test -- --maxWorkers=4
- run: npm run integration
env:
CI: true
- run: bash <(curl -s https://codecov.io/bash)
2,223 changes: 1,730 additions & 493 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -75,9 +75,11 @@
"jsdoc-babel": "0.5.0",
"lint-staged": "10.5.3",
"metro-react-native-babel-preset": "0.59.0",
"mongodb-runner": "mongodb-js/runner",
"parse-server": "github:parse-community/parse-server#master",
"prettier": "2.2.1",
"regenerator-runtime": "0.13.5",
"start-server-and-test": "^1.11.6",
"vinyl-source-stream": "2.0.0"
},
"optionalDependencies": {
@@ -93,7 +95,9 @@
"watch:browser": "cross-env PARSE_BUILD=browser npm run watch",
"watch:node": "cross-env PARSE_BUILD=node npm run watch",
"watch:react-native": "cross-env PARSE_BUILD=react-native npm run watch",
"integration": "cross-env TESTING=1 jasmine --config=jasmine.json",
"integration": " cross-env TESTING=1 jasmine --config=jasmine.json",
"preintegration": "mongodb-runner start",
"postintegration": "mongodb-runner stop",
"docs": "jsdoc -c ./jsdoc-conf.json ./src",
"prepare": "npm run build",
"release_docs": "./release_docs.sh",
129 changes: 108 additions & 21 deletions src/ParseUser.js
Original file line number Diff line number Diff line change
@@ -636,21 +636,74 @@ class ParseUser extends ParseObject {
* saves the session to disk, so you can retrieve the currently logged in
* user using <code>current</code>.
*
* @param {string} username The username (or email) to log in with.
* @param {string} password The password to log in with.
* @param {string | object} usernameOrObject The username (or email) to log in with or an object with auth fields.
* @param {string | object} passwordOrOptions The password to log in with or options if usernameOrObject is an object.
* @param {object} options
* @static
* @returns {Promise} A promise that is fulfilled with the user when
* the login completes.
*/
static logIn(username: string, password: string, options?: FullOptions) {
if (typeof username !== 'string') {
return Promise.reject(new ParseError(ParseError.OTHER_CAUSE, 'Username must be a string.'));
} else if (typeof password !== 'string') {
static logIn(
usernameOrObject: string | Object,
passwordOrOptions: string | FullOptions,
options?: FullOptions
) {
const usernameIsObject = typeof usernameOrObject === 'object';
if (typeof usernameOrObject !== 'string') {
if (!usernameIsObject) {
return Promise.reject(
new ParseError(
ParseError.OTHER_CAUSE,
'Username must be a string or an object with username and password keys.'
)
);
}
if (typeof usernameOrObject.username !== 'string') {
return Promise.reject(
new ParseError(
ParseError.OTHER_CAUSE,
'Auth payload should contain username key with string value'
)
);
}
if (typeof usernameOrObject.password !== 'string') {
return Promise.reject(
new ParseError(
ParseError.OTHER_CAUSE,
'Auth payload should contain password key with string value'
)
);
}
if (usernameOrObject.authData && typeof usernameOrObject.authData !== 'object') {
return Promise.reject(
new ParseError(ParseError.OTHER_CAUSE, 'authData should be an object')
);
}
if (
Object.keys(usernameOrObject).some(
key => !['authData', 'username', 'password'].includes(key)
)
) {
return Promise.reject(
new ParseError(
ParseError.OTHER_CAUSE,
'This operation only support authData, username and password keys'
)
);
}
}
if (typeof passwordOrOptions !== 'string' && !usernameIsObject) {
return Promise.reject(new ParseError(ParseError.OTHER_CAUSE, 'Password must be a string.'));
}
const user = new this();
user._finishFetch({ username: username, password: password });
user._finishFetch(
usernameIsObject
? usernameOrObject
: {
username: usernameOrObject,
password: passwordOrOptions,
}
);
return user.logIn(options);
}

@@ -737,6 +790,32 @@ class ParseUser extends ParseObject {
return user.linkWith(provider, options, saveOpts);
}

/**
* Ask Parse server for an auth challenge (ex: WebAuthn login/signup)
*
* @param data
* @static
* @returns {Promise}
*/
static challenge(data: {
authData?: AuthData,
username?: string,
password?: string,
challengeData: any,
}): Promise<{ challengeData: any }> {
if (!data.challengeData) {
return Promise.reject(
new ParseError(ParseError.OTHER_CAUSE, 'challengeData is required for the challenge.')
);
}
if (data.username && !data.password) {
return Promise.reject(
new ParseError(ParseError.OTHER_CAUSE, 'Running challenge via username require password.')
);
}
return CoreManager.getUserController().challenge(data);
}

/**
* Logs out the currently logged in user session. This will remove the
* session from disk, log out of linked services, and future calls to
@@ -1080,22 +1159,30 @@ const DefaultController = {
const auth = {
username: user.get('username'),
password: user.get('password'),
authData: user.get('authData'),
};
return RESTController.request(options.usePost ? 'POST' : 'GET', 'login', auth, options).then(
response => {
user._migrateId(response.objectId);
user._setExisted(true);
stateController.setPendingOp(user._getStateIdentifier(), 'username', undefined);
stateController.setPendingOp(user._getStateIdentifier(), 'password', undefined);
response.password = undefined;
user._finishFetch(response);
if (!canUseCurrentUser) {
// We can't set the current user, so just return the one we logged in
return Promise.resolve(user);
}
return DefaultController.setCurrentUser(user);
return RESTController.request(
options.usePost || auth.authData ? 'POST' : 'GET',
'login',
auth,
options
).then(response => {
user._migrateId(response.objectId);
user._setExisted(true);
stateController.setPendingOp(user._getStateIdentifier(), 'username', undefined);
stateController.setPendingOp(user._getStateIdentifier(), 'password', undefined);
response.password = undefined;
user._finishFetch(response);
if (!canUseCurrentUser) {
// We can't set the current user, so just return the one we logged in
return Promise.resolve(user);
}
);
return DefaultController.setCurrentUser(user);
});
},

challenge(data: any): Promise<{ challengeData: any }> {
return CoreManager.getRESTController().request('POST', 'challenge', data);
},

become(user: ParseUser, options: RequestOptions): Promise<ParseUser> {
139 changes: 136 additions & 3 deletions src/__tests__/ParseUser-test.js
Original file line number Diff line number Diff line change
@@ -322,12 +322,14 @@ describe('ParseUser', () => {
it('fail login when invalid username or password is used', done => {
ParseUser.enableUnsafeCurrentUser();
ParseUser._clearCache();
ParseUser.logIn({}, 'password')
ParseUser.logIn(undefined, 'password')
.then(null, err => {
expect(err.code).toBe(ParseError.OTHER_CAUSE);
expect(err.message).toBe('Username must be a string.');
expect(err.message).toBe(
'Username must be a string or an object with username and password keys.'
);

return ParseUser.logIn('username', {});
return ParseUser.logIn('username', undefined);
})
.then(null, err => {
expect(err.code).toBe(ParseError.OTHER_CAUSE);
@@ -337,6 +339,92 @@ describe('ParseUser', () => {
});
});

it('fail login when invalid object passed via username', async () => {
ParseUser.enableUnsafeCurrentUser();
ParseUser._clearCache();
try {
await ParseUser.logIn({}, 'password');
expect(false).toBeTrue();
} catch (err) {
expect(err.code).toBe(ParseError.OTHER_CAUSE);
expect(err.message).toBe('Auth payload should contain username key with string value');
}
try {
await ParseUser.logIn({ username: 'test' }, {});
expect(false).toBeTrue();
} catch (err) {
expect(err.code).toBe(ParseError.OTHER_CAUSE);
expect(err.message).toBe('Auth payload should contain password key with string value');
}
try {
await ParseUser.logIn({ username: 'test', password: 'test', authData: 'test' }, {});
expect(false).toBeTrue();
} catch (err) {
expect(err.code).toBe(ParseError.OTHER_CAUSE);
expect(err.message).toBe('authData should be an object');
}
try {
await ParseUser.logIn(
{
username: 'test',
password: 'test',
authData: { test: true },
aField: true,
},
{}
);
expect(false).toBeTrue();
} catch (err) {
expect(err.code).toBe(ParseError.OTHER_CAUSE);
expect(err.message).toBe('This operation only support authData, username and password keys');
}
});

it('should login with username as object', async () => {
ParseUser.enableUnsafeCurrentUser();
ParseUser._clearCache();
CoreManager.setRESTController({
request(method, path, body) {
expect(method).toBe('POST');
expect(path).toBe('login');
expect(body.username).toBe('username');
expect(body.password).toBe('password');
expect(body.authData).toEqual({ test: { data: true } });

return Promise.resolve(
{
objectId: 'uid2',
username: 'username',
sessionToken: '123abc',
authDataResponse: {
test: { challenge: 'test' },
},
},
200
);
},
ajax() {},
});
const u = await ParseUser.logIn({
username: 'username',
password: 'password',
authData: { test: { data: true } },
});
expect(u.id).toBe('uid2');
expect(u.getSessionToken()).toBe('123abc');
expect(u.isCurrent()).toBe(true);
expect(u.authenticated()).toBe(true);
expect(ParseUser.current()).toBe(u);
ParseUser._clearCache();
const current = ParseUser.current();
expect(current instanceof ParseUser).toBe(true);
expect(current.id).toBe('uid2');
expect(current.authenticated()).toBe(true);
expect(current.get('authDataResponse')).toEqual({
test: { challenge: 'test' },
});
});

it('preserves changes when logging in', done => {
ParseUser.enableUnsafeCurrentUser();
ParseUser._clearCache();
@@ -1843,4 +1931,49 @@ describe('ParseUser', () => {
const user = new CustomUser();
expect(user.test).toBe(true);
});

it('should return challenge', async () => {
ParseUser.enableUnsafeCurrentUser();
ParseUser._clearCache();
CoreManager.setRESTController({
request(method, path, body) {
expect(method).toBe('POST');
expect(path).toBe('challenge');
expect(body.username).toBe('username');
expect(body.password).toBe('password');
expect(body.challengeData).toEqual({ test: { data: true } });

return Promise.resolve(
{
challengeData: { test: { token: true } },
},
200
);
},
ajax() {},
});

try {
await ParseUser.challenge({
username: 'username',
challengeData: { test: { data: true } },
});
} catch (e) {
expect(e.message).toContain('Running challenge via username require password.');
}

try {
await ParseUser.challenge({});
} catch (e) {
expect(e.message).toContain('challengeData is required for the challenge.');
}

const res = await ParseUser.challenge({
username: 'username',
password: 'password',
challengeData: { test: { data: true } },
});

expect(res.challengeData).toEqual({ test: { token: true } });
});
});