fix: Authentication provider credentials are usable across Parse Server apps; fixes security vulnerability [GHSA-837q-jhwx-cmpv](https://github.com/parse-community/parse-server/security/advisories/GHSA-837q-jhwx-cmpv) (#9668)
This commit is contained in:
307
spec/Adapters/Auth/microsoft.spec.js
Normal file
307
spec/Adapters/Auth/microsoft.spec.js
Normal file
@@ -0,0 +1,307 @@
|
||||
const MicrosoftAdapter = require('../../../lib/Adapters/Auth/microsoft').default;
|
||||
|
||||
describe('MicrosoftAdapter', function () {
|
||||
let adapter;
|
||||
const validOptions = {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
enableInsecureAuth: false,
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new MicrosoftAdapter.constructor();
|
||||
});
|
||||
|
||||
describe('Test configuration errors', function () {
|
||||
it('should throw error for missing options', function () {
|
||||
const invalidOptions = [null, undefined, {}, { clientId: 'validClientId' }];
|
||||
|
||||
for (const options of invalidOptions) {
|
||||
expect(() => {
|
||||
adapter.validateOptions(options);
|
||||
}).toThrow();
|
||||
}
|
||||
});
|
||||
|
||||
it('should validate options successfully with valid parameters', function () {
|
||||
expect(() => {
|
||||
adapter.validateOptions(validOptions);
|
||||
}).not.toThrow();
|
||||
expect(adapter.clientId).toBe(validOptions.clientId);
|
||||
expect(adapter.clientSecret).toBe(validOptions.clientSecret);
|
||||
expect(adapter.enableInsecureAuth).toBe(validOptions.enableInsecureAuth);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test getUserFromAccessToken', function () {
|
||||
it('should fetch user successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.microsoft.com/v1.0/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ id: 'validUserId' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const user = await adapter.getUserFromAccessToken('validToken');
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith('https://graph.microsoft.com/v1.0/me', {
|
||||
headers: {
|
||||
Authorization: 'Bearer validToken',
|
||||
},
|
||||
method: 'GET',
|
||||
});
|
||||
expect(user).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
|
||||
it('should throw error for invalid response', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.microsoft.com/v1.0/me',
|
||||
method: 'GET',
|
||||
response: { ok: false },
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken('invalidToken')).toBeRejectedWith(
|
||||
new Error('Microsoft API request failed.')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test getAccessTokenFromCode', function () {
|
||||
it('should fetch token successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ access_token: 'validToken' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com' };
|
||||
const token = await adapter.getAccessTokenFromCode(authData);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith('https://login.microsoftonline.com/common/oauth2/v2.0/token', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: jasmine.any(URLSearchParams),
|
||||
});
|
||||
expect(token).toEqual('validToken');
|
||||
});
|
||||
|
||||
it('should throw error for invalid response', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||
method: 'POST',
|
||||
response: { ok: false },
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'invalidCode', redirect_uri: 'http://example.com' };
|
||||
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWith(
|
||||
new Error('Microsoft API request failed.')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test secure authentication flow', function () {
|
||||
it('should exchange code for access token and fetch user data', async function () {
|
||||
spyOn(adapter, 'getAccessTokenFromCode').and.returnValue(Promise.resolve('validToken'));
|
||||
spyOn(adapter, 'getUserFromAccessToken').and.returnValue(Promise.resolve({ id: 'validUserId' }));
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com' };
|
||||
await adapter.beforeFind(authData);
|
||||
|
||||
expect(authData.access_token).toBe('validToken');
|
||||
expect(authData.id).toBe('validUserId');
|
||||
});
|
||||
|
||||
it('should throw error if user data cannot be fetched', async function () {
|
||||
spyOn(adapter, 'getAccessTokenFromCode').and.returnValue(Promise.resolve('validToken'));
|
||||
spyOn(adapter, 'getUserFromAccessToken').and.throwError('Microsoft API request failed.');
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com' };
|
||||
await expectAsync(adapter.beforeFind(authData)).toBeRejectedWith(
|
||||
new Error('Microsoft API request failed.')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test insecure authentication flow', function () {
|
||||
beforeEach(function () {
|
||||
adapter.enableInsecureAuth = true;
|
||||
});
|
||||
|
||||
it('should validate insecure auth and match user id', async function () {
|
||||
spyOn(adapter, 'getUserFromAccessToken').and.returnValue(
|
||||
Promise.resolve({ id: 'validUserId' })
|
||||
);
|
||||
|
||||
const authData = { access_token: 'validToken', id: 'validUserId' };
|
||||
await expectAsync(adapter.beforeFind(authData)).toBeResolved();
|
||||
});
|
||||
|
||||
it('should throw error if insecure auth user id does not match', async function () {
|
||||
spyOn(adapter, 'getUserFromAccessToken').and.returnValue(
|
||||
Promise.resolve({ id: 'invalidUserId' })
|
||||
);
|
||||
|
||||
const authData = { access_token: 'validToken', id: 'validUserId' };
|
||||
await expectAsync(adapter.beforeFind(authData)).toBeRejectedWith(
|
||||
new Error('Microsoft auth is invalid for this user.')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('MicrosoftAdapter E2E Tests', () => {
|
||||
beforeEach(async () => {
|
||||
// Simulate reconfiguring the server with Microsoft auth options
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
microsoft: {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
enableInsecureAuth: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should authenticate user successfully using MicrosoftAdapter', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ access_token: 'validAccessToken' }),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://graph.microsoft.com/v1.0/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ id: 'user123' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
|
||||
const user = await Parse.User.logInWith('microsoft', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle invalid code error gracefully', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||
method: 'POST',
|
||||
response: { ok: false, statusText: 'Invalid code' },
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'invalidCode', redirect_uri: 'http://example.com/callback' };
|
||||
|
||||
await expectAsync(Parse.User.logInWith('microsoft', { authData })).toBeRejectedWithError(
|
||||
'Microsoft API request failed.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when fetching user data fails', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ access_token: 'validAccessToken' }),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://graph.microsoft.com/v1.0/me',
|
||||
method: 'GET',
|
||||
response: { ok: false, statusText: 'Unauthorized' },
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
|
||||
|
||||
await expectAsync(Parse.User.logInWith('microsoft', { authData })).toBeRejectedWithError(
|
||||
'Microsoft API request failed.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow insecure auth when enabled', async () => {
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.microsoft.com/v1.0/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
id: 'user123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
microsoft: {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
enableInsecureAuth: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const authData = { access_token: 'validAccessToken', id: 'user123' };
|
||||
const user = await Parse.User.logInWith('microsoft', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('should reject insecure auth when user id does not match', async () => {
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.microsoft.com/v1.0/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
id: 'incorrectUser',
|
||||
}),
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
microsoft: {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
enableInsecureAuth: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const authData = { access_token: 'validAccessToken', id: 'incorrectUserId' };
|
||||
await expectAsync(Parse.User.logInWith('microsoft', { authData })).toBeRejectedWithError(
|
||||
'Microsoft auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user