Skip to content

Commit

Permalink
Add support for Facebook Limited Login (#7219)
Browse files Browse the repository at this point in the history
* Add support for Facebook Limited auth

* Add tests

* Fix tests

* Fix tests

* Add entry to changelog

* Cleanup
  • Loading branch information
miguel-s authored Feb 24, 2021
1 parent ec8f784 commit 92f50f6
Show file tree
Hide file tree
Showing 3 changed files with 483 additions and 6 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ ___
- NEW: LiveQuery support for $and, $nor, $containedBy, $geoWithin, $geoIntersects queries [#7113](https://github.com/parse-community/parse-server/pull/7113). Thanks to [dplewis](https://github.com/dplewis)
- NEW: Supporting patterns in LiveQuery server's config parameter `classNames` [#7131](https://github.com/parse-community/parse-server/pull/7131). Thanks to [Nes-si](https://github.com/Nes-si)
- NEW: `requireAnyUserRoles` and `requireAllUserRoles` for Parse Cloud validator. [#7097](https://github.com/parse-community/parse-server/pull/7097). Thanks to [dblythy](https://github.com/dblythy)
- NEW: Support Facebook Limited Login [#7219](https://github.com/parse-community/parse-server/pull/7219). Thanks to [miguel-s](https://github.com/miguel-s)
- IMPROVE: Retry transactions on MongoDB when it fails due to transient error [#7187](https://github.com/parse-community/parse-server/pull/7187). Thanks to [Antonio Davi Macedo Coelho de Castro](https://github.com/davimacedo).
- IMPROVE: Bump tests to use Mongo 4.4.4 [#7184](https://github.com/parse-community/parse-server/pull/7184). Thanks to [Antonio Davi Macedo Coelho de Castro](https://github.com/davimacedo).
- IMPROVE: Added new account lockout policy option `accountLockout.unlockOnPasswordReset` to automatically unlock account on password reset. [#7146](https://github.com/parse-community/parse-server/pull/7146). Thanks to [Manuel Trezza](https://github.com/mtrezza).
Expand Down
384 changes: 384 additions & 0 deletions spec/AuthenticationAdapters.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1756,3 +1756,387 @@ describe('microsoft graph auth adapter', () => {
});
});
});

describe('facebook limited auth adapter', () => {
const facebook = require('../lib/Adapters/Auth/facebook');
const jwt = require('jsonwebtoken');
const util = require('util');

// TODO: figure out a way to run this test alongside facebook classic tests
xit('(using client id as string) should throw error with missing id_token', async () => {
try {
await facebook.validateAuthData({}, { clientId: 'secret' });
fail();
} catch (e) {
expect(e.message).toBe('Facebook auth is not configured.');
}
});

// TODO: figure out a way to run this test alongside facebook classic tests
xit('(using client id as array) should throw error with missing id_token', async () => {
try {
await facebook.validateAuthData({}, { clientId: ['secret'] });
fail();
} catch (e) {
expect(e.message).toBe('Facebook auth is not configured.');
}
});

it('should not decode invalid id_token', async () => {
try {
await facebook.validateAuthData(
{ id: 'the_user_id', token: 'the_token' },
{ clientId: 'secret' }
);
fail();
} catch (e) {
expect(e.message).toBe('provided token does not decode as JWT');
}
});

it('should throw error if public key used to encode token is not available', async () => {
const fakeDecodedToken = {
header: { kid: '789', alg: 'RS256' },
};
try {
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);

await facebook.validateAuthData(
{ id: 'the_user_id', token: 'the_token' },
{ clientId: 'secret' }
);
fail();
} catch (e) {
expect(e.message).toBe(
`Unable to find matching key for Key ID: ${fakeDecodedToken.header.kid}`
);
}
});

it('should use algorithm from key header to verify id_token', async () => {
const fakeClaim = {
iss: 'https://facebook.com',
aud: 'secret',
exp: Date.now(),
sub: 'the_user_id',
};
const fakeDecodedToken = {
header: { kid: '123', alg: 'RS256' },
};
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
const fakeGetSigningKeyAsyncFunction = () => {
return {
kid: '123',
rsaPublicKey: 'the_rsa_public_key',
};
};
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);

const result = await facebook.validateAuthData(
{ id: 'the_user_id', token: 'the_token' },
{ clientId: 'secret' }
);
expect(result).toEqual(fakeClaim);
expect(jwt.verify.calls.first().args[2].algorithms).toEqual(fakeDecodedToken.header.alg);
});

it('should not verify invalid id_token', async () => {
const fakeDecodedToken = {
header: { kid: '123', alg: 'RS256' },
};
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return {
kid: '123',
rsaPublicKey: 'the_rsa_public_key',
};
};
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);

try {
await facebook.validateAuthData(
{ id: 'the_user_id', token: 'the_token' },
{ clientId: 'secret' }
);
fail();
} catch (e) {
expect(e.message).toBe('jwt malformed');
}
});

it('(using client id as array) should not verify invalid id_token', async () => {
try {
await facebook.validateAuthData(
{ id: 'the_user_id', token: 'the_token' },
{ clientId: ['secret'] }
);
fail();
} catch (e) {
expect(e.message).toBe('provided token does not decode as JWT');
}
});

it('(using client id as string) should verify id_token', async () => {
const fakeClaim = {
iss: 'https://facebook.com',
aud: 'secret',
exp: Date.now(),
sub: 'the_user_id',
};
const fakeDecodedToken = {
header: { kid: '123', alg: 'RS256' },
};
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return {
kid: '123',
rsaPublicKey: 'the_rsa_public_key',
};
};
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);

const result = await facebook.validateAuthData(
{ id: 'the_user_id', token: 'the_token' },
{ clientId: 'secret' }
);
expect(result).toEqual(fakeClaim);
});

it('(using client id as array) should verify id_token', async () => {
const fakeClaim = {
iss: 'https://facebook.com',
aud: 'secret',
exp: Date.now(),
sub: 'the_user_id',
};
const fakeDecodedToken = {
header: { kid: '123', alg: 'RS256' },
};
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return {
kid: '123',
rsaPublicKey: 'the_rsa_public_key',
};
};
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);

const result = await facebook.validateAuthData(
{ id: 'the_user_id', token: 'the_token' },
{ clientId: ['secret'] }
);
expect(result).toEqual(fakeClaim);
});

it('(using client id as array with multiple items) should verify id_token', async () => {
const fakeClaim = {
iss: 'https://facebook.com',
aud: 'secret',
exp: Date.now(),
sub: 'the_user_id',
};
const fakeDecodedToken = {
header: { kid: '123', alg: 'RS256' },
};
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return {
kid: '123',
rsaPublicKey: 'the_rsa_public_key',
};
};
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);

const result = await facebook.validateAuthData(
{ id: 'the_user_id', token: 'the_token' },
{ clientId: ['secret', 'secret 123'] }
);
expect(result).toEqual(fakeClaim);
});

it('(using client id as string) should throw error with with invalid jwt issuer', async () => {
const fakeClaim = {
iss: 'https://not.facebook.com',
sub: 'the_user_id',
};
const fakeDecodedToken = {
header: { kid: '123', alg: 'RS256' },
};
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return {
kid: '123',
rsaPublicKey: 'the_rsa_public_key',
};
};
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);

try {
await facebook.validateAuthData(
{ id: 'the_user_id', token: 'the_token' },
{ clientId: 'secret' }
);
fail();
} catch (e) {
expect(e.message).toBe(
'id token not issued by correct OpenID provider - expected: https://facebook.com | from: https://not.facebook.com'
);
}
});

// TODO: figure out a way to generate our own facebook signed tokens, perhaps with a parse facebook account
// and a private key
xit('(using client id as array) should throw error with with invalid jwt issuer', async () => {
const fakeClaim = {
iss: 'https://not.facebook.com',
sub: 'the_user_id',
};
const fakeDecodedToken = {
header: { kid: '123', alg: 'RS256' },
};
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return {
kid: '123',
rsaPublicKey: 'the_rsa_public_key',
};
};
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);

try {
await facebook.validateAuthData(
{
id: 'INSERT ID HERE',
token: 'INSERT FACEBOOK TOKEN HERE WITH INVALID JWT ISSUER',
},
{ clientId: ['INSERT CLIENT ID HERE'] }
);
fail();
} catch (e) {
expect(e.message).toBe(
'id token not issued by correct OpenID provider - expected: https://facebook.com | from: https://not.facebook.com'
);
}
});

it('(using client id as string) should throw error with with invalid jwt issuer', async () => {
const fakeClaim = {
iss: 'https://not.facebook.com',
sub: 'the_user_id',
};
const fakeDecodedToken = {
header: { kid: '123', alg: 'RS256' },
};
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return {
kid: '123',
rsaPublicKey: 'the_rsa_public_key',
};
};
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);

try {
await facebook.validateAuthData(
{
id: 'INSERT ID HERE',
token: 'INSERT FACEBOOK TOKEN HERE WITH INVALID JWT ISSUER',
},
{ clientId: 'INSERT CLIENT ID HERE' }
);
fail();
} catch (e) {
expect(e.message).toBe(
'id token not issued by correct OpenID provider - expected: https://facebook.com | from: https://not.facebook.com'
);
}
});

// TODO: figure out a way to generate our own facebook signed tokens, perhaps with a parse facebook account
// and a private key
xit('(using client id as string) should throw error with invalid jwt clientId', async () => {
try {
await facebook.validateAuthData(
{
id: 'INSERT ID HERE',
token: 'INSERT FACEBOOK TOKEN HERE',
},
{ clientId: 'secret' }
);
fail();
} catch (e) {
expect(e.message).toBe('jwt audience invalid. expected: secret');
}
});

// TODO: figure out a way to generate our own facebook signed tokens, perhaps with a parse facebook account
// and a private key
xit('(using client id as array) should throw error with invalid jwt clientId', async () => {
try {
await facebook.validateAuthData(
{
id: 'INSERT ID HERE',
token: 'INSERT FACEBOOK TOKEN HERE',
},
{ clientId: ['secret'] }
);
fail();
} catch (e) {
expect(e.message).toBe('jwt audience invalid. expected: secret');
}
});

// TODO: figure out a way to generate our own facebook signed tokens, perhaps with a parse facebook account
// and a private key
xit('should throw error with invalid user id', async () => {
try {
await facebook.validateAuthData(
{
id: 'invalid user',
token: 'INSERT FACEBOOK TOKEN HERE',
},
{ clientId: 'INSERT CLIENT ID HERE' }
);
fail();
} catch (e) {
expect(e.message).toBe('auth data is invalid for this user.');
}
});

it('should throw error with with invalid user id', async () => {
const fakeClaim = {
iss: 'https://facebook.com',
aud: 'invalid_client_id',
sub: 'a_different_user_id',
};
const fakeDecodedToken = {
header: { kid: '123', alg: 'RS256' },
};
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return {
kid: '123',
rsaPublicKey: 'the_rsa_public_key',
};
};
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);

try {
await facebook.validateAuthData(
{ id: 'the_user_id', token: 'the_token' },
{ clientId: 'secret' }
);
fail();
} catch (e) {
expect(e.message).toBe('auth data is invalid for this user.');
}
});
});
Loading

0 comments on commit 92f50f6

Please sign in to comment.