Skip to content

Commit 92f50f6

Browse files
authored
Add support for Facebook Limited Login (#7219)
* Add support for Facebook Limited auth * Add tests * Fix tests * Fix tests * Add entry to changelog * Cleanup
1 parent ec8f784 commit 92f50f6

File tree

3 files changed

+483
-6
lines changed

3 files changed

+483
-6
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ ___
2121
- 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)
2222
- 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)
2323
- 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)
24+
- NEW: Support Facebook Limited Login [#7219](https://github.com/parse-community/parse-server/pull/7219). Thanks to [miguel-s](https://github.com/miguel-s)
2425
- 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).
2526
- 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).
2627
- 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).

spec/AuthenticationAdapters.spec.js

+384
Original file line numberDiff line numberDiff line change
@@ -1756,3 +1756,387 @@ describe('microsoft graph auth adapter', () => {
17561756
});
17571757
});
17581758
});
1759+
1760+
describe('facebook limited auth adapter', () => {
1761+
const facebook = require('../lib/Adapters/Auth/facebook');
1762+
const jwt = require('jsonwebtoken');
1763+
const util = require('util');
1764+
1765+
// TODO: figure out a way to run this test alongside facebook classic tests
1766+
xit('(using client id as string) should throw error with missing id_token', async () => {
1767+
try {
1768+
await facebook.validateAuthData({}, { clientId: 'secret' });
1769+
fail();
1770+
} catch (e) {
1771+
expect(e.message).toBe('Facebook auth is not configured.');
1772+
}
1773+
});
1774+
1775+
// TODO: figure out a way to run this test alongside facebook classic tests
1776+
xit('(using client id as array) should throw error with missing id_token', async () => {
1777+
try {
1778+
await facebook.validateAuthData({}, { clientId: ['secret'] });
1779+
fail();
1780+
} catch (e) {
1781+
expect(e.message).toBe('Facebook auth is not configured.');
1782+
}
1783+
});
1784+
1785+
it('should not decode invalid id_token', async () => {
1786+
try {
1787+
await facebook.validateAuthData(
1788+
{ id: 'the_user_id', token: 'the_token' },
1789+
{ clientId: 'secret' }
1790+
);
1791+
fail();
1792+
} catch (e) {
1793+
expect(e.message).toBe('provided token does not decode as JWT');
1794+
}
1795+
});
1796+
1797+
it('should throw error if public key used to encode token is not available', async () => {
1798+
const fakeDecodedToken = {
1799+
header: { kid: '789', alg: 'RS256' },
1800+
};
1801+
try {
1802+
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
1803+
1804+
await facebook.validateAuthData(
1805+
{ id: 'the_user_id', token: 'the_token' },
1806+
{ clientId: 'secret' }
1807+
);
1808+
fail();
1809+
} catch (e) {
1810+
expect(e.message).toBe(
1811+
`Unable to find matching key for Key ID: ${fakeDecodedToken.header.kid}`
1812+
);
1813+
}
1814+
});
1815+
1816+
it('should use algorithm from key header to verify id_token', async () => {
1817+
const fakeClaim = {
1818+
iss: 'https://facebook.com',
1819+
aud: 'secret',
1820+
exp: Date.now(),
1821+
sub: 'the_user_id',
1822+
};
1823+
const fakeDecodedToken = {
1824+
header: { kid: '123', alg: 'RS256' },
1825+
};
1826+
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
1827+
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
1828+
const fakeGetSigningKeyAsyncFunction = () => {
1829+
return {
1830+
kid: '123',
1831+
rsaPublicKey: 'the_rsa_public_key',
1832+
};
1833+
};
1834+
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
1835+
1836+
const result = await facebook.validateAuthData(
1837+
{ id: 'the_user_id', token: 'the_token' },
1838+
{ clientId: 'secret' }
1839+
);
1840+
expect(result).toEqual(fakeClaim);
1841+
expect(jwt.verify.calls.first().args[2].algorithms).toEqual(fakeDecodedToken.header.alg);
1842+
});
1843+
1844+
it('should not verify invalid id_token', async () => {
1845+
const fakeDecodedToken = {
1846+
header: { kid: '123', alg: 'RS256' },
1847+
};
1848+
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
1849+
const fakeGetSigningKeyAsyncFunction = () => {
1850+
return {
1851+
kid: '123',
1852+
rsaPublicKey: 'the_rsa_public_key',
1853+
};
1854+
};
1855+
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
1856+
1857+
try {
1858+
await facebook.validateAuthData(
1859+
{ id: 'the_user_id', token: 'the_token' },
1860+
{ clientId: 'secret' }
1861+
);
1862+
fail();
1863+
} catch (e) {
1864+
expect(e.message).toBe('jwt malformed');
1865+
}
1866+
});
1867+
1868+
it('(using client id as array) should not verify invalid id_token', async () => {
1869+
try {
1870+
await facebook.validateAuthData(
1871+
{ id: 'the_user_id', token: 'the_token' },
1872+
{ clientId: ['secret'] }
1873+
);
1874+
fail();
1875+
} catch (e) {
1876+
expect(e.message).toBe('provided token does not decode as JWT');
1877+
}
1878+
});
1879+
1880+
it('(using client id as string) should verify id_token', async () => {
1881+
const fakeClaim = {
1882+
iss: 'https://facebook.com',
1883+
aud: 'secret',
1884+
exp: Date.now(),
1885+
sub: 'the_user_id',
1886+
};
1887+
const fakeDecodedToken = {
1888+
header: { kid: '123', alg: 'RS256' },
1889+
};
1890+
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
1891+
const fakeGetSigningKeyAsyncFunction = () => {
1892+
return {
1893+
kid: '123',
1894+
rsaPublicKey: 'the_rsa_public_key',
1895+
};
1896+
};
1897+
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
1898+
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
1899+
1900+
const result = await facebook.validateAuthData(
1901+
{ id: 'the_user_id', token: 'the_token' },
1902+
{ clientId: 'secret' }
1903+
);
1904+
expect(result).toEqual(fakeClaim);
1905+
});
1906+
1907+
it('(using client id as array) should verify id_token', async () => {
1908+
const fakeClaim = {
1909+
iss: 'https://facebook.com',
1910+
aud: 'secret',
1911+
exp: Date.now(),
1912+
sub: 'the_user_id',
1913+
};
1914+
const fakeDecodedToken = {
1915+
header: { kid: '123', alg: 'RS256' },
1916+
};
1917+
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
1918+
const fakeGetSigningKeyAsyncFunction = () => {
1919+
return {
1920+
kid: '123',
1921+
rsaPublicKey: 'the_rsa_public_key',
1922+
};
1923+
};
1924+
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
1925+
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
1926+
1927+
const result = await facebook.validateAuthData(
1928+
{ id: 'the_user_id', token: 'the_token' },
1929+
{ clientId: ['secret'] }
1930+
);
1931+
expect(result).toEqual(fakeClaim);
1932+
});
1933+
1934+
it('(using client id as array with multiple items) should verify id_token', async () => {
1935+
const fakeClaim = {
1936+
iss: 'https://facebook.com',
1937+
aud: 'secret',
1938+
exp: Date.now(),
1939+
sub: 'the_user_id',
1940+
};
1941+
const fakeDecodedToken = {
1942+
header: { kid: '123', alg: 'RS256' },
1943+
};
1944+
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
1945+
const fakeGetSigningKeyAsyncFunction = () => {
1946+
return {
1947+
kid: '123',
1948+
rsaPublicKey: 'the_rsa_public_key',
1949+
};
1950+
};
1951+
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
1952+
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
1953+
1954+
const result = await facebook.validateAuthData(
1955+
{ id: 'the_user_id', token: 'the_token' },
1956+
{ clientId: ['secret', 'secret 123'] }
1957+
);
1958+
expect(result).toEqual(fakeClaim);
1959+
});
1960+
1961+
it('(using client id as string) should throw error with with invalid jwt issuer', async () => {
1962+
const fakeClaim = {
1963+
iss: 'https://not.facebook.com',
1964+
sub: 'the_user_id',
1965+
};
1966+
const fakeDecodedToken = {
1967+
header: { kid: '123', alg: 'RS256' },
1968+
};
1969+
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
1970+
const fakeGetSigningKeyAsyncFunction = () => {
1971+
return {
1972+
kid: '123',
1973+
rsaPublicKey: 'the_rsa_public_key',
1974+
};
1975+
};
1976+
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
1977+
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
1978+
1979+
try {
1980+
await facebook.validateAuthData(
1981+
{ id: 'the_user_id', token: 'the_token' },
1982+
{ clientId: 'secret' }
1983+
);
1984+
fail();
1985+
} catch (e) {
1986+
expect(e.message).toBe(
1987+
'id token not issued by correct OpenID provider - expected: https://facebook.com | from: https://not.facebook.com'
1988+
);
1989+
}
1990+
});
1991+
1992+
// TODO: figure out a way to generate our own facebook signed tokens, perhaps with a parse facebook account
1993+
// and a private key
1994+
xit('(using client id as array) should throw error with with invalid jwt issuer', async () => {
1995+
const fakeClaim = {
1996+
iss: 'https://not.facebook.com',
1997+
sub: 'the_user_id',
1998+
};
1999+
const fakeDecodedToken = {
2000+
header: { kid: '123', alg: 'RS256' },
2001+
};
2002+
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
2003+
const fakeGetSigningKeyAsyncFunction = () => {
2004+
return {
2005+
kid: '123',
2006+
rsaPublicKey: 'the_rsa_public_key',
2007+
};
2008+
};
2009+
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
2010+
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
2011+
2012+
try {
2013+
await facebook.validateAuthData(
2014+
{
2015+
id: 'INSERT ID HERE',
2016+
token: 'INSERT FACEBOOK TOKEN HERE WITH INVALID JWT ISSUER',
2017+
},
2018+
{ clientId: ['INSERT CLIENT ID HERE'] }
2019+
);
2020+
fail();
2021+
} catch (e) {
2022+
expect(e.message).toBe(
2023+
'id token not issued by correct OpenID provider - expected: https://facebook.com | from: https://not.facebook.com'
2024+
);
2025+
}
2026+
});
2027+
2028+
it('(using client id as string) should throw error with with invalid jwt issuer', async () => {
2029+
const fakeClaim = {
2030+
iss: 'https://not.facebook.com',
2031+
sub: 'the_user_id',
2032+
};
2033+
const fakeDecodedToken = {
2034+
header: { kid: '123', alg: 'RS256' },
2035+
};
2036+
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
2037+
const fakeGetSigningKeyAsyncFunction = () => {
2038+
return {
2039+
kid: '123',
2040+
rsaPublicKey: 'the_rsa_public_key',
2041+
};
2042+
};
2043+
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
2044+
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
2045+
2046+
try {
2047+
await facebook.validateAuthData(
2048+
{
2049+
id: 'INSERT ID HERE',
2050+
token: 'INSERT FACEBOOK TOKEN HERE WITH INVALID JWT ISSUER',
2051+
},
2052+
{ clientId: 'INSERT CLIENT ID HERE' }
2053+
);
2054+
fail();
2055+
} catch (e) {
2056+
expect(e.message).toBe(
2057+
'id token not issued by correct OpenID provider - expected: https://facebook.com | from: https://not.facebook.com'
2058+
);
2059+
}
2060+
});
2061+
2062+
// TODO: figure out a way to generate our own facebook signed tokens, perhaps with a parse facebook account
2063+
// and a private key
2064+
xit('(using client id as string) should throw error with invalid jwt clientId', async () => {
2065+
try {
2066+
await facebook.validateAuthData(
2067+
{
2068+
id: 'INSERT ID HERE',
2069+
token: 'INSERT FACEBOOK TOKEN HERE',
2070+
},
2071+
{ clientId: 'secret' }
2072+
);
2073+
fail();
2074+
} catch (e) {
2075+
expect(e.message).toBe('jwt audience invalid. expected: secret');
2076+
}
2077+
});
2078+
2079+
// TODO: figure out a way to generate our own facebook signed tokens, perhaps with a parse facebook account
2080+
// and a private key
2081+
xit('(using client id as array) should throw error with invalid jwt clientId', async () => {
2082+
try {
2083+
await facebook.validateAuthData(
2084+
{
2085+
id: 'INSERT ID HERE',
2086+
token: 'INSERT FACEBOOK TOKEN HERE',
2087+
},
2088+
{ clientId: ['secret'] }
2089+
);
2090+
fail();
2091+
} catch (e) {
2092+
expect(e.message).toBe('jwt audience invalid. expected: secret');
2093+
}
2094+
});
2095+
2096+
// TODO: figure out a way to generate our own facebook signed tokens, perhaps with a parse facebook account
2097+
// and a private key
2098+
xit('should throw error with invalid user id', async () => {
2099+
try {
2100+
await facebook.validateAuthData(
2101+
{
2102+
id: 'invalid user',
2103+
token: 'INSERT FACEBOOK TOKEN HERE',
2104+
},
2105+
{ clientId: 'INSERT CLIENT ID HERE' }
2106+
);
2107+
fail();
2108+
} catch (e) {
2109+
expect(e.message).toBe('auth data is invalid for this user.');
2110+
}
2111+
});
2112+
2113+
it('should throw error with with invalid user id', async () => {
2114+
const fakeClaim = {
2115+
iss: 'https://facebook.com',
2116+
aud: 'invalid_client_id',
2117+
sub: 'a_different_user_id',
2118+
};
2119+
const fakeDecodedToken = {
2120+
header: { kid: '123', alg: 'RS256' },
2121+
};
2122+
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
2123+
const fakeGetSigningKeyAsyncFunction = () => {
2124+
return {
2125+
kid: '123',
2126+
rsaPublicKey: 'the_rsa_public_key',
2127+
};
2128+
};
2129+
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
2130+
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
2131+
2132+
try {
2133+
await facebook.validateAuthData(
2134+
{ id: 'the_user_id', token: 'the_token' },
2135+
{ clientId: 'secret' }
2136+
);
2137+
fail();
2138+
} catch (e) {
2139+
expect(e.message).toBe('auth data is invalid for this user.');
2140+
}
2141+
});
2142+
});

0 commit comments

Comments
 (0)