VYPR
Medium severity6.9OSV Advisory· Published Mar 21, 2025· Updated Apr 15, 2026

CVE-2025-30168

CVE-2025-30168

Description

Parse Server is an open source backend that can be deployed to any infrastructure that can run Node.js. Prior to 7.5.2 and 8.0.2, the 3rd party authentication handling of Parse Server allows the authentication credentials of some specific authentication providers to be used across multiple Parse Server apps. For example, if a user signed up using the same authentication provider in two unrelated Parse Server apps, the credentials stored by one app can be used to authenticate the same user in the other app. Note that this only affects Parse Server apps that specifically use an affected 3rd party authentication provider for user authentication, for example by setting the Parse Server option auth to configure a Parse Server authentication adapter. The fix of this vulnerability requires to upgrade Parse Server to a version that includes the bug fix, as well as upgrade the client app to send a secure payload, which is different from the previous insecure payload. This vulnerability is fixed in 7.5.2 and 8.0.2.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
parse-servernpm
< 7.5.27.5.2
parse-servernpm
>= 8.0.0, < 8.0.28.0.2

Affected products

1

Patches

4
2ff9c71030bc

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)

59 files changed · +5968 1661
  • spec/Adapters/Auth/BaseCodeAdapter.spec.js+182 0 added
    @@ -0,0 +1,182 @@
    +const BaseAuthCodeAdapter = require('../../../lib/Adapters/Auth/BaseCodeAuthAdapter').default;
    +
    +describe('BaseAuthCodeAdapter', function () {
    +  let adapter;
    +  const adapterName = 'TestAdapter';
    +  const validOptions = {
    +    clientId: 'validClientId',
    +    clientSecret: 'validClientSecret',
    +  };
    +
    +  class TestAuthCodeAdapter extends BaseAuthCodeAdapter {
    +    async getUserFromAccessToken(accessToken) {
    +      if (accessToken === 'validAccessToken') {
    +        return { id: 'validUserId' };
    +      }
    +      throw new Error('Invalid access token');
    +    }
    +
    +    async getAccessTokenFromCode(authData) {
    +      if (authData.code === 'validCode') {
    +        return 'validAccessToken';
    +      }
    +      throw new Error('Invalid code');
    +    }
    +  }
    +
    +  beforeEach(function () {
    +    adapter = new TestAuthCodeAdapter(adapterName);
    +  });
    +
    +  describe('validateOptions', function () {
    +    it('should throw error if options are missing', function () {
    +      expect(() => adapter.validateOptions(null)).toThrowError(`${adapterName} options are required.`);
    +    });
    +
    +    it('should throw error if clientId is missing in secure mode', function () {
    +      expect(() =>
    +        adapter.validateOptions({ clientSecret: 'validClientSecret' })
    +      ).toThrowError(`${adapterName} clientId is required.`);
    +    });
    +
    +    it('should throw error if clientSecret is missing in secure mode', function () {
    +      expect(() =>
    +        adapter.validateOptions({ clientId: 'validClientId' })
    +      ).toThrowError(`${adapterName} clientSecret is required.`);
    +    });
    +
    +    it('should not throw error for valid options', function () {
    +      expect(() => adapter.validateOptions(validOptions)).not.toThrow();
    +      expect(adapter.clientId).toBe('validClientId');
    +      expect(adapter.clientSecret).toBe('validClientSecret');
    +      expect(adapter.enableInsecureAuth).toBeUndefined();
    +    });
    +
    +    it('should allow insecure mode without clientId or clientSecret', function () {
    +      const options = { enableInsecureAuth: true };
    +      expect(() => adapter.validateOptions(options)).not.toThrow();
    +      expect(adapter.enableInsecureAuth).toBe(true);
    +    });
    +  });
    +
    +  describe('beforeFind', function () {
    +    it('should throw error if code is missing in secure mode', async function () {
    +      adapter.validateOptions(validOptions);
    +      const authData = { access_token: 'validAccessToken' };
    +
    +      await expectAsync(adapter.beforeFind(authData)).toBeRejectedWithError(
    +        `${adapterName} code is required.`
    +      );
    +    });
    +
    +    it('should throw error if access token is missing in insecure mode', async function () {
    +      adapter.validateOptions({ enableInsecureAuth: true });
    +      const authData = {};
    +
    +      await expectAsync(adapter.beforeFind(authData)).toBeRejectedWithError(
    +        `${adapterName} auth is invalid for this user.`
    +      );
    +    });
    +
    +    it('should throw error if user ID does not match in insecure mode', async function () {
    +      adapter.validateOptions({ enableInsecureAuth: true });
    +      const authData = { id: 'invalidUserId', access_token: 'validAccessToken' };
    +
    +      await expectAsync(adapter.beforeFind(authData)).toBeRejectedWithError(
    +        `${adapterName} auth is invalid for this user.`
    +      );
    +    });
    +
    +    it('should process valid secure payload and update authData', async function () {
    +      adapter.validateOptions(validOptions);
    +      const authData = { code: 'validCode' };
    +
    +      await adapter.beforeFind(authData);
    +
    +      expect(authData.access_token).toBe('validAccessToken');
    +      expect(authData.id).toBe('validUserId');
    +      expect(authData.code).toBeUndefined();
    +    });
    +
    +    it('should process valid insecure payload', async function () {
    +      adapter.validateOptions({ enableInsecureAuth: true });
    +      const authData = { id: 'validUserId', access_token: 'validAccessToken' };
    +
    +      await expectAsync(adapter.beforeFind(authData)).toBeResolved();
    +    });
    +  });
    +
    +  describe('getUserFromAccessToken', function () {
    +    it('should throw error if not implemented in base class', async function () {
    +      const baseAdapter = new BaseAuthCodeAdapter(adapterName);
    +
    +      await expectAsync(baseAdapter.getUserFromAccessToken('test')).toBeRejectedWithError(
    +        'getUserFromAccessToken is not implemented'
    +      );
    +    });
    +
    +    it('should return valid user for valid access token', async function () {
    +      const user = await adapter.getUserFromAccessToken('validAccessToken', {});
    +      expect(user).toEqual({ id: 'validUserId' });
    +    });
    +
    +    it('should throw error for invalid access token', async function () {
    +      await expectAsync(adapter.getUserFromAccessToken('invalidAccessToken', {})).toBeRejectedWithError(
    +        'Invalid access token'
    +      );
    +    });
    +  });
    +
    +  describe('getAccessTokenFromCode', function () {
    +    it('should throw error if not implemented in base class', async function () {
    +      const baseAdapter = new BaseAuthCodeAdapter(adapterName);
    +
    +      await expectAsync(baseAdapter.getAccessTokenFromCode({ code: 'test' })).toBeRejectedWithError(
    +        'getAccessTokenFromCode is not implemented'
    +      );
    +    });
    +
    +    it('should return valid access token for valid code', async function () {
    +      const accessToken = await adapter.getAccessTokenFromCode({ code: 'validCode' });
    +      expect(accessToken).toBe('validAccessToken');
    +    });
    +
    +    it('should throw error for invalid code', async function () {
    +      await expectAsync(adapter.getAccessTokenFromCode({ code: 'invalidCode' })).toBeRejectedWithError(
    +        'Invalid code'
    +      );
    +    });
    +  });
    +
    +  describe('validateLogin', function () {
    +    it('should return user id from authData', function () {
    +      const authData = { id: 'validUserId' };
    +      const result = adapter.validateLogin(authData);
    +      expect(result).toEqual({ id: 'validUserId' });
    +    });
    +  });
    +
    +  describe('validateSetUp', function () {
    +    it('should return user id from authData', function () {
    +      const authData = { id: 'validUserId' };
    +      const result = adapter.validateSetUp(authData);
    +      expect(result).toEqual({ id: 'validUserId' });
    +    });
    +  });
    +
    +  describe('afterFind', function () {
    +    it('should return user id from authData', function () {
    +      const authData = { id: 'validUserId' };
    +      const result = adapter.afterFind(authData);
    +      expect(result).toEqual({ id: 'validUserId' });
    +    });
    +  });
    +
    +  describe('validateUpdate', function () {
    +    it('should return user id from authData', function () {
    +      const authData = { id: 'validUserId' };
    +      const result = adapter.validateUpdate(authData);
    +      expect(result).toEqual({ id: 'validUserId' });
    +    });
    +  });
    +});
    
  • spec/Adapters/Auth/gcenter.spec.js+220 0 added
    @@ -0,0 +1,220 @@
    +const GameCenterAuth = require('../../../lib/Adapters/Auth/gcenter').default;
    +const { pki } = require('node-forge');
    +const fs = require('fs');
    +const path = require('path');
    +
    +describe('GameCenterAuth Adapter', function () {
    +  let adapter;
    +
    +  beforeEach(function () {
    +    adapter = new GameCenterAuth.constructor();
    +
    +    const gcProd4 = fs.readFileSync(path.resolve(__dirname, '../../support/cert/gc-prod-4.cer'));
    +    const digicertPem = fs.readFileSync(path.resolve(__dirname, '../../support/cert/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem')).toString();
    +
    +    mockFetch([
    +      {
    +        url: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
    +        method: 'GET',
    +        response: {
    +          ok: true,
    +          headers: new Map(),
    +          arrayBuffer: () => Promise.resolve(
    +            gcProd4.buffer.slice(gcProd4.byteOffset, gcProd4.byteOffset + gcProd4.length)
    +          ),
    +        },
    +      },
    +      {
    +        url: 'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
    +        method: 'GET',
    +        response: {
    +          ok: true,
    +          headers: new Map([['content-type', 'application/x-pem-file'], ['content-length', digicertPem.length.toString()]]),
    +          text: () => Promise.resolve(digicertPem),
    +        },
    +      }
    +    ]);
    +  });
    +
    +  describe('Test config failing due to missing params or wrong types', function () {
    +    it('should throw error for invalid options', async function () {
    +      const invalidOptions = [
    +        null,
    +        undefined,
    +        {},
    +        { bundleId: '' },
    +        { enableInsecureAuth: false }, // Missing bundleId in secure mode
    +      ];
    +
    +      for (const options of invalidOptions) {
    +        expect(() => adapter.validateOptions(options)).withContext(JSON.stringify(options)).toThrow()
    +      }
    +    });
    +
    +    it('should validate options successfully with valid parameters', function () {
    +      const validOptions = { bundleId: 'com.valid.app', enableInsecureAuth: false };
    +      expect(() => adapter.validateOptions(validOptions)).not.toThrow();
    +    });
    +  });
    +
    +  describe('Test payload failing due to missing params or wrong types', function () {
    +    it('should throw error for missing authData fields', async function () {
    +      await expectAsync(adapter.validateAuthData({})).toBeRejectedWithError(
    +        'AuthData id is missing.'
    +      );
    +    });
    +  });
    +
    +  describe('Test payload fails due to incorrect appId / certificate', function () {
    +    it('should throw error for invalid publicKeyUrl', async function () {
    +      const invalidPublicKeyUrl = 'https://malicious.url.com/key.cer';
    +
    +      spyOn(adapter, 'fetchCertificate').and.throwError(
    +        new Error('Invalid publicKeyUrl')
    +      );
    +
    +      await expectAsync(
    +        adapter.getAppleCertificate(invalidPublicKeyUrl)
    +      ).toBeRejectedWithError('Invalid publicKeyUrl: https://malicious.url.com/key.cer');
    +    });
    +
    +    it('should throw error for invalid signature verification', async function () {
    +      const fakePublicKey = 'invalid-key';
    +      const fakeAuthData = {
    +        id: '1234567',
    +        publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
    +        timestamp: 1460981421303,
    +        salt: 'saltST==',
    +        signature: 'invalidSignature',
    +      };
    +
    +      spyOn(adapter, 'getAppleCertificate').and.returnValue(Promise.resolve(fakePublicKey));
    +      spyOn(adapter, 'verifySignature').and.throwError('Invalid signature.');
    +
    +      await expectAsync(adapter.validateAuthData(fakeAuthData)).toBeRejectedWithError(
    +        'Invalid signature.'
    +      );
    +    });
    +  });
    +
    +  describe('Test payload passing', function () {
    +    it('should successfully process valid payload and save auth data', async function () {
    +      const validAuthData = {
    +        id: '1234567',
    +        publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
    +        timestamp: 1460981421303,
    +        salt: 'saltST==',
    +        signature: 'validSignature',
    +        bundleId: 'com.valid.app',
    +      };
    +
    +      spyOn(adapter, 'getAppleCertificate').and.returnValue(Promise.resolve('validKey'));
    +      spyOn(adapter, 'verifySignature').and.returnValue(true);
    +
    +      await expectAsync(adapter.validateAuthData(validAuthData)).toBeResolved();
    +    });
    +  });
    +
    +  describe('Certificate and Signature Validation', function () {
    +    it('should fetch and validate Apple certificate', async function () {
    +      const certUrl = 'https://static.gc.apple.com/public-key/gc-prod-4.cer';
    +      const mockCertificate = 'mockCertificate';
    +
    +      spyOn(adapter, 'fetchCertificate').and.returnValue(
    +        Promise.resolve({ certificate: mockCertificate, headers: new Map() })
    +      );
    +      spyOn(pki, 'certificateFromPem').and.returnValue({});
    +
    +      adapter.cache[certUrl] = mockCertificate;
    +
    +      const cert = await adapter.getAppleCertificate(certUrl);
    +      expect(cert).toBe(mockCertificate);
    +    });
    +
    +    it('should verify signature successfully', async function () {
    +      const authData = {
    +        id: 'G:1965586982',
    +        publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
    +        timestamp: 1565257031287,
    +        signature:
    +          'uqLBTr9Uex8zCpc1UQ1MIDMitb+HUat2Mah4Kw6AVLSGe0gGNJXlih2i5X+0ZwVY0S9zY2NHWi2gFjmhjt/4kxWGMkupqXX5H/qhE2m7hzox6lZJpH98ZEUbouWRfZX2ZhUlCkAX09oRNi7fI7mWL1/o88MaI/y6k6tLr14JTzmlxgdyhw+QRLxRPA6NuvUlRSJpyJ4aGtNH5/wHdKQWL8nUnFYiYmaY8R7IjzNxPfy8UJTUWmeZvMSgND4u8EjADPsz7ZtZyWAPi8kYcAb6M8k0jwLD3vrYCB8XXyO2RQb/FY2TM4zJuI7PzLlvvgOJXbbfVtHx7Evnm5NYoyzgzw==',
    +        salt: 'DzqqrQ==',
    +      };
    +
    +      adapter.bundleId = 'cloud.xtralife.gamecenterauth';
    +      adapter.enableInsecureAuth = false;
    +
    +      spyOn(adapter, 'verifyPublicKeyIssuer').and.returnValue();
    +
    +      const publicKey = await adapter.getAppleCertificate(authData.publicKeyUrl);
    +
    +      expect(() => adapter.verifySignature(publicKey, authData)).not.toThrow();
    +
    +    });
    +
    +    it('should not use bundle id from authData payload in secure mode', async function () {
    +      const authData = {
    +        id: 'G:1965586982',
    +        publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
    +        timestamp: 1565257031287,
    +        signature:
    +          'uqLBTr9Uex8zCpc1UQ1MIDMitb+HUat2Mah4Kw6AVLSGe0gGNJXlih2i5X+0ZwVY0S9zY2NHWi2gFjmhjt/4kxWGMkupqXX5H/qhE2m7hzox6lZJpH98ZEUbouWRfZX2ZhUlCkAX09oRNi7fI7mWL1/o88MaI/y6k6tLr14JTzmlxgdyhw+QRLxRPA6NuvUlRSJpyJ4aGtNH5/wHdKQWL8nUnFYiYmaY8R7IjzNxPfy8UJTUWmeZvMSgND4u8EjADPsz7ZtZyWAPi8kYcAb6M8k0jwLD3vrYCB8XXyO2RQb/FY2TM4zJuI7PzLlvvgOJXbbfVtHx7Evnm5NYoyzgzw==',
    +        salt: 'DzqqrQ==',
    +        bundleId: 'com.example.insecure.app',
    +      };
    +
    +      adapter.bundleId = 'cloud.xtralife.gamecenterauth';
    +      adapter.enableInsecureAuth = false;
    +
    +      spyOn(adapter, 'verifyPublicKeyIssuer').and.returnValue();
    +
    +      const publicKey = await adapter.getAppleCertificate(authData.publicKeyUrl);
    +
    +      expect(() => adapter.verifySignature(publicKey, authData)).not.toThrow();
    +
    +    });
    +
    +    it('should not use bundle id from authData payload in insecure mode', async function () {
    +      const authData = {
    +        id: 'G:1965586982',
    +        publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
    +        timestamp: 1565257031287,
    +        signature:
    +          'uqLBTr9Uex8zCpc1UQ1MIDMitb+HUat2Mah4Kw6AVLSGe0gGNJXlih2i5X+0ZwVY0S9zY2NHWi2gFjmhjt/4kxWGMkupqXX5H/qhE2m7hzox6lZJpH98ZEUbouWRfZX2ZhUlCkAX09oRNi7fI7mWL1/o88MaI/y6k6tLr14JTzmlxgdyhw+QRLxRPA6NuvUlRSJpyJ4aGtNH5/wHdKQWL8nUnFYiYmaY8R7IjzNxPfy8UJTUWmeZvMSgND4u8EjADPsz7ZtZyWAPi8kYcAb6M8k0jwLD3vrYCB8XXyO2RQb/FY2TM4zJuI7PzLlvvgOJXbbfVtHx7Evnm5NYoyzgzw==',
    +        salt: 'DzqqrQ==',
    +        bundleId: 'com.example.insecure.app',
    +      };
    +
    +      adapter.bundleId = 'cloud.xtralife.gamecenterauth';
    +      adapter.enableInsecureAuth = true;
    +
    +      spyOn(adapter, 'verifyPublicKeyIssuer').and.returnValue();
    +
    +      const publicKey = await adapter.getAppleCertificate(authData.publicKeyUrl);
    +
    +      expect(() => adapter.verifySignature(publicKey, authData)).not.toThrow();
    +
    +    });
    +
    +    it('can  use bundle id from authData payload in insecure mode', async function () {
    +      const authData = {
    +        id: 'G:1965586982',
    +        publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
    +        timestamp: 1565257031287,
    +        signature:
    +          'uqLBTr9Uex8zCpc1UQ1MIDMitb+HUat2Mah4Kw6AVLSGe0gGNJXlih2i5X+0ZwVY0S9zY2NHWi2gFjmhjt/4kxWGMkupqXX5H/qhE2m7hzox6lZJpH98ZEUbouWRfZX2ZhUlCkAX09oRNi7fI7mWL1/o88MaI/y6k6tLr14JTzmlxgdyhw+QRLxRPA6NuvUlRSJpyJ4aGtNH5/wHdKQWL8nUnFYiYmaY8R7IjzNxPfy8UJTUWmeZvMSgND4u8EjADPsz7ZtZyWAPi8kYcAb6M8k0jwLD3vrYCB8XXyO2RQb/FY2TM4zJuI7PzLlvvgOJXbbfVtHx7Evnm5NYoyzgzw==',
    +        salt: 'DzqqrQ==',
    +        bundleId: 'cloud.xtralife.gamecenterauth',
    +      };
    +
    +      adapter.enableInsecureAuth = true;
    +
    +      spyOn(adapter, 'verifyPublicKeyIssuer').and.returnValue();
    +
    +      const publicKey = await adapter.getAppleCertificate(authData.publicKeyUrl);
    +
    +      expect(() => adapter.verifySignature(publicKey, authData)).not.toThrow();
    +
    +    });
    +  });
    +});
    
  • spec/Adapters/Auth/github.spec.js+285 0 added
    @@ -0,0 +1,285 @@
    +const GitHubAdapter = require('../../../lib/Adapters/Auth/github').default;
    +
    +describe('GitHubAdapter', function () {
    +  let adapter;
    +  const validOptions = {
    +    clientId: 'validClientId',
    +    clientSecret: 'validClientSecret',
    +  };
    +
    +  beforeEach(function () {
    +    adapter = new GitHubAdapter.constructor();
    +    adapter.validateOptions(validOptions);
    +  });
    +
    +  describe('getAccessTokenFromCode', function () {
    +    it('should fetch an access token successfully', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://github.com/login/oauth/access_token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                access_token: 'mockAccessToken',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const code = 'validCode';
    +      const token = await adapter.getAccessTokenFromCode(code);
    +
    +      expect(token).toBe('mockAccessToken');
    +    });
    +
    +    it('should throw an error if the response is not ok', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://github.com/login/oauth/access_token',
    +          method: 'POST',
    +          response: {
    +            ok: false,
    +            statusText: 'Bad Request',
    +          },
    +        },
    +      ]);
    +
    +      const code = 'invalidCode';
    +
    +      await expectAsync(adapter.getAccessTokenFromCode(code)).toBeRejectedWithError(
    +        'Failed to exchange code for token: Bad Request'
    +      );
    +    });
    +
    +    it('should throw an error if the response contains an error', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://github.com/login/oauth/access_token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                error: 'invalid_grant',
    +                error_description: 'Code is invalid',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const code = 'invalidCode';
    +
    +      await expectAsync(adapter.getAccessTokenFromCode(code)).toBeRejectedWithError('Code is invalid');
    +    });
    +  });
    +
    +  describe('getUserFromAccessToken', function () {
    +    it('should fetch user data successfully', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.github.com/user',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                id: 'mockUserId',
    +                login: 'mockUserLogin',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const accessToken = 'validAccessToken';
    +      const user = await adapter.getUserFromAccessToken(accessToken);
    +
    +      expect(user).toEqual({ id: 'mockUserId', login: 'mockUserLogin' });
    +    });
    +
    +    it('should throw an error if the response is not ok', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.github.com/user',
    +          method: 'GET',
    +          response: {
    +            ok: false,
    +            statusText: 'Unauthorized',
    +          },
    +        },
    +      ]);
    +
    +      const accessToken = 'invalidAccessToken';
    +
    +      await expectAsync(adapter.getUserFromAccessToken(accessToken)).toBeRejectedWithError(
    +        'Failed to fetch GitHub user: Unauthorized'
    +      );
    +    });
    +
    +    it('should throw an error if user data is invalid', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.github.com/user',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve({}),
    +          },
    +        },
    +      ]);
    +
    +      const accessToken = 'validAccessToken';
    +
    +      await expectAsync(adapter.getUserFromAccessToken(accessToken)).toBeRejectedWithError(
    +        'Invalid GitHub user data received.'
    +      );
    +    });
    +  });
    +
    +  describe('GitHubAdapter E2E Test', function () {
    +    beforeEach(async function () {
    +      await reconfigureServer({
    +        auth: {
    +          github: {
    +            clientId: 'validClientId',
    +            clientSecret: 'validClientSecret',
    +          },
    +        },
    +      });
    +    });
    +
    +    it('should log in user using GitHub adapter successfully', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://github.com/login/oauth/access_token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                access_token: 'mockAccessToken123',
    +              }),
    +          },
    +        },
    +        {
    +          url: 'https://api.github.com/user',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                id: 'mockUserId',
    +                login: 'mockUserLogin',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { code: 'validCode' };
    +      const user = await Parse.User.logInWith('github', { authData });
    +
    +      expect(user.id).toBeDefined();
    +    });
    +
    +    it('should handle error when GitHub returns invalid code', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://github.com/login/oauth/access_token',
    +          method: 'POST',
    +          response: {
    +            ok: false,
    +            statusText: 'Invalid code',
    +          },
    +        },
    +      ]);
    +
    +      const authData = { code: 'invalidCode' };
    +
    +      await expectAsync(Parse.User.logInWith('github', { authData })).toBeRejectedWithError(
    +        'Failed to exchange code for token: Invalid code'
    +      );
    +    });
    +
    +    it('should handle error when GitHub returns invalid user data', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://github.com/login/oauth/access_token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                access_token: 'mockAccessToken123',
    +              }),
    +          },
    +        },
    +        {
    +          url: 'https://api.github.com/user',
    +          method: 'GET',
    +          response: {
    +            ok: false,
    +            statusText: 'Unauthorized',
    +          },
    +        },
    +      ]);
    +
    +      const authData = { code: 'validCode' };
    +
    +      await expectAsync(Parse.User.logInWith('github', { authData })).toBeRejectedWithError(
    +        'Failed to fetch GitHub user: Unauthorized'
    +      );
    +    });
    +
    +    it('e2e secure does not support insecure payload', async function () {
    +      mockFetch();
    +      const authData = { id: 'mockUserId', access_token: 'mockAccessToken123' };
    +      await expectAsync(Parse.User.logInWith('github', { authData })).toBeRejectedWithError(
    +        'GitHub code is required.'
    +      );
    +    });
    +
    +    it('e2e insecure does support secure payload', async function () {
    +      await reconfigureServer({
    +        auth: {
    +          github: {
    +            clientId: 'validClientId',
    +            clientSecret: 'validClientSecret',
    +            enableInsecureAuth: true,
    +          },
    +        },
    +      });
    +
    +      mockFetch([
    +        {
    +          url: 'https://github.com/login/oauth/access_token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                access_token: 'mockAccessToken123',
    +              }),
    +          },
    +        },
    +        {
    +          url: 'https://api.github.com/user',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                id: 'mockUserId',
    +                login: 'mockUserLogin',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { code: 'validCode' };
    +      const user = await Parse.User.logInWith('github', { authData });
    +
    +      expect(user.id).toBeDefined();
    +    });
    +  });
    +});
    
  • spec/Adapters/Auth/gpgames.spec.js+356 0 added
    @@ -0,0 +1,356 @@
    +const GooglePlayGamesServicesAdapter = require('../../../lib/Adapters/Auth/gpgames').default;
    +
    +describe('GooglePlayGamesServicesAdapter', function () {
    +  let adapter;
    +
    +  beforeEach(function () {
    +    adapter = new GooglePlayGamesServicesAdapter.constructor();
    +    adapter.clientId = 'validClientId';
    +    adapter.clientSecret = 'validClientSecret';
    +  });
    +
    +  describe('getAccessTokenFromCode', function () {
    +    it('should fetch an access token successfully', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://oauth2.googleapis.com/token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                access_token: 'mockAccessToken',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const code = 'validCode';
    +      const authData = { redirectUri: 'http://example.com' };
    +      const token = await adapter.getAccessTokenFromCode(code, authData);
    +
    +      expect(token).toBe('mockAccessToken');
    +    });
    +
    +    it('should throw an error if the response is not ok', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://oauth2.googleapis.com/token',
    +          method: 'POST',
    +          response: {
    +            ok: false,
    +            statusText: 'Bad Request',
    +          },
    +        },
    +      ]);
    +
    +      const code = 'invalidCode';
    +      const authData = { redirectUri: 'http://example.com' };
    +
    +      await expectAsync(adapter.getAccessTokenFromCode(code, authData)).toBeRejectedWithError(
    +        'Failed to exchange code for token: Bad Request'
    +      );
    +    });
    +
    +    it('should throw an error if the response contains an error', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://oauth2.googleapis.com/token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                error: 'invalid_grant',
    +                error_description: 'Code is invalid',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const code = 'invalidCode';
    +      const authData = { redirectUri: 'http://example.com' };
    +
    +      await expectAsync(adapter.getAccessTokenFromCode(code, authData)).toBeRejectedWithError(
    +        'Code is invalid'
    +      );
    +    });
    +  });
    +
    +  describe('getUserFromAccessToken', function () {
    +    it('should fetch user data successfully', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://www.googleapis.com/games/v1/players/mockUserId',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                playerId: 'mockUserId',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const accessToken = 'validAccessToken';
    +      const authData = { id: 'mockUserId' };
    +      const user = await adapter.getUserFromAccessToken(accessToken, authData);
    +
    +      expect(user).toEqual({ id: 'mockUserId' });
    +    });
    +
    +    it('should throw an error if the response is not ok', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://www.googleapis.com/games/v1/players/mockUserId',
    +          method: 'GET',
    +          response: {
    +            ok: false,
    +            statusText: 'Unauthorized',
    +          },
    +        },
    +      ]);
    +
    +      const accessToken = 'invalidAccessToken';
    +      const authData = { id: 'mockUserId' };
    +
    +      await expectAsync(adapter.getUserFromAccessToken(accessToken, authData)).toBeRejectedWithError(
    +        'Failed to fetch Google Play Games Services user: Unauthorized'
    +      );
    +    });
    +
    +    it('should throw an error if user data is invalid', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://www.googleapis.com/games/v1/players/mockUserId',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve({}),
    +          },
    +        },
    +      ]);
    +
    +      const accessToken = 'validAccessToken';
    +      const authData = { id: 'mockUserId' };
    +
    +      await expectAsync(adapter.getUserFromAccessToken(accessToken, authData)).toBeRejectedWithError(
    +        'Invalid Google Play Games Services user data received.'
    +      );
    +    });
    +
    +    it('should throw an error if playerId does not match the provided user ID', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://www.googleapis.com/games/v1/players/mockUserId',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                playerId: 'anotherUserId',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const accessToken = 'validAccessToken';
    +      const authData = { id: 'mockUserId' };
    +
    +      await expectAsync(adapter.getUserFromAccessToken(accessToken, authData)).toBeRejectedWithError(
    +        'Invalid Google Play Games Services user data received.'
    +      );
    +    });
    +  });
    +
    +  describe('GooglePlayGamesServicesAdapter E2E Test', function () {
    +    beforeEach(async function () {
    +      await reconfigureServer({
    +        auth: {
    +          gpgames: {
    +            clientId: 'validClientId',
    +            clientSecret: 'validClientSecret',
    +          },
    +        },
    +      });
    +    });
    +
    +    it('should log in user successfully with valid code', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://oauth2.googleapis.com/token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                access_token: 'mockAccessToken123',
    +              }),
    +          },
    +        },
    +        {
    +          url: 'https://www.googleapis.com/games/v1/players/mockUserId',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                playerId: 'mockUserId',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = {
    +        code: 'validCode',
    +        id: 'mockUserId',
    +        redirectUri: 'http://example.com',
    +      };
    +
    +      const user = await Parse.User.logInWith('gpgames', { authData });
    +
    +      expect(user.id).toBeDefined();
    +      expect(global.fetch).toHaveBeenCalledWith(
    +        'https://oauth2.googleapis.com/token',
    +        jasmine.any(Object)
    +      );
    +      expect(global.fetch).toHaveBeenCalledWith(
    +        'https://www.googleapis.com/games/v1/players/mockUserId',
    +        jasmine.any(Object)
    +      );
    +    });
    +
    +    it('should handle error when the token exchange fails', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://oauth2.googleapis.com/token',
    +          method: 'POST',
    +          response: {
    +            ok: false,
    +            statusText: 'Invalid code',
    +          },
    +        },
    +      ]);
    +
    +      const authData = {
    +        code: 'invalidCode',
    +        redirectUri: 'http://example.com',
    +      };
    +
    +      await expectAsync(Parse.User.logInWith('gpgames', { authData })).toBeRejectedWithError(
    +        'Failed to exchange code for token: Invalid code'
    +      );
    +
    +      expect(global.fetch).toHaveBeenCalledWith(
    +        'https://oauth2.googleapis.com/token',
    +        jasmine.any(Object)
    +      );
    +    });
    +
    +    it('should handle error when user data fetch fails', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://oauth2.googleapis.com/token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                access_token: 'mockAccessToken123',
    +              }),
    +          },
    +        },
    +        {
    +          url: 'https://www.googleapis.com/games/v1/players/mockUserId',
    +          method: 'GET',
    +          response: {
    +            ok: false,
    +            statusText: 'Unauthorized',
    +          },
    +        },
    +      ]);
    +
    +      const authData = {
    +        code: 'validCode',
    +        id: 'mockUserId',
    +        redirectUri: 'http://example.com',
    +      };
    +
    +      await expectAsync(Parse.User.logInWith('gpgames', { authData })).toBeRejectedWithError(
    +        'Failed to fetch Google Play Games Services user: Unauthorized'
    +      );
    +
    +      expect(global.fetch).toHaveBeenCalledWith(
    +        'https://oauth2.googleapis.com/token',
    +        jasmine.any(Object)
    +      );
    +      expect(global.fetch).toHaveBeenCalledWith(
    +        'https://www.googleapis.com/games/v1/players/mockUserId',
    +        jasmine.any(Object)
    +      );
    +    });
    +
    +    it('should handle error when user data is invalid', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://oauth2.googleapis.com/token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                access_token: 'mockAccessToken123',
    +              }),
    +          },
    +        },
    +        {
    +          url: 'https://www.googleapis.com/games/v1/players/mockUserId',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                playerId: 'anotherUserId',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = {
    +        code: 'validCode',
    +        id: 'mockUserId',
    +        redirectUri: 'http://example.com',
    +      };
    +
    +      await expectAsync(Parse.User.logInWith('gpgames', { authData })).toBeRejectedWithError(
    +        'Invalid Google Play Games Services user data received.'
    +      );
    +
    +      expect(global.fetch).toHaveBeenCalledWith(
    +        'https://oauth2.googleapis.com/token',
    +        jasmine.any(Object)
    +      );
    +      expect(global.fetch).toHaveBeenCalledWith(
    +        'https://www.googleapis.com/games/v1/players/mockUserId',
    +        jasmine.any(Object)
    +      );
    +    });
    +
    +    it('should handle error when no code or access token is provided', async function () {
    +      mockFetch();
    +
    +      const authData = {
    +        id: 'mockUserId',
    +      };
    +
    +      await expectAsync(Parse.User.logInWith('gpgames', { authData })).toBeRejectedWithError(
    +        'gpgames code is required.'
    +      );
    +
    +      expect(global.fetch).not.toHaveBeenCalled();
    +    });
    +  });
    +
    +});
    +
    
  • spec/Adapters/Auth/instagram.spec.js+258 0 added
    @@ -0,0 +1,258 @@
    +const InstagramAdapter = require('../../../lib/Adapters/Auth/instagram').default;
    +
    +describe('InstagramAdapter', function () {
    +  let adapter;
    +
    +  beforeEach(function () {
    +    adapter = new InstagramAdapter.constructor();
    +    adapter.clientId = 'validClientId';
    +    adapter.clientSecret = 'validClientSecret';
    +    adapter.redirectUri = 'https://example.com/callback';
    +  });
    +
    +  describe('getAccessTokenFromCode', function () {
    +    it('should fetch an access token successfully', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.instagram.com/oauth/access_token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                access_token: 'mockAccessToken',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { code: 'validCode' };
    +      const token = await adapter.getAccessTokenFromCode(authData);
    +
    +      expect(token).toBe('mockAccessToken');
    +    });
    +
    +    it('should throw an error if the response contains an error', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.instagram.com/oauth/access_token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                error: 'invalid_grant',
    +                error_description: 'Code is invalid',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { code: 'invalidCode' };
    +
    +      await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWithError(
    +        'Code is invalid'
    +      );
    +    });
    +  });
    +
    +  describe('getUserFromAccessToken', function () {
    +    it('should fetch user data successfully', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://graph.instagram.com/me?fields=id&access_token=mockAccessToken',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                id: 'mockUserId',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const accessToken = 'mockAccessToken';
    +      const authData = { id: 'mockUserId' };
    +      const user = await adapter.getUserFromAccessToken(accessToken, authData);
    +
    +      expect(user).toEqual({ id: 'mockUserId' });
    +    });
    +
    +    it('should throw an error if user ID does not match authData', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://graph.instagram.com/me?fields=id&access_token=mockAccessToken',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                id: 'differentUserId',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const accessToken = 'mockAccessToken';
    +      const authData = { id: 'mockUserId' };
    +
    +      await expectAsync(adapter.getUserFromAccessToken(accessToken, authData)).toBeRejectedWithError(
    +        'Instagram auth is invalid for this user.'
    +      );
    +    });
    +  });
    +
    +  describe('InstagramAdapter E2E Test', function () {
    +    beforeEach(async function () {
    +      await reconfigureServer({
    +        auth: {
    +          instagram: {
    +            clientId: 'validClientId',
    +            clientSecret: 'validClientSecret',
    +            redirectUri: 'https://example.com/callback',
    +          },
    +        },
    +      });
    +    });
    +
    +    it('should log in user successfully with valid code', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.instagram.com/oauth/access_token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                access_token: 'mockAccessToken123',
    +              }),
    +          },
    +        },
    +        {
    +          url: 'https://graph.instagram.com/me?fields=id&access_token=mockAccessToken123',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                id: 'mockUserId',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = {
    +        code: 'validCode',
    +        id: 'mockUserId',
    +      };
    +
    +      const user = await Parse.User.logInWith('instagram', { authData });
    +
    +      expect(user.id).toBeDefined();
    +    });
    +
    +    it('should handle error when access token exchange fails', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.instagram.com/oauth/access_token',
    +          method: 'POST',
    +          response: {
    +            ok: false,
    +            statusText: 'Invalid code',
    +          },
    +        },
    +      ]);
    +
    +      const authData = {
    +        code: 'invalidCode',
    +      };
    +
    +      await expectAsync(Parse.User.logInWith('instagram', { authData })).toBeRejectedWith(
    +        new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Instagram API request failed.')
    +      );
    +    });
    +
    +    it('should handle error when user data fetch fails', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.instagram.com/oauth/access_token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                access_token: 'mockAccessToken123',
    +              }),
    +          },
    +        },
    +        {
    +          url: 'https://graph.instagram.com/me?fields=id&access_token=mockAccessToken123',
    +          method: 'GET',
    +          response: {
    +            ok: false,
    +            statusText: 'Unauthorized',
    +          },
    +        },
    +      ]);
    +
    +      const authData = {
    +        code: 'validCode',
    +        id: 'mockUserId',
    +      };
    +
    +      await expectAsync(Parse.User.logInWith('instagram', { authData })).toBeRejectedWith(
    +        new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Instagram API request failed.')
    +      );
    +    });
    +
    +    it('should handle error when user data is invalid', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.instagram.com/oauth/access_token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                access_token: 'mockAccessToken123',
    +              }),
    +          },
    +        },
    +        {
    +          url: 'https://graph.instagram.com/me?fields=id&access_token=mockAccessToken123',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                id: 'differentUserId',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = {
    +        code: 'validCode',
    +        id: 'mockUserId',
    +      };
    +
    +      await expectAsync(Parse.User.logInWith('instagram', { authData })).toBeRejectedWithError(
    +        'Instagram auth is invalid for this user.'
    +      );
    +    });
    +
    +    it('should handle error when no code or access token is provided', async function () {
    +      mockFetch();
    +
    +      const authData = {
    +        id: 'mockUserId',
    +      };
    +
    +      await expectAsync(Parse.User.logInWith('instagram', { authData })).toBeRejectedWithError(
    +        'Instagram code is required.'
    +      );
    +    });
    +  });
    +
    +});
    
  • spec/Adapters/Auth/line.spec.js+309 0 added
    @@ -0,0 +1,309 @@
    +const LineAdapter = require('../../../lib/Adapters/Auth/line').default;
    +describe('LineAdapter', function () {
    +  let adapter;
    +
    +  beforeEach(function () {
    +    adapter = new LineAdapter.constructor();
    +    adapter.clientId = 'validClientId';
    +    adapter.clientSecret = 'validClientSecret';
    +  });
    +
    +  describe('getAccessTokenFromCode', function () {
    +    it('should throw an error if code is missing in authData', async function () {
    +      const authData = { redirect_uri: 'http://example.com' };
    +
    +      await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWithError(
    +        'Line auth is invalid for this user.'
    +      );
    +    });
    +
    +    it('should fetch an access token successfully', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.line.me/oauth2/v2.1/token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                access_token: 'mockAccessToken',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = {
    +        code: 'validCode',
    +        redirect_uri: 'http://example.com',
    +      };
    +
    +      const token = await adapter.getAccessTokenFromCode(authData);
    +
    +      expect(token).toBe('mockAccessToken');
    +    });
    +
    +    it('should throw an error if response is not ok', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.line.me/oauth2/v2.1/token',
    +          method: 'POST',
    +          response: {
    +            ok: false,
    +            statusText: 'Bad Request',
    +          },
    +        },
    +      ]);
    +
    +      const authData = {
    +        code: 'invalidCode',
    +        redirect_uri: 'http://example.com',
    +      };
    +
    +      await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWithError(
    +        'Failed to exchange code for token: Bad Request'
    +      );
    +    });
    +
    +    it('should throw an error if response contains an error object', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.line.me/oauth2/v2.1/token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                error: 'invalid_grant',
    +                error_description: 'Code is invalid',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = {
    +        code: 'invalidCode',
    +        redirect_uri: 'http://example.com',
    +      };
    +
    +      await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWithError(
    +        'Code is invalid'
    +      );
    +    });
    +  });
    +
    +  describe('getUserFromAccessToken', function () {
    +    it('should fetch user data successfully', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.line.me/v2/profile',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                userId: 'mockUserId',
    +                displayName: 'mockDisplayName',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const accessToken = 'validAccessToken';
    +      const user = await adapter.getUserFromAccessToken(accessToken);
    +
    +      expect(user).toEqual({
    +        userId: 'mockUserId',
    +        displayName: 'mockDisplayName',
    +      });
    +    });
    +
    +    it('should throw an error if response is not ok', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.line.me/v2/profile',
    +          method: 'GET',
    +          response: {
    +            ok: false,
    +            statusText: 'Unauthorized',
    +          },
    +        },
    +      ]);
    +
    +      const accessToken = 'invalidAccessToken';
    +
    +      await expectAsync(adapter.getUserFromAccessToken(accessToken)).toBeRejectedWithError(
    +        'Failed to fetch Line user: Unauthorized'
    +      );
    +    });
    +
    +    it('should throw an error if user data is invalid', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.line.me/v2/profile',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve({}),
    +          },
    +        },
    +      ]);
    +
    +      const accessToken = 'validAccessToken';
    +
    +      await expectAsync(adapter.getUserFromAccessToken(accessToken)).toBeRejectedWithError(
    +        'Invalid Line user data received.'
    +      );
    +    });
    +  });
    +
    +  describe('LineAdapter E2E Test', function () {
    +    beforeEach(async function () {
    +      await reconfigureServer({
    +        auth: {
    +          line: {
    +            clientId: 'validClientId',
    +            clientSecret: 'validClientSecret',
    +          },
    +        },
    +      });
    +    });
    +
    +    it('should log in user successfully with valid code', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.line.me/oauth2/v2.1/token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                access_token: 'mockAccessToken123',
    +              }),
    +          },
    +        },
    +        {
    +          url: 'https://api.line.me/v2/profile',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                userId: 'mockUserId',
    +                displayName: 'mockDisplayName',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = {
    +        code: 'validCode',
    +        redirect_uri: 'http://example.com',
    +      };
    +
    +      const user = await Parse.User.logInWith('line', { authData });
    +
    +      expect(user.id).toBeDefined();
    +    });
    +
    +    it('should handle error when token exchange fails', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.line.me/oauth2/v2.1/token',
    +          method: 'POST',
    +          response: {
    +            ok: false,
    +            statusText: 'Invalid code',
    +          },
    +        },
    +      ]);
    +
    +      const authData = {
    +        code: 'invalidCode',
    +        redirect_uri: 'http://example.com',
    +      };
    +
    +      await expectAsync(Parse.User.logInWith('line', { authData })).toBeRejectedWithError(
    +        'Failed to exchange code for token: Invalid code'
    +      );
    +    });
    +
    +    it('should handle error when user data fetch fails', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.line.me/oauth2/v2.1/token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                access_token: 'mockAccessToken123',
    +              }),
    +          },
    +        },
    +        {
    +          url: 'https://api.line.me/v2/profile',
    +          method: 'GET',
    +          response: {
    +            ok: false,
    +            statusText: 'Unauthorized',
    +          },
    +        },
    +      ]);
    +
    +      const authData = {
    +        code: 'validCode',
    +        redirect_uri: 'http://example.com',
    +      };
    +
    +      await expectAsync(Parse.User.logInWith('line', { authData })).toBeRejectedWithError(
    +        'Failed to fetch Line user: Unauthorized'
    +      );
    +    });
    +
    +    it('should handle error when user data is invalid', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.line.me/oauth2/v2.1/token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                access_token: 'mockAccessToken123',
    +              }),
    +          },
    +        },
    +        {
    +          url: 'https://api.line.me/v2/profile',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve({}),
    +          },
    +        },
    +      ]);
    +
    +      const authData = {
    +        code: 'validCode',
    +        redirect_uri: 'http://example.com',
    +      };
    +
    +      await expectAsync(Parse.User.logInWith('line', { authData })).toBeRejectedWithError(
    +        'Invalid Line user data received.'
    +      );
    +    });
    +
    +    it('should handle error when no code is provided', async function () {
    +      mockFetch();
    +
    +      const authData = {
    +        redirect_uri: 'http://example.com',
    +      };
    +
    +      await expectAsync(Parse.User.logInWith('line', { authData })).toBeRejectedWithError(
    +        'Line code is required.'
    +      );
    +    });
    +  });
    +
    +});
    
  • spec/Adapters/Auth/linkedIn.spec.js+312 0 added
    @@ -0,0 +1,312 @@
    +
    +const LinkedInAdapter = require('../../../lib/Adapters/Auth/linkedin').default;
    +describe('LinkedInAdapter', function () {
    +  let adapter;
    +  const validOptions = {
    +    clientId: 'validClientId',
    +    clientSecret: 'validClientSecret',
    +    enableInsecureAuth: false,
    +  };
    +
    +  beforeEach(function () {
    +    adapter = new LinkedInAdapter.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 beforeFind', function () {
    +    it('should throw error for invalid payload', async function () {
    +      adapter.enableInsecureAuth = true;
    +
    +      const payloads = [{}, { access_token: null }];
    +
    +      for (const payload of payloads) {
    +        await expectAsync(adapter.beforeFind(payload)).toBeRejectedWith(
    +          new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'LinkedIn auth is invalid for this user.')
    +        );
    +      }
    +    });
    +
    +    it('should process secure payload and set auth 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', is_mobile_sdk: false };
    +
    +      await adapter.beforeFind(authData);
    +
    +      expect(authData.access_token).toBe('validToken');
    +      expect(authData.id).toBe('validUserId');
    +    });
    +
    +    it('should validate insecure auth and match user id', async function () {
    +      adapter.enableInsecureAuth = true;
    +      spyOn(adapter, 'getUserFromAccessToken').and.returnValue(
    +        Promise.resolve({ id: 'validUserId' })
    +      );
    +
    +      const authData = { access_token: 'validToken', id: 'validUserId', is_mobile_sdk: false };
    +
    +      await expectAsync(adapter.beforeFind(authData)).toBeResolved();
    +    });
    +
    +    it('should throw error if insecure auth user id does not match', async function () {
    +      adapter.enableInsecureAuth = true;
    +      spyOn(adapter, 'getUserFromAccessToken').and.returnValue(
    +        Promise.resolve({ id: 'invalidUserId' })
    +      );
    +
    +      const authData = { access_token: 'validToken', id: 'validUserId', is_mobile_sdk: false };
    +
    +      await expectAsync(adapter.beforeFind(authData)).toBeRejectedWith(
    +        new Error('LinkedIn auth is invalid for this user.')
    +      );
    +    });
    +  });
    +
    +  describe('Test getUserFromAccessToken', function () {
    +    it('should fetch user successfully', async function () {
    +      global.fetch = jasmine.createSpy().and.returnValue(
    +        Promise.resolve({
    +          ok: true,
    +          json: () => Promise.resolve({ id: 'validUserId' }),
    +        })
    +      );
    +
    +      const user = await adapter.getUserFromAccessToken('validToken', false);
    +
    +      expect(global.fetch).toHaveBeenCalledWith('https://api.linkedin.com/v2/me', {
    +        headers: {
    +          Authorization: `Bearer validToken`,
    +          'x-li-format': 'json',
    +          'x-li-src': undefined,
    +        },
    +      });
    +      expect(user).toEqual({ id: 'validUserId' });
    +    });
    +
    +    it('should throw error for invalid response', async function () {
    +      global.fetch = jasmine.createSpy().and.returnValue(
    +        Promise.resolve({ ok: false })
    +      );
    +
    +      await expectAsync(adapter.getUserFromAccessToken('invalidToken', false)).toBeRejectedWith(
    +        new Error('LinkedIn API request failed.')
    +      );
    +    });
    +  });
    +
    +  describe('Test getAccessTokenFromCode', function () {
    +    it('should fetch token successfully', async function () {
    +      global.fetch = jasmine.createSpy().and.returnValue(
    +        Promise.resolve({
    +          ok: true,
    +          json: () => Promise.resolve({ access_token: 'validToken' }),
    +        })
    +      );
    +
    +      const tokenResponse = await adapter.getAccessTokenFromCode('validCode', 'http://example.com');
    +
    +      expect(global.fetch).toHaveBeenCalledWith('https://www.linkedin.com/oauth/v2/accessToken', {
    +        method: 'POST',
    +        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    +        body: jasmine.any(URLSearchParams),
    +      });
    +      expect(tokenResponse).toEqual('validToken');
    +    });
    +
    +    it('should throw error for invalid response', async function () {
    +      global.fetch = jasmine.createSpy().and.returnValue(
    +        Promise.resolve({ ok: false })
    +      );
    +
    +      await expectAsync(
    +        adapter.getAccessTokenFromCode('invalidCode', 'http://example.com')
    +      ).toBeRejectedWith(new Error('LinkedIn API request failed.'));
    +    });
    +  });
    +
    +  describe('Test validate methods', function () {
    +    const authData = { id: 'validUserId', access_token: 'validToken' };
    +
    +    it('validateLogin should return user id', function () {
    +      const result = adapter.validateLogin(authData);
    +      expect(result).toEqual({ id: 'validUserId' });
    +    });
    +
    +    it('validateSetUp should return user id', function () {
    +      const result = adapter.validateSetUp(authData);
    +      expect(result).toEqual({ id: 'validUserId' });
    +    });
    +
    +    it('validateUpdate should return user id', function () {
    +      const result = adapter.validateUpdate(authData);
    +      expect(result).toEqual({ id: 'validUserId' });
    +    });
    +
    +    it('afterFind should return user id', function () {
    +      const result = adapter.afterFind(authData);
    +      expect(result).toEqual({ id: 'validUserId' });
    +    });
    +  });
    +
    +  describe('LinkedInAdapter E2E Test', function () {
    +    beforeEach(async function () {
    +      await reconfigureServer({
    +        auth: {
    +          linkedin: {
    +            clientId: 'validClientId',
    +            clientSecret: 'validClientSecret',
    +          },
    +        },
    +      });
    +    });
    +
    +    it('should log in user using LinkedIn adapter successfully (secure)', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://www.linkedin.com/oauth/v2/accessToken',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                access_token: 'mockAccessToken123',
    +              }),
    +          },
    +        },
    +        {
    +          url: 'https://api.linkedin.com/v2/me',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                id: 'mockUserId',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { code: 'validCode', redirect_uri: 'https://example.com/callback' };
    +      const user = await Parse.User.logInWith('linkedin', { authData });
    +
    +      expect(user.id).toBeDefined();
    +      expect(global.fetch).toHaveBeenCalledWith(
    +        'https://www.linkedin.com/oauth/v2/accessToken',
    +        jasmine.any(Object)
    +      );
    +      expect(global.fetch).toHaveBeenCalledWith(
    +        'https://api.linkedin.com/v2/me',
    +        jasmine.any(Object)
    +      );
    +    });
    +
    +    it('should handle error when LinkedIn returns invalid user data', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://www.linkedin.com/oauth/v2/accessToken',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                access_token: 'mockAccessToken123',
    +              }),
    +          },
    +        },
    +        {
    +          url: 'https://api.linkedin.com/v2/me',
    +          method: 'GET',
    +          response: {
    +            ok: false,
    +            statusText: 'Unauthorized',
    +          },
    +        },
    +      ]);
    +
    +      const authData = { code: 'validCode', redirect_uri: 'https://example.com/callback' };
    +
    +      await expectAsync(Parse.User.logInWith('linkedin', { authData })).toBeRejectedWithError(
    +        'LinkedIn API request failed.'
    +      );
    +
    +      expect(global.fetch).toHaveBeenCalledWith(
    +        'https://www.linkedin.com/oauth/v2/accessToken',
    +        jasmine.any(Object)
    +      );
    +      expect(global.fetch).toHaveBeenCalledWith(
    +        'https://api.linkedin.com/v2/me',
    +        jasmine.any(Object)
    +      );
    +    });
    +
    +    it('secure does not support insecure payload if not enabled', async function () {
    +      mockFetch();
    +      const authData = { id: 'mockUserId', access_token: 'mockAccessToken123' };
    +      await expectAsync(Parse.User.logInWith('linkedin', { authData })).toBeRejectedWithError(
    +        'LinkedIn code is required.'
    +      );
    +
    +      expect(global.fetch).not.toHaveBeenCalled();
    +    });
    +
    +    it('insecure mode supports insecure payload if enabled', async function () {
    +      await reconfigureServer({
    +        auth: {
    +          linkedin: {
    +            clientId: 'validClientId',
    +            clientSecret: 'validClientSecret',
    +            enableInsecureAuth: true,
    +          },
    +        },
    +      });
    +
    +      mockFetch([
    +        {
    +          url: 'https://api.linkedin.com/v2/me',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                id: 'mockUserId',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { id: 'mockUserId', access_token: 'mockAccessToken123' };
    +      const user = await Parse.User.logInWith('linkedin', { authData });
    +
    +      expect(user.id).toBeDefined();
    +      expect(global.fetch).toHaveBeenCalledWith(
    +        'https://api.linkedin.com/v2/me',
    +        jasmine.any(Object)
    +      );
    +    });
    +  });
    +});
    
  • spec/Adapters/Auth/microsoft.spec.js+307 0 added
    @@ -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.'
    +      );
    +    });
    +  });
    +
    +});
    
  • spec/Adapters/Auth/oauth2.spec.js+305 0 added
    @@ -0,0 +1,305 @@
    +const OAuth2Adapter = require('../../../lib/Adapters/Auth/oauth2').default;
    +
    +describe('OAuth2Adapter', () => {
    +  let adapter;
    +
    +  const validOptions = {
    +    tokenIntrospectionEndpointUrl: 'https://provider.com/introspect',
    +    useridField: 'sub',
    +    appidField: 'aud',
    +    appIds: ['valid-app-id'],
    +    authorizationHeader: 'Bearer validAuthToken',
    +  };
    +
    +  beforeEach(() => {
    +    adapter = new OAuth2Adapter.constructor();
    +    adapter.validateOptions(validOptions);
    +  });
    +
    +  describe('validateAppId', () => {
    +    it('should validate app ID successfully', async () => {
    +      const authData = { access_token: 'validAccessToken' };
    +      const mockResponse = {
    +        [validOptions.appidField]: 'valid-app-id',
    +      };
    +
    +      mockFetch([
    +        {
    +          url: validOptions.tokenIntrospectionEndpointUrl,
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve(mockResponse),
    +          },
    +        },
    +      ]);
    +
    +      await expectAsync(
    +        adapter.validateAppId(validOptions.appIds, authData, validOptions)
    +      ).toBeResolved();
    +    });
    +
    +    it('should throw an error if app ID is invalid', async () => {
    +      const authData = { access_token: 'validAccessToken' };
    +      const mockResponse = {
    +        [validOptions.appidField]: 'invalid-app-id',
    +      };
    +
    +      mockFetch([
    +        {
    +          url: validOptions.tokenIntrospectionEndpointUrl,
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve(mockResponse),
    +          },
    +        },
    +      ]);
    +
    +      await expectAsync(
    +        adapter.validateAppId(validOptions.appIds, authData, validOptions)
    +      ).toBeRejectedWithError('OAuth2: Invalid app ID.');
    +    });
    +  });
    +
    +  describe('validateAuthData', () => {
    +    it('should validate auth data successfully', async () => {
    +      const authData = { id: 'user-id', access_token: 'validAccessToken' };
    +      const mockResponse = {
    +        active: true,
    +        [validOptions.useridField]: 'user-id',
    +      };
    +
    +      mockFetch([
    +        {
    +          url: validOptions.tokenIntrospectionEndpointUrl,
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve(mockResponse),
    +          },
    +        },
    +      ]);
    +
    +      await expectAsync(
    +        adapter.validateAuthData(authData, null, validOptions)
    +      ).toBeResolvedTo({});
    +    });
    +
    +    it('should throw an error if the token is inactive', async () => {
    +      const authData = { id: 'user-id', access_token: 'validAccessToken' };
    +      const mockResponse = { active: false };
    +
    +      mockFetch([
    +        {
    +          url: validOptions.tokenIntrospectionEndpointUrl,
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve(mockResponse),
    +          },
    +        },
    +      ]);
    +
    +      await expectAsync(
    +        adapter.validateAuthData(authData, null, validOptions)
    +      ).toBeRejectedWith(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 access token is invalid for this user.'));
    +    });
    +
    +    it('should throw an error if user ID does not match', async () => {
    +      const authData = { id: 'user-id', access_token: 'validAccessToken' };
    +      const mockResponse = {
    +        active: true,
    +        [validOptions.useridField]: 'different-user-id',
    +      };
    +
    +      mockFetch([
    +        {
    +          url: validOptions.tokenIntrospectionEndpointUrl,
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve(mockResponse),
    +          },
    +        },
    +      ]);
    +
    +      await expectAsync(
    +        adapter.validateAuthData(authData, null, validOptions)
    +      ).toBeRejectedWithError('OAuth2 access token is invalid for this user.');
    +    });
    +  });
    +
    +  describe('requestTokenInfo', () => {
    +    it('should fetch token info successfully', async () => {
    +      const mockResponse = { active: true };
    +
    +      mockFetch([
    +        {
    +          url: validOptions.tokenIntrospectionEndpointUrl,
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve(mockResponse),
    +          },
    +        },
    +      ]);
    +
    +      const result = await adapter.requestTokenInfo(
    +        'validAccessToken',
    +        validOptions
    +      );
    +
    +      expect(result).toEqual(mockResponse);
    +    });
    +
    +    it('should throw an error if the introspection endpoint URL is missing', async () => {
    +      const options = { ...validOptions, tokenIntrospectionEndpointUrl: null };
    +
    +      expect(
    +        () => adapter.validateOptions(options)
    +      ).toThrow(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 token introspection endpoint URL is missing.'));
    +    });
    +
    +    it('should throw an error if the response is not ok', async () => {
    +      mockFetch([
    +        {
    +          url: validOptions.tokenIntrospectionEndpointUrl,
    +          method: 'POST',
    +          response: {
    +            ok: false,
    +            statusText: 'Bad Request',
    +          },
    +        },
    +      ]);
    +
    +      await expectAsync(
    +        adapter.requestTokenInfo('invalidAccessToken')
    +      ).toBeRejectedWithError('OAuth2 token introspection request failed.');
    +    });
    +  });
    +
    +  describe('OAuth2Adapter E2E Tests', () => {
    +    beforeEach(async () => {
    +      // Simulate reconfiguring the server with OAuth2 auth options
    +      await reconfigureServer({
    +        auth: {
    +          mockOauth: {
    +            tokenIntrospectionEndpointUrl: 'https://provider.com/introspect',
    +            useridField: 'sub',
    +            appidField: 'aud',
    +            appIds: ['valid-app-id'],
    +            authorizationHeader: 'Bearer validAuthToken',
    +            oauth2: true
    +          },
    +        },
    +      });
    +    });
    +
    +    it('should validate and authenticate user successfully', async () => {
    +      mockFetch([
    +        {
    +          url: 'https://provider.com/introspect',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve({
    +              active: true,
    +              sub: 'user123',
    +              aud: 'valid-app-id',
    +            }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { access_token: 'validAccessToken', id: 'user123' };
    +      const user = await Parse.User.logInWith('mockOauth', { authData });
    +
    +      expect(user.id).toBeDefined();
    +      expect(user.get('authData').mockOauth.id).toEqual('user123');
    +    });
    +
    +    it('should reject authentication for inactive token', async () => {
    +      mockFetch([
    +        {
    +          url: 'https://provider.com/introspect',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve({ active: false, aud: ['valid-app-id'] }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { access_token: 'inactiveToken', id: 'user123' };
    +      await expectAsync(Parse.User.logInWith('mockOauth', { authData })).toBeRejectedWith(
    +        new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 access token is invalid for this user.')
    +      );
    +    });
    +
    +    it('should reject authentication for mismatched user ID', async () => {
    +      mockFetch([
    +        {
    +          url: 'https://provider.com/introspect',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve({
    +              active: true,
    +              sub: 'different-user',
    +              aud: 'valid-app-id',
    +            }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { access_token: 'validAccessToken', id: 'user123' };
    +      await expectAsync(Parse.User.logInWith('mockOauth', { authData })).toBeRejectedWith(
    +        new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 access token is invalid for this user.')
    +      );
    +    });
    +
    +    it('should reject authentication for invalid app ID', async () => {
    +      mockFetch([
    +        {
    +          url: 'https://provider.com/introspect',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve({
    +              active: true,
    +              sub: 'user123',
    +              aud: 'invalid-app-id',
    +            }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { access_token: 'validAccessToken', id: 'user123' };
    +      await expectAsync(Parse.User.logInWith('mockOauth', { authData })).toBeRejectedWithError(
    +        'OAuth2: Invalid app ID.'
    +      );
    +    });
    +
    +    it('should handle error when token introspection endpoint is missing', async () => {
    +      await reconfigureServer({
    +        auth: {
    +          mockOauth: {
    +            tokenIntrospectionEndpointUrl: null,
    +            useridField: 'sub',
    +            appidField: 'aud',
    +            appIds: ['valid-app-id'],
    +            authorizationHeader: 'Bearer validAuthToken',
    +            oauth2: true
    +          },
    +        },
    +      });
    +
    +      const authData = { access_token: 'validAccessToken', id: 'user123' };
    +      await expectAsync(Parse.User.logInWith('mockOauth', { authData })).toBeRejectedWith(
    +        new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 token introspection endpoint URL is missing.')
    +      );
    +    });
    +  });
    +
    +});
    
  • spec/Adapters/Auth/qq.spec.js+252 0 added
    @@ -0,0 +1,252 @@
    +const QqAdapter = require('../../../lib/Adapters/Auth/qq').default;
    +
    +describe('QqAdapter', () => {
    +  let adapter;
    +
    +  beforeEach(() => {
    +    adapter = new QqAdapter.constructor();
    +  });
    +
    +  describe('getUserFromAccessToken', () => {
    +    it('should fetch user data successfully', async () => {
    +      const mockResponse = `callback({"client_id":"validAppId","openid":"user123"})`;
    +
    +      mockFetch([
    +        {
    +          url: 'https://graph.qq.com/oauth2.0/me',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            text: () => Promise.resolve(mockResponse),
    +          },
    +        },
    +      ]);
    +
    +      const result = await adapter.getUserFromAccessToken('validAccessToken');
    +
    +      expect(result).toEqual({ client_id: 'validAppId', openid: 'user123' });
    +    });
    +
    +    it('should throw an error if the API request fails', async () => {
    +      mockFetch([
    +        {
    +          url: 'https://graph.qq.com/oauth2.0/me',
    +          method: 'GET',
    +          response: {
    +            ok: false,
    +            statusText: 'Unauthorized',
    +          },
    +        },
    +      ]);
    +
    +      await expectAsync(
    +        adapter.getUserFromAccessToken('invalidAccessToken')
    +      ).toBeRejectedWithError('qq API request failed.');
    +    });
    +  });
    +
    +  describe('getAccessTokenFromCode', () => {
    +    it('should fetch access token successfully', async () => {
    +      const mockResponse = `callback({"access_token":"validAccessToken","expires_in":3600,"refresh_token":"refreshToken"})`;
    +
    +      mockFetch([
    +        {
    +          url: 'https://graph.qq.com/oauth2.0/token',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            text: () => Promise.resolve(mockResponse),
    +          },
    +        },
    +      ]);
    +
    +      const result = await adapter.getAccessTokenFromCode({
    +        code: 'validCode',
    +        redirect_uri: 'https://your-redirect-uri.com/callback',
    +      });
    +
    +      expect(result).toBe('validAccessToken');
    +    });
    +
    +    it('should throw an error if the API request fails', async () => {
    +      mockFetch([
    +        {
    +          url: 'https://graph.qq.com/oauth2.0/token',
    +          method: 'GET',
    +          response: {
    +            ok: false,
    +            statusText: 'Bad Request',
    +          },
    +        },
    +      ]);
    +
    +      await expectAsync(
    +        adapter.getAccessTokenFromCode({
    +          code: 'invalidCode',
    +          redirect_uri: 'https://your-redirect-uri.com/callback',
    +        })
    +      ).toBeRejectedWithError('qq API request failed.');
    +    });
    +  });
    +
    +  describe('parseResponseData', () => {
    +    it('should parse valid callback response data', () => {
    +      const response = `callback({"key":"value"})`;
    +      const result = adapter.parseResponseData(response);
    +
    +      expect(result).toEqual({ key: 'value' });
    +    });
    +
    +    it('should throw an error if the response data is invalid', () => {
    +      const response = 'invalid response';
    +
    +      expect(() => adapter.parseResponseData(response)).toThrowError(
    +        'qq auth is invalid for this user.'
    +      );
    +    });
    +  });
    +
    +  describe('QqAdapter E2E Test', () => {
    +    beforeEach(async () => {
    +      await reconfigureServer({
    +        auth: {
    +          qq: {
    +            clientId: 'validAppId',
    +            clientSecret: 'validAppSecret',
    +          },
    +        },
    +      });
    +    });
    +
    +    it('should log in user using Qq adapter successfully', async () => {
    +      mockFetch([
    +        {
    +          url: 'https://graph.qq.com/oauth2.0/token',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            text: () =>
    +              Promise.resolve(
    +                `callback({"access_token":"mockAccessToken","expires_in":3600})`
    +              ),
    +          },
    +        },
    +        {
    +          url: 'https://graph.qq.com/oauth2.0/me',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            text: () =>
    +              Promise.resolve(
    +                `callback({"client_id":"validAppId","openid":"user123"})`
    +              ),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { code: 'validCode', redirect_uri: 'https://your-redirect-uri.com/callback' };
    +      const user = await Parse.User.logInWith('qq', { authData });
    +
    +      expect(user.id).toBeDefined();
    +    });
    +
    +    it('should handle error when Qq returns invalid code', async () => {
    +      mockFetch([
    +        {
    +          url: 'https://graph.qq.com/oauth2.0/token',
    +          method: 'GET',
    +          response: {
    +            ok: false,
    +            statusText: 'Invalid code',
    +          },
    +        },
    +      ]);
    +
    +      const authData = { code: 'invalidCode', redirect_uri: 'https://your-redirect-uri.com/callback' };
    +
    +      await expectAsync(Parse.User.logInWith('qq', { authData })).toBeRejectedWithError(
    +        'qq API request failed.'
    +      );
    +    });
    +
    +    it('should handle error when Qq returns invalid user data', async () => {
    +      mockFetch([
    +        {
    +          url: 'https://graph.qq.com/oauth2.0/token',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            text: () =>
    +              Promise.resolve(
    +                `callback({"access_token":"mockAccessToken","expires_in":3600})`
    +              ),
    +          },
    +        },
    +        {
    +          url: 'https://graph.qq.com/oauth2.0/me',
    +          method: 'GET',
    +          response: {
    +            ok: false,
    +            statusText: 'Unauthorized',
    +          },
    +        },
    +      ]);
    +
    +      const authData = { code: 'validCode', redirect_uri: 'https://your-redirect-uri.com/callback' };
    +
    +      await expectAsync(Parse.User.logInWith('qq', { authData })).toBeRejectedWithError(
    +        'qq API request failed.'
    +      );
    +    });
    +
    +    it('e2e secure does not support insecure payload', async () => {
    +      mockFetch();
    +      const authData = { id: 'mockUserId', access_token: 'mockAccessToken' };
    +      await expectAsync(Parse.User.logInWith('qq', { authData })).toBeRejectedWithError(
    +        'qq code is required.'
    +      );
    +    });
    +
    +    it('e2e insecure does support secure payload', async () => {
    +      await reconfigureServer({
    +        auth: {
    +          qq: {
    +            appId: 'validAppId',
    +            appSecret: 'validAppSecret',
    +            enableInsecureAuth: true,
    +          },
    +        },
    +      });
    +
    +      mockFetch([
    +        {
    +          url: 'https://graph.qq.com/oauth2.0/token',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            text: () =>
    +              Promise.resolve(
    +                `callback({"access_token":"mockAccessToken","expires_in":3600})`
    +              ),
    +          },
    +        },
    +        {
    +          url: 'https://graph.qq.com/oauth2.0/me',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            text: () =>
    +              Promise.resolve(
    +                `callback({"client_id":"validAppId","openid":"user123"})`
    +              ),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { code: 'validCode', redirect_uri: 'https://your-redirect-uri.com/callback' };
    +      const user = await Parse.User.logInWith('qq', { authData });
    +
    +      expect(user.id).toBeDefined();
    +    });
    +  });
    +});
    
  • spec/Adapters/Auth/spotify.spec.js+113 0 added
    @@ -0,0 +1,113 @@
    +const SpotifyAdapter = require('../../../lib/Adapters/Auth/spotify').default;
    +
    +describe('SpotifyAdapter', () => {
    +  let adapter;
    +
    +  beforeEach(() => {
    +    adapter = new SpotifyAdapter.constructor();
    +  });
    +
    +  describe('getUserFromAccessToken', () => {
    +    it('should fetch user data successfully', async () => {
    +      const mockResponse = {
    +        id: 'spotifyUser123',
    +      };
    +
    +      mockFetch([
    +        {
    +          url: 'https://api.spotify.com/v1/me',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve(mockResponse),
    +          },
    +        },
    +      ]);
    +
    +      const result = await adapter.getUserFromAccessToken('validAccessToken');
    +
    +      expect(result).toEqual({ id: 'spotifyUser123' });
    +    });
    +
    +    it('should throw an error if the API request fails', async () => {
    +      mockFetch([
    +        {
    +          url: 'https://api.spotify.com/v1/me',
    +          method: 'GET',
    +          response: {
    +            ok: false,
    +            statusText: 'Unauthorized',
    +          },
    +        },
    +      ]);
    +
    +      await expectAsync(adapter.getUserFromAccessToken('invalidAccessToken')).toBeRejectedWithError(
    +        'Spotify API request failed.'
    +      );
    +    });
    +  });
    +
    +  describe('getAccessTokenFromCode', () => {
    +    it('should fetch access token successfully', async () => {
    +      const mockResponse = {
    +        access_token: 'validAccessToken',
    +        expires_in: 3600,
    +        refresh_token: 'refreshToken',
    +      };
    +
    +      mockFetch([
    +        {
    +          url: 'https://accounts.spotify.com/api/token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve(mockResponse),
    +          },
    +        },
    +      ]);
    +
    +      const authData = {
    +        code: 'validCode',
    +        redirect_uri: 'https://your-redirect-uri.com/callback',
    +        code_verifier: 'validCodeVerifier',
    +      };
    +
    +      const result = await adapter.getAccessTokenFromCode(authData);
    +
    +      expect(result).toEqual(mockResponse);
    +    });
    +
    +    it('should throw an error if authData is missing required fields', async () => {
    +      const authData = {
    +        redirect_uri: 'https://your-redirect-uri.com/callback',
    +      };
    +
    +      await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWithError(
    +        'Spotify auth configuration authData.code and/or authData.redirect_uri and/or authData.code_verifier.'
    +      );
    +    });
    +
    +    it('should throw an error if the API request fails', async () => {
    +      mockFetch([
    +        {
    +          url: 'https://accounts.spotify.com/api/token',
    +          method: 'POST',
    +          response: {
    +            ok: false,
    +            statusText: 'Bad Request',
    +          },
    +        },
    +      ]);
    +
    +      const authData = {
    +        code: 'invalidCode',
    +        redirect_uri: 'https://your-redirect-uri.com/callback',
    +        code_verifier: 'invalidCodeVerifier',
    +      };
    +
    +      await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWithError(
    +        'Spotify API request failed.'
    +      );
    +    });
    +  });
    +});
    
  • spec/Adapters/Auth/twitter.spec.js+120 0 added
    @@ -0,0 +1,120 @@
    +const TwitterAuthAdapter = require('../../../lib/Adapters/Auth/twitter').default;
    +
    +describe('TwitterAuthAdapter', function () {
    +  let adapter;
    +  const validOptions = {
    +    consumer_key: 'validConsumerKey',
    +    consumer_secret: 'validConsumerSecret',
    +  };
    +
    +  beforeEach(function () {
    +    adapter = new TwitterAuthAdapter.constructor();
    +  });
    +
    +  describe('Test configuration errors', function () {
    +    it('should throw an error when options are missing', function () {
    +      expect(() => adapter.validateOptions()).toThrowError('Twitter auth options are required.');
    +    });
    +
    +    it('should throw an error when consumer_key and consumer_secret are missing for secure auth', function () {
    +      const options = { enableInsecureAuth: false };
    +      expect(() => adapter.validateOptions(options)).toThrowError(
    +        'Consumer key and secret are required for secure Twitter auth.'
    +      );
    +    });
    +
    +    it('should not throw an error when valid options are provided', function () {
    +      expect(() => adapter.validateOptions(validOptions)).not.toThrow();
    +    });
    +  });
    +
    +  describe('Validate Insecure Auth', function () {
    +    it('should throw an error if oauth_token or oauth_token_secret are missing', async function () {
    +      const authData = { oauth_token: 'validToken' }; // Missing oauth_token_secret
    +      await expectAsync(adapter.validateInsecureAuth(authData, validOptions)).toBeRejectedWithError(
    +        'Twitter insecure auth requires oauth_token and oauth_token_secret.'
    +      );
    +    });
    +
    +    it('should validate insecure auth successfully when data matches', async function () {
    +      spyOn(adapter, 'request').and.returnValue(
    +        Promise.resolve({
    +          json: () => Promise.resolve({ id: 'validUserId' }),
    +        })
    +      );
    +
    +      const authData = {
    +        id: 'validUserId',
    +        oauth_token: 'validToken',
    +        oauth_token_secret: 'validSecret',
    +      };
    +      await expectAsync(adapter.validateInsecureAuth(authData, validOptions)).toBeResolved();
    +    });
    +
    +    it('should throw an error when user ID does not match', async function () {
    +      spyOn(adapter, 'request').and.returnValue(
    +        Promise.resolve({
    +          json: () => Promise.resolve({ id: 'invalidUserId' }),
    +        })
    +      );
    +
    +      const authData = {
    +        id: 'validUserId',
    +        oauth_token: 'validToken',
    +        oauth_token_secret: 'validSecret',
    +      };
    +      await expectAsync(adapter.validateInsecureAuth(authData, validOptions)).toBeRejectedWithError(
    +        'Twitter auth is invalid for this user.'
    +      );
    +    });
    +  });
    +
    +  describe('End-to-End Tests', function () {
    +    beforeEach(async function () {
    +      await reconfigureServer({
    +        auth: {
    +          twitter: validOptions,
    +        }
    +      })
    +    });
    +
    +    it('should authenticate user successfully using validateAuthData', async function () {
    +      spyOn(adapter, 'exchangeAccessToken').and.returnValue(
    +        Promise.resolve({ oauth_token: 'validToken', user_id: 'validUserId' })
    +      );
    +
    +      const authData = {
    +        oauth_token: 'validToken',
    +        oauth_verifier: 'validVerifier',
    +      };
    +      await expectAsync(adapter.validateAuthData(authData, validOptions)).toBeResolved();
    +      expect(authData.id).toBe('validUserId');
    +      expect(authData.auth_token).toBe('validToken');
    +    });
    +
    +    it('should handle multiple configurations and validate successfully', async function () {
    +      const authData = {
    +        consumer_key: 'validConsumerKey',
    +        oauth_token: 'validToken',
    +        oauth_token_secret: 'validSecret',
    +      };
    +
    +      const optionsArray = [
    +        { consumer_key: 'invalidKey', consumer_secret: 'invalidSecret' },
    +        validOptions,
    +      ];
    +
    +      const selectedOption = adapter.handleMultipleConfigurations(authData, optionsArray);
    +      expect(selectedOption).toEqual(validOptions);
    +    });
    +
    +    it('should throw an error when no matching configuration is found', function () {
    +      const authData = { consumer_key: 'missingKey' };
    +      const optionsArray = [validOptions];
    +
    +      expect(() => adapter.handleMultipleConfigurations(authData, optionsArray)).toThrowError(
    +        'Twitter auth is invalid for this user.'
    +      );
    +    });
    +  });
    +});
    
  • spec/Adapters/Auth/wechat.spec.js+234 0 added
    @@ -0,0 +1,234 @@
    +const WeChatAdapter = require('../../../lib/Adapters/Auth/wechat').default;
    +
    +describe('WeChatAdapter', function () {
    +  let adapter;
    +
    +  beforeEach(function () {
    +    adapter = new WeChatAdapter.constructor();
    +  });
    +
    +  describe('Test getUserFromAccessToken', function () {
    +    it('should fetch user successfully', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.weixin.qq.com/sns/auth?access_token=validToken&openid=validOpenId',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve({ errcode: 0, id: 'validUserId' }),
    +          },
    +        },
    +      ]);
    +
    +      const user = await adapter.getUserFromAccessToken('validToken', { id: 'validOpenId' });
    +
    +      expect(global.fetch).toHaveBeenCalledWith(
    +        'https://api.weixin.qq.com/sns/auth?access_token=validToken&openid=validOpenId'
    +      );
    +      expect(user).toEqual({ errcode: 0, id: 'validUserId' });
    +    });
    +
    +    it('should throw error for invalid response', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.weixin.qq.com/sns/auth?access_token=invalidToken&openid=undefined',
    +          method: 'GET',
    +          response: {
    +            ok: false,
    +            json: () => Promise.resolve({ errcode: 40013, errmsg: 'Invalid token' }),
    +          },
    +        },
    +      ]);
    +
    +      await expectAsync(adapter.getUserFromAccessToken('invalidToken', 'invalidOpenId')).toBeRejectedWith(
    +        jasmine.objectContaining({ message: 'WeChat auth is invalid for this user.' })
    +      );
    +    });
    +  });
    +
    +  describe('Test getAccessTokenFromCode', function () {
    +    it('should fetch access token successfully', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=validCode&grant_type=authorization_code',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve({ access_token: 'validToken', errcode: 0 }),
    +          },
    +        },
    +      ]);
    +
    +      adapter.validateOptions({ clientId: 'validAppId', clientSecret: 'validAppSecret' });
    +      const authData = { code: 'validCode' };
    +      const token = await adapter.getAccessTokenFromCode(authData);
    +
    +      expect(global.fetch).toHaveBeenCalledWith(
    +        'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=validCode&grant_type=authorization_code'
    +      );
    +      expect(token).toEqual('validToken');
    +    });
    +
    +    it('should throw error for invalid response', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=invalidCode&grant_type=authorization_code',
    +          method: 'GET',
    +          response: {
    +            ok: false,
    +            json: () => Promise.resolve({ errcode: 40029, errmsg: 'Invalid code' }),
    +          },
    +        },
    +      ]);
    +      adapter.validateOptions({ clientId: 'validAppId', clientSecret: 'validAppSecret' });
    +
    +      const authData = { code: 'invalidCode' };
    +
    +      await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWith(
    +        jasmine.objectContaining({ message: 'WeChat auth is invalid for this user.' })
    +      );
    +    });
    +  });
    +
    +  describe('WeChatAdapter E2E Tests', function () {
    +    beforeEach(async () => {
    +      await reconfigureServer({
    +        auth: {
    +          wechat: {
    +            clientId: 'validAppId',
    +            clientSecret: 'validAppSecret',
    +            enableInsecureAuth: false,
    +          },
    +        },
    +      });
    +    });
    +
    +    it('should authenticate user successfully using WeChatAdapter', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=validCode&grant_type=authorization_code',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve({ access_token: 'validAccessToken', openid: 'user123', errcode: 0 }),
    +          },
    +        },
    +        {
    +          url: 'https://api.weixin.qq.com/sns/auth?access_token=validAccessToken&openid=user123',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve({ errcode: 0, id: 'user123' }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
    +      const user = await Parse.User.logInWith('wechat', { authData });
    +
    +      expect(user.id).toBeDefined();
    +    });
    +
    +    it('should handle invalid code error gracefully', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=invalidCode&grant_type=authorization_code',
    +          method: 'GET',
    +          response: {
    +            ok: false,
    +            json: () => Promise.resolve({ errcode: 40029, errmsg: 'Invalid code' }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { code: 'invalidCode', redirect_uri: 'http://example.com/callback' };
    +
    +      await expectAsync(Parse.User.logInWith('wechat', { authData })).toBeRejectedWith(
    +        jasmine.objectContaining({ message: 'WeChat auth is invalid for this user.' })
    +      );
    +    });
    +
    +    it('should handle error when fetching user data fails', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=validCode&grant_type=authorization_code',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve({ access_token: 'validAccessToken', openid: 'user123', errcode: 0 }),
    +          },
    +        },
    +        {
    +          url: 'https://api.weixin.qq.com/sns/auth?access_token=validAccessToken&openid=user123',
    +          method: 'GET',
    +          response: {
    +            ok: false,
    +            json: () => Promise.resolve({ errcode: 40013, errmsg: 'Invalid token' }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
    +
    +      await expectAsync(Parse.User.logInWith('wechat', { authData })).toBeRejectedWith(
    +        jasmine.objectContaining({ message: 'WeChat auth is invalid for this user.' })
    +      );
    +    });
    +
    +    it('should allow insecure auth when enabled', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.weixin.qq.com/sns/auth?access_token=validAccessToken&openid=user123',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve({ errcode: 0, id: 'user123' }),
    +          },
    +        },
    +      ]);
    +
    +      await reconfigureServer({
    +        auth: {
    +          wechat: {
    +            appId: 'validAppId',
    +            appSecret: 'validAppSecret',
    +            enableInsecureAuth: true,
    +          },
    +        },
    +      });
    +
    +      const authData = { access_token: 'validAccessToken', id: 'user123' };
    +      const user = await Parse.User.logInWith('wechat', { authData });
    +
    +      expect(user.id).toBeDefined();
    +    });
    +
    +    it('should reject insecure auth when user id does not match', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.weixin.qq.com/sns/auth?access_token=validAccessToken&openid=incorrectUserId',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve({ errcode: 0, id: 'incorrectUser' }),
    +          },
    +        },
    +      ]);
    +
    +      await reconfigureServer({
    +        auth: {
    +          wechat: {
    +            appId: 'validAppId',
    +            appSecret: 'validAppSecret',
    +            enableInsecureAuth: true,
    +          },
    +        },
    +      });
    +
    +      const authData = { access_token: 'validAccessToken', id: 'incorrectUserId' };
    +      await expectAsync(Parse.User.logInWith('wechat', { authData })).toBeRejectedWith(
    +        jasmine.objectContaining({ message: 'WeChat auth is invalid for this user.' })
    +      );
    +    });
    +  });
    +});
    
  • spec/Adapters/Auth/weibo.spec.js+204 0 added
    @@ -0,0 +1,204 @@
    +const WeiboAdapter = require('../../../lib/Adapters/Auth/weibo').default;
    +
    +describe('WeiboAdapter', function () {
    +  let adapter;
    +
    +  beforeEach(function () {
    +    adapter = new WeiboAdapter.constructor();
    +  });
    +
    +  describe('Test configuration errors', function () {
    +    it('should throw error if code or redirect_uri is missing', async function () {
    +      const invalidAuthData = [
    +        {},
    +        { code: 'validCode' },
    +        { redirect_uri: 'http://example.com/callback' },
    +      ];
    +
    +      for (const authData of invalidAuthData) {
    +        await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWith(
    +          jasmine.objectContaining({
    +            message: 'Weibo auth requires code and redirect_uri to be sent.',
    +          })
    +        );
    +      }
    +    });
    +  });
    +
    +  describe('Test getUserFromAccessToken', function () {
    +    it('should fetch user successfully', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.weibo.com/oauth2/get_token_info',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve({ uid: 'validUserId' }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { id: 'validUserId' };
    +      const user = await adapter.getUserFromAccessToken('validToken', authData);
    +
    +      expect(global.fetch).toHaveBeenCalledWith(
    +        'https://api.weibo.com/oauth2/get_token_info',
    +        jasmine.objectContaining({
    +          method: 'POST',
    +          headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    +        })
    +      );
    +      expect(user).toEqual({ id: 'validUserId' });
    +    });
    +
    +    it('should throw error for invalid response', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.weibo.com/oauth2/get_token_info',
    +          method: 'POST',
    +          response: {
    +            ok: false,
    +            json: () => Promise.resolve({}),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { id: 'invalidUserId' };
    +      await expectAsync(adapter.getUserFromAccessToken('invalidToken', authData)).toBeRejectedWith(
    +        jasmine.objectContaining({
    +          message: 'Weibo auth is invalid for this user.',
    +        })
    +      );
    +    });
    +  });
    +
    +  describe('Test getAccessTokenFromCode', function () {
    +    it('should fetch access token successfully', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.weibo.com/oauth2/access_token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve({ access_token: 'validToken', uid: 'validUserId' }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
    +      const token = await adapter.getAccessTokenFromCode(authData);
    +
    +      expect(global.fetch).toHaveBeenCalledWith(
    +        'https://api.weibo.com/oauth2/access_token',
    +        jasmine.objectContaining({
    +          method: 'POST',
    +          headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    +        })
    +      );
    +      expect(token).toEqual('validToken');
    +    });
    +
    +    it('should throw error for invalid response', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.weibo.com/oauth2/access_token',
    +          method: 'POST',
    +          response: {
    +            ok: false,
    +            json: () => Promise.resolve({ errcode: 40029 }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { code: 'invalidCode', redirect_uri: 'http://example.com/callback' };
    +      await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWith(
    +        jasmine.objectContaining({
    +          message: 'Weibo auth is invalid for this user.',
    +        })
    +      );
    +    });
    +  });
    +
    +  describe('WeiboAdapter E2E Tests', function () {
    +    beforeEach(async () => {
    +      await reconfigureServer({
    +        auth: {
    +          weibo: {
    +            clientId: 'validAppId',
    +            clientSecret: 'validAppSecret',
    +          },
    +        }
    +      });
    +    });
    +
    +    it('should authenticate user successfully using WeiboAdapter', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.weibo.com/oauth2/access_token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve({ access_token: 'validAccessToken', uid: 'user123' }),
    +          },
    +        },
    +        {
    +          url: 'https://api.weibo.com/oauth2/get_token_info',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve({ uid: 'user123' }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
    +      const user = await Parse.User.logInWith('weibo', { authData });
    +
    +      expect(user.id).toBeDefined();
    +    });
    +
    +    it('should handle invalid code error gracefully', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.weibo.com/oauth2/access_token',
    +          method: 'POST',
    +          response: {
    +            ok: false,
    +            json: () => Promise.resolve({ errcode: 40029 }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { code: 'invalidCode', redirect_uri: 'http://example.com/callback' };
    +      await expectAsync(Parse.User.logInWith('weibo', { authData })).toBeRejectedWith(
    +        jasmine.objectContaining({ message: 'Weibo auth is invalid for this user.' })
    +      );
    +    });
    +
    +    it('should handle error when fetching user data fails', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.weibo.com/oauth2/access_token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve({ access_token: 'validAccessToken', uid: 'user123' }),
    +          },
    +        },
    +        {
    +          url: 'https://api.weibo.com/oauth2/get_token_info',
    +          method: 'POST',
    +          response: {
    +            ok: false,
    +            json: () => Promise.resolve({}),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
    +      await expectAsync(Parse.User.logInWith('weibo', { authData })).toBeRejectedWith(
    +        jasmine.objectContaining({ message: 'Weibo auth is invalid for this user.' })
    +      );
    +    });
    +  });
    +});
    
  • spec/AuthenticationAdapters.spec.js+7 811 modified
    @@ -3,99 +3,8 @@ const Config = require('../lib/Config');
     const defaultColumns = require('../lib/Controllers/SchemaController').defaultColumns;
     const authenticationLoader = require('../lib/Adapters/Auth');
     const path = require('path');
    -const responses = {
    -  gpgames: { playerId: 'userId' },
    -  instagram: { id: 'userId' },
    -  janrainengage: { stat: 'ok', profile: { identifier: 'userId' } },
    -  janraincapture: { stat: 'ok', result: 'userId' },
    -  line: { userId: 'userId' },
    -  vkontakte: { response: [{ id: 'userId' }] },
    -  google: { sub: 'userId' },
    -  wechat: { errcode: 0 },
    -  weibo: { uid: 'userId' },
    -  qq: 'callback( {"openid":"userId"} );', // yes it's like that, run eval in the client :P
    -  phantauth: { sub: 'userId' },
    -  microsoft: { id: 'userId', mail: 'userMail' },
    -};
     
     describe('AuthenticationProviders', function () {
    -  [
    -    'apple',
    -    'gcenter',
    -    'gpgames',
    -    'facebook',
    -    'github',
    -    'instagram',
    -    'google',
    -    'linkedin',
    -    'meetup',
    -    'twitter',
    -    'janrainengage',
    -    'janraincapture',
    -    'line',
    -    'vkontakte',
    -    'qq',
    -    'spotify',
    -    'wechat',
    -    'weibo',
    -    'phantauth',
    -    'microsoft',
    -    'keycloak',
    -  ].map(function (providerName) {
    -    it('Should validate structure of ' + providerName, done => {
    -      const provider = require('../lib/Adapters/Auth/' + providerName);
    -      jequal(typeof provider.validateAuthData, 'function');
    -      jequal(typeof provider.validateAppId, 'function');
    -      const validateAuthDataPromise = provider.validateAuthData({}, {});
    -      const validateAppIdPromise = provider.validateAppId('app', 'key', {});
    -      jequal(validateAuthDataPromise.constructor, Promise.prototype.constructor);
    -      jequal(validateAppIdPromise.constructor, Promise.prototype.constructor);
    -      validateAuthDataPromise.then(
    -        () => {},
    -        () => {}
    -      );
    -      validateAppIdPromise.then(
    -        () => {},
    -        () => {}
    -      );
    -      done();
    -    });
    -
    -    it(`should provide the right responses for adapter ${providerName}`, async () => {
    -      const noResponse = ['twitter', 'apple', 'gcenter', 'google', 'keycloak'];
    -      if (noResponse.includes(providerName)) {
    -        return;
    -      }
    -      spyOn(require('../lib/Adapters/Auth/httpsRequest'), 'get').and.callFake(options => {
    -        if (
    -          options ===
    -            'https://oauth.vk.com/access_token?client_id=appId&client_secret=appSecret&v=5.123&grant_type=client_credentials' ||
    -          options ===
    -            'https://oauth.vk.com/access_token?client_id=appId&client_secret=appSecret&v=5.124&grant_type=client_credentials'
    -        ) {
    -          return {
    -            access_token: 'access_token',
    -          };
    -        }
    -        return Promise.resolve(responses[providerName] || { id: 'userId' });
    -      });
    -      spyOn(require('../lib/Adapters/Auth/httpsRequest'), 'request').and.callFake(() => {
    -        return Promise.resolve(responses[providerName] || { id: 'userId' });
    -      });
    -      const provider = require('../lib/Adapters/Auth/' + providerName);
    -      let params = {};
    -      if (providerName === 'vkontakte') {
    -        params = {
    -          appIds: 'appId',
    -          appSecret: 'appSecret',
    -        };
    -        await provider.validateAuthData({ id: 'userId' }, params);
    -        params.appVersion = '5.123';
    -      }
    -      await provider.validateAuthData({ id: 'userId' }, params);
    -    });
    -  });
    -
       const getMockMyOauthProvider = function () {
         return {
           authData: {
    @@ -568,46 +477,6 @@ describe('AuthenticationProviders', function () {
       });
     });
     
    -describe('instagram auth adapter', () => {
    -  const instagram = require('../lib/Adapters/Auth/instagram');
    -  const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
    -
    -  it('should use default api', async () => {
    -    spyOn(httpsRequest, 'get').and.callFake(() => {
    -      return Promise.resolve({ data: { id: 'userId' } });
    -    });
    -    await instagram.validateAuthData({ id: 'userId', access_token: 'the_token' }, {});
    -    expect(httpsRequest.get).toHaveBeenCalledWith(
    -      'https://graph.instagram.com/me?fields=id&access_token=the_token'
    -    );
    -  });
    -  it('response object without data child', async () => {
    -    spyOn(httpsRequest, 'get').and.callFake(() => {
    -      return Promise.resolve({ id: 'userId' });
    -    });
    -    await instagram.validateAuthData({ id: 'userId', access_token: 'the_token' }, {});
    -    expect(httpsRequest.get).toHaveBeenCalledWith(
    -      'https://graph.instagram.com/me?fields=id&access_token=the_token'
    -    );
    -  });
    -  it('should pass in api url', async () => {
    -    spyOn(httpsRequest, 'get').and.callFake(() => {
    -      return Promise.resolve({ data: { id: 'userId' } });
    -    });
    -    await instagram.validateAuthData(
    -      {
    -        id: 'userId',
    -        access_token: 'the_token',
    -        apiURL: 'https://new-api.instagram.com/v1/',
    -      },
    -      {}
    -    );
    -    expect(httpsRequest.get).toHaveBeenCalledWith(
    -      'https://new-api.instagram.com/v1/me?fields=id&access_token=the_token'
    -    );
    -  });
    -});
    -
     describe('google auth adapter', () => {
       const google = require('../lib/Adapters/Auth/google');
       const jwt = require('jsonwebtoken');
    @@ -730,35 +599,6 @@ describe('google auth adapter', () => {
       });
     });
     
    -describe('google play games service auth', () => {
    -  const gpgames = require('../lib/Adapters/Auth/gpgames');
    -  const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
    -
    -  it('validateAuthData should pass validation', async () => {
    -    spyOn(httpsRequest, 'get').and.callFake(() => {
    -      return Promise.resolve({ playerId: 'userId' });
    -    });
    -    await gpgames.validateAuthData({
    -      id: 'userId',
    -      access_token: 'access_token',
    -    });
    -  });
    -
    -  it('validateAuthData should throw error', async () => {
    -    spyOn(httpsRequest, 'get').and.callFake(() => {
    -      return Promise.resolve({ playerId: 'invalid' });
    -    });
    -    try {
    -      await gpgames.validateAuthData({
    -        id: 'userId',
    -        access_token: 'access_token',
    -      });
    -    } catch (e) {
    -      expect(e.message).toBe('Google Play Games Services - authData is invalid for this user.');
    -    }
    -  });
    -});
    -
     describe('keycloak auth adapter', () => {
       const keycloak = require('../lib/Adapters/Auth/keycloak');
       const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
    @@ -987,433 +827,6 @@ describe('keycloak auth adapter', () => {
       });
     });
     
    -describe('oauth2 auth adapter', () => {
    -  const oauth2 = require('../lib/Adapters/Auth/oauth2');
    -  const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
    -
    -  it('properly loads OAuth2 adapter via the "oauth2" option', () => {
    -    const options = {
    -      oauth2Authentication: {
    -        oauth2: true,
    -      },
    -    };
    -    const loadedAuthAdapter = authenticationLoader.loadAuthAdapter('oauth2Authentication', options);
    -    expect(loadedAuthAdapter.adapter).toEqual(oauth2);
    -  });
    -
    -  it('properly loads OAuth2 adapter with options', () => {
    -    const options = {
    -      oauth2Authentication: {
    -        oauth2: true,
    -        tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
    -        useridField: 'sub',
    -        appidField: 'appId',
    -        appIds: ['a', 'b'],
    -        authorizationHeader: 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=',
    -        debug: true,
    -      },
    -    };
    -    const loadedAuthAdapter = authenticationLoader.loadAuthAdapter('oauth2Authentication', options);
    -    const appIds = loadedAuthAdapter.appIds;
    -    const providerOptions = loadedAuthAdapter.providerOptions;
    -    expect(providerOptions.tokenIntrospectionEndpointUrl).toEqual('https://example.com/introspect');
    -    expect(providerOptions.useridField).toEqual('sub');
    -    expect(providerOptions.appidField).toEqual('appId');
    -    expect(appIds).toEqual(['a', 'b']);
    -    expect(providerOptions.authorizationHeader).toEqual('Basic dXNlcm5hbWU6cGFzc3dvcmQ=');
    -    expect(providerOptions.debug).toEqual(true);
    -  });
    -
    -  it('validateAppId should fail if OAuth2 tokenIntrospectionEndpointUrl is not configured properly', async () => {
    -    const options = {
    -      oauth2Authentication: {
    -        oauth2: true,
    -        appIds: ['a', 'b'],
    -        appidField: 'appId',
    -      },
    -    };
    -    const authData = {
    -      id: 'fakeid',
    -      access_token: 'sometoken',
    -    };
    -    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
    -      'oauth2Authentication',
    -      options
    -    );
    -    try {
    -      await adapter.validateAppId(appIds, authData, providerOptions);
    -    } catch (e) {
    -      expect(e.message).toBe(
    -        'OAuth2 token introspection endpoint URL is missing from configuration!'
    -      );
    -    }
    -  });
    -
    -  it('validateAppId appidField optional', async () => {
    -    const options = {
    -      oauth2Authentication: {
    -        oauth2: true,
    -        tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
    -      },
    -    };
    -    const authData = {
    -      id: 'fakeid',
    -      access_token: 'sometoken',
    -    };
    -    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
    -      'oauth2Authentication',
    -      options
    -    );
    -    try {
    -      await adapter.validateAppId(appIds, authData, providerOptions);
    -    } catch (e) {
    -      // Should not reach here
    -      fail(e);
    -    }
    -  });
    -
    -  it('validateAppId should fail without appIds', async () => {
    -    const options = {
    -      oauth2Authentication: {
    -        oauth2: true,
    -        tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
    -        appidField: 'appId',
    -      },
    -    };
    -    const authData = {
    -      id: 'fakeid',
    -      access_token: 'sometoken',
    -    };
    -    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
    -      'oauth2Authentication',
    -      options
    -    );
    -    try {
    -      await adapter.validateAppId(appIds, authData, providerOptions);
    -    } catch (e) {
    -      expect(e.message).toBe(
    -        'OAuth2 configuration is missing the client app IDs ("appIds" config parameter).'
    -      );
    -    }
    -  });
    -
    -  it('validateAppId should fail empty appIds', async () => {
    -    const options = {
    -      oauth2Authentication: {
    -        oauth2: true,
    -        tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
    -        appidField: 'appId',
    -        appIds: [],
    -      },
    -    };
    -    const authData = {
    -      id: 'fakeid',
    -      access_token: 'sometoken',
    -    };
    -    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
    -      'oauth2Authentication',
    -      options
    -    );
    -    try {
    -      await adapter.validateAppId(appIds, authData, providerOptions);
    -    } catch (e) {
    -      expect(e.message).toBe(
    -        'OAuth2 configuration is missing the client app IDs ("appIds" config parameter).'
    -      );
    -    }
    -  });
    -
    -  it('validateAppId invalid accessToken', async () => {
    -    const options = {
    -      oauth2Authentication: {
    -        oauth2: true,
    -        tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
    -        appidField: 'appId',
    -        appIds: ['a', 'b'],
    -      },
    -    };
    -    const authData = {
    -      id: 'fakeid',
    -      access_token: 'sometoken',
    -    };
    -    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
    -      'oauth2Authentication',
    -      options
    -    );
    -    spyOn(httpsRequest, 'request').and.callFake(() => {
    -      return Promise.resolve({});
    -    });
    -    try {
    -      await adapter.validateAppId(appIds, authData, providerOptions);
    -    } catch (e) {
    -      expect(e.message).toBe('OAuth2 access token is invalid for this user.');
    -    }
    -  });
    -
    -  it('validateAppId invalid accessToken appId', async () => {
    -    const options = {
    -      oauth2Authentication: {
    -        oauth2: true,
    -        tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
    -        appidField: 'appId',
    -        appIds: ['a', 'b'],
    -      },
    -    };
    -    const authData = {
    -      id: 'fakeid',
    -      access_token: 'sometoken',
    -    };
    -    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
    -      'oauth2Authentication',
    -      options
    -    );
    -    spyOn(httpsRequest, 'request').and.callFake(() => {
    -      return Promise.resolve({ active: true });
    -    });
    -    try {
    -      await adapter.validateAppId(appIds, authData, providerOptions);
    -    } catch (e) {
    -      expect(e.message).toBe(
    -        "OAuth2: the access_token's appID is empty or is not in the list of permitted appIDs in the auth configuration."
    -      );
    -    }
    -  });
    -
    -  it('validateAppId valid accessToken appId', async () => {
    -    const options = {
    -      oauth2Authentication: {
    -        oauth2: true,
    -        tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
    -        appidField: 'appId',
    -        appIds: ['a', 'b'],
    -      },
    -    };
    -    const authData = {
    -      id: 'fakeid',
    -      access_token: 'sometoken',
    -    };
    -    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
    -      'oauth2Authentication',
    -      options
    -    );
    -    spyOn(httpsRequest, 'request').and.callFake(() => {
    -      return Promise.resolve({
    -        active: true,
    -        appId: 'a',
    -      });
    -    });
    -    try {
    -      await adapter.validateAppId(appIds, authData, providerOptions);
    -    } catch (e) {
    -      // Should not enter here
    -      fail(e);
    -    }
    -  });
    -
    -  it('validateAppId valid accessToken appId array', async () => {
    -    const options = {
    -      oauth2Authentication: {
    -        oauth2: true,
    -        tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
    -        appidField: 'appId',
    -        appIds: ['a', 'b'],
    -      },
    -    };
    -    const authData = {
    -      id: 'fakeid',
    -      access_token: 'sometoken',
    -    };
    -    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
    -      'oauth2Authentication',
    -      options
    -    );
    -    spyOn(httpsRequest, 'request').and.callFake(() => {
    -      return Promise.resolve({
    -        active: true,
    -        appId: ['a'],
    -      });
    -    });
    -    try {
    -      await adapter.validateAppId(appIds, authData, providerOptions);
    -    } catch (e) {
    -      // Should not enter here
    -      fail(e);
    -    }
    -  });
    -
    -  it('validateAppId valid accessToken invalid appId', async () => {
    -    const options = {
    -      oauth2Authentication: {
    -        oauth2: true,
    -        tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
    -        appidField: 'appId',
    -        appIds: ['a', 'b'],
    -      },
    -    };
    -    const authData = {
    -      id: 'fakeid',
    -      access_token: 'sometoken',
    -    };
    -    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
    -      'oauth2Authentication',
    -      options
    -    );
    -    spyOn(httpsRequest, 'request').and.callFake(() => {
    -      return Promise.resolve({
    -        active: true,
    -        appId: 'unknown',
    -      });
    -    });
    -    try {
    -      await adapter.validateAppId(appIds, authData, providerOptions);
    -    } catch (e) {
    -      expect(e.message).toBe(
    -        "OAuth2: the access_token's appID is empty or is not in the list of permitted appIDs in the auth configuration."
    -      );
    -    }
    -  });
    -
    -  it('validateAuthData should fail if OAuth2 tokenIntrospectionEndpointUrl is not configured properly', async () => {
    -    const options = {
    -      oauth2Authentication: {
    -        oauth2: true,
    -      },
    -    };
    -    const authData = {
    -      id: 'fakeid',
    -      access_token: 'sometoken',
    -    };
    -    const { adapter, providerOptions } = authenticationLoader.loadAuthAdapter(
    -      'oauth2Authentication',
    -      options
    -    );
    -    try {
    -      await adapter.validateAuthData(authData, providerOptions);
    -    } catch (e) {
    -      expect(e.message).toBe(
    -        'OAuth2 token introspection endpoint URL is missing from configuration!'
    -      );
    -    }
    -  });
    -
    -  it('validateAuthData invalid accessToken', async () => {
    -    const options = {
    -      oauth2Authentication: {
    -        oauth2: true,
    -        tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
    -        useridField: 'sub',
    -        appidField: 'appId',
    -        appIds: ['a', 'b'],
    -        authorizationHeader: 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=',
    -      },
    -    };
    -    const authData = {
    -      id: 'fakeid',
    -      access_token: 'sometoken',
    -    };
    -    const { adapter, providerOptions } = authenticationLoader.loadAuthAdapter(
    -      'oauth2Authentication',
    -      options
    -    );
    -    spyOn(httpsRequest, 'request').and.callFake(() => {
    -      return Promise.resolve({});
    -    });
    -    try {
    -      await adapter.validateAuthData(authData, providerOptions);
    -    } catch (e) {
    -      expect(e.message).toBe('OAuth2 access token is invalid for this user.');
    -    }
    -    expect(httpsRequest.request).toHaveBeenCalledWith(
    -      {
    -        hostname: 'example.com',
    -        path: '/introspect',
    -        method: 'POST',
    -        headers: {
    -          'Content-Type': 'application/x-www-form-urlencoded',
    -          'Content-Length': 15,
    -          Authorization: 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=',
    -        },
    -      },
    -      'token=sometoken'
    -    );
    -  });
    -
    -  it('validateAuthData valid accessToken', async () => {
    -    const options = {
    -      oauth2Authentication: {
    -        oauth2: true,
    -        tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
    -        useridField: 'sub',
    -        appidField: 'appId',
    -        appIds: ['a', 'b'],
    -      },
    -    };
    -    const authData = {
    -      id: 'fakeid',
    -      access_token: 'sometoken',
    -    };
    -    const { adapter, providerOptions } = authenticationLoader.loadAuthAdapter(
    -      'oauth2Authentication',
    -      options
    -    );
    -    spyOn(httpsRequest, 'request').and.callFake(() => {
    -      return Promise.resolve({
    -        active: true,
    -        sub: 'fakeid',
    -      });
    -    });
    -    try {
    -      await adapter.validateAuthData(authData, providerOptions);
    -    } catch (e) {
    -      // Should not enter here
    -      fail(e);
    -    }
    -    expect(httpsRequest.request).toHaveBeenCalledWith(
    -      {
    -        hostname: 'example.com',
    -        path: '/introspect',
    -        method: 'POST',
    -        headers: {
    -          'Content-Type': 'application/x-www-form-urlencoded',
    -          'Content-Length': 15,
    -        },
    -      },
    -      'token=sometoken'
    -    );
    -  });
    -
    -  it('validateAuthData valid accessToken without useridField', async () => {
    -    const options = {
    -      oauth2Authentication: {
    -        oauth2: true,
    -        tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
    -        appidField: 'appId',
    -        appIds: ['a', 'b'],
    -      },
    -    };
    -    const authData = {
    -      id: 'fakeid',
    -      access_token: 'sometoken',
    -    };
    -    const { adapter, providerOptions } = authenticationLoader.loadAuthAdapter(
    -      'oauth2Authentication',
    -      options
    -    );
    -    spyOn(httpsRequest, 'request').and.callFake(() => {
    -      return Promise.resolve({
    -        active: true,
    -        sub: 'fakeid',
    -      });
    -    });
    -    try {
    -      await adapter.validateAuthData(authData, providerOptions);
    -    } catch (e) {
    -      // Should not enter here
    -      fail(e);
    -    }
    -  });
    -});
    -
     describe('apple signin auth adapter', () => {
       const apple = require('../lib/Adapters/Auth/apple');
       const jwt = require('jsonwebtoken');
    @@ -1722,206 +1135,17 @@ describe('apple signin auth adapter', () => {
       });
     });
     
    -describe('Apple Game Center Auth adapter', () => {
    -  const gcenter = require('../lib/Adapters/Auth/gcenter');
    -  const fs = require('fs');
    -  const testCert = fs.readFileSync(__dirname + '/support/cert/game_center.pem');
    -  const testCert2 = fs.readFileSync(__dirname + '/support/cert/game_center.pem');
    -
    -  it('can load adapter', async () => {
    -    const options = {
    -      gcenter: {
    -        rootCertificateUrl:
    -          'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
    -      },
    -    };
    -    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
    -      'gcenter',
    -      options
    -    );
    -    await adapter.validateAppId(
    -      appIds,
    -      { publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
    -      providerOptions
    -    );
    -  });
    -
    -  it('validateAuthData should validate', async () => {
    -    const options = {
    -      gcenter: {
    -        rootCertificateUrl:
    -          'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
    -      },
    -    };
    -    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
    -      'gcenter',
    -      options
    -    );
    -    await adapter.validateAppId(
    -      appIds,
    -      { publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
    -      providerOptions
    -    );
    -    // real token is used
    -    const authData = {
    -      id: 'G:1965586982',
    -      publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
    -      timestamp: 1565257031287,
    -      signature:
    -        'uqLBTr9Uex8zCpc1UQ1MIDMitb+HUat2Mah4Kw6AVLSGe0gGNJXlih2i5X+0ZwVY0S9zY2NHWi2gFjmhjt/4kxWGMkupqXX5H/qhE2m7hzox6lZJpH98ZEUbouWRfZX2ZhUlCkAX09oRNi7fI7mWL1/o88MaI/y6k6tLr14JTzmlxgdyhw+QRLxRPA6NuvUlRSJpyJ4aGtNH5/wHdKQWL8nUnFYiYmaY8R7IjzNxPfy8UJTUWmeZvMSgND4u8EjADPsz7ZtZyWAPi8kYcAb6M8k0jwLD3vrYCB8XXyO2RQb/FY2TM4zJuI7PzLlvvgOJXbbfVtHx7Evnm5NYoyzgzw==',
    -      salt: 'DzqqrQ==',
    -      bundleId: 'cloud.xtralife.gamecenterauth',
    -    };
    -    gcenter.cache['https://static.gc.apple.com/public-key/gc-prod-4.cer'] = testCert;
    -    await gcenter.validateAuthData(authData);
    -  });
    -
    -  it('validateAuthData invalid signature id', async () => {
    -    gcenter.cache['https://static.gc.apple.com/public-key/gc-prod-4.cer'] = testCert;
    -    gcenter.cache['https://static.gc.apple.com/public-key/gc-prod-6.cer'] = testCert2;
    -    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
    -      'gcenter',
    -      {}
    -    );
    -    await adapter.validateAppId(
    -      appIds,
    -      { publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
    -      providerOptions
    -    );
    -    const authData = {
    -      id: 'G:1965586982',
    -      publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-6.cer',
    -      timestamp: 1565257031287,
    -      signature: '1234',
    -      salt: 'DzqqrQ==',
    -      bundleId: 'com.example.com',
    -    };
    -    await expectAsync(gcenter.validateAuthData(authData)).toBeRejectedWith(
    -      new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Apple Game Center - invalid signature')
    -    );
    -  });
    -
    -  it('validateAuthData invalid public key http url', async () => {
    -    const options = {
    -      gcenter: {
    -        rootCertificateUrl:
    -          'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
    -      },
    -    };
    -    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
    -      'gcenter',
    -      options
    -    );
    -    await adapter.validateAppId(
    -      appIds,
    -      { publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
    -      providerOptions
    -    );
    -    const publicKeyUrls = [
    -      'example.com',
    -      'http://static.gc.apple.com/public-key/gc-prod-4.cer',
    -      'https://developer.apple.com/assets/elements/badges/download-on-the-app-store.svg',
    -      'https://example.com/ \\.apple.com/public_key.cer',
    -      'https://example.com/ &.apple.com/public_key.cer',
    -    ];
    -    await Promise.all(
    -      publicKeyUrls.map(publicKeyUrl =>
    -        expectAsync(
    -          gcenter.validateAuthData({
    -            id: 'G:1965586982',
    -            timestamp: 1565257031287,
    -            publicKeyUrl,
    -            signature: '1234',
    -            salt: 'DzqqrQ==',
    -            bundleId: 'com.example.com',
    -          })
    -        ).toBeRejectedWith(
    -          new Parse.Error(
    -            Parse.Error.SCRIPT_FAILED,
    -            `Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}`
    -          )
    -        )
    -      )
    -    );
    -  });
    -
    -  it('should not validate Symantec Cert', async () => {
    -    const options = {
    -      gcenter: {
    -        rootCertificateUrl:
    -          'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
    -      },
    -    };
    -    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
    -      'gcenter',
    -      options
    -    );
    -    await adapter.validateAppId(
    -      appIds,
    -      { publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
    -      providerOptions
    -    );
    -    expect(() =>
    -      gcenter.verifyPublicKeyIssuer(
    -        testCert,
    -        'https://static.gc.apple.com/public-key/gc-prod-4.cer'
    -      )
    -    );
    -  });
    -
    -  it('adapter should load default cert', async () => {
    -    const options = {
    -      gcenter: {},
    -    };
    -    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
    -      'gcenter',
    -      options
    -    );
    -    await adapter.validateAppId(
    -      appIds,
    -      { publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
    -      providerOptions
    -    );
    -    const previous = new Date();
    -    await adapter.validateAppId(
    -      appIds,
    -      { publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
    -      providerOptions
    -    );
    -
    -    const duration = new Date().getTime() - previous.getTime();
    -    expect(duration <= 1).toBe(true);
    -  });
    -
    -  it('adapter should throw', async () => {
    -    const options = {
    -      gcenter: {
    -        rootCertificateUrl: 'https://example.com',
    -      },
    -    };
    -    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
    -      'gcenter',
    -      options
    -    );
    -    await expectAsync(
    -      adapter.validateAppId(
    -        appIds,
    -        { publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
    -        providerOptions
    -      )
    -    ).toBeRejectedWith(
    -      new Parse.Error(
    -        Parse.Error.OBJECT_NOT_FOUND,
    -        'Apple Game Center auth adapter parameter `rootCertificateURL` is invalid.'
    -      )
    -    );
    -  });
    -});
    -
     describe('phant auth adapter', () => {
       const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
     
       it('validateAuthData should throw for invalid auth', async () => {
    +    await reconfigureServer({
    +      auth: {
    +        phantauth: {
    +          enableInsecureAuth: true,
    +        }
    +      }
    +    })
         const authData = {
           id: 'fakeid',
           access_token: 'sometoken',
    @@ -1938,34 +1162,6 @@ describe('phant auth adapter', () => {
       });
     });
     
    -describe('microsoft graph auth adapter', () => {
    -  const microsoft = require('../lib/Adapters/Auth/microsoft');
    -  const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
    -
    -  it('should use access_token for validation is passed and responds with id and mail', async () => {
    -    spyOn(httpsRequest, 'get').and.callFake(() => {
    -      return Promise.resolve({ id: 'userId', mail: 'userMail' });
    -    });
    -    await microsoft.validateAuthData({
    -      id: 'userId',
    -      access_token: 'the_token',
    -    });
    -  });
    -
    -  it('should fail to validate Microsoft Graph auth with bad token', done => {
    -    const authData = {
    -      id: 'fake-id',
    -      mail: 'fake@mail.com',
    -      access_token: 'very.long.bad.token',
    -    };
    -    microsoft.validateAuthData(authData).then(done.fail, err => {
    -      expect(err.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
    -      expect(err.message).toBe('Microsoft Graph auth is invalid for this user.');
    -      done();
    -    });
    -  });
    -});
    -
     describe('facebook limited auth adapter', () => {
       const facebook = require('../lib/Adapters/Auth/facebook');
       const jwt = require('jsonwebtoken');
    
  • spec/AuthenticationAdaptersV2.spec.js+3 3 modified
    @@ -355,16 +355,16 @@ describe('Auth Adapter features', () => {
         const authData = user.get('authData').modernAdapter3;
         expect(authData).toEqual({ foo: 'bar' });
         for (const call of afterSpy.calls.all()) {
    -      const args = call.args[0];
    +      const args = call.args[2];
           if (args.user) {
             user._objCount = args.user._objCount;
             break;
           }
         }
         expect(afterSpy).toHaveBeenCalledWith(
    -      { ip: '127.0.0.1', user, master: false },
           { id: 'modernAdapter3Data' },
    -      undefined
    +      undefined,
    +      { ip: '127.0.0.1', user, master: false },
         );
         expect(spy).toHaveBeenCalled();
       });
    
  • spec/.eslintrc.json+1 0 modified
    @@ -5,6 +5,7 @@
         "globals": {
           "Parse": true,
           "reconfigureServer": true,
    +      "mockFetch": true,
           "createTestUser": true,
           "jfail": true,
           "ok": true,
    
  • spec/helper.js+20 0 modified
    @@ -424,6 +424,25 @@ function mockShortLivedAuth() {
       return auth;
     }
     
    +function mockFetch(mockResponses) {
    +  global.fetch = jasmine.createSpy('fetch').and.callFake((url, options = { }) => {
    +    options.method ||= 'GET';
    +    const mockResponse = mockResponses.find(
    +      (mock) => mock.url === url && mock.method === options.method
    +    );
    +
    +    if (mockResponse) {
    +      return Promise.resolve(mockResponse.response);
    +    }
    +
    +    return Promise.resolve({
    +      ok: false,
    +      statusText: 'Unknown URL or method',
    +    });
    +  });
    +}
    +
    +
     // This is polluting, but, it makes it way easier to directly port old tests.
     global.Parse = Parse;
     global.TestObject = TestObject;
    @@ -439,6 +458,7 @@ global.arrayContains = arrayContains;
     global.jequal = jequal;
     global.range = range;
     global.reconfigureServer = reconfigureServer;
    +global.mockFetch = mockFetch;
     global.defaultConfiguration = defaultConfiguration;
     global.mockCustomAuthenticator = mockCustomAuthenticator;
     global.mockFacebookAuthenticator = mockFacebookAuthenticator;
    
  • spec/SecurityCheckGroups.spec.js+3 0 modified
    @@ -32,13 +32,15 @@ describe('Security Check Groups', () => {
           config.masterKey = 'aMoreSecur3Passwor7!';
           config.security.enableCheckLog = false;
           config.allowClientClassCreation = false;
    +      config.enableInsecureAuthAdapters = false;
           await reconfigureServer(config);
     
           const group = new CheckGroupServerConfig();
           await group.run();
           expect(group.checks()[0].checkState()).toBe(CheckState.success);
           expect(group.checks()[1].checkState()).toBe(CheckState.success);
           expect(group.checks()[2].checkState()).toBe(CheckState.success);
    +      expect(group.checks()[4].checkState()).toBe(CheckState.success);
         });
     
         it('checks fail correctly', async () => {
    @@ -52,6 +54,7 @@ describe('Security Check Groups', () => {
           expect(group.checks()[0].checkState()).toBe(CheckState.fail);
           expect(group.checks()[1].checkState()).toBe(CheckState.fail);
           expect(group.checks()[2].checkState()).toBe(CheckState.fail);
    +      expect(group.checks()[4].checkState()).toBe(CheckState.fail);
         });
       });
     
    
  • spec/support/cert/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem+38 0 added
    @@ -0,0 +1,38 @@
    +-----BEGIN CERTIFICATE-----
    +MIIGsDCCBJigAwIBAgIQCK1AsmDSnEyfXs2pvZOu2TANBgkqhkiG9w0BAQwFADBi
    +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
    +d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg
    +RzQwHhcNMjEwNDI5MDAwMDAwWhcNMzYwNDI4MjM1OTU5WjBpMQswCQYDVQQGEwJV
    +UzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRy
    +dXN0ZWQgRzQgQ29kZSBTaWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMIIC
    +IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1bQvQtAorXi3XdU5WRuxiEL1
    +M4zrPYGXcMW7xIUmMJ+kjmjYXPXrNCQH4UtP03hD9BfXHtr50tVnGlJPDqFX/IiZ
    +wZHMgQM+TXAkZLON4gh9NH1MgFcSa0OamfLFOx/y78tHWhOmTLMBICXzENOLsvsI
    +8IrgnQnAZaf6mIBJNYc9URnokCF4RS6hnyzhGMIazMXuk0lwQjKP+8bqHPNlaJGi
    +TUyCEUhSaN4QvRRXXegYE2XFf7JPhSxIpFaENdb5LpyqABXRN/4aBpTCfMjqGzLm
    +ysL0p6MDDnSlrzm2q2AS4+jWufcx4dyt5Big2MEjR0ezoQ9uo6ttmAaDG7dqZy3S
    +vUQakhCBj7A7CdfHmzJawv9qYFSLScGT7eG0XOBv6yb5jNWy+TgQ5urOkfW+0/tv
    +k2E0XLyTRSiDNipmKF+wc86LJiUGsoPUXPYVGUztYuBeM/Lo6OwKp7ADK5GyNnm+
    +960IHnWmZcy740hQ83eRGv7bUKJGyGFYmPV8AhY8gyitOYbs1LcNU9D4R+Z1MI3s
    +MJN2FKZbS110YU0/EpF23r9Yy3IQKUHw1cVtJnZoEUETWJrcJisB9IlNWdt4z4FK
    +PkBHX8mBUHOFECMhWWCKZFTBzCEa6DgZfGYczXg4RTCZT/9jT0y7qg0IU0F8WD1H
    +s/q27IwyCQLMbDwMVhECAwEAAaOCAVkwggFVMBIGA1UdEwEB/wQIMAYBAf8CAQAw
    +HQYDVR0OBBYEFGg34Ou2O/hfEYb7/mF7CIhl9E5CMB8GA1UdIwQYMBaAFOzX44LS
    +cV1kTN8uZz/nupiuHA9PMA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEF
    +BQcDAzB3BggrBgEFBQcBAQRrMGkwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRp
    +Z2ljZXJ0LmNvbTBBBggrBgEFBQcwAoY1aHR0cDovL2NhY2VydHMuZGlnaWNlcnQu
    +Y29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcnQwQwYDVR0fBDwwOjA4oDagNIYy
    +aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5j
    +cmwwHAYDVR0gBBUwEzAHBgVngQwBAzAIBgZngQwBBAEwDQYJKoZIhvcNAQEMBQAD
    +ggIBADojRD2NCHbuj7w6mdNW4AIapfhINPMstuZ0ZveUcrEAyq9sMCcTEp6QRJ9L
    +/Z6jfCbVN7w6XUhtldU/SfQnuxaBRVD9nL22heB2fjdxyyL3WqqQz/WTauPrINHV
    +UHmImoqKwba9oUgYftzYgBoRGRjNYZmBVvbJ43bnxOQbX0P4PpT/djk9ntSZz0rd
    +KOtfJqGVWEjVGv7XJz/9kNF2ht0csGBc8w2o7uCJob054ThO2m67Np375SFTWsPK
    +6Wrxoj7bQ7gzyE84FJKZ9d3OVG3ZXQIUH0AzfAPilbLCIXVzUstG2MQ0HKKlS43N
    +b3Y3LIU/Gs4m6Ri+kAewQ3+ViCCCcPDMyu/9KTVcH4k4Vfc3iosJocsL6TEa/y4Z
    +XDlx4b6cpwoG1iZnt5LmTl/eeqxJzy6kdJKt2zyknIYf48FWGysj/4+16oh7cGvm
    +oLr9Oj9FpsToFpFSi0HASIRLlk2rREDjjfAVKM7t8RhWByovEMQMCGQ8M4+uKIw8
    +y4+ICw2/O/TOHnuO77Xry7fwdxPm5yg/rBKupS8ibEH5glwVZsxsDsrFhsP2JjMM
    +B0ug0wcCampAMEhLNKhRILutG4UI4lkNbcoFUCvqShyepf2gpx8GdOfy1lKQ/a+F
    +SCH5Vzu0nAPthkX0tGFuv2jiJmCG6sivqf6UHedjGzqGVnhO
    +-----END CERTIFICATE-----
    
  • spec/support/cert/gc-prod-4.cer+0 0 added
  • spec/support/jasmine.json+1 1 modified
    @@ -1,6 +1,6 @@
     {
       "spec_dir": "spec",
    -  "spec_files": ["*spec.js"],
    +  "spec_files": ["**/*.[sS]pec.js"],
       "helpers": ["helper.js"],
       "random": true
     }
    
  • spec/TwitterAuth.spec.js+0 97 removed
    @@ -1,97 +0,0 @@
    -const twitter = require('../lib/Adapters/Auth/twitter');
    -
    -describe('Twitter Auth', () => {
    -  it('should use the proper configuration', () => {
    -    // Multiple options, consumer_key found
    -    expect(
    -      twitter.handleMultipleConfigurations(
    -        {
    -          consumer_key: 'hello',
    -        },
    -        [
    -          {
    -            consumer_key: 'hello',
    -          },
    -          {
    -            consumer_key: 'world',
    -          },
    -        ]
    -      ).consumer_key
    -    ).toEqual('hello');
    -
    -    // Multiple options, consumer_key not found
    -    expect(function () {
    -      twitter.handleMultipleConfigurations(
    -        {
    -          consumer_key: 'some',
    -        },
    -        [
    -          {
    -            consumer_key: 'hello',
    -          },
    -          {
    -            consumer_key: 'world',
    -          },
    -        ]
    -      );
    -    }).toThrow();
    -
    -    // Multiple options, consumer_key not found
    -    expect(function () {
    -      twitter.handleMultipleConfigurations(
    -        {
    -          auth_token: 'token',
    -        },
    -        [
    -          {
    -            consumer_key: 'hello',
    -          },
    -          {
    -            consumer_key: 'world',
    -          },
    -        ]
    -      );
    -    }).toThrow();
    -
    -    // Single configuration and consumer_key set
    -    expect(
    -      twitter.handleMultipleConfigurations(
    -        {
    -          consumer_key: 'hello',
    -        },
    -        {
    -          consumer_key: 'hello',
    -        }
    -      ).consumer_key
    -    ).toEqual('hello');
    -
    -    // General case, only 1 config, no consumer_key set
    -    expect(
    -      twitter.handleMultipleConfigurations(
    -        {
    -          auth_token: 'token',
    -        },
    -        {
    -          consumer_key: 'hello',
    -        }
    -      ).consumer_key
    -    ).toEqual('hello');
    -  });
    -
    -  it('Should fail with missing options', done => {
    -    try {
    -      twitter.validateAuthData(
    -        {
    -          consumer_key: 'key',
    -          consumer_secret: 'secret',
    -          auth_token: 'token',
    -          auth_token_secret: 'secret',
    -        },
    -        undefined
    -      );
    -    } catch (error) {
    -      jequal(error.message, 'Twitter auth configuration missing');
    -      done();
    -    }
    -  });
    -});
    
  • src/Adapters/Auth/apple.js+44 0 modified
    @@ -1,3 +1,47 @@
    +/**
    + * Parse Server authentication adapter for Apple.
    + *
    + * @class AppleAdapter
    + * @param {Object} options - Configuration options for the adapter.
    + * @param {string} options.clientId - Your Apple App ID.
    + *
    + * @param {Object} authData - The authentication data provided by the client.
    + * @param {string} authData.id - The user ID obtained from Apple.
    + * @param {string} authData.token - The token obtained from Apple.
    + *
    + * @description
    + * ## Parse Server Configuration
    + * To configure Parse Server for Apple authentication, use the following structure:
    + * ```json
    + * {
    + *   "auth": {
    + *     "apple": {
    + *       "clientId": "12345"
    + *     }
    + *   }
    + * }
    + * ```
    + *
    + * ## Expected `authData` from the Client
    + * The adapter expects the client to provide the following `authData` payload:
    + * - `authData.id` (**string**, required): The user ID obtained from Apple.
    + * - `authData.token` (**string**, required): The token obtained from Apple.
    + *
    + * Parse Server stores the required authentication data in the database.
    + *
    + * ### Example AuthData from Apple
    + * ```json
    + * {
    + *   "apple": {
    + *     "id": "1234567",
    + *     "token": "xxxxx.yyyyy.zzzzz"
    + *   }
    + * }
    + * ```
    + *
    + * @see {@link https://developer.apple.com/documentation/signinwithapplerestapi Sign in with Apple REST API Documentation}
    + */
    +
     // Apple SignIn Auth
     // https://developer.apple.com/documentation/signinwithapplerestapi
     
    
  • src/Adapters/Auth/AuthAdapter.js+17 8 modified
    @@ -40,35 +40,35 @@ export class AuthAdapter {
        * Legacy usage, if provided it will be triggered when authData related to this provider is touched (signup/update/login)
        * otherwise you should implement validateSetup, validateLogin and validateUpdate
        * @param {Object} authData The client provided authData
    -   * @param {Parse.Cloud.TriggerRequest} request
        * @param {Object} options additional adapter options
    +   * @param {Parse.Cloud.TriggerRequest} request
        * @returns {Promise<ParseAuthResponse|void|undefined>}
        */
    -  validateAuthData(authData, request, options) {
    +  validateAuthData(authData, options, request) {
         return Promise.resolve({});
       }
     
       /**
        * Triggered when user provide for the first time this auth provider
        * could be a register or the user adding a new auth service
        * @param {Object} authData The client provided authData
    -   * @param {Parse.Cloud.TriggerRequest} request
        * @param {Object} options additional adapter options
    +   * @param {Parse.Cloud.TriggerRequest} request
        * @returns {Promise<ParseAuthResponse|void|undefined>}
        */
    -  validateSetUp(authData, req, options) {
    +  validateSetUp(authData, options, req) {
         return Promise.resolve({});
       }
     
       /**
        * Triggered when user provide authData related to this provider
        * The user is not logged in and has already set this provider before
        * @param {Object} authData The client provided authData
    -   * @param {Parse.Cloud.TriggerRequest} request
        * @param {Object} options additional adapter options
    +   * @param {Parse.Cloud.TriggerRequest} request
        * @returns {Promise<ParseAuthResponse|void|undefined>}
        */
    -  validateLogin(authData, req, options) {
    +  validateLogin(authData, options, req) {
         return Promise.resolve({});
       }
     
    @@ -80,10 +80,18 @@ export class AuthAdapter {
        * @param {Parse.Cloud.TriggerRequest} request
        * @returns {Promise<ParseAuthResponse|void|undefined>}
        */
    -  validateUpdate(authData, req, options) {
    +  validateUpdate(authData, options, req) {
         return Promise.resolve({});
       }
     
    +  /**
    +   * Triggered when user is looked up by authData with this provider. Override the `id` field if needed.
    +   * @param {Object} authData The client provided authData
    +   */
    +  beforeFind(authData) {
    +
    +  }
    +
       /**
        * Triggered in pre authentication process if needed (like webauthn, SMS OTP)
        * @param {Object} challengeData Data provided by the client
    @@ -100,9 +108,10 @@ export class AuthAdapter {
        * Triggered when auth data is fetched
        * @param {Object} authData authData
        * @param {Object} options additional adapter options
    +   * @param {Parse.Cloud.TriggerRequest} request
        * @returns {Promise<Object>} Any overrides required to authData
        */
    -  afterFind(authData, options) {
    +  afterFind(authData, options, request) {
         return Promise.resolve({});
       }
     
    
  • src/Adapters/Auth/BaseCodeAuthAdapter.js+112 0 added
    @@ -0,0 +1,112 @@
    +// abstract class for auth code adapters
    +import AuthAdapter from './AuthAdapter';
    +export default class BaseAuthCodeAdapter extends AuthAdapter {
    +  constructor(adapterName) {
    +    super();
    +    this.adapterName = adapterName;
    +  }
    +  validateOptions(options) {
    +
    +    if (!options) {
    +      throw new Error(`${this.adapterName} options are required.`);
    +    }
    +
    +    this.enableInsecureAuth = options.enableInsecureAuth;
    +    if (this.enableInsecureAuth) {
    +      return;
    +    }
    +
    +    this.clientId = options.clientId;
    +    this.clientSecret = options.clientSecret;
    +
    +    if (!this.clientId) {
    +      throw new Error(`${this.adapterName} clientId is required.`);
    +    }
    +
    +    if (!this.clientSecret) {
    +      throw new Error(`${this.adapterName} clientSecret is required.`);
    +    }
    +  }
    +
    +  async beforeFind(authData) {
    +    if (this.enableInsecureAuth && !authData?.code) {
    +      if (!authData?.access_token) {
    +        throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `${this.adapterName} auth is invalid for this user.`);
    +      }
    +
    +      const user = await this.getUserFromAccessToken(authData.access_token, authData);
    +
    +      if (user.id !== authData.id) {
    +        throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `${this.adapterName} auth is invalid for this user.`);
    +      }
    +
    +      return;
    +    }
    +
    +    if (!authData?.code) {
    +      throw new Parse.Error(Parse.Error.VALIDATION_ERROR, `${this.adapterName} code is required.`);
    +    }
    +
    +    const access_token = await this.getAccessTokenFromCode(authData);
    +    const user = await this.getUserFromAccessToken(access_token, authData);
    +
    +    if (authData.id && user.id !== authData.id) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `${this.adapterName} auth is invalid for this user.`);
    +    }
    +
    +    authData.access_token = access_token;
    +    authData.id = user.id;
    +
    +    delete authData.code;
    +    delete authData.redirect_uri;
    +
    +  }
    +
    +  async getUserFromAccessToken() {
    +    // abstract method
    +    throw new Error('getUserFromAccessToken is not implemented');
    +  }
    +
    +  async getAccessTokenFromCode() {
    +    // abstract method
    +    throw new Error('getAccessTokenFromCode is not implemented');
    +  }
    +
    +  validateLogin(authData) {
    +    // User validation is already done in beforeFind
    +    return {
    +      id: authData.id,
    +    }
    +  }
    +
    +  validateSetUp(authData) {
    +    // User validation is already done in beforeFind
    +    return {
    +      id: authData.id,
    +    }
    +  }
    +
    +  afterFind(authData) {
    +    return {
    +      id: authData.id,
    +    }
    +  }
    +
    +  validateUpdate(authData) {
    +    // User validation is already done in beforeFind
    +    return {
    +      id: authData.id,
    +    }
    +
    +  }
    +
    +  parseResponseData(data) {
    +    const startPos = data.indexOf('(');
    +    const endPos = data.indexOf(')');
    +    if (startPos === -1 || endPos === -1) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `${this.adapterName} auth is invalid for this user.`);
    +    }
    +    const jsonData = data.substring(startPos + 1, endPos);
    +    return JSON.parse(jsonData);
    +  }
    +}
    
  • src/Adapters/Auth/facebook.js+60 0 modified
    @@ -1,3 +1,63 @@
    +/**
    + * Parse Server authentication adapter for Facebook.
    + *
    + * @class FacebookAdapter
    + * @param {Object} options - The adapter configuration options.
    + * @param {string} options.appSecret - Your Facebook App Secret. Required for secure authentication.
    + * @param {string[]} options.appIds - An array of Facebook App IDs. Required for validating the app.
    + *
    + * @description
    + * ## Parse Server Configuration
    + * To configure Parse Server for Facebook authentication, use the following structure:
    + * ```json
    + * {
    + *   "auth": {
    + *     "facebook": {
    + *       "appSecret": "your-app-secret",
    + *       "appIds": ["your-app-id"]
    + *     }
    + *   }
    + * }
    + * ```
    + *
    + * The adapter supports the following authentication methods:
    + * - **Standard Login**: Requires `id` and `access_token`.
    + * - **Limited Login**: Requires `id` and `token`.
    + *
    + * ## Auth Payloads
    + * ### Standard Login Payload
    + * ```json
    + * {
    + *   "facebook": {
    + *     "id": "1234567",
    + *     "access_token": "abc123def456ghi789"
    + *   }
    + * }
    + * ```
    + *
    + * ### Limited Login Payload
    + * ```json
    + * {
    + *   "facebook": {
    + *     "id": "1234567",
    + *     "token": "xxxxx.yyyyy.zzzzz"
    + *   }
    + * }
    + * ```
    + *
    + * ## Notes
    + * - **Standard Login**: Use `id` and `access_token` for full functionality.
    + * - **Limited Login**: Use `id` and `token` (JWT) when tracking is opted out (e.g., via Apple's App Tracking Transparency).
    + * - Supported Parse Server versions:
    + *   - `>= 6.5.6 < 7`
    + *   - `>= 7.0.1`
    + *
    + * Secure authentication is recommended to ensure proper data protection and compliance with Facebook's guidelines.
    + *
    + * @see {@link https://developers.facebook.com/docs/facebook-login/limited-login/ Facebook Limited Login}
    + * @see {@link https://developers.facebook.com/docs/facebook-login/facebook-login-for-business/ Facebook Login for Business}
    + */
    +
     // Helper functions for accessing the Facebook Graph API.
     const Parse = require('parse/node').Parse;
     const crypto = require('crypto');
    
  • src/Adapters/Auth/gcenter.js+209 165 modified
    @@ -1,195 +1,239 @@
    -/* Apple Game Center Auth
    -https://developer.apple.com/documentation/gamekit/gklocalplayer/1515407-generateidentityverificationsign#discussion
    -
    -const authData = {
    -  publicKeyUrl: 'https://valid.apple.com/public/timeout.cer',
    -  timestamp: 1460981421303,
    -  signature: 'PoDwf39DCN464B49jJCU0d9Y0J',
    -  salt: 'saltST==',
    -  bundleId: 'com.valid.app'
    -  id: 'playerId',
    -};
    -*/
    -
    -const { Parse } = require('parse/node');
    -const crypto = require('crypto');
    -const https = require('https');
    -const { pki } = require('node-forge');
    -const ca = { cert: null, url: null };
    -const cache = {}; // (publicKey -> cert) cache
    -
    -function verifyPublicKeyUrl(publicKeyUrl) {
    -  try {
    -    const regex = /^https:\/\/(?:[-_A-Za-z0-9]+\.){0,}apple\.com\/.*\.cer$/;
    -    return regex.test(publicKeyUrl);
    -  } catch (error) {
    -    return false;
    +/**
    + * Parse Server authentication adapter for Apple Game Center.
    + *
    + * @class AppleGameCenterAdapter
    + * @param {Object} options - Configuration options for the adapter.
    + * @param {string} options.bundleId - Your Apple Game Center bundle ID. Required for secure authentication.
    + * @param {boolean} [options.enableInsecureAuth=false] - **[DEPRECATED]** Enable insecure authentication (not recommended).
    + *
    + * @param {Object} authData - The authentication data provided by the client.
    + * @param {string} authData.id - The user ID obtained from Apple Game Center.
    + * @param {string} authData.publicKeyUrl - The public key URL obtained from Apple Game Center.
    + * @param {string} authData.timestamp - The timestamp obtained from Apple Game Center.
    + * @param {string} authData.signature - The signature obtained from Apple Game Center.
    + * @param {string} authData.salt - The salt obtained from Apple Game Center.
    + * @param {string} [authData.bundleId] - **[DEPRECATED]** The bundle ID obtained from Apple Game Center (required for insecure authentication).
    + *
    + * @description
    + * ## Parse Server Configuration
    + * The following `authData` fields are required:
    + * `id`, `publicKeyUrl`, `timestamp`, `signature`, and `salt`. These fields are validated against the configured `bundleId` for additional security.
    + *
    + * To configure Parse Server for Apple Game Center authentication, use the following structure:
    + * ```json
    + * {
    + *  "auth": {
    + *    "gcenter": {
    + *     "bundleId": "com.valid.app"
    + *  }
    + * }
    + * ```
    + *
    + * ## Insecure Authentication (Not Recommended)
    + * The following `authData` fields are required for insecure authentication:
    + * `id`, `publicKeyUrl`, `timestamp`, `signature`, `salt`, and `bundleId` (**[DEPRECATED]**). This flow is insecure and poses potential security risks.
    + *
    + * To configure Parse Server for insecure authentication, use the following structure:
    + * ```json
    + * {
    + *   "auth": {
    + *    "gcenter": {
    + *      "enableInsecureAuth": true
    + *   }
    + * }
    + * ```
    + *
    + * ### Deprecation Notice
    + * The `enableInsecureAuth` option and `authData.bundleId` parameter are deprecated and may be removed in future releases. Use secure authentication with the `bundleId` configured in the `options` object instead.
    + *
    + *
    + * @example <caption>Secure Authentication Example</caption>
    + * // Example authData for secure authentication:
    + * const authData = {
    + *   gcenter: {
    + *     id: "1234567",
    + *     publicKeyUrl: "https://valid.apple.com/public/timeout.cer",
    + *     timestamp: 1460981421303,
    + *     salt: "saltST==",
    + *     signature: "PoDwf39DCN464B49jJCU0d9Y0J"
    + *   }
    + * };
    + *
    + * @example <caption>Insecure Authentication Example (Not Recommended)</caption>
    + * // Example authData for insecure authentication:
    + * const authData = {
    + *   gcenter: {
    + *     id: "1234567",
    + *     publicKeyUrl: "https://valid.apple.com/public/timeout.cer",
    + *     timestamp: 1460981421303,
    + *     salt: "saltST==",
    + *     signature: "PoDwf39DCN464B49jJCU0d9Y0J",
    + *     bundleId: "com.valid.app" // Deprecated.
    + *   }
    + * };
    + *
    + * @see {@link https://developer.apple.com/documentation/gamekit/gklocalplayer/3516283-fetchitems Apple Game Center Documentation}
    + */
    +/* global BigInt */
    +
    +import crypto from 'crypto';
    +import { asn1, pki } from 'node-forge';
    +import AuthAdapter from './AuthAdapter';
    +class GameCenterAuth extends AuthAdapter {
    +  constructor() {
    +    super();
    +    this.ca = { cert: null, url: null };
    +    this.cache = {};
    +    this.bundleId = '';
       }
    -}
     
    -function convertX509CertToPEM(X509Cert) {
    -  const pemPreFix = '-----BEGIN CERTIFICATE-----\n';
    -  const pemPostFix = '-----END CERTIFICATE-----';
    +  validateOptions(options) {
    +    if (!options) {
    +      throw new Error('Game center auth options are required.');
    +    }
     
    -  const base64 = X509Cert;
    -  const certBody = base64.match(new RegExp('.{0,64}', 'g')).join('\n');
    +    if (!this.loadingPromise) {
    +      this.loadingPromise = this.loadCertificate(options);
    +    }
     
    -  return pemPreFix + certBody + pemPostFix;
    -}
    +    this.enableInsecureAuth = options.enableInsecureAuth;
    +    this.bundleId = options.bundleId;
     
    -async function getAppleCertificate(publicKeyUrl) {
    -  if (!verifyPublicKeyUrl(publicKeyUrl)) {
    -    throw new Parse.Error(
    -      Parse.Error.OBJECT_NOT_FOUND,
    -      `Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}`
    -    );
    -  }
    -  if (cache[publicKeyUrl]) {
    -    return cache[publicKeyUrl];
    -  }
    -  const url = new URL(publicKeyUrl);
    -  const headOptions = {
    -    hostname: url.hostname,
    -    path: url.pathname,
    -    method: 'HEAD',
    -  };
    -  const cert_headers = await new Promise((resolve, reject) =>
    -    https.get(headOptions, res => resolve(res.headers)).on('error', reject)
    -  );
    -  const validContentTypes = ['application/x-x509-ca-cert', 'application/pkix-cert'];
    -  if (
    -    !validContentTypes.includes(cert_headers['content-type']) ||
    -    cert_headers['content-length'] == null ||
    -    cert_headers['content-length'] > 10000
    -  ) {
    -    throw new Parse.Error(
    -      Parse.Error.OBJECT_NOT_FOUND,
    -      `Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}`
    -    );
    +    if (!this.enableInsecureAuth && !this.bundleId) {
    +      throw new Error('bundleId is required for secure auth.');
    +    }
       }
    -  const { certificate, headers } = await getCertificate(publicKeyUrl);
    -  if (headers['cache-control']) {
    -    const expire = headers['cache-control'].match(/max-age=([0-9]+)/);
    -    if (expire) {
    -      cache[publicKeyUrl] = certificate;
    -      // we'll expire the cache entry later, as per max-age
    -      setTimeout(() => {
    -        delete cache[publicKeyUrl];
    -      }, parseInt(expire[1], 10) * 1000);
    +
    +  async loadCertificate(options) {
    +    const rootCertificateUrl =
    +      options.rootCertificateUrl ||
    +      'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem';
    +
    +    if (this.ca.url === rootCertificateUrl) {
    +      return rootCertificateUrl;
         }
    +
    +    const { certificate, headers } = await this.fetchCertificate(rootCertificateUrl);
    +
    +    if (
    +      headers.get('content-type') !== 'application/x-pem-file' ||
    +      !headers.get('content-length') ||
    +      parseInt(headers.get('content-length'), 10) > 10000
    +    ) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid rootCertificateURL.');
    +    }
    +
    +    this.ca.cert = pki.certificateFromPem(certificate);
    +    this.ca.url = rootCertificateUrl;
    +
    +    return rootCertificateUrl;
       }
    -  return verifyPublicKeyIssuer(certificate, publicKeyUrl);
    -}
     
    -function getCertificate(url, buffer) {
    -  return new Promise((resolve, reject) => {
    -    https
    -      .get(url, res => {
    -        const data = [];
    -        res.on('data', chunk => {
    -          data.push(chunk);
    -        });
    -        res.on('end', () => {
    -          if (buffer) {
    -            resolve({ certificate: Buffer.concat(data), headers: res.headers });
    -            return;
    -          }
    -          let cert = '';
    -          for (const chunk of data) {
    -            cert += chunk.toString('base64');
    -          }
    -          const certificate = convertX509CertToPEM(cert);
    -          resolve({ certificate, headers: res.headers });
    -        });
    -      })
    -      .on('error', reject);
    -  });
    -}
    +  verifyPublicKeyUrl(publicKeyUrl) {
    +    const regex = /^https:\/\/(?:[-_A-Za-z0-9]+\.){0,}apple\.com\/.*\.cer$/;
    +    return regex.test(publicKeyUrl);
    +  }
     
    -function convertTimestampToBigEndian(timestamp) {
    -  const buffer = Buffer.alloc(8);
    +  async fetchCertificate(url) {
    +    const response = await fetch(url);
    +    if (!response.ok) {
    +      throw new Error(`Failed to fetch certificate: ${url}`);
    +    }
     
    -  const high = ~~(timestamp / 0xffffffff);
    -  const low = timestamp % (0xffffffff + 0x1);
    +    const contentType = response.headers.get('content-type');
    +    const isPem = contentType?.includes('application/x-pem-file');
     
    -  buffer.writeUInt32BE(parseInt(high, 10), 0);
    -  buffer.writeUInt32BE(parseInt(low, 10), 4);
    +    if (isPem) {
    +      const certificate = await response.text();
    +      return { certificate, headers: response.headers };
    +    }
     
    -  return buffer;
    -}
    +    const data = await response.arrayBuffer();
    +    const binaryData = Buffer.from(data);
     
    -function verifySignature(publicKey, authData) {
    -  const verifier = crypto.createVerify('sha256');
    -  verifier.update(authData.playerId, 'utf8');
    -  verifier.update(authData.bundleId, 'utf8');
    -  verifier.update(convertTimestampToBigEndian(authData.timestamp));
    -  verifier.update(authData.salt, 'base64');
    +    const asn1Cert = asn1.fromDer(binaryData.toString('binary'));
    +    const forgeCert = pki.certificateFromAsn1(asn1Cert);
    +    const certificate = pki.certificateToPem(forgeCert);
     
    -  if (!verifier.verify(publicKey, authData.signature, 'base64')) {
    -    throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Apple Game Center - invalid signature');
    +    return { certificate, headers: response.headers };
       }
    -}
     
    -function verifyPublicKeyIssuer(cert, publicKeyUrl) {
    -  const publicKeyCert = pki.certificateFromPem(cert);
    -  if (!ca.cert) {
    -    throw new Parse.Error(
    -      Parse.Error.OBJECT_NOT_FOUND,
    -      'Apple Game Center auth adapter parameter `rootCertificateURL` is invalid.'
    -    );
    +  async getAppleCertificate(publicKeyUrl) {
    +    if (!this.verifyPublicKeyUrl(publicKeyUrl)) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `Invalid publicKeyUrl: ${publicKeyUrl}`);
    +    }
    +
    +    if (this.cache[publicKeyUrl]) {
    +      return this.cache[publicKeyUrl];
    +    }
    +
    +    const { certificate, headers } = await this.fetchCertificate(publicKeyUrl);
    +    const cacheControl = headers.get('cache-control');
    +    const expire = cacheControl?.match(/max-age=([0-9]+)/);
    +
    +    this.verifyPublicKeyIssuer(certificate, publicKeyUrl);
    +
    +    if (expire) {
    +      this.cache[publicKeyUrl] = certificate;
    +      setTimeout(() => delete this.cache[publicKeyUrl], parseInt(expire[1], 10) * 1000);
    +    }
    +
    +    return certificate;
       }
    -  try {
    -    if (!ca.cert.verify(publicKeyCert)) {
    +
    +  verifyPublicKeyIssuer(cert, publicKeyUrl) {
    +    const publicKeyCert = pki.certificateFromPem(cert);
    +
    +    if (!this.ca.cert) {
           throw new Parse.Error(
             Parse.Error.OBJECT_NOT_FOUND,
    -        `Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}`
    +        'Root certificate is invalid or missing.'
           );
         }
    -  } catch (e) {
    -    throw new Parse.Error(
    -      Parse.Error.OBJECT_NOT_FOUND,
    -      `Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}`
    -    );
    -  }
    -  return cert;
    -}
     
    -// Returns a promise that fulfills if this user id is valid.
    -async function validateAuthData(authData) {
    -  if (!authData.id) {
    -    throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Apple Game Center - authData id missing');
    +    if (!this.ca.cert.verify(publicKeyCert)) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `Invalid publicKeyUrl: ${publicKeyUrl}`);
    +    }
       }
    -  authData.playerId = authData.id;
    -  const publicKey = await getAppleCertificate(authData.publicKeyUrl);
    -  return verifySignature(publicKey, authData);
    -}
     
    -// Returns a promise that fulfills if this app id is valid.
    -async function validateAppId(appIds, authData, options = {}) {
    -  if (!options.rootCertificateUrl) {
    -    options.rootCertificateUrl =
    -      'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem';
    +  verifySignature(publicKey, authData) {
    +    const bundleId = this.bundleId || (this.enableInsecureAuth && authData.bundleId);
    +
    +    const verifier = crypto.createVerify('sha256');
    +    verifier.update(Buffer.from(authData.id, 'utf8'));
    +    verifier.update(Buffer.from(bundleId, 'utf8'));
    +    verifier.update(this.convertTimestampToBigEndian(authData.timestamp));
    +    verifier.update(Buffer.from(authData.salt, 'base64'));
    +
    +    if (!verifier.verify(publicKey, authData.signature, 'base64')) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid signature.');
    +    }
       }
    -  if (ca.url === options.rootCertificateUrl) {
    -    return;
    +
    +  async validateAuthData(authData) {
    +
    +    const requiredKeys = ['id', 'publicKeyUrl', 'timestamp', 'signature', 'salt'];
    +    if (this.enableInsecureAuth) {
    +      requiredKeys.push('bundleId');
    +    }
    +
    +    for (const key of requiredKeys) {
    +      if (!authData[key]) {
    +        throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `AuthData ${key} is missing.`);
    +      }
    +    }
    +
    +    await this.loadingPromise;
    +
    +    const publicKey = await this.getAppleCertificate(authData.publicKeyUrl);
    +    this.verifySignature(publicKey, authData);
       }
    -  const { certificate, headers } = await getCertificate(options.rootCertificateUrl, true);
    -  if (
    -    headers['content-type'] !== 'application/x-pem-file' ||
    -    headers['content-length'] == null ||
    -    headers['content-length'] > 10000
    -  ) {
    -    throw new Parse.Error(
    -      Parse.Error.OBJECT_NOT_FOUND,
    -      'Apple Game Center auth adapter parameter `rootCertificateURL` is invalid.'
    -    );
    +
    +  convertTimestampToBigEndian(timestamp) {
    +    const buffer = Buffer.alloc(8);
    +    buffer.writeBigUInt64BE(BigInt(timestamp));
    +    return buffer;
       }
    -  ca.cert = pki.certificateFromPem(certificate);
    -  ca.url = options.rootCertificateUrl;
     }
     
    -module.exports = {
    -  validateAppId,
    -  validateAuthData,
    -  cache,
    -};
    +export default new GameCenterAuth();
    
  • src/Adapters/Auth/github.js+122 30 modified
    @@ -1,35 +1,127 @@
    -// Helper functions for accessing the github API.
    -var Parse = require('parse/node').Parse;
    -const httpsRequest = require('./httpsRequest');
    -
    -// Returns a promise that fulfills iff this user id is valid.
    -function validateAuthData(authData) {
    -  return request('user', authData.access_token).then(data => {
    -    if (data && data.id == authData.id) {
    -      return;
    +/**
    + * Parse Server authentication adapter for GitHub.
    + * @class GitHubAdapter
    + * @param {Object} options - The adapter configuration options.
    + * @param {string} options.clientId - The GitHub App Client ID. Required for secure authentication.
    + * @param {string} options.clientSecret - The GitHub App Client Secret. Required for secure authentication.
    + * @param {boolean} [options.enableInsecureAuth=false] - **[DEPRECATED]** Enable insecure authentication (not recommended).
    + *
    + * @param {Object} authData - The authentication data provided by the client.
    + * @param {string} authData.code - The authorization code from GitHub. Required for secure authentication.
    + * @param {string} [authData.id] - **[DEPRECATED]** The GitHub user ID (required for insecure authentication).
    + * @param {string} [authData.access_token] - **[DEPRECATED]** The GitHub access token (required for insecure authentication).
    + *
    + * @description
    + * ## Parse Server Configuration
    + * * To configure Parse Server for GitHub authentication, use the following structure:
    + * ```json
    + * {
    + *  "auth": {
    + *   "github": {
    + *     "clientId": "12345",
    + *     "clientSecret": "abcde"
    + *   }
    + * }
    + * ```
    + *
    + * The GitHub adapter exchanges the `authData.code` provided by the client for an access token using GitHub's OAuth API. The following `authData` field is required:
    + * - `code`
    + *
    + * ## Insecure Authentication (Not Recommended)
    + * Insecure authentication uses the `authData.id` and `authData.access_token` provided by the client. This flow is insecure, deprecated, and poses potential security risks. The following `authData` fields are required:
    + * - `id` (**[DEPRECATED]**): The GitHub user ID.
    + * - `access_token` (**[DEPRECATED]**): The GitHub access token.
    + * To configure Parse Server for insecure authentication, use the following structure:
    + * ```json
    + * {
    + *  "auth": {
    + *    "github": {
    + *    "enableInsecureAuth": true
    + *  }
    + * }
    + * ```
    + *
    + * ### Deprecation Notice
    + * The `enableInsecureAuth` option and insecure `authData` fields (`id`, `access_token`) are deprecated and will be removed in future versions. Use secure authentication with `clientId` and `clientSecret`.
    + *
    + * @example <caption>Secure Authentication Example</caption>
    + * // Example authData for secure authentication:
    + * const authData = {
    + *   github: {
    + *     code: "abc123def456ghi789"
    + *   }
    + * };
    + *
    + * @example <caption>Insecure Authentication Example (Not Recommended)</caption>
    + * // Example authData for insecure authentication:
    + * const authData = {
    + *   github: {
    + *     id: "1234567",
    + *     access_token: "abc123def456ghi789" // Deprecated.
    + *   }
    + * };
    + *
    + * @note `enableInsecureAuth` will be removed in future versions. Use secure authentication with `clientId` and `clientSecret`.
    + * @note Secure authentication exchanges the `code` provided by the client for an access token using GitHub's OAuth API.
    + *
    + * @see {@link https://docs.github.com/en/developers/apps/authorizing-oauth-apps GitHub OAuth Documentation}
    + */
    +
    +import BaseCodeAuthAdapter from './BaseCodeAuthAdapter';
    +class GitHubAdapter extends BaseCodeAuthAdapter {
    +  constructor() {
    +    super('GitHub');
    +  }
    +  async getAccessTokenFromCode(authData) {
    +    const tokenUrl = 'https://github.com/login/oauth/access_token';
    +    const response = await fetch(tokenUrl, {
    +      method: 'POST',
    +      headers: {
    +        'Content-Type': 'application/json',
    +        Accept: 'application/json',
    +      },
    +      body: JSON.stringify({
    +        client_id: this.clientId,
    +        client_secret: this.clientSecret,
    +        code: authData.code,
    +      }),
    +    });
    +
    +    if (!response.ok) {
    +      throw new Parse.Error(Parse.Error.VALIDATION_ERROR, `Failed to exchange code for token: ${response.statusText}`);
         }
    -    throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Github auth is invalid for this user.');
    -  });
    -}
     
    -// Returns a promise that fulfills iff this app id is valid.
    -function validateAppId() {
    -  return Promise.resolve();
    -}
    +    const data = await response.json();
    +    if (data.error) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, data.error_description || data.error);
    +    }
    +
    +    return data.access_token;
    +  }
    +
    +  async getUserFromAccessToken(accessToken) {
    +    const userApiUrl = 'https://api.github.com/user';
    +    const response = await fetch(userApiUrl, {
    +      method: 'GET',
    +      headers: {
    +        Authorization: `Bearer ${accessToken}`,
    +        Accept: 'application/json',
    +      },
    +    });
    +
    +    if (!response.ok) {
    +      throw new Parse.Error(Parse.Error.VALIDATION_ERROR, `Failed to fetch GitHub user: ${response.statusText}`);
    +    }
    +
    +    const userData = await response.json();
    +    if (!userData.id || !userData.login) {
    +      throw new Parse.Error(Parse.Error.VALIDATION_ERROR, 'Invalid GitHub user data received.');
    +    }
    +
    +    return userData;
    +  }
     
    -// A promisey wrapper for api requests
    -function request(path, access_token) {
    -  return httpsRequest.get({
    -    host: 'api.github.com',
    -    path: '/' + path,
    -    headers: {
    -      Authorization: 'bearer ' + access_token,
    -      'User-Agent': 'parse-server',
    -    },
    -  });
     }
     
    -module.exports = {
    -  validateAppId: validateAppId,
    -  validateAuthData: validateAuthData,
    -};
    +export default new GitHubAdapter();
    +
    
  • src/Adapters/Auth/google.js+44 0 modified
    @@ -1,3 +1,47 @@
    +/**
    + * Parse Server authentication adapter for Google.
    + *
    + * @class GoogleAdapter
    + * @param {Object} options - The adapter configuration options.
    + * @param {string} options.clientId - Your Google application Client ID. Required for authentication.
    + *
    + * @description
    + * ## Parse Server Configuration
    + * To configure Parse Server for Google authentication, use the following structure:
    + * ```json
    + * {
    + *   "auth": {
    + *     "google": {
    + *       "clientId": "your-client-id"
    + *     }
    + *   }
    + * }
    + * ```
    + *
    + * The adapter requires the following `authData` fields:
    + * - **id**: The Google user ID.
    + * - **id_token**: The Google ID token.
    + * - **access_token**: The Google access token.
    + *
    + * ## Auth Payload
    + * ### Example Auth Data Payload
    + * ```json
    + * {
    + *   "google": {
    + *     "id": "1234567",
    + *     "id_token": "xxxxx.yyyyy.zzzzz",
    + *     "access_token": "abc123def456ghi789"
    + *   }
    + * }
    + * ```
    + *
    + * ## Notes
    + * - Ensure your Google Client ID is configured properly in the Parse Server configuration.
    + * - The `id_token` and `access_token` are validated against Google's authentication services.
    + *
    + * @see {@link https://developers.google.com/identity/sign-in/web/backend-auth Google Authentication Documentation}
    + */
    +
     'use strict';
     
     // Helper functions for accessing the google API.
    
  • src/Adapters/Auth/gpgames.js+135 29 modified
    @@ -1,33 +1,139 @@
    -/* Google Play Game Services
    -https://developers.google.com/games/services/web/api/players/get
    -
    -const authData = {
    -  id: 'playerId',
    -  access_token: 'token',
    -};
    -*/
    -const { Parse } = require('parse/node');
    -const httpsRequest = require('./httpsRequest');
    -
    -// Returns a promise that fulfills if this user id is valid.
    -async function validateAuthData(authData) {
    -  const response = await httpsRequest.get(
    -    `https://www.googleapis.com/games/v1/players/${authData.id}?access_token=${authData.access_token}`
    -  );
    -  if (!(response && response.playerId === authData.id)) {
    -    throw new Parse.Error(
    -      Parse.Error.OBJECT_NOT_FOUND,
    -      'Google Play Games Services - authData is invalid for this user.'
    -    );
    +/**
    + * Parse Server authentication adapter for Google Play Games Services.
    + *
    + * @class GooglePlayGamesServicesAdapter
    + * @param {Object} options - The adapter configuration options.
    + * @param {string} options.clientId - Your Google Play Games Services App Client ID. Required for secure authentication.
    + * @param {string} options.clientSecret - Your Google Play Games Services App Client Secret. Required for secure authentication.
    + * @param {boolean} [options.enableInsecureAuth=false] - **[DEPRECATED]** Enable insecure authentication (not recommended).
    + *
    + * @description
    + * ## Parse Server Configuration
    + * To configure Parse Server for Google Play Games Services authentication, use the following structure:
    + * ```json
    + * {
    + *   "auth": {
    + *     "gpgames": {
    + *       "clientId": "your-client-id",
    + *       "clientSecret": "your-client-secret"
    + *     }
    + *   }
    + * }
    + * ```
    + * ### Insecure Configuration (Not Recommended)
    + * ```json
    + * {
    + *   "auth": {
    + *     "gpgames": {
    + *       "enableInsecureAuth": true
    + *     }
    + *   }
    + * }
    + * ```
    + *
    + * The adapter requires the following `authData` fields:
    + * - **Secure Authentication**: `code`, `redirect_uri`.
    + * - **Insecure Authentication (Not Recommended)**: `id`, `access_token`.
    + *
    + * ## Auth Payloads
    + * ### Secure Authentication Payload
    + * ```json
    + * {
    + *   "gpgames": {
    + *     "code": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    + *     "redirect_uri": "https://example.com/callback"
    + *   }
    + * }
    + * ```
    + *
    + * ### Insecure Authentication Payload (Not Recommended)
    + * ```json
    + * {
    + *   "gpgames": {
    + *     "id": "123456789",
    + *     "access_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    + *   }
    + * }
    + * ```
    + *
    + * ## Notes
    + * - `enableInsecureAuth` is **not recommended** and may be removed in future versions. Use secure authentication with `code` and `redirect_uri`.
    + * - Secure authentication exchanges the `code` provided by the client for an access token using Google Play Games Services' OAuth API.
    + *
    + * @see {@link https://developers.google.com/games/services/console/enabling Google Play Games Services Authentication Documentation}
    + */
    +
    +import BaseCodeAuthAdapter from './BaseCodeAuthAdapter';
    +class GooglePlayGamesServicesAdapter extends BaseCodeAuthAdapter {
    +  constructor() {
    +    super("gpgames");
    +  }
    +
    +  async getAccessTokenFromCode(authData) {
    +    const tokenUrl = 'https://oauth2.googleapis.com/token';
    +    const response = await fetch(tokenUrl, {
    +      method: 'POST',
    +      headers: {
    +        'Content-Type': 'application/json',
    +        Accept: 'application/json',
    +      },
    +      body: JSON.stringify({
    +        client_id: this.clientId,
    +        client_secret: this.clientSecret,
    +        code: authData.code,
    +        redirect_uri: authData.redirectUri,
    +        grant_type: 'authorization_code',
    +      }),
    +    });
    +
    +    if (!response.ok) {
    +      throw new Parse.Error(
    +        Parse.Error.VALIDATION_ERROR,
    +        `Failed to exchange code for token: ${response.statusText}`
    +      );
    +    }
    +
    +    const data = await response.json();
    +    if (data.error) {
    +      throw new Parse.Error(
    +        Parse.Error.OBJECT_NOT_FOUND,
    +        data.error_description || data.error
    +      );
    +    }
    +
    +    return data.access_token;
    +  }
    +
    +  async getUserFromAccessToken(accessToken, authData) {
    +    const userApiUrl = `https://www.googleapis.com/games/v1/players/${authData.id}`;
    +    const response = await fetch(userApiUrl, {
    +      method: 'GET',
    +      headers: {
    +        Authorization: `Bearer ${accessToken}`,
    +        Accept: 'application/json',
    +      },
    +    });
    +
    +    if (!response.ok) {
    +      throw new Parse.Error(
    +        Parse.Error.VALIDATION_ERROR,
    +        `Failed to fetch Google Play Games Services user: ${response.statusText}`
    +      );
    +    }
    +
    +    const userData = await response.json();
    +    if (!userData.playerId || userData.playerId !== authData.id) {
    +      throw new Parse.Error(
    +        Parse.Error.VALIDATION_ERROR,
    +        'Invalid Google Play Games Services user data received.'
    +      );
    +    }
    +
    +    return {
    +      id: userData.playerId
    +    };
       }
    -}
     
    -// Returns a promise that fulfills if this app id is valid.
    -function validateAppId() {
    -  return Promise.resolve();
     }
     
    -module.exports = {
    -  validateAppId,
    -  validateAuthData,
    -};
    +export default new GooglePlayGamesServicesAdapter();
    
  • src/Adapters/Auth/index.js+22 21 modified
    @@ -3,30 +3,31 @@ import Parse from 'parse/node';
     import AuthAdapter from './AuthAdapter';
     
     const apple = require('./apple');
    -const gcenter = require('./gcenter');
    -const gpgames = require('./gpgames');
    +const digits = require('./twitter'); // digits tokens are validated by twitter
     const facebook = require('./facebook');
    -const instagram = require('./instagram');
    -const linkedin = require('./linkedin');
    -const meetup = require('./meetup');
    -import mfa from './mfa';
    +import gcenter from './gcenter';
    +import github from './github';
     const google = require('./google');
    -const github = require('./github');
    -const twitter = require('./twitter');
    -const spotify = require('./spotify');
    -const digits = require('./twitter'); // digits tokens are validated by twitter
    -const janrainengage = require('./janrainengage');
    +import gpgames from './gpgames';
    +import instagram from './instagram';
     const janraincapture = require('./janraincapture');
    -const line = require('./line');
    -const vkontakte = require('./vkontakte');
    -const qq = require('./qq');
    -const wechat = require('./wechat');
    -const weibo = require('./weibo');
    -const oauth2 = require('./oauth2');
    -const phantauth = require('./phantauth');
    -const microsoft = require('./microsoft');
    +const janrainengage = require('./janrainengage');
     const keycloak = require('./keycloak');
     const ldap = require('./ldap');
    +import line from './line';
    +import linkedin from './linkedin';
    +const meetup = require('./meetup');
    +import mfa from './mfa';
    +import microsoft from './microsoft';
    +import oauth2 from './oauth2';
    +const phantauth = require('./phantauth');
    +import qq from './qq';
    +import spotify from './spotify';
    +import twitter from './twitter';
    +const vkontakte = require('./vkontakte');
    +import wechat from './wechat';
    +import weibo from './weibo';
    +
     
     const anonymous = {
       validateAuthData: () => {
    @@ -241,9 +242,9 @@ module.exports = function (authOptions = {}, enableAnonymousUsers = true) {
               };
               const result = afterFind.call(
                 adapter,
    -            requestObject,
                 authData[provider],
    -            providerOptions
    +            providerOptions,
    +            requestObject,
               );
               if (result) {
                 authData[provider] = result;
    
  • src/Adapters/Auth/instagram.js+117 23 modified
    @@ -1,27 +1,121 @@
    -// Helper functions for accessing the instagram API.
    -var Parse = require('parse/node').Parse;
    -const httpsRequest = require('./httpsRequest');
    -const defaultURL = 'https://graph.instagram.com/';
    -
    -// Returns a promise that fulfills if this user id is valid.
    -function validateAuthData(authData) {
    -  const apiURL = authData.apiURL || defaultURL;
    -  const path = `${apiURL}me?fields=id&access_token=${authData.access_token}`;
    -  return httpsRequest.get(path).then(response => {
    -    const user = response.data ? response.data : response;
    -    if (user && user.id == authData.id) {
    -      return;
    +/**
    + * Parse Server authentication adapter for Instagram.
    + *
    + * @class InstagramAdapter
    + * @param {Object} options - The adapter configuration options.
    + * @param {string} options.clientId - Your Instagram App Client ID. Required for secure authentication.
    + * @param {string} options.clientSecret - Your Instagram App Client Secret. Required for secure authentication.
    + * @param {boolean} [options.enableInsecureAuth=false] - **[DEPRECATED]** Enable insecure authentication (not recommended).
    + *
    + * @description
    + * ## Parse Server Configuration
    + * To configure Parse Server for Instagram authentication, use the following structure:
    + * ```json
    + * {
    + *   "auth": {
    + *     "instagram": {
    + *       "clientId": "your-client-id",
    + *       "clientSecret": "your-client-secret"
    + *     }
    + *   }
    + * }
    + * ```
    + * ### Insecure Configuration (Not Recommended)
    + * ```json
    + * {
    + *   "auth": {
    + *     "instagram": {
    + *       "enableInsecureAuth": true
    + *     }
    + *   }
    + * }
    + * ```
    + *
    + * The adapter requires the following `authData` fields:
    + * - **Secure Authentication**: `code`, `redirect_uri`.
    + * - **Insecure Authentication (Deprecated)**: `id`, `access_token`.
    + *
    + * ## Auth Payloads
    + * ### Secure Authentication Payload
    + * ```json
    + * {
    + *   "instagram": {
    + *     "code": "lmn789opq012rst345uvw",
    + *     "redirect_uri": "https://example.com/callback"
    + *   }
    + * }
    + * ```
    + *
    + * ### Insecure Authentication Payload (Deprecated)
    + * ```json
    + * {
    + *   "instagram": {
    + *     "id": "1234567",
    + *     "access_token": "AQXNnd2hIT6z9bHFzZz2Kp1ghiMz_RtyuvwXYZ123abc"
    + *   }
    + * }
    + * ```
    + *
    + * ## Notes
    + * - `enableInsecureAuth` is **deprecated** and will be removed in future versions. Use secure authentication with `code` and `redirect_uri`.
    + * - Secure authentication exchanges the `code` and `redirect_uri` provided by the client for an access token using Instagram's OAuth flow.
    + *
    + * @see {@link https://developers.facebook.com/docs/instagram-basic-display-api/getting-started Instagram Basic Display API - Getting Started}
    + */
    +
    +
    +import BaseAuthCodeAdapter from './BaseCodeAuthAdapter';
    +class InstagramAdapter extends BaseAuthCodeAdapter {
    +  constructor() {
    +    super('Instagram');
    +  }
    +
    +  async getAccessTokenFromCode(authData) {
    +    const response = await fetch('https://api.instagram.com/oauth/access_token', {
    +      method: 'POST',
    +      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    +      body: new URLSearchParams({
    +        client_id: this.clientId,
    +        client_secret: this.clientSecret,
    +        grant_type: 'authorization_code',
    +        redirect_uri: this.redirectUri,
    +        code: authData.code
    +      })
    +    });
    +
    +    if (!response.ok) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Instagram API request failed.');
    +    }
    +
    +    const data = await response.json();
    +    if (data.error) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, data.error_description || data.error);
    +    }
    +
    +    return data.access_token;
    +  }
    +
    +  async getUserFromAccessToken(accessToken, authData) {
    +    const defaultURL = 'https://graph.instagram.com/';
    +    const apiURL = authData.apiURL || defaultURL;
    +    const path = `${apiURL}me?fields=id&access_token=${accessToken}`;
    +
    +    const response = await fetch(path);
    +
    +    if (!response.ok) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Instagram API request failed.');
    +    }
    +
    +    const user = await response.json();
    +    if (user?.id !== authData.id) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Instagram auth is invalid for this user.');
    +    }
    +
    +    return {
    +      id: user.id,
         }
    -    throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Instagram auth is invalid for this user.');
    -  });
    -}
     
    -// Returns a promise that fulfills iff this app id is valid.
    -function validateAppId() {
    -  return Promise.resolve();
    +  }
     }
     
    -module.exports = {
    -  validateAppId,
    -  validateAuthData,
    -};
    +export default new InstagramAdapter();
    
  • src/Adapters/Auth/janraincapture.js+45 0 modified
    @@ -1,3 +1,48 @@
    +/**
    + * Parse Server authentication adapter for Janrain Capture API.
    + *
    + * @class JanrainCapture
    + * @param {Object} options - The adapter configuration options.
    + * @param {String} options.janrain_capture_host - The Janrain Capture API host.
    + *
    + * @param {Object} authData - The authentication data provided by the client.
    + * @param {String} authData.id - The Janrain Capture user ID.
    + * @param {String} authData.access_token - The Janrain Capture access token.
    + *
    + * @description
    + * ## Parse Server Configuration
    + * To configure Parse Server for Janrain Capture authentication, use the following structure:
    + * ```json
    + * {
    + *   "auth": {
    + *     "janrain": {
    + *       "janrain_capture_host": "your-janrain-capture-host"
    + *     }
    + *   }
    + * }
    + * ```
    + *
    + * The adapter requires the following `authData` fields:
    + * - `id`: The Janrain Capture user ID.
    + * - `access_token`: An authorized Janrain Capture access token for the user.
    + *
    + * ## Auth Payload Example
    + * ```json
    + * {
    + *   "janrain": {
    + *     "id": "user's Janrain Capture ID as a string",
    + *     "access_token": "an authorized Janrain Capture access token for the user"
    + *   }
    + * }
    + * ```
    + *
    + * ## Notes
    + * Parse Server validates the provided `authData` using the Janrain Capture API.
    + *
    + * @see {@link https://docs.janrain.com/api/registration/entity/#entity Janrain Capture API Documentation}
    + */
    +
    +
     // Helper functions for accessing the Janrain Capture API.
     var Parse = require('parse/node').Parse;
     var querystring = require('querystring');
    
  • src/Adapters/Auth/janrainengage.js+9 0 modified
    @@ -2,9 +2,18 @@
     var httpsRequest = require('./httpsRequest');
     var Parse = require('parse/node').Parse;
     var querystring = require('querystring');
    +import Config from '../../Config';
    +import Deprecator from '../../Deprecator/Deprecator';
     
     // Returns a promise that fulfills iff this user id is valid.
     function validateAuthData(authData, options) {
    +  const config = Config.get(Parse.applicationId);
    +
    +  Deprecator.logRuntimeDeprecation({ usage: 'janrainengage adapter' });
    +  if (!config?.auth?.janrainengage?.enableInsecureAuth || !config.enableInsecureAuthAdapters) {
    +    throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'janrainengage adapter only works with enableInsecureAuth: true');
    +  }
    +
       return apiRequest(options.api_key, authData.auth_token).then(data => {
         //successful response will have a "stat" (status) of 'ok' and a profile node with an identifier
         //see: http://developers.janrain.com/overview/social-login/identity-providers/user-profile-data/#normalized-user-profile-data
    
  • src/Adapters/Auth/keycloak.js+67 34 modified
    @@ -1,37 +1,70 @@
    -/*
    -  # Parse Server Keycloak Authentication
    -
    -  ## Keycloak `authData`
    -
    -  ```
    -    {
    -      "keycloak": {
    -        "access_token": "access token you got from keycloak JS client authentication",
    -        "id": "the id retrieved from client authentication in Keycloak",
    -        "roles": ["the roles retrieved from client authentication in Keycloak"],
    -        "groups": ["the groups retrieved from client authentication in Keycloak"]
    -      }
    -    }
    -  ```
    -
    -  The authentication module will test if the authData is the same as the
    -  userinfo oauth call, comparing the attributes
    -
    -  Copy the JSON config file generated on Keycloak (https://www.keycloak.org/docs/latest/securing_apps/index.html#_javascript_adapter)
    -  and paste it inside of a folder (Ex.: `auth/keycloak.json`) in your server.
    -
    -  The options passed to Parse server:
    -
    -  ```
    -    {
    -      auth: {
    -        keycloak: {
    -          config: require(`./auth/keycloak.json`)
    -        }
    -      }
    -    }
    -  ```
    -*/
    +/**
    + * Parse Server authentication adapter for Keycloak.
    + *
    + * @class KeycloakAdapter
    + * @param {Object} options - The adapter configuration options.
    + * @param {Object} options.config - The Keycloak configuration object, typically loaded from a JSON file.
    + * @param {String} options.config.auth-server-url - The Keycloak authentication server URL.
    + * @param {String} options.config.realm - The Keycloak realm name.
    + * @param {String} options.config.client-id - The Keycloak client ID.
    + *
    + * @param {Object} authData - The authentication data provided by the client.
    + * @param {String} authData.access_token - The Keycloak access token retrieved during client authentication.
    + * @param {String} authData.id - The user ID retrieved from Keycloak during client authentication.
    + * @param {Array} [authData.roles] - The roles assigned to the user in Keycloak (optional).
    + * @param {Array} [authData.groups] - The groups assigned to the user in Keycloak (optional).
    + *
    + * @description
    + * ## Parse Server Configuration
    + * To configure Parse Server for Keycloak authentication, use the following structure:
    + * ```javascript
    + * {
    + *   "auth": {
    + *     "keycloak": {
    + *       "config": require('./auth/keycloak.json')
    + *     }
    + *   }
    + * }
    + * ```
    + * Ensure the `keycloak.json` configuration file is generated from Keycloak's setup guide and includes:
    + * - `auth-server-url`: The Keycloak authentication server URL.
    + * - `realm`: The Keycloak realm name.
    + * - `client-id`: The Keycloak client ID.
    + *
    + * ## Auth Data
    + * The adapter requires the following `authData` fields:
    + * - `access_token`: The Keycloak access token retrieved during client authentication.
    + * - `id`: The user ID retrieved from Keycloak during client authentication.
    + * - `roles` (optional): The roles assigned to the user in Keycloak.
    + * - `groups` (optional): The groups assigned to the user in Keycloak.
    + *
    + * ## Auth Payload Example
    + * ### Example Auth Data
    + * ```json
    + * {
    + *   "keycloak": {
    + *     "access_token": "an authorized Keycloak access token for the user",
    + *     "id": "user's Keycloak ID as a string",
    + *     "roles": ["admin", "user"],
    + *     "groups": ["group1", "group2"]
    + *   }
    + * }
    + * ```
    + *
    + * ## Notes
    + * - Parse Server validates the provided `authData` by making a `userinfo` call to Keycloak and ensures the attributes match those returned by Keycloak.
    + *
    + * ## Keycloak Configuration
    + * To configure Keycloak, copy the JSON configuration file generated from Keycloak's setup guide:
    + * - [Keycloak Securing Apps Documentation](https://www.keycloak.org/docs/latest/securing_apps/index.html#_javascript_adapter)
    + *
    + * Place the configuration file on your server, for example:
    + * - `auth/keycloak.json`
    + *
    + * For more information on Keycloak authentication, see:
    + * - [Securing Apps Documentation](https://www.keycloak.org/docs/latest/securing_apps/)
    + * - [Server Administration Documentation](https://www.keycloak.org/docs/latest/server_admin/)
    + */
     
     const { Parse } = require('parse/node');
     const httpsRequest = require('./httpsRequest');
    
  • src/Adapters/Auth/ldap.js+75 0 modified
    @@ -1,3 +1,78 @@
    +/**
    + * Parse Server authentication adapter for LDAP.
    + *
    + * @class LDAP
    + * @param {Object} options - The adapter configuration options.
    + * @param {String} options.url - The LDAP server URL. Must start with `ldap://` or `ldaps://`.
    + * @param {String} options.suffix - The LDAP suffix for user distinguished names (DN).
    + * @param {String} [options.dn] - The distinguished name (DN) template for user authentication. Replace `{{id}}` with the username.
    + * @param {Object} [options.tlsOptions] - Options for LDAPS TLS connections.
    + * @param {String} [options.groupCn] - The common name (CN) of the group to verify user membership.
    + * @param {String} [options.groupFilter] - The LDAP search filter for groups, with `{{id}}` replaced by the username.
    + *
    + * @param {Object} authData - The authentication data provided by the client.
    + * @param {String} authData.id - The user's LDAP username.
    + * @param {String} authData.password - The user's LDAP password.
    + *
    + * @description
    + * ## Parse Server Configuration
    + * To configure Parse Server for LDAP authentication, use the following structure:
    + * ```javascript
    + * {
    + *   auth: {
    + *     ldap: {
    + *       url: 'ldaps://ldap.example.com',
    + *       suffix: 'ou=users,dc=example,dc=com',
    + *       groupCn: 'admins',
    + *       groupFilter: '(memberUid={{id}})',
    + *       tlsOptions: {
    + *         rejectUnauthorized: false
    + *       }
    + *     }
    + *   }
    + * }
    + * ```
    + *
    + * ## Authentication Process
    + * 1. Validates the provided `authData` using an LDAP bind operation.
    + * 2. Optionally, verifies that the user belongs to a specific group by performing an LDAP search using the provided `groupCn` or `groupFilter`.
    + *
    + * ## Auth Payload
    + * The adapter requires the following `authData` fields:
    + * - `id`: The user's LDAP username.
    + * - `password`: The user's LDAP password.
    + *
    + * ### Example Auth Payload
    + * ```json
    + * {
    + *   "ldap": {
    + *     "id": "jdoe",
    + *     "password": "password123"
    + *   }
    + * }
    + * ```
    + *
    + * @example <caption>Configuration Example</caption>
    + * // Example Parse Server configuration:
    + * const config = {
    + *   auth: {
    + *     ldap: {
    + *       url: 'ldaps://ldap.example.com',
    + *       suffix: 'ou=users,dc=example,dc=com',
    + *       groupCn: 'admins',
    + *       groupFilter: '(memberUid={{id}})',
    + *       tlsOptions: {
    + *         rejectUnauthorized: false
    + *       }
    + *     }
    + *   }
    + * };
    + *
    + * @see {@link https://ldap.com/ LDAP Basics}
    + * @see {@link https://ldap.com/ldap-filters/ LDAP Filters}
    + */
    +
    +
     const ldapjs = require('ldapjs');
     const Parse = require('parse/node').Parse;
     
    
  • src/Adapters/Auth/line.js+138 31 modified
    @@ -1,36 +1,143 @@
    -// Helper functions for accessing the line API.
    -var Parse = require('parse/node').Parse;
    -const httpsRequest = require('./httpsRequest');
    -
    -// Returns a promise that fulfills if this user id is valid.
    -function validateAuthData(authData) {
    -  return request('profile', authData.access_token).then(response => {
    -    if (response && response.userId && response.userId === authData.id) {
    -      return;
    +/**
    + * Parse Server authentication adapter for Line.
    + *
    + * @class LineAdapter
    + * @param {Object} options - The adapter configuration options.
    + * @param {string} options.clientId - Your Line App Client ID. Required for secure authentication.
    + * @param {string} options.clientSecret - Your Line App Client Secret. Required for secure authentication.
    + * @param {boolean} [options.enableInsecureAuth=false] - **[DEPRECATED]** Enable insecure authentication (not recommended).
    + *
    + * @description
    + * ## Parse Server Configuration
    + * To configure Parse Server for Line authentication, use the following structure:
    + * ### Secure Configuration
    + * ```json
    + * {
    + *   "auth": {
    + *     "line": {
    + *       "clientId": "your-client-id",
    + *       "clientSecret": "your-client-secret"
    + *     }
    + *   }
    + * }
    + * ```
    + * ### Insecure Configuration (Not Recommended)
    + * ```json
    + * {
    + *   "auth": {
    + *     "line": {
    + *       "enableInsecureAuth": true
    + *     }
    + *   }
    + * }
    + * ```
    + *
    + * The adapter requires the following `authData` fields:
    + * - **Secure Authentication**: `code`, `redirect_uri`.
    + * - **Insecure Authentication (Not Recommended)**: `id`, `access_token`.
    + *
    + * ## Auth Payloads
    + * ### Secure Authentication Payload
    + * ```json
    + * {
    + *   "line": {
    + *     "code": "xxxxxxxxx",
    + *     "redirect_uri": "https://example.com/callback"
    + *   }
    + * }
    + * ```
    + *
    + * ### Insecure Authentication Payload (Not Recommended)
    + * ```json
    + * {
    + *   "line": {
    + *     "id": "1234567",
    + *     "access_token": "xxxxxxxxx"
    + *   }
    + * }
    + * ```
    + *
    + * ## Notes
    + * - `enableInsecureAuth` is **not recommended** and will be removed in future versions. Use secure authentication with `clientId` and `clientSecret`.
    + * - Secure authentication exchanges the `code` and `redirect_uri` provided by the client for an access token using Line's OAuth flow.
    + *
    + * @see {@link https://developers.line.biz/en/docs/line-login/integrate-line-login/ Line Login Documentation}
    + */
    +
    +import BaseCodeAuthAdapter from './BaseCodeAuthAdapter';
    +
    +class LineAdapter extends BaseCodeAuthAdapter {
    +  constructor() {
    +    super('Line');
    +  }
    +
    +  async getAccessTokenFromCode(authData) {
    +    if (!authData.code) {
    +      throw new Parse.Error(
    +        Parse.Error.OBJECT_NOT_FOUND,
    +        'Line auth is invalid for this user.'
    +      );
         }
    -    throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Line auth is invalid for this user.');
    -  });
    -}
     
    -// Returns a promise that fulfills iff this app id is valid.
    -function validateAppId() {
    -  return Promise.resolve();
    -}
    +    const tokenUrl = 'https://api.line.me/oauth2/v2.1/token';
    +    const response = await fetch(tokenUrl, {
    +      method: 'POST',
    +      headers: {
    +        'Content-Type': 'application/x-www-form-urlencoded',
    +      },
    +      body: new URLSearchParams({
    +        client_id: this.clientId,
    +        client_secret: this.clientSecret,
    +        grant_type: 'authorization_code',
    +        redirect_uri: authData.redirect_uri,
    +        code: authData.code,
    +      }),
    +    });
    +
    +    if (!response.ok) {
    +      throw new Parse.Error(
    +        Parse.Error.OBJECT_NOT_FOUND,
    +        `Failed to exchange code for token: ${response.statusText}`
    +      );
    +    }
    +
    +    const data = await response.json();
    +    if (data.error) {
    +      throw new Parse.Error(
    +        Parse.Error.OBJECT_NOT_FOUND,
    +        data.error_description || data.error
    +      );
    +    }
    +
    +    return data.access_token;
    +  }
    +
    +  async getUserFromAccessToken(accessToken) {
    +    const userApiUrl = 'https://api.line.me/v2/profile';
    +    const response = await fetch(userApiUrl, {
    +      method: 'GET',
    +      headers: {
    +        Authorization: `Bearer ${accessToken}`,
    +      },
    +    });
    +
    +    if (!response.ok) {
    +      throw new Parse.Error(
    +        Parse.Error.OBJECT_NOT_FOUND,
    +        `Failed to fetch Line user: ${response.statusText}`
    +      );
    +    }
    +
    +    const userData = await response.json();
    +    if (!userData?.userId) {
    +      throw new Parse.Error(
    +        Parse.Error.VALIDATION_ERROR,
    +        'Invalid Line user data received.'
    +      );
    +    }
     
    -// A promisey wrapper for api requests
    -function request(path, access_token) {
    -  var options = {
    -    host: 'api.line.me',
    -    path: '/v2/' + path,
    -    method: 'GET',
    -    headers: {
    -      Authorization: 'Bearer ' + access_token,
    -    },
    -  };
    -  return httpsRequest.get(options);
    +    return userData;
    +  }
     }
     
    -module.exports = {
    -  validateAppId: validateAppId,
    -  validateAuthData: validateAuthData,
    -};
    +export default new LineAdapter();
    
  • src/Adapters/Auth/linkedin.js+107 32 modified
    @@ -1,40 +1,115 @@
    -// Helper functions for accessing the linkedin API.
    -var Parse = require('parse/node').Parse;
    -const httpsRequest = require('./httpsRequest');
    +/**
    + * Parse Server authentication adapter for LinkedIn.
    + *
    + * @class LinkedInAdapter
    + * @param {Object} options - The adapter configuration options.
    + * @param {string} options.clientId - Your LinkedIn App Client ID. Required for secure authentication.
    + * @param {string} options.clientSecret - Your LinkedIn App Client Secret. Required for secure authentication.
    + * @param {boolean} [options.enableInsecureAuth=false] - **[DEPRECATED]** Enable insecure authentication (not recommended).
    + *
    + * @description
    + * ## Parse Server Configuration
    + * To configure Parse Server for LinkedIn authentication, use the following structure:
    + * ### Secure Configuration
    + * ```json
    + * {
    + *   "auth": {
    + *     "linkedin": {
    + *       "clientId": "your-client-id",
    + *       "clientSecret": "your-client-secret"
    + *     }
    + *   }
    + * }
    + * ```
    + * ### Insecure Configuration (Not Recommended)
    + * ```json
    + * {
    + *   "auth": {
    + *     "linkedin": {
    + *       "enableInsecureAuth": true
    + *     }
    + *   }
    + * }
    + * ```
    + *
    + * The adapter requires the following `authData` fields:
    + * - **Secure Authentication**: `code`, `redirect_uri`, and optionally `is_mobile_sdk`.
    + * - **Insecure Authentication (Not Recommended)**: `id`, `access_token`, and optionally `is_mobile_sdk`.
    + *
    + * ## Auth Payloads
    + * ### Secure Authentication Payload
    + * ```json
    + * {
    + *   "linkedin": {
    + *     "code": "lmn789opq012rst345uvw",
    + *     "redirect_uri": "https://your-redirect-uri.com/callback",
    + *     "is_mobile_sdk": true
    + *   }
    + * }
    + * ```
    + *
    + * ### Insecure Authentication Payload (Not Recommended)
    + * ```json
    + * {
    + *   "linkedin": {
    + *     "id": "7654321",
    + *     "access_token": "AQXNnd2hIT6z9bHFzZz2Kp1ghiMz_RtyuvwXYZ123abc",
    + *     "is_mobile_sdk": true
    + *   }
    + * }
    + * ```
    + *
    + * ## Notes
    + * - Secure authentication exchanges the `code` and `redirect_uri` provided by the client for an access token using LinkedIn's OAuth API.
    + * - Insecure authentication validates the user ID and access token directly, bypassing OAuth flows. This method is **not recommended** and may introduce security vulnerabilities.
    + * - `enableInsecureAuth` is **deprecated** and may be removed in future versions.
    + *
    + * @see {@link https://learn.microsoft.com/en-us/linkedin/shared/authentication/authentication LinkedIn Authentication Documentation}
    + */
     
    -// Returns a promise that fulfills iff this user id is valid.
    -function validateAuthData(authData) {
    -  return request('me', authData.access_token, authData.is_mobile_sdk).then(data => {
    -    if (data && data.id == authData.id) {
    -      return;
    +import BaseAuthCodeAdapter from './BaseCodeAuthAdapter';
    +class LinkedInAdapter extends BaseAuthCodeAdapter {
    +  constructor() {
    +    super('LinkedIn');
    +  }
    +  async getUserFromAccessToken(access_token, authData) {
    +    const response = await fetch('https://api.linkedin.com/v2/me', {
    +      headers: {
    +        Authorization: `Bearer ${access_token}`,
    +        'x-li-format': 'json',
    +        'x-li-src': authData?.is_mobile_sdk ? 'msdk' : undefined,
    +      },
    +    });
    +
    +    if (!response.ok) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'LinkedIn API request failed.');
         }
    -    throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Linkedin auth is invalid for this user.');
    -  });
    -}
     
    -// Returns a promise that fulfills iff this app id is valid.
    -function validateAppId() {
    -  return Promise.resolve();
    -}
    +    return response.json();
    +  }
    +
    +  async getAccessTokenFromCode(authData) {
    +    const response = await fetch('https://www.linkedin.com/oauth/v2/accessToken', {
    +      method: 'POST',
    +      headers: {
    +        'Content-Type': 'application/x-www-form-urlencoded',
    +      },
    +      body: new URLSearchParams({
    +        grant_type: 'authorization_code',
    +        code: authData.code,
    +        redirect_uri: authData.redirect_uri,
    +        client_id: this.clientId,
    +        client_secret: this.clientSecret,
    +      }),
    +    });
     
    -// A promisey wrapper for api requests
    -function request(path, access_token, is_mobile_sdk) {
    -  var headers = {
    -    Authorization: 'Bearer ' + access_token,
    -    'x-li-format': 'json',
    -  };
    +    if (!response.ok) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'LinkedIn API request failed.');
    +    }
     
    -  if (is_mobile_sdk) {
    -    headers['x-li-src'] = 'msdk';
    +    const json = await response.json();
    +    return json.access_token;
       }
    -  return httpsRequest.get({
    -    host: 'api.linkedin.com',
    -    path: '/v2/' + path,
    -    headers: headers,
    -  });
     }
     
    -module.exports = {
    -  validateAppId: validateAppId,
    -  validateAuthData: validateAuthData,
    -};
    +export default new LinkedInAdapter();
    
  • src/Adapters/Auth/meetup.js+15 6 modified
    @@ -1,15 +1,24 @@
     // Helper functions for accessing the meetup API.
     var Parse = require('parse/node').Parse;
     const httpsRequest = require('./httpsRequest');
    +import Config from '../../Config';
    +import Deprecator from '../../Deprecator/Deprecator';
     
     // Returns a promise that fulfills iff this user id is valid.
    -function validateAuthData(authData) {
    -  return request('member/self', authData.access_token).then(data => {
    -    if (data && data.id == authData.id) {
    -      return;
    -    }
    +async function validateAuthData(authData) {
    +  const config = Config.get(Parse.applicationId);
    +  const meetupConfig = config.auth.meetup;
    +
    +  Deprecator.logRuntimeDeprecation({ usage: 'meetup adapter' });
    +
    +  if (!meetupConfig?.enableInsecureAuth) {
    +    throw new Parse.Error('Meetup only works with enableInsecureAuth: true');
    +  }
    +
    +  const data = await request('member/self', authData.access_token);
    +  if (data?.id !== authData.id) {
         throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Meetup auth is invalid for this user.');
    -  });
    +  }
     }
     
     // Returns a promise that fulfills iff this app id is valid.
    
  • src/Adapters/Auth/mfa.js+79 1 modified
    @@ -1,3 +1,81 @@
    +/**
    + * Parse Server authentication adapter for Multi-Factor Authentication (MFA).
    + *
    + * @class MFAAdapter
    + * @param {Object} options - The adapter options.
    + * @param {Array<String>} options.options - Supported MFA methods. Must include `"SMS"` or `"TOTP"`.
    + * @param {Number} [options.digits=6] - The number of digits for the one-time password (OTP). Must be between 4 and 10.
    + * @param {Number} [options.period=30] - The validity period of the OTP in seconds. Must be greater than 10.
    + * @param {String} [options.algorithm="SHA1"] - The algorithm used for TOTP generation. Defaults to `"SHA1"`.
    + * @param {Function} [options.sendSMS] - A callback function for sending SMS OTPs. Required if `"SMS"` is included in `options`.
    + *
    + * @description
    + * ## Parse Server Configuration
    + * To configure Parse Server for MFA, use the following structure:
    + * ```javascript
    + * {
    + *   auth: {
    + *     mfa: {
    + *       options: ["SMS", "TOTP"],
    + *       digits: 6,
    + *       period: 30,
    + *       algorithm: "SHA1",
    + *       sendSMS: (token, mobile) => {
    + *         // Send the SMS using your preferred SMS provider.
    + *         console.log(`Sending SMS to ${mobile} with token: ${token}`);
    + *       }
    + *     }
    + *   }
    + * }
    + * ```
    + *
    + * ## MFA Methods
    + * - **SMS**:
    + *   - Requires a valid mobile number.
    + *   - Sends a one-time password (OTP) via SMS for login or verification.
    + *   - Uses the `sendSMS` callback for sending the OTP.
    + *
    + * - **TOTP**:
    + *   - Requires a secret key for setup.
    + *   - Validates the user's OTP against a time-based one-time password (TOTP) generated using the secret key.
    + *   - Supports configurable digits, period, and algorithm for TOTP generation.
    + *
    + * ## MFA Payload
    + * The adapter requires the following `authData` fields:
    + * - **For SMS-based MFA**:
    + *   - `mobile`: The user's mobile number (required for setup).
    + *   - `token`: The OTP provided by the user for login or verification.
    + * - **For TOTP-based MFA**:
    + *   - `secret`: The TOTP secret key for the user (required for setup).
    + *   - `token`: The OTP provided by the user for login or verification.
    + *
    + * ## Example Payloads
    + * ### SMS Setup Payload
    + * ```json
    + * {
    + *   "mobile": "+1234567890"
    + * }
    + * ```
    + *
    + * ### TOTP Setup Payload
    + * ```json
    + * {
    + *   "secret": "BASE32ENCODEDSECRET",
    + *   "token": "123456"
    + * }
    + * ```
    + *
    + * ### Login Payload
    + * ```json
    + * {
    + *   "token": "123456"
    + * }
    + * ```
    + *
    + * @see {@link https://en.wikipedia.org/wiki/Time-based_One-Time_Password_algorithm Time-based One-Time Password Algorithm (TOTP)}
    + * @see {@link https://tools.ietf.org/html/rfc6238 RFC 6238: TOTP: Time-Based One-Time Password Algorithm}
    + */
    +
     import { TOTP, Secret } from 'otpauth';
     import { randomString } from '../../cryptoUtils';
     import AuthAdapter from './AuthAdapter';
    @@ -113,7 +191,7 @@ class MFAAdapter extends AuthAdapter {
         }
         throw 'Invalid MFA data';
       }
    -  afterFind(req, authData) {
    +  afterFind(authData, options, req) {
         if (req.master) {
           return;
         }
    
  • src/Adapters/Auth/microsoft.js+103 31 modified
    @@ -1,37 +1,109 @@
    -// Helper functions for accessing the microsoft graph API.
    -var Parse = require('parse/node').Parse;
    -const httpsRequest = require('./httpsRequest');
    +/**
    + * Parse Server authentication adapter for Microsoft.
    + *
    + * @class MicrosoftAdapter
    + * @param {Object} options - The adapter configuration options.
    + * @param {string} options.clientId - Your Microsoft App Client ID. Required for secure authentication.
    + * @param {string} options.clientSecret - Your Microsoft App Client Secret. Required for secure authentication.
    + * @param {boolean} [options.enableInsecureAuth=false] - **[DEPRECATED]** Enable insecure authentication (not recommended).
    + *
    + * @description
    + * ## Parse Server Configuration
    + * To configure Parse Server for Microsoft authentication, use the following structure:
    + * ### Secure Configuration
    + * ```json
    + * {
    + *   "auth": {
    + *     "microsoft": {
    + *       "clientId": "your-client-id",
    + *       "clientSecret": "your-client-secret"
    + *     }
    + *   }
    + * }
    + * ```
    + * ### Insecure Configuration (Not Recommended)
    + * ```json
    + * {
    + *   "auth": {
    + *     "microsoft": {
    + *       "enableInsecureAuth": true
    + *     }
    + *   }
    + * }
    + * ```
    + *
    + * The adapter requires the following `authData` fields:
    + * - **Secure Authentication**: `code`, `redirect_uri`.
    + * - **Insecure Authentication (Not Recommended)**: `id`, `access_token`.
    + *
    + * ## Auth Payloads
    + * ### Secure Authentication Payload
    + * ```json
    + * {
    + *   "microsoft": {
    + *     "code": "lmn789opq012rst345uvw",
    + *     "redirect_uri": "https://your-redirect-uri.com/callback"
    + *   }
    + * }
    + * ```
    + * ### Insecure Authentication Payload (Not Recommended)
    + * ```json
    + * {
    + *   "microsoft": {
    + *     "id": "7654321",
    + *     "access_token": "AQXNnd2hIT6z9bHFzZz2Kp1ghiMz_RtyuvwXYZ123abc"
    + *   }
    + * }
    + * ```
    + *
    + * ## Notes
    + * - Secure authentication exchanges the `code` and `redirect_uri` provided by the client for an access token using Microsoft's OAuth API.
    + * - **Insecure authentication** validates the user ID and access token directly, bypassing OAuth flows (not recommended). This method is deprecated and may be removed in future versions.
    + *
    + * @see {@link https://docs.microsoft.com/en-us/graph/auth/auth-concepts Microsoft Authentication Documentation}
    + */
     
    -// Returns a promise that fulfills if this user mail is valid.
    -function validateAuthData(authData) {
    -  return request('me', authData.access_token).then(response => {
    -    if (response && response.id && response.id == authData.id) {
    -      return;
    +import BaseAuthCodeAdapter from './BaseCodeAuthAdapter';
    +class MicrosoftAdapter extends BaseAuthCodeAdapter {
    +  constructor() {
    +    super('Microsoft');
    +  }
    +  async getUserFromAccessToken(access_token) {
    +    const userResponse = await fetch('https://graph.microsoft.com/v1.0/me', {
    +      headers: {
    +        Authorization: 'Bearer ' + access_token,
    +      },
    +    });
    +
    +    if (!userResponse.ok) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Microsoft API request failed.');
         }
    -    throw new Parse.Error(
    -      Parse.Error.OBJECT_NOT_FOUND,
    -      'Microsoft Graph auth is invalid for this user.'
    -    );
    -  });
    -}
     
    -// Returns a promise that fulfills if this app id is valid.
    -function validateAppId() {
    -  return Promise.resolve();
    -}
    +    return userResponse.json();
    +  }
    +
    +  async getAccessTokenFromCode(authData) {
    +    const response = await fetch('https://login.microsoftonline.com/common/oauth2/v2.0/token', {
    +      method: 'POST',
    +      headers: {
    +        'Content-Type': 'application/x-www-form-urlencoded',
    +      },
    +      body: new URLSearchParams({
    +        client_id: this.clientId,
    +        client_secret: this.clientSecret,
    +        grant_type: 'authorization_code',
    +        redirect_uri: authData.redirect_uri,
    +        code: authData.code,
    +      }),
    +    });
    +
    +    if (!response.ok) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Microsoft API request failed.');
    +    }
     
    -// A promisey wrapper for api requests
    -function request(path, access_token) {
    -  return httpsRequest.get({
    -    host: 'graph.microsoft.com',
    -    path: '/v1.0/' + path,
    -    headers: {
    -      Authorization: 'Bearer ' + access_token,
    -    },
    -  });
    +    const json = await response.json();
    +    return json.access_token;
    +  }
     }
     
    -module.exports = {
    -  validateAppId: validateAppId,
    -  validateAuthData: validateAuthData,
    -};
    +export default new MicrosoftAdapter();
    
  • src/Adapters/Auth/oauth2.js+101 117 modified
    @@ -1,137 +1,121 @@
    -/*
    - * This auth adapter is based on the OAuth 2.0 Token Introspection specification.
    - * See RFC 7662 for details (https://tools.ietf.org/html/rfc7662).
    - * It's purpose is to validate OAuth2 access tokens using the OAuth2 provider's
    - * token introspection endpoint (if implemented by the provider).
    +/**
    + * Parse Server authentication adapter for OAuth2 Token Introspection.
      *
    - * The adapter accepts the following config parameters:
    + * @class OAuth2Adapter
    + * @param {Object} options - The adapter configuration options.
    + * @param {string} options.tokenIntrospectionEndpointUrl - The URL of the token introspection endpoint. Required.
    + * @param {boolean} options.oauth2 - Indicates that the request should be handled by the OAuth2 adapter. Required.
    + * @param {string} [options.useridField] - The field in the introspection response that contains the user ID. Optional.
    + * @param {string} [options.appidField] - The field in the introspection response that contains the app ID. Optional.
    + * @param {string[]} [options.appIds] - List of allowed app IDs. Required if `appidField` is defined.
    + * @param {string} [options.authorizationHeader] - The Authorization header value for the introspection request. Optional.
      *
    - * 1. "tokenIntrospectionEndpointUrl" (string, required)
    - *      The URL of the token introspection endpoint of the OAuth2 provider that
    - *      issued the access token to the client that is to be validated.
    - *
    - * 2. "useridField" (string, optional)
    - *      The name of the field in the token introspection response that contains
    - *      the userid. If specified, it will be used to verify the value of the "id"
    - *      field in the "authData" JSON that is coming from the client.
    - *      This can be the "aud" (i.e. audience), the "sub" (i.e. subject) or the
    - *      "username" field in the introspection response, but since only the
    - *      "active" field is required and all other reponse fields are optional
    - *      in the RFC, it has to be optional in this adapter as well.
    - *      Default: - (undefined)
    - *
    - * 3. "appidField" (string, optional)
    - *      The name of the field in the token introspection response that contains
    - *      the appId of the client. If specified, it will be used to verify it's
    - *      value against the set of appIds in the adapter config. The concept of
    - *      appIds comes from the two major social login providers
    - *      (Google and Facebook). They have not yet implemented the token
    - *      introspection endpoint, but the concept can be valid for any OAuth2
    - *      provider.
    - *      Default: - (undefined)
    - *
    - * 4. "appIds" (array of strings, required if appidField is defined)
    - *      A set of appIds that are used to restrict accepted access tokens based
    - *      on a specific field's value in the token introspection response.
    - *      Default: - (undefined)
    - *
    - * 5. "authorizationHeader" (string, optional)
    - *      The value of the "Authorization" HTTP header in requests sent to the
    - *      introspection endpoint. It must contain the raw value.
    - *      Thus if HTTP Basic authorization is to be used, it must contain the
    - *      "Basic" string, followed by whitespace, then by the base64 encoded
    - *      version of the concatenated <username> + ":" + <password> string.
    - *      Eg. "Basic dXNlcm5hbWU6cGFzc3dvcmQ="
    + * @description
    + * ## Parse Server Configuration
    + * To configure Parse Server for OAuth2 Token Introspection, use the following structure:
    + * ```json
    + * {
    + *   "auth": {
    + *     "oauth2Provider": {
    + *       "tokenIntrospectionEndpointUrl": "https://provider.com/introspect",
    + *       "useridField": "sub",
    + *       "appidField": "aud",
    + *       "appIds": ["my-app-id"],
    + *       "authorizationHeader": "Basic dXNlcm5hbWU6cGFzc3dvcmQ=",
    + *       "oauth2": true
    + *     }
    + *   }
    + * }
    + * ```
      *
    - * The adapter expects requests with the following authData JSON:
    + * The adapter requires the following `authData` fields:
    + * - `id`: The user ID provided by the client.
    + * - `access_token`: The access token provided by the client.
      *
    + * ## Auth Payload
    + * ### Example Auth Payload
    + * ```json
      * {
    - *   "someadapter": {
    - *     "id": "user's OAuth2 provider-specific id as a string",
    - *     "access_token": "an authorized OAuth2 access token for the user",
    + *   "oauth2": {
    + *     "id": "user-id",
    + *     "access_token": "access-token"
      *   }
      * }
    + * ```
    + *
    + * ## Notes
    + * - `tokenIntrospectionEndpointUrl` is mandatory and should point to a valid OAuth2 provider's introspection endpoint.
    + * - If `appidField` is defined, `appIds` must also be specified to validate the app ID in the introspection response.
    + * - `authorizationHeader` can be used to authenticate requests to the token introspection endpoint.
    + *
    + * @see {@link https://datatracker.ietf.org/doc/html/rfc7662 OAuth 2.0 Token Introspection Specification}
      */
     
    -const Parse = require('parse/node').Parse;
    -const querystring = require('querystring');
    -const httpsRequest = require('./httpsRequest');
    -
    -const INVALID_ACCESS = 'OAuth2 access token is invalid for this user.';
    -const INVALID_ACCESS_APPID =
    -  "OAuth2: the access_token's appID is empty or is not in the list of permitted appIDs in the auth configuration.";
    -const MISSING_APPIDS =
    -  'OAuth2 configuration is missing the client app IDs ("appIds" config parameter).';
    -const MISSING_URL = 'OAuth2 token introspection endpoint URL is missing from configuration!';
    -
    -// Returns a promise that fulfills if this user id is valid.
    -function validateAuthData(authData, options) {
    -  return requestTokenInfo(options, authData.access_token).then(response => {
    -    if (
    -      !response ||
    -      !response.active ||
    -      (options.useridField && authData.id !== response[options.useridField])
    -    ) {
    -      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, INVALID_ACCESS);
    +
    +import AuthAdapter from './AuthAdapter';
    +
    +class OAuth2Adapter extends AuthAdapter {
    +  validateOptions(options) {
    +    super.validateOptions(options);
    +
    +    if (!options.tokenIntrospectionEndpointUrl) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 token introspection endpoint URL is missing.');
    +    }
    +    if (options.appidField && !options.appIds?.length) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 configuration is missing app IDs.');
         }
    -  });
    -}
     
    -function validateAppId(appIds, authData, options) {
    -  if (!options || !options.appidField) {
    -    return Promise.resolve();
    +    this.tokenIntrospectionEndpointUrl = options.tokenIntrospectionEndpointUrl;
    +    this.useridField = options.useridField;
    +    this.appidField = options.appidField;
    +    this.appIds = options.appIds;
    +    this.authorizationHeader = options.authorizationHeader;
       }
    -  if (!appIds || appIds.length === 0) {
    -    throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, MISSING_APPIDS);
    -  }
    -  return requestTokenInfo(options, authData.access_token).then(response => {
    -    if (!response || !response.active) {
    -      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, INVALID_ACCESS);
    +
    +  async validateAppId(authData) {
    +    if (!this.appidField) {
    +      return;
         }
    -    const appidField = options.appidField;
    -    if (!response[appidField]) {
    -      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, INVALID_ACCESS_APPID);
    +
    +    const response = await this.requestTokenInfo(authData.access_token);
    +
    +    const appIdFieldValue = response[this.appidField];
    +    const isValidAppId = Array.isArray(appIdFieldValue)
    +      ? appIdFieldValue.some(appId => this.appIds.includes(appId))
    +      : this.appIds.includes(appIdFieldValue);
    +
    +    if (!isValidAppId) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2: Invalid app ID.');
         }
    -    const responseValue = response[appidField];
    -    if (!Array.isArray(responseValue) && appIds.includes(responseValue)) {
    -      return;
    -    } else if (
    -      Array.isArray(responseValue) &&
    -      responseValue.some(appId => appIds.includes(appId))
    -    ) {
    -      return;
    -    } else {
    -      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, INVALID_ACCESS_APPID);
    +  }
    +
    +  async validateAuthData(authData) {
    +    const response = await this.requestTokenInfo(authData.access_token);
    +
    +    if (!response.active || (this.useridField && authData.id !== response[this.useridField])) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 access token is invalid for this user.');
         }
    -  });
    -}
     
    -// A promise wrapper for requests to the OAuth2 token introspection endpoint.
    -function requestTokenInfo(options, access_token) {
    -  if (!options || !options.tokenIntrospectionEndpointUrl) {
    -    throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, MISSING_URL);
    +    return {};
       }
    -  const parsedUrl = new URL(options.tokenIntrospectionEndpointUrl);
    -  const postData = querystring.stringify({
    -    token: access_token,
    -  });
    -  const headers = {
    -    'Content-Type': 'application/x-www-form-urlencoded',
    -    'Content-Length': Buffer.byteLength(postData),
    -  };
    -  if (options.authorizationHeader) {
    -    headers['Authorization'] = options.authorizationHeader;
    +
    +  async requestTokenInfo(accessToken) {
    +    const response = await fetch(this.tokenIntrospectionEndpointUrl, {
    +      method: 'POST',
    +      headers: {
    +        'Content-Type': 'application/x-www-form-urlencoded',
    +        ...(this.authorizationHeader && { Authorization: this.authorizationHeader })
    +      },
    +      body: new URLSearchParams({ token: accessToken })
    +    });
    +
    +    if (!response.ok) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 token introspection request failed.');
    +    }
    +
    +    return response.json();
       }
    -  const postOptions = {
    -    hostname: parsedUrl.hostname,
    -    path: parsedUrl.pathname,
    -    method: 'POST',
    -    headers: headers,
    -  };
    -  return httpsRequest.request(postOptions, postData);
     }
     
    -module.exports = {
    -  validateAppId: validateAppId,
    -  validateAuthData: validateAuthData,
    -};
    +export default new OAuth2Adapter();
    +
    
  • src/Adapters/Auth/phantauth.js+15 6 modified
    @@ -7,15 +7,24 @@
     
     const { Parse } = require('parse/node');
     const httpsRequest = require('./httpsRequest');
    +import Config from '../../Config';
    +import Deprecator from '../../Deprecator/Deprecator';
     
     // Returns a promise that fulfills if this user id is valid.
    -function validateAuthData(authData) {
    -  return request('auth/userinfo', authData.access_token).then(data => {
    -    if (data && data.sub == authData.id) {
    -      return;
    -    }
    +async function validateAuthData(authData) {
    +  const config = Config.get(Parse.applicationId);
    +
    +  Deprecator.logRuntimeDeprecation({ usage: 'phantauth adapter' });
    +
    +  const phantauthConfig = config.auth.phantauth;
    +  if (!phantauthConfig?.enableInsecureAuth) {
    +    throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'PhantAuth only works with enableInsecureAuth: true');
    +  }
    +
    +  const data = await request('auth/userinfo', authData.access_token);
    +  if (data?.sub !== authData.id) {
         throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'PhantAuth auth is invalid for this user.');
    -  });
    +  }
     }
     
     // Returns a promise that fulfills if this app id is valid.
    
  • src/Adapters/Auth/qq.js+105 34 modified
    @@ -1,41 +1,112 @@
    -// Helper functions for accessing the qq Graph API.
    -const httpsRequest = require('./httpsRequest');
    -var Parse = require('parse/node').Parse;
    -
    -// Returns a promise that fulfills iff this user id is valid.
    -function validateAuthData(authData) {
    -  return graphRequest('me?access_token=' + authData.access_token).then(function (data) {
    -    if (data && data.openid == authData.id) {
    -      return;
    +/**
    + * Parse Server authentication adapter for QQ.
    + *
    + * @class QqAdapter
    + * @param {Object} options - The adapter configuration options.
    + * @param {string} options.clientId - Your QQ App ID. Required for secure authentication.
    + * @param {string} options.clientSecret - Your QQ App Secret. Required for secure authentication.
    + * @param {boolean} [options.enableInsecureAuth=false] - **[DEPRECATED]** Enable insecure authentication (not recommended).
    + *
    + * @description
    + * ## Parse Server Configuration
    + * To configure Parse Server for QQ authentication, use the following structure:
    + * ### Secure Configuration
    + * ```json
    + * {
    + *   "auth": {
    + *     "qq": {
    + *       "clientId": "your-app-id",
    + *       "clientSecret": "your-app-secret"
    + *     }
    + *   }
    + * }
    + * ```
    + * ### Insecure Configuration (Not Recommended)
    + * ```json
    + * {
    + *   "auth": {
    + *     "qq": {
    + *       "enableInsecureAuth": true
    + *     }
    + *   }
    + * }
    + * ```
    + *
    + * The adapter requires the following `authData` fields:
    + * - **Secure Authentication**: `code`, `redirect_uri`.
    + * - **Insecure Authentication (Not Recommended)**: `id`, `access_token`.
    + *
    + * ## Auth Payloads
    + * ### Secure Authentication Payload
    + * ```json
    + * {
    + *   "qq": {
    + *     "code": "abcd1234",
    + *     "redirect_uri": "https://your-redirect-uri.com/callback"
    + *   }
    + * }
    + * ```
    + * ### Insecure Authentication Payload (Not Recommended)
    + * ```json
    + * {
    + *   "qq": {
    + *     "id": "1234567",
    + *     "access_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    + *   }
    + * }
    + * ```
    + *
    + * ## Notes
    + * - Secure authentication exchanges the `code` and `redirect_uri` provided by the client for an access token using QQ's OAuth API.
    + * - **Insecure authentication** validates the `id` and `access_token` directly, bypassing OAuth flows. This approach is not recommended and may be deprecated in future versions.
    + *
    + * @see {@link https://wiki.connect.qq.com/ QQ Authentication Documentation}
    + */
    +
    +import BaseAuthCodeAdapter from './BaseCodeAuthAdapter';
    +class QqAdapter extends BaseAuthCodeAdapter {
    +  constructor() {
    +    super('qq');
    +  }
    +
    +  async getUserFromAccessToken(access_token) {
    +    const response = await fetch('https://graph.qq.com/oauth2.0/me', {
    +      headers: {
    +        Authorization: `Bearer ${access_token}`,
    +      },
    +    });
    +
    +    if (!response.ok) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'qq API request failed.');
         }
    -    throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'qq auth is invalid for this user.');
    -  });
    -}
     
    -// Returns a promise that fulfills if this app id is valid.
    -function validateAppId() {
    -  return Promise.resolve();
    -}
    +    const data = await response.text();
    +    return this.parseResponseData(data);
    +  }
     
    -// A promisey wrapper for qq graph requests.
    -function graphRequest(path) {
    -  return httpsRequest.get('https://graph.qq.com/oauth2.0/' + path, true).then(data => {
    -    return parseResponseData(data);
    -  });
    -}
    +  async getAccessTokenFromCode(authData) {
    +    const response = await fetch('https://graph.qq.com/oauth2.0/token', {
    +      method: 'GET',
    +      headers: {
    +        'Content-Type': 'application/x-www-form-urlencoded',
    +      },
    +      body: new URLSearchParams({
    +        grant_type: 'authorization_code',
    +        client_id: this.clientId,
    +        client_secret: this.clientSecret,
    +        redirect_uri: authData.redirect_uri,
    +        code: authData.code,
    +      }).toString(),
    +    });
    +
    +    if (!response.ok) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'qq API request failed.');
    +    }
     
    -function parseResponseData(data) {
    -  const starPos = data.indexOf('(');
    -  const endPos = data.indexOf(')');
    -  if (starPos == -1 || endPos == -1) {
    -    throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'qq auth is invalid for this user.');
    +    const text = await response.text();
    +    const data = this.parseResponseData(text);
    +    return data.access_token;
       }
    -  data = data.substring(starPos + 1, endPos - 1);
    -  return JSON.parse(data);
     }
     
    -module.exports = {
    -  validateAppId,
    -  validateAuthData,
    -  parseResponseData,
    -};
    +export default new QqAdapter();
    
  • src/Adapters/Auth/spotify.js+112 38 modified
    @@ -1,44 +1,118 @@
    -// Helper functions for accessing the Spotify API.
    -const httpsRequest = require('./httpsRequest');
    -var Parse = require('parse/node').Parse;
    -
    -// Returns a promise that fulfills iff this user id is valid.
    -function validateAuthData(authData) {
    -  return request('me', authData.access_token).then(data => {
    -    if (data && data.id == authData.id) {
    -      return;
    -    }
    -    throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Spotify auth is invalid for this user.');
    -  });
    -}
    +/**
    + * Parse Server authentication adapter for Spotify.
    + *
    + * @class SpotifyAdapter
    + * @param {Object} options - The adapter configuration options.
    + * @param {string} options.clientId - Your Spotify application's Client ID. Required for secure authentication.
    + * @param {boolean} [options.enableInsecureAuth=false] - **[DEPRECATED]** Enable insecure authentication (not recommended).
    + *
    + * @description
    + * ## Parse Server Configuration
    + * To configure Parse Server for Spotify authentication, use the following structure:
    + * ### Secure Configuration
    + * ```json
    + * {
    + *   "auth": {
    + *     "spotify": {
    + *       "clientId": "your-client-id"
    + *     }
    + *   }
    + * }
    + * ```
    + * ### Insecure Configuration (Not Recommended)
    + * ```json
    + * {
    + *   "auth": {
    + *     "spotify": {
    + *       "enableInsecureAuth": true
    + *     }
    + *   }
    + * }
    + * ```
    + *
    + * The adapter requires the following `authData` fields:
    + * - **Secure Authentication**: `code`, `redirect_uri`, and `code_verifier`.
    + * - **Insecure Authentication (Not Recommended)**: `id`, `access_token`.
    + *
    + * ## Auth Payloads
    + * ### Secure Authentication Payload
    + * ```json
    + * {
    + *   "spotify": {
    + *     "code": "abc123def456ghi789",
    + *     "redirect_uri": "https://example.com/callback",
    + *     "code_verifier": "secure-code-verifier"
    + *   }
    + * }
    + * ```
    + * ### Insecure Authentication Payload (Not Recommended)
    + * ```json
    + * {
    + *   "spotify": {
    + *     "id": "1234567",
    + *     "access_token": "abc123def456ghi789"
    + *   }
    + * }
    + * ```
    + *
    + * ## Notes
    + * - `enableInsecureAuth` is **not recommended** and bypasses secure flows by validating the user ID and access token directly. This method is not suitable for production environments and may be removed in future versions.
    + * - Secure authentication exchanges the `code` provided by the client for an access token using Spotify's OAuth API. This method ensures greater security and is the recommended approach.
    + *
    + * @see {@link https://developer.spotify.com/documentation/web-api/tutorials/getting-started Spotify OAuth Documentation}
    + */
     
    -// Returns a promise that fulfills if this app id is valid.
    -async function validateAppId(appIds, authData) {
    -  const access_token = authData.access_token;
    -  if (!Array.isArray(appIds)) {
    -    throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'appIds must be an array.');
    +import BaseAuthCodeAdapter from './BaseCodeAuthAdapter';
    +class SpotifyAdapter extends BaseAuthCodeAdapter {
    +  constructor() {
    +    super('spotify');
       }
    -  if (!appIds.length) {
    -    throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Spotify auth is not configured.');
    -  }
    -  const data = await request('me', access_token);
    -  if (!data || !appIds.includes(data.id)) {
    -    throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Spotify auth is invalid for this user.');
    +
    +  async getUserFromAccessToken(access_token) {
    +    const response = await fetch('https://api.spotify.com/v1/me', {
    +      headers: {
    +        Authorization: 'Bearer ' + access_token,
    +      },
    +    });
    +
    +    if (!response.ok) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Spotify API request failed.');
    +    }
    +
    +    const user = await response.json();
    +    return {
    +      id: user.id,
    +    };
       }
    -}
     
    -// A promisey wrapper for Spotify API requests.
    -function request(path, access_token) {
    -  return httpsRequest.get({
    -    host: 'api.spotify.com',
    -    path: '/v1/' + path,
    -    headers: {
    -      Authorization: 'Bearer ' + access_token,
    -    },
    -  });
    +  async getAccessTokenFromCode(authData) {
    +    if (!authData.code || !authData.redirect_uri || !authData.code_verifier) {
    +      throw new Parse.Error(
    +        Parse.Error.OBJECT_NOT_FOUND,
    +        'Spotify auth configuration authData.code and/or authData.redirect_uri and/or authData.code_verifier.'
    +      );
    +    }
    +
    +    const response = await fetch('https://accounts.spotify.com/api/token', {
    +      method: 'POST',
    +      headers: {
    +        'Content-Type': 'application/x-www-form-urlencoded',
    +      },
    +      body: new URLSearchParams({
    +        grant_type: 'authorization_code',
    +        code: authData.code,
    +        redirect_uri: authData.redirect_uri,
    +        code_verifier: authData.code_verifier,
    +        client_id: this.clientId,
    +      }),
    +    });
    +
    +    if (!response.ok) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Spotify API request failed.');
    +    }
    +
    +    return response.json();
    +  }
     }
     
    -module.exports = {
    -  validateAppId: validateAppId,
    -  validateAuthData: validateAuthData,
    -};
    +export default new SpotifyAdapter();
    
  • src/Adapters/Auth/twitter.js+233 40 modified
    @@ -1,51 +1,244 @@
    -// Helper functions for accessing the twitter API.
    -var OAuth = require('./OAuth1Client');
    -var Parse = require('parse/node').Parse;
    -
    -// Returns a promise that fulfills iff this user id is valid.
    -function validateAuthData(authData, options) {
    -  if (!options) {
    -    throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Twitter auth configuration missing');
    -  }
    -  options = handleMultipleConfigurations(authData, options);
    -  var client = new OAuth(options);
    -  client.host = 'api.twitter.com';
    -  client.auth_token = authData.auth_token;
    -  client.auth_token_secret = authData.auth_token_secret;
    -
    -  return client.get('/1.1/account/verify_credentials.json').then(data => {
    -    if (data && data.id_str == '' + authData.id) {
    +/**
    + * Parse Server authentication adapter for Twitter.
    + *
    + * @class TwitterAdapter
    + * @param {Object} options - The adapter configuration options.
    + * @param {string} options.consumerKey - The Twitter App Consumer Key. Required for secure authentication.
    + * @param {string} options.consumerSecret - The Twitter App Consumer Secret. Required for secure authentication.
    + * @param {boolean} [options.enableInsecureAuth=false] - **[DEPRECATED]** Enable insecure authentication (not recommended).
    + *
    + * @description
    + * ## Parse Server Configuration
    + * To configure Parse Server for Twitter authentication, use the following structure:
    + * ### Secure Configuration
    + * ```json
    + * {
    + *   "auth": {
    + *     "twitter": {
    + *       "consumerKey": "your-consumer-key",
    + *       "consumerSecret": "your-consumer-secret"
    + *     }
    + *   }
    + * }
    + * ```
    + * ### Insecure Configuration (Not Recommended)
    + * ```json
    + * {
    + *   "auth": {
    + *     "twitter": {
    + *       "enableInsecureAuth": true
    + *     }
    + *   }
    + * }
    + * ```
    + *
    + * The adapter requires the following `authData` fields:
    + * - **Secure Authentication**: `oauth_token`, `oauth_verifier`.
    + * - **Insecure Authentication (Not Recommended)**: `id`, `oauth_token`, `oauth_token_secret`.
    + *
    + * ## Auth Payloads
    + * ### Secure Authentication Payload
    + * ```json
    + * {
    + *   "twitter": {
    + *     "oauth_token": "1234567890-abc123def456",
    + *     "oauth_verifier": "abc123def456"
    + *   }
    + * }
    + * ```
    + *
    + * ### Insecure Authentication Payload (Not Recommended)
    + * ```json
    + * {
    + *   "twitter": {
    + *     "id": "1234567890",
    + *     "oauth_token": "1234567890-abc123def456",
    + *     "oauth_token_secret": "1234567890-abc123def456"
    + *   }
    + * }
    + * ```
    + *
    + * ## Notes
    + * - **Deprecation Notice**: `enableInsecureAuth` and insecure fields (`id`, `oauth_token_secret`) are **deprecated** and may be removed in future versions. Use secure authentication with `consumerKey` and `consumerSecret`.
    + * - Secure authentication exchanges the `oauth_token` and `oauth_verifier` provided by the client for an access token using Twitter's OAuth API.
    + *
    + * @see {@link https://developer.twitter.com/en/docs/authentication/oauth-1-0a Twitter OAuth Documentation}
    + */
    +
    +import Config from '../../Config';
    +import querystring from 'querystring';
    +import AuthAdapter from './AuthAdapter';
    +
    +class TwitterAuthAdapter extends AuthAdapter {
    +  validateOptions(options) {
    +    if (!options) {
    +      throw new Error('Twitter auth options are required.');
    +    }
    +
    +    this.enableInsecureAuth = options.enableInsecureAuth;
    +
    +    if (!this.enableInsecureAuth && (!options.consumer_key || !options.consumer_secret)) {
    +      throw new Error('Consumer key and secret are required for secure Twitter auth.');
    +    }
    +  }
    +
    +  async validateAuthData(authData, options) {
    +    const config = Config.get(Parse.applicationId);
    +    const twitterConfig = config.auth.twitter;
    +
    +    if (this.enableInsecureAuth && twitterConfig && config.enableInsecureAuthAdapters) {
    +      return this.validateInsecureAuth(authData, options);
    +    }
    +
    +    if (!options.consumer_key || !options.consumer_secret) {
    +      throw new Parse.Error(
    +        Parse.Error.OBJECT_NOT_FOUND,
    +        'Twitter auth configuration missing consumer_key and/or consumer_secret.'
    +      );
    +    }
    +
    +    const accessTokenData = await this.exchangeAccessToken(authData);
    +
    +    if (accessTokenData?.oauth_token && accessTokenData?.user_id) {
    +      authData.id = accessTokenData.user_id;
    +      authData.auth_token = accessTokenData.oauth_token;
           return;
         }
    -    throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Twitter auth is invalid for this user.');
    -  });
    -}
     
    -// Returns a promise that fulfills iff this app id is valid.
    -function validateAppId() {
    -  return Promise.resolve();
    -}
    +    throw new Parse.Error(
    +      Parse.Error.OBJECT_NOT_FOUND,
    +      'Twitter auth is invalid for this user.'
    +    );
    +  }
    +
    +  async validateInsecureAuth(authData, options) {
    +    if (!authData.oauth_token || !authData.oauth_token_secret) {
    +      throw new Parse.Error(
    +        Parse.Error.OBJECT_NOT_FOUND,
    +        'Twitter insecure auth requires oauth_token and oauth_token_secret.'
    +      );
    +    }
    +
    +    options = this.handleMultipleConfigurations(authData, options);
    +
    +    const data = await this.request(authData, options);
    +    const parsedData = await data.json();
    +
    +    if (parsedData?.id === authData.id) {
    +      return;
    +    }
    +
    +    throw new Parse.Error(
    +      Parse.Error.OBJECT_NOT_FOUND,
    +      'Twitter auth is invalid for this user.'
    +    );
    +  }
     
    -function handleMultipleConfigurations(authData, options) {
    -  if (Array.isArray(options)) {
    -    const consumer_key = authData.consumer_key;
    -    if (!consumer_key) {
    -      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Twitter auth is invalid for this user.');
    +  async exchangeAccessToken(authData) {
    +    const accessTokenRequestOptions = {
    +      method: 'POST',
    +      headers: {
    +        'Content-Type': 'application/x-www-form-urlencoded',
    +      },
    +      body: querystring.stringify({
    +        oauth_token: authData.oauth_token,
    +        oauth_verifier: authData.oauth_verifier,
    +      }),
    +    };
    +
    +    const response = await fetch('https://api.twitter.com/oauth/access_token', accessTokenRequestOptions);
    +    if (!response.ok) {
    +      throw new Error('Failed to exchange access token.');
         }
    -    options = options.filter(option => {
    -      return option.consumer_key == consumer_key;
    +
    +    return response.json();
    +  }
    +
    +  handleMultipleConfigurations(authData, options) {
    +    if (Array.isArray(options)) {
    +      const consumer_key = authData.consumer_key;
    +
    +      if (!consumer_key) {
    +        throw new Parse.Error(
    +          Parse.Error.OBJECT_NOT_FOUND,
    +          'Twitter auth is invalid for this user.'
    +        );
    +      }
    +
    +      options = options.filter(option => option.consumer_key === consumer_key);
    +
    +      if (options.length === 0) {
    +        throw new Parse.Error(
    +          Parse.Error.OBJECT_NOT_FOUND,
    +          'Twitter auth is invalid for this user.'
    +        );
    +      }
    +
    +      return options[0];
    +    }
    +
    +    return options;
    +  }
    +
    +  async request(authData, options) {
    +    const { consumer_key, consumer_secret } = options;
    +
    +    const oauth = {
    +      consumer_key,
    +      consumer_secret,
    +      auth_token: authData.oauth_token,
    +      auth_token_secret: authData.oauth_token_secret,
    +    };
    +
    +    const url = new URL('https://api.twitter.com/2/users/me');
    +
    +    const response = await fetch(url, {
    +      headers: {
    +        Authorization: 'Bearer ' + oauth.auth_token,
    +      },
    +      body: JSON.stringify(oauth),
         });
     
    -    if (options.length == 0) {
    -      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Twitter auth is invalid for this user.');
    +    if (!response.ok) {
    +      throw new Error('Failed to fetch user data.');
    +    }
    +
    +    return response;
    +  }
    +
    +  async beforeFind(authData) {
    +    if (this.enableInsecureAuth && !authData?.code) {
    +      if (!authData?.access_token) {
    +        throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Twitter auth is invalid for this user.');
    +      }
    +
    +      const user = await this.getUserFromAccessToken(authData.access_token, authData);
    +
    +      if (user.id !== authData.id) {
    +        throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Twitter auth is invalid for this user.');
    +      }
    +
    +      return;
    +    }
    +
    +    if (!authData?.code) {
    +      throw new Parse.Error(Parse.Error.VALIDATION_ERROR, 'Twitter code is required.');
         }
    -    options = options[0];
    +
    +    const access_token = await this.exchangeAccessToken(authData);
    +    const user = await this.getUserFromAccessToken(access_token, authData);
    +
    +
    +    authData.access_token = access_token;
    +    authData.id = user.id;
    +
    +    delete authData.code;
    +    delete authData.redirect_uri;
    +  }
    +
    +  validateAppId() {
    +    return Promise.resolve();
       }
    -  return options;
     }
     
    -module.exports = {
    -  validateAppId,
    -  validateAuthData,
    -  handleMultipleConfigurations,
    -};
    +export default new TwitterAuthAdapter();
    
  • src/Adapters/Auth/vkontakte.js+23 19 modified
    @@ -4,28 +4,32 @@
     
     const httpsRequest = require('./httpsRequest');
     var Parse = require('parse/node').Parse;
    +import Config from '../../Config';
    +import Deprecator from '../../Deprecator/Deprecator';
     
     // Returns a promise that fulfills iff this user id is valid.
    -function validateAuthData(authData, params) {
    -  return vkOAuth2Request(params).then(function (response) {
    -    if (response && response.access_token) {
    -      return request(
    -        'api.vk.com',
    -        'method/users.get?access_token=' + authData.access_token + '&v=' + params.apiVersion
    -      ).then(function (response) {
    -        if (
    -          response &&
    -          response.response &&
    -          response.response.length &&
    -          response.response[0].id == authData.id
    -        ) {
    -          return;
    -        }
    -        throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Vk auth is invalid for this user.');
    -      });
    -    }
    +async function validateAuthData(authData, params) {
    +  const config = Config.get(Parse.applicationId);
    +  Deprecator.logRuntimeDeprecation({ usage: 'vkontakte adapter' });
    +
    +  const vkConfig = config.auth.vkontakte;
    +  if (!vkConfig?.enableInsecureAuth || !config.enableInsecureAuthAdapters) {
    +    throw new Parse.Error('Vk only works with enableInsecureAuth: true');
    +  }
    +
    +  const response = await vkOAuth2Request(params);
    +  if (!response?.access_token) {
         throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Vk appIds or appSecret is incorrect.');
    -  });
    +  }
    +
    +  const vkUser = await request(
    +    'api.vk.com',
    +    `method/users.get?access_token=${authData.access_token}&v=${params.apiVersion}`
    +  );
    +
    +  if (!vkUser?.response?.length || vkUser.response[0].id !== authData.id) {
    +    throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Vk auth is invalid for this user.');
    +  }
     }
     
     function vkOAuth2Request(params) {
    
  • src/Adapters/Auth/wechat.js+115 25 modified
    @@ -1,30 +1,120 @@
    -// Helper functions for accessing the WeChat Graph API.
    -const httpsRequest = require('./httpsRequest');
    -var Parse = require('parse/node').Parse;
    -
    -// Returns a promise that fulfills iff this user id is valid.
    -function validateAuthData(authData) {
    -  return graphRequest('auth?access_token=' + authData.access_token + '&openid=' + authData.id).then(
    -    function (data) {
    -      if (data.errcode == 0) {
    -        return;
    -      }
    -      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'wechat auth is invalid for this user.');
    +/**
    + * Parse Server authentication adapter for WeChat.
    + *
    + * @class WeChatAdapter
    + * @param {Object} options - The adapter options object.
    + * @param {boolean} [options.enableInsecureAuth=false] - **[DEPRECATED]** Enable insecure authentication (not recommended).
    + * @param {string} options.clientId - Your WeChat App ID.
    + * @param {string} options.clientSecret - Your WeChat App Secret.
    + *
    + * @description
    + * ## Parse Server Configuration
    + * To configure Parse Server for WeChat authentication, use the following structure:
    + * ### Secure Configuration (Recommended)
    + * ```json
    + * {
    + *   "auth": {
    + *     "wechat": {
    + *       "clientId": "your-client-id",
    + *       "clientSecret": "your-client-secret"
    + *     }
    + *   }
    + * }
    + * ```
    + * ### Insecure Configuration (Not Recommended)
    + * ```json
    + * {
    + *   "auth": {
    + *     "wechat": {
    + *       "enableInsecureAuth": true
    + *     }
    + *   }
    + * }
    + * ```
    + *
    + * The adapter requires the following `authData` fields:
    + * - **With `enableInsecureAuth` (Not Recommended)**: `id`, `access_token`.
    + * - **Without `enableInsecureAuth`**: `code`.
    + *
    + * ## Auth Payloads
    + * ### Secure Authentication Payload (Recommended)
    + * ```json
    + * {
    + *   "wechat": {
    + *     "code": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    + *   }
    + * }
    + * ```
    + * ### Insecure Authentication Payload (Not Recommended)
    + * ```json
    + * {
    + *   "wechat": {
    + *     "id": "1234567",
    + *     "access_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    + *   }
    + * }
    + * ```
    + *
    + * ## Notes
    + * - With `enableInsecureAuth`, the adapter directly validates the `id` and `access_token` sent by the client.
    + * - Without `enableInsecureAuth`, the adapter uses the `code` provided by the client to exchange for an access token via WeChat's OAuth API.
    + * - The `enableInsecureAuth` flag is **deprecated** and may be removed in future versions. Use secure authentication with the `code` field instead.
    + *
    + * @example <caption>Auth Data Example</caption>
    + * // Example authData provided by the client:
    + * const authData = {
    + *   wechat: {
    + *     code: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    + *   }
    + * };
    + *
    + * @see {@link https://developers.weixin.qq.com/doc/offiaccount/en/OA_Web_Apps/Wechat_webpage_authorization.html WeChat Authentication Documentation}
    + */
    +
    +import BaseAuthCodeAdapter from './BaseCodeAuthAdapter';
    +
    +class WeChatAdapter extends BaseAuthCodeAdapter {
    +  constructor() {
    +    super('WeChat');
    +  }
    +
    +  async getUserFromAccessToken(access_token, authData) {
    +    const response = await fetch(
    +      `https://api.weixin.qq.com/sns/auth?access_token=${access_token}&openid=${authData.id}`
    +    );
    +
    +    const data = await response.json();
    +
    +    if (!response.ok || data.errcode !== 0) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'WeChat auth is invalid for this user.');
         }
    -  );
    -}
     
    -// Returns a promise that fulfills if this app id is valid.
    -function validateAppId() {
    -  return Promise.resolve();
    -}
    +    return data;
    +  }
    +
    +  async getAccessTokenFromCode(authData) {
    +    if (!authData.code) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'WeChat auth requires a code to be sent.');
    +    }
    +
    +    const appId = this.clientId;
    +    const appSecret = this.clientSecret
    +
    +
    +    const response = await fetch(
    +      `https://api.weixin.qq.com/sns/oauth2/access_token?appid=${appId}&secret=${appSecret}&code=${authData.code}&grant_type=authorization_code`
    +    );
    +
    +    const data = await response.json();
    +
    +    if (!response.ok || data.errcode) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'WeChat auth is invalid for this user.');
    +    }
    +
    +    authData.id = data.openid;
     
    -// A promisey wrapper for WeChat graph requests.
    -function graphRequest(path) {
    -  return httpsRequest.get('https://api.weixin.qq.com/sns/' + path);
    +    return data.access_token;
    +  }
     }
     
    -module.exports = {
    -  validateAppId,
    -  validateAuthData,
    -};
    +export default new WeChatAdapter();
    
  • src/Adapters/Auth/weibo.js+144 36 modified
    @@ -1,41 +1,149 @@
    -// Helper functions for accessing the weibo Graph API.
    -var httpsRequest = require('./httpsRequest');
    -var Parse = require('parse/node').Parse;
    -var querystring = require('querystring');
    -
    -// Returns a promise that fulfills iff this user id is valid.
    -function validateAuthData(authData) {
    -  return graphRequest(authData.access_token).then(function (data) {
    -    if (data && data.uid == authData.id) {
    -      return;
    +/**
    + * Parse Server authentication adapter for Weibo.
    + *
    + * @class WeiboAdapter
    + * @param {Object} options - The adapter configuration options.
    + * @param {boolean} [options.enableInsecureAuth=false] - **[DEPRECATED]** Enable insecure authentication (not recommended).
    + * @param {string} options.clientId - Your Weibo client ID.
    + * @param {string} options.clientSecret - Your Weibo client secret.
    + *
    + * @description
    + * ## Parse Server Configuration
    + * To configure Parse Server for Weibo authentication, use the following structure:
    + * ### Secure Configuration
    + * ```json
    + * {
    + *   "auth": {
    + *     "weibo": {
    + *       "clientId": "your-client-id",
    + *       "clientSecret": "your-client-secret"
    + *     }
    + *   }
    + * }
    + * ```
    + * ### Insecure Configuration (Not Recommended)
    + * ```json
    + * {
    + *   "auth": {
    + *     "weibo": {
    + *       "enableInsecureAuth": true
    + *     }
    + *   }
    + * }
    + * ```
    + *
    + * The adapter requires the following `authData` fields:
    + * - **Secure Authentication**: `code`, `redirect_uri`.
    + * - **Insecure Authentication (Not Recommended)**: `id`, `access_token`.
    + *
    + * ## Auth Payloads
    + * ### Secure Authentication Payload
    + * ```json
    + * {
    + *   "weibo": {
    + *     "code": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    + *     "redirect_uri": "https://example.com/callback"
    + *   }
    + * }
    + * ```
    + * ### Insecure Authentication Payload (Not Recommended)
    + * ```json
    + * {
    + *   "weibo": {
    + *     "id": "1234567",
    + *     "access_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    + *   }
    + * }
    + * ```
    + *
    + * ## Notes
    + * - **Insecure Authentication**: When `enableInsecureAuth` is enabled, the adapter directly validates the `id` and `access_token` provided by the client.
    + * - **Secure Authentication**: When `enableInsecureAuth` is disabled, the adapter exchanges the `code` and `redirect_uri` for an access token using Weibo's OAuth API.
    + * - `enableInsecureAuth` is **deprecated** and may be removed in future versions. Use secure authentication with `code` and `redirect_uri`.
    + *
    + * @example <caption>Auth Data Example (Secure)</caption>
    + * const authData = {
    + *   weibo: {
    + *     code: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    + *     redirect_uri: "https://example.com/callback"
    + *   }
    + * };
    + *
    + * @example <caption>Auth Data Example (Insecure - Not Recommended)</caption>
    + * const authData = {
    + *   weibo: {
    + *     id: "1234567",
    + *     access_token: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    + *   }
    + * };
    + *
    + * @see {@link https://open.weibo.com/wiki/Oauth2/access_token Weibo Authentication Documentation}
    + */
    +
    +import BaseAuthCodeAdapter from './BaseCodeAuthAdapter';
    +import querystring from 'querystring';
    +
    +class WeiboAdapter extends BaseAuthCodeAdapter {
    +  constructor() {
    +    super('Weibo');
    +  }
    +
    +  async getUserFromAccessToken(access_token) {
    +    const postData = querystring.stringify({
    +      access_token: access_token,
    +    });
    +
    +    const response = await fetch('https://api.weibo.com/oauth2/get_token_info', {
    +      method: 'POST',
    +      headers: {
    +        'Content-Type': 'application/x-www-form-urlencoded',
    +      },
    +      body: postData,
    +    });
    +
    +    const data = await response.json();
    +
    +    if (!response.ok) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Weibo auth is invalid for this user.');
         }
    -    throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'weibo auth is invalid for this user.');
    -  });
    -}
     
    -// Returns a promise that fulfills if this app id is valid.
    -function validateAppId() {
    -  return Promise.resolve();
    -}
    +    return {
    +      id: data.uid,
    +    }
    +  }
    +
    +  async getAccessTokenFromCode(authData) {
    +    if (!authData?.code || !authData?.redirect_uri) {
    +      throw new Parse.Error(
    +        Parse.Error.OBJECT_NOT_FOUND,
    +        'Weibo auth requires code and redirect_uri to be sent.'
    +      );
    +    }
    +
    +    const postData = querystring.stringify({
    +      client_id: this.clientId,
    +      client_secret: this.clientSecret,
    +      grant_type: 'authorization_code',
    +      code: authData.code,
    +      redirect_uri: authData.redirect_uri,
    +    });
    +
    +    const response = await fetch('https://api.weibo.com/oauth2/access_token', {
    +      method: 'POST',
    +      headers: {
    +        'Content-Type': 'application/x-www-form-urlencoded',
    +      },
    +      body: postData,
    +    });
    +
    +    const data = await response.json();
    +
    +    if (!response.ok || data.errcode) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Weibo auth is invalid for this user.');
    +    }
     
    -// A promisey wrapper for weibo graph requests.
    -function graphRequest(access_token) {
    -  var postData = querystring.stringify({
    -    access_token: access_token,
    -  });
    -  var options = {
    -    hostname: 'api.weibo.com',
    -    path: '/oauth2/get_token_info',
    -    method: 'POST',
    -    headers: {
    -      'Content-Type': 'application/x-www-form-urlencoded',
    -      'Content-Length': Buffer.byteLength(postData),
    -    },
    -  };
    -  return httpsRequest.request(options, postData);
    +    return data.access_token;
    +  }
     }
     
    -module.exports = {
    -  validateAppId,
    -  validateAuthData,
    -};
    +export default new WeiboAdapter();
    
  • src/Auth.js+27 18 modified
    @@ -411,26 +411,35 @@ Auth.prototype._getAllRolesNamesForRoleIds = function (roleIDs, names = [], quer
         });
     };
     
    -const findUsersWithAuthData = (config, authData) => {
    +const findUsersWithAuthData = async (config, authData, beforeFind) => {
       const providers = Object.keys(authData);
    -  const query = providers
    -    .reduce((memo, provider) => {
    -      if (!authData[provider] || (authData && !authData[provider].id)) {
    -        return memo;
    +
    +  const queries = await Promise.all(
    +    providers.map(async provider => {
    +      const providerAuthData = authData[provider];
    +
    +      const adapter = config.authDataManager.getValidatorForProvider(provider)?.adapter;
    +      if (beforeFind && typeof adapter?.beforeFind === 'function') {
    +        await adapter.beforeFind(providerAuthData);
           }
    -      const queryKey = `authData.${provider}.id`;
    -      const query = {};
    -      query[queryKey] = authData[provider].id;
    -      memo.push(query);
    -      return memo;
    -    }, [])
    -    .filter(q => {
    -      return typeof q !== 'undefined';
    -    });
     
    -  return query.length > 0
    -    ? config.database.find('_User', { $or: query }, { limit: 2 })
    -    : Promise.resolve([]);
    +      if (!providerAuthData?.id) {
    +        return null;
    +      }
    +
    +      return { [`authData.${provider}.id`]: providerAuthData.id };
    +    })
    +  );
    +
    +  // Filter out null queries
    +  const validQueries = queries.filter(query => query !== null);
    +
    +  if (!validQueries.length) {
    +    return [];
    +  }
    +
    +  // Perform database query
    +  return config.database.find('_User', { $or: validQueries }, { limit: 2 });
     };
     
     const hasMutatedAuthData = (authData, userAuthData) => {
    @@ -533,7 +542,7 @@ const handleAuthDataValidation = async (authData, req, foundUser) => {
             acc.authData[provider] = null;
             continue;
           }
    -      const { validator } = req.config.authDataManager.getValidatorForProvider(provider);
    +      const { validator } = req.config.authDataManager.getValidatorForProvider(provider) || {};
           const authProvider = (req.config.auth || {})[provider] || {};
           if (!validator || authProvider.enabled === false) {
             throw new Parse.Error(
    
  • src/cli/parse-server.js+1 0 modified
    @@ -32,6 +32,7 @@ runner({
       help,
       usage: '[options] <path/to/configuration.json>',
       start: function (program, options, logOptions) {
    +
         if (!options.appId || !options.masterKey) {
           program.outputHelp();
           console.error('');
    
  • src/Config.js+12 0 modified
    @@ -20,6 +20,7 @@ import {
       SecurityOptions,
     } from './Options/Definitions';
     import ParseServer from './cloud-code/Parse.Server';
    +import Deprecator from './Deprecator/Deprecator';
     
     function removeTrailingSlash(str) {
       if (!str) {
    @@ -84,6 +85,7 @@ export class Config {
         pages,
         security,
         enforcePrivateUsers,
    +    enableInsecureAuthAdapters,
         schema,
         requestKeywordDenylist,
         allowExpiredAuthDataToken,
    @@ -129,6 +131,7 @@ export class Config {
         this.validateSecurityOptions(security);
         this.validateSchemaOptions(schema);
         this.validateEnforcePrivateUsers(enforcePrivateUsers);
    +    this.validateEnableInsecureAuthAdapters(enableInsecureAuthAdapters);
         this.validateAllowExpiredAuthDataToken(allowExpiredAuthDataToken);
         this.validateRequestKeywordDenylist(requestKeywordDenylist);
         this.validateRateLimit(rateLimit);
    @@ -504,6 +507,15 @@ export class Config {
         }
       }
     
    +  static validateEnableInsecureAuthAdapters(enableInsecureAuthAdapters) {
    +    if (enableInsecureAuthAdapters && typeof enableInsecureAuthAdapters !== 'boolean') {
    +      throw 'Parse Server option enableInsecureAuthAdapters must be a boolean.';
    +    }
    +    if (enableInsecureAuthAdapters) {
    +      Deprecator.logRuntimeDeprecation({ usage: 'insecure adapter' });
    +    }
    +  }
    +
       get mount() {
         var mount = this._mount;
         if (this.publicServerURL) {
    
  • src/Deprecator/Deprecations.js+4 1 modified
    @@ -15,4 +15,7 @@
      *
      * If there are no deprecations, this must return an empty array.
      */
    -module.exports = [{ optionKey: 'encodeParseObjectInCloudFunction', changeNewDefault: 'true' }];
    +module.exports = [
    +  { optionKey: 'encodeParseObjectInCloudFunction', changeNewDefault: 'true' },
    +  { optionKey: 'enableInsecureAuthAdapters', changeNewDefault: 'false' },
    +];
    
  • src/Options/Definitions.js+7 0 modified
    @@ -233,6 +233,13 @@ module.exports.ParseServerOptions = {
         action: parsers.booleanParser,
         default: false,
       },
    +  enableInsecureAuthAdapters: {
    +    env: 'PARSE_SERVER_ENABLE_INSECURE_AUTH_ADAPTERS',
    +    help:
    +      'Enable (or disable) insecure auth adapters, defaults to true. Insecure auth adapters are deprecated and it is recommended to disable them.',
    +    action: parsers.booleanParser,
    +    default: true,
    +  },
       encodeParseObjectInCloudFunction: {
         env: 'PARSE_SERVER_ENCODE_PARSE_OBJECT_IN_CLOUD_FUNCTION',
         help:
    
  • src/Options/docs.js+1 0 modified
    @@ -43,6 +43,7 @@
      * @property {Boolean} enableAnonymousUsers Enable (or disable) anonymous users, defaults to true
      * @property {Boolean} enableCollationCaseComparison Optional. If set to `true`, the collation rule of case comparison for queries and indexes is enabled. Enable this option to run Parse Server with MongoDB Atlas Serverless or AWS Amazon DocumentDB. If `false`, the collation rule of case comparison is disabled. Default is `false`.
      * @property {Boolean} enableExpressErrorHandler Enables the default express error handler for all errors
    + * @property {Boolean} enableInsecureAuthAdapters Enable (or disable) insecure auth adapters, defaults to true. Insecure auth adapters are deprecated and it is recommended to disable them.
      * @property {Boolean} encodeParseObjectInCloudFunction If set to `true`, a `Parse.Object` that is in the payload when calling a Cloud Function will be converted to an instance of `Parse.Object`. If `false`, the object will not be converted and instead be a plain JavaScript object, which contains the raw data of a `Parse.Object` but is not an actual instance of `Parse.Object`. Default is `false`. <br><br>ℹ️ The expected behavior would be that the object is converted to an instance of `Parse.Object`, so you would normally set this option to `true`. The default is `false` because this is a temporary option that has been introduced to avoid a breaking change when fixing a bug where JavaScript objects are not converted to actual instances of `Parse.Object`.
      * @property {String} encryptionKey Key for encrypting your files
      * @property {Boolean} enforcePrivateUsers Set to true if new users should be created without public read and write access.
    
  • src/Options/index.js+4 0 modified
    @@ -159,6 +159,10 @@ export interface ParseServerOptions {
       /* Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication
       :ENV: PARSE_SERVER_AUTH_PROVIDERS */
       auth: ?{ [string]: AuthAdapter };
    +  /* Enable (or disable) insecure auth adapters, defaults to true. Insecure auth adapters are deprecated and it is recommended to disable them.
    +  :ENV: PARSE_SERVER_ENABLE_INSECURE_AUTH_ADAPTERS
    +  :DEFAULT: true */
    +  enableInsecureAuthAdapters: ?boolean;
       /* Max file size for uploads, defaults to 20mb
       :DEFAULT: 20mb */
       maxUploadSize: ?string;
    
  • src/RestWrite.js+3 4 modified
    @@ -458,9 +458,8 @@ RestWrite.prototype.validateAuthData = function () {
       var providers = Object.keys(authData);
       if (providers.length > 0) {
         const canHandleAuthData = providers.some(provider => {
    -      var providerAuthData = authData[provider];
    -      var hasToken = providerAuthData && providerAuthData.id;
    -      return hasToken || providerAuthData === null;
    +      const providerAuthData = authData[provider] || {};
    +      return !!Object.keys(providerAuthData).length;
         });
         if (canHandleAuthData || hasUsernameAndPassword || this.auth.isMaster || this.getUserId()) {
           return this.handleAuthData(authData);
    @@ -520,7 +519,7 @@ RestWrite.prototype.ensureUniqueAuthDataId = async function () {
     };
     
     RestWrite.prototype.handleAuthData = async function (authData) {
    -  const r = await Auth.findUsersWithAuthData(this.config, authData);
    +  const r = await Auth.findUsersWithAuthData(this.config, authData, true);
       const results = this.filteredObjectsByACL(r);
     
       const userId = this.getUserId();
    
  • src/Security/CheckGroups/CheckGroupServerConfig.js+11 0 modified
    @@ -69,6 +69,17 @@ class CheckGroupServerConfig extends CheckGroup {
               }
             },
           }),
    +      new Check({
    +        title: 'Insecure auth adapters disabled',
    +        warning:
    +          "Attackers may explore insecure auth adapters' vulnerabilities and log in on behalf of another user.",
    +        solution: "Change Parse Server configuration to 'enableInsecureAuthAdapters: false'.",
    +        check: () => {
    +          if (config.enableInsecureAuthAdapters !== false) {
    +            throw 1;
    +          }
    +        },
    +      }),
         ];
       }
     }
    
5ef0440c8e76

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) (#9667)

59 files changed · +5968 1661
  • spec/Adapters/Auth/BaseCodeAdapter.spec.js+182 0 added
    @@ -0,0 +1,182 @@
    +const BaseAuthCodeAdapter = require('../../../lib/Adapters/Auth/BaseCodeAuthAdapter').default;
    +
    +describe('BaseAuthCodeAdapter', function () {
    +  let adapter;
    +  const adapterName = 'TestAdapter';
    +  const validOptions = {
    +    clientId: 'validClientId',
    +    clientSecret: 'validClientSecret',
    +  };
    +
    +  class TestAuthCodeAdapter extends BaseAuthCodeAdapter {
    +    async getUserFromAccessToken(accessToken) {
    +      if (accessToken === 'validAccessToken') {
    +        return { id: 'validUserId' };
    +      }
    +      throw new Error('Invalid access token');
    +    }
    +
    +    async getAccessTokenFromCode(authData) {
    +      if (authData.code === 'validCode') {
    +        return 'validAccessToken';
    +      }
    +      throw new Error('Invalid code');
    +    }
    +  }
    +
    +  beforeEach(function () {
    +    adapter = new TestAuthCodeAdapter(adapterName);
    +  });
    +
    +  describe('validateOptions', function () {
    +    it('should throw error if options are missing', function () {
    +      expect(() => adapter.validateOptions(null)).toThrowError(`${adapterName} options are required.`);
    +    });
    +
    +    it('should throw error if clientId is missing in secure mode', function () {
    +      expect(() =>
    +        adapter.validateOptions({ clientSecret: 'validClientSecret' })
    +      ).toThrowError(`${adapterName} clientId is required.`);
    +    });
    +
    +    it('should throw error if clientSecret is missing in secure mode', function () {
    +      expect(() =>
    +        adapter.validateOptions({ clientId: 'validClientId' })
    +      ).toThrowError(`${adapterName} clientSecret is required.`);
    +    });
    +
    +    it('should not throw error for valid options', function () {
    +      expect(() => adapter.validateOptions(validOptions)).not.toThrow();
    +      expect(adapter.clientId).toBe('validClientId');
    +      expect(adapter.clientSecret).toBe('validClientSecret');
    +      expect(adapter.enableInsecureAuth).toBeUndefined();
    +    });
    +
    +    it('should allow insecure mode without clientId or clientSecret', function () {
    +      const options = { enableInsecureAuth: true };
    +      expect(() => adapter.validateOptions(options)).not.toThrow();
    +      expect(adapter.enableInsecureAuth).toBe(true);
    +    });
    +  });
    +
    +  describe('beforeFind', function () {
    +    it('should throw error if code is missing in secure mode', async function () {
    +      adapter.validateOptions(validOptions);
    +      const authData = { access_token: 'validAccessToken' };
    +
    +      await expectAsync(adapter.beforeFind(authData)).toBeRejectedWithError(
    +        `${adapterName} code is required.`
    +      );
    +    });
    +
    +    it('should throw error if access token is missing in insecure mode', async function () {
    +      adapter.validateOptions({ enableInsecureAuth: true });
    +      const authData = {};
    +
    +      await expectAsync(adapter.beforeFind(authData)).toBeRejectedWithError(
    +        `${adapterName} auth is invalid for this user.`
    +      );
    +    });
    +
    +    it('should throw error if user ID does not match in insecure mode', async function () {
    +      adapter.validateOptions({ enableInsecureAuth: true });
    +      const authData = { id: 'invalidUserId', access_token: 'validAccessToken' };
    +
    +      await expectAsync(adapter.beforeFind(authData)).toBeRejectedWithError(
    +        `${adapterName} auth is invalid for this user.`
    +      );
    +    });
    +
    +    it('should process valid secure payload and update authData', async function () {
    +      adapter.validateOptions(validOptions);
    +      const authData = { code: 'validCode' };
    +
    +      await adapter.beforeFind(authData);
    +
    +      expect(authData.access_token).toBe('validAccessToken');
    +      expect(authData.id).toBe('validUserId');
    +      expect(authData.code).toBeUndefined();
    +    });
    +
    +    it('should process valid insecure payload', async function () {
    +      adapter.validateOptions({ enableInsecureAuth: true });
    +      const authData = { id: 'validUserId', access_token: 'validAccessToken' };
    +
    +      await expectAsync(adapter.beforeFind(authData)).toBeResolved();
    +    });
    +  });
    +
    +  describe('getUserFromAccessToken', function () {
    +    it('should throw error if not implemented in base class', async function () {
    +      const baseAdapter = new BaseAuthCodeAdapter(adapterName);
    +
    +      await expectAsync(baseAdapter.getUserFromAccessToken('test')).toBeRejectedWithError(
    +        'getUserFromAccessToken is not implemented'
    +      );
    +    });
    +
    +    it('should return valid user for valid access token', async function () {
    +      const user = await adapter.getUserFromAccessToken('validAccessToken', {});
    +      expect(user).toEqual({ id: 'validUserId' });
    +    });
    +
    +    it('should throw error for invalid access token', async function () {
    +      await expectAsync(adapter.getUserFromAccessToken('invalidAccessToken', {})).toBeRejectedWithError(
    +        'Invalid access token'
    +      );
    +    });
    +  });
    +
    +  describe('getAccessTokenFromCode', function () {
    +    it('should throw error if not implemented in base class', async function () {
    +      const baseAdapter = new BaseAuthCodeAdapter(adapterName);
    +
    +      await expectAsync(baseAdapter.getAccessTokenFromCode({ code: 'test' })).toBeRejectedWithError(
    +        'getAccessTokenFromCode is not implemented'
    +      );
    +    });
    +
    +    it('should return valid access token for valid code', async function () {
    +      const accessToken = await adapter.getAccessTokenFromCode({ code: 'validCode' });
    +      expect(accessToken).toBe('validAccessToken');
    +    });
    +
    +    it('should throw error for invalid code', async function () {
    +      await expectAsync(adapter.getAccessTokenFromCode({ code: 'invalidCode' })).toBeRejectedWithError(
    +        'Invalid code'
    +      );
    +    });
    +  });
    +
    +  describe('validateLogin', function () {
    +    it('should return user id from authData', function () {
    +      const authData = { id: 'validUserId' };
    +      const result = adapter.validateLogin(authData);
    +      expect(result).toEqual({ id: 'validUserId' });
    +    });
    +  });
    +
    +  describe('validateSetUp', function () {
    +    it('should return user id from authData', function () {
    +      const authData = { id: 'validUserId' };
    +      const result = adapter.validateSetUp(authData);
    +      expect(result).toEqual({ id: 'validUserId' });
    +    });
    +  });
    +
    +  describe('afterFind', function () {
    +    it('should return user id from authData', function () {
    +      const authData = { id: 'validUserId' };
    +      const result = adapter.afterFind(authData);
    +      expect(result).toEqual({ id: 'validUserId' });
    +    });
    +  });
    +
    +  describe('validateUpdate', function () {
    +    it('should return user id from authData', function () {
    +      const authData = { id: 'validUserId' };
    +      const result = adapter.validateUpdate(authData);
    +      expect(result).toEqual({ id: 'validUserId' });
    +    });
    +  });
    +});
    
  • spec/Adapters/Auth/gcenter.spec.js+220 0 added
    @@ -0,0 +1,220 @@
    +const GameCenterAuth = require('../../../lib/Adapters/Auth/gcenter').default;
    +const { pki } = require('node-forge');
    +const fs = require('fs');
    +const path = require('path');
    +
    +describe('GameCenterAuth Adapter', function () {
    +  let adapter;
    +
    +  beforeEach(function () {
    +    adapter = new GameCenterAuth.constructor();
    +
    +    const gcProd4 = fs.readFileSync(path.resolve(__dirname, '../../support/cert/gc-prod-4.cer'));
    +    const digicertPem = fs.readFileSync(path.resolve(__dirname, '../../support/cert/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem')).toString();
    +
    +   mockFetch([
    +    {
    +      url: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
    +      method: 'GET',
    +      response: {
    +        ok: true,
    +        headers: new Map(),
    +        arrayBuffer: () => Promise.resolve(
    +          gcProd4.buffer.slice(gcProd4.byteOffset, gcProd4.byteOffset + gcProd4.length)
    +        ),
    +      },
    +    },
    +    {
    +      url: 'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
    +      method: 'GET',
    +      response: {
    +        ok: true,
    +        headers: new Map([['content-type', 'application/x-pem-file'], ['content-length', digicertPem.length.toString()]]),
    +        text: () => Promise.resolve(digicertPem),
    +      },
    +    }
    +  ]);
    +  });
    +
    +  describe('Test config failing due to missing params or wrong types', function () {
    +    it('should throw error for invalid options', async function () {
    +      const invalidOptions = [
    +        null,
    +        undefined,
    +        {},
    +        { bundleId: '' },
    +        { enableInsecureAuth: false }, // Missing bundleId in secure mode
    +      ];
    +
    +      for (const options of invalidOptions) {
    +        expect(() => adapter.validateOptions(options)).withContext(JSON.stringify(options)).toThrow()
    +      }
    +    });
    +
    +    it('should validate options successfully with valid parameters', function () {
    +      const validOptions = { bundleId: 'com.valid.app', enableInsecureAuth: false };
    +      expect(() => adapter.validateOptions(validOptions)).not.toThrow();
    +    });
    +  });
    +
    +  describe('Test payload failing due to missing params or wrong types', function () {
    +    it('should throw error for missing authData fields', async function () {
    +      await expectAsync(adapter.validateAuthData({})).toBeRejectedWithError(
    +        'AuthData id is missing.'
    +      );
    +    });
    +  });
    +
    +  describe('Test payload fails due to incorrect appId / certificate', function () {
    +    it('should throw error for invalid publicKeyUrl', async function () {
    +      const invalidPublicKeyUrl = 'https://malicious.url.com/key.cer';
    +
    +      spyOn(adapter, 'fetchCertificate').and.throwError(
    +        new Error('Invalid publicKeyUrl')
    +      );
    +
    +      await expectAsync(
    +        adapter.getAppleCertificate(invalidPublicKeyUrl)
    +      ).toBeRejectedWithError('Invalid publicKeyUrl: https://malicious.url.com/key.cer');
    +    });
    +
    +    it('should throw error for invalid signature verification', async function () {
    +      const fakePublicKey = 'invalid-key';
    +      const fakeAuthData = {
    +        id: '1234567',
    +        publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
    +        timestamp: 1460981421303,
    +        salt: 'saltST==',
    +        signature: 'invalidSignature',
    +      };
    +
    +      spyOn(adapter, 'getAppleCertificate').and.returnValue(Promise.resolve(fakePublicKey));
    +      spyOn(adapter, 'verifySignature').and.throwError('Invalid signature.');
    +
    +      await expectAsync(adapter.validateAuthData(fakeAuthData)).toBeRejectedWithError(
    +        'Invalid signature.'
    +      );
    +    });
    +  });
    +
    +  describe('Test payload passing', function () {
    +    it('should successfully process valid payload and save auth data', async function () {
    +      const validAuthData = {
    +        id: '1234567',
    +        publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
    +        timestamp: 1460981421303,
    +        salt: 'saltST==',
    +        signature: 'validSignature',
    +        bundleId: 'com.valid.app',
    +      };
    +
    +      spyOn(adapter, 'getAppleCertificate').and.returnValue(Promise.resolve('validKey'));
    +      spyOn(adapter, 'verifySignature').and.returnValue(true);
    +
    +      await expectAsync(adapter.validateAuthData(validAuthData)).toBeResolved();
    +    });
    +  });
    +
    +  describe('Certificate and Signature Validation', function () {
    +    it('should fetch and validate Apple certificate', async function () {
    +      const certUrl = 'https://static.gc.apple.com/public-key/gc-prod-4.cer';
    +      const mockCertificate = 'mockCertificate';
    +
    +      spyOn(adapter, 'fetchCertificate').and.returnValue(
    +        Promise.resolve({ certificate: mockCertificate, headers: new Map() })
    +      );
    +      spyOn(pki, 'certificateFromPem').and.returnValue({});
    +
    +      adapter.cache[certUrl] = mockCertificate;
    +
    +      const cert = await adapter.getAppleCertificate(certUrl);
    +      expect(cert).toBe(mockCertificate);
    +    });
    +
    +    it('should verify signature successfully', async function () {
    +      const authData = {
    +        id: 'G:1965586982',
    +        publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
    +        timestamp: 1565257031287,
    +        signature:
    +          'uqLBTr9Uex8zCpc1UQ1MIDMitb+HUat2Mah4Kw6AVLSGe0gGNJXlih2i5X+0ZwVY0S9zY2NHWi2gFjmhjt/4kxWGMkupqXX5H/qhE2m7hzox6lZJpH98ZEUbouWRfZX2ZhUlCkAX09oRNi7fI7mWL1/o88MaI/y6k6tLr14JTzmlxgdyhw+QRLxRPA6NuvUlRSJpyJ4aGtNH5/wHdKQWL8nUnFYiYmaY8R7IjzNxPfy8UJTUWmeZvMSgND4u8EjADPsz7ZtZyWAPi8kYcAb6M8k0jwLD3vrYCB8XXyO2RQb/FY2TM4zJuI7PzLlvvgOJXbbfVtHx7Evnm5NYoyzgzw==',
    +        salt: 'DzqqrQ==',
    +      };
    +
    +      adapter.bundleId = 'cloud.xtralife.gamecenterauth';
    +      adapter.enableInsecureAuth = false;
    +
    +      spyOn(adapter, 'verifyPublicKeyIssuer').and.returnValue();
    +
    +      const publicKey = await adapter.getAppleCertificate(authData.publicKeyUrl);
    +
    +      expect(() => adapter.verifySignature(publicKey, authData)).not.toThrow();
    +
    +    });
    +
    +    it('should not use bundle id from authData payload in secure mode', async function () {
    +      const authData = {
    +        id: 'G:1965586982',
    +        publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
    +        timestamp: 1565257031287,
    +        signature:
    +          'uqLBTr9Uex8zCpc1UQ1MIDMitb+HUat2Mah4Kw6AVLSGe0gGNJXlih2i5X+0ZwVY0S9zY2NHWi2gFjmhjt/4kxWGMkupqXX5H/qhE2m7hzox6lZJpH98ZEUbouWRfZX2ZhUlCkAX09oRNi7fI7mWL1/o88MaI/y6k6tLr14JTzmlxgdyhw+QRLxRPA6NuvUlRSJpyJ4aGtNH5/wHdKQWL8nUnFYiYmaY8R7IjzNxPfy8UJTUWmeZvMSgND4u8EjADPsz7ZtZyWAPi8kYcAb6M8k0jwLD3vrYCB8XXyO2RQb/FY2TM4zJuI7PzLlvvgOJXbbfVtHx7Evnm5NYoyzgzw==',
    +        salt: 'DzqqrQ==',
    +        bundleId: 'com.example.insecure.app',
    +      };
    +
    +      adapter.bundleId = 'cloud.xtralife.gamecenterauth';
    +      adapter.enableInsecureAuth = false;
    +
    +      spyOn(adapter, 'verifyPublicKeyIssuer').and.returnValue();
    +
    +      const publicKey = await adapter.getAppleCertificate(authData.publicKeyUrl);
    +
    +      expect(() => adapter.verifySignature(publicKey, authData)).not.toThrow();
    +
    +    });
    +
    +    it('should not use bundle id from authData payload in insecure mode', async function () {
    +      const authData = {
    +        id: 'G:1965586982',
    +        publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
    +        timestamp: 1565257031287,
    +        signature:
    +          'uqLBTr9Uex8zCpc1UQ1MIDMitb+HUat2Mah4Kw6AVLSGe0gGNJXlih2i5X+0ZwVY0S9zY2NHWi2gFjmhjt/4kxWGMkupqXX5H/qhE2m7hzox6lZJpH98ZEUbouWRfZX2ZhUlCkAX09oRNi7fI7mWL1/o88MaI/y6k6tLr14JTzmlxgdyhw+QRLxRPA6NuvUlRSJpyJ4aGtNH5/wHdKQWL8nUnFYiYmaY8R7IjzNxPfy8UJTUWmeZvMSgND4u8EjADPsz7ZtZyWAPi8kYcAb6M8k0jwLD3vrYCB8XXyO2RQb/FY2TM4zJuI7PzLlvvgOJXbbfVtHx7Evnm5NYoyzgzw==',
    +        salt: 'DzqqrQ==',
    +        bundleId: 'com.example.insecure.app',
    +      };
    +
    +      adapter.bundleId = 'cloud.xtralife.gamecenterauth';
    +      adapter.enableInsecureAuth = true;
    +
    +      spyOn(adapter, 'verifyPublicKeyIssuer').and.returnValue();
    +
    +      const publicKey = await adapter.getAppleCertificate(authData.publicKeyUrl);
    +
    +      expect(() => adapter.verifySignature(publicKey, authData)).not.toThrow();
    +
    +    });
    +
    +    it('can  use bundle id from authData payload in insecure mode', async function () {
    +      const authData = {
    +        id: 'G:1965586982',
    +        publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
    +        timestamp: 1565257031287,
    +        signature:
    +          'uqLBTr9Uex8zCpc1UQ1MIDMitb+HUat2Mah4Kw6AVLSGe0gGNJXlih2i5X+0ZwVY0S9zY2NHWi2gFjmhjt/4kxWGMkupqXX5H/qhE2m7hzox6lZJpH98ZEUbouWRfZX2ZhUlCkAX09oRNi7fI7mWL1/o88MaI/y6k6tLr14JTzmlxgdyhw+QRLxRPA6NuvUlRSJpyJ4aGtNH5/wHdKQWL8nUnFYiYmaY8R7IjzNxPfy8UJTUWmeZvMSgND4u8EjADPsz7ZtZyWAPi8kYcAb6M8k0jwLD3vrYCB8XXyO2RQb/FY2TM4zJuI7PzLlvvgOJXbbfVtHx7Evnm5NYoyzgzw==',
    +        salt: 'DzqqrQ==',
    +        bundleId: 'cloud.xtralife.gamecenterauth',
    +      };
    +
    +      adapter.enableInsecureAuth = true;
    +
    +      spyOn(adapter, 'verifyPublicKeyIssuer').and.returnValue();
    +
    +      const publicKey = await adapter.getAppleCertificate(authData.publicKeyUrl);
    +
    +      expect(() => adapter.verifySignature(publicKey, authData)).not.toThrow();
    +
    +    });
    +  });
    +});
    
  • spec/Adapters/Auth/github.spec.js+285 0 added
    @@ -0,0 +1,285 @@
    +const GitHubAdapter = require('../../../lib/Adapters/Auth/github').default;
    +
    +describe('GitHubAdapter', function () {
    +  let adapter;
    +  const validOptions = {
    +    clientId: 'validClientId',
    +    clientSecret: 'validClientSecret',
    +  };
    +
    +  beforeEach(function () {
    +    adapter = new GitHubAdapter.constructor();
    +    adapter.validateOptions(validOptions);
    +  });
    +
    +  describe('getAccessTokenFromCode', function () {
    +    it('should fetch an access token successfully', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://github.com/login/oauth/access_token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                access_token: 'mockAccessToken',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const code = 'validCode';
    +      const token = await adapter.getAccessTokenFromCode(code);
    +
    +      expect(token).toBe('mockAccessToken');
    +    });
    +
    +    it('should throw an error if the response is not ok', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://github.com/login/oauth/access_token',
    +          method: 'POST',
    +          response: {
    +            ok: false,
    +            statusText: 'Bad Request',
    +          },
    +        },
    +      ]);
    +
    +      const code = 'invalidCode';
    +
    +      await expectAsync(adapter.getAccessTokenFromCode(code)).toBeRejectedWithError(
    +        'Failed to exchange code for token: Bad Request'
    +      );
    +    });
    +
    +    it('should throw an error if the response contains an error', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://github.com/login/oauth/access_token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                error: 'invalid_grant',
    +                error_description: 'Code is invalid',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const code = 'invalidCode';
    +
    +      await expectAsync(adapter.getAccessTokenFromCode(code)).toBeRejectedWithError('Code is invalid');
    +    });
    +  });
    +
    +  describe('getUserFromAccessToken', function () {
    +    it('should fetch user data successfully', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.github.com/user',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                id: 'mockUserId',
    +                login: 'mockUserLogin',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const accessToken = 'validAccessToken';
    +      const user = await adapter.getUserFromAccessToken(accessToken);
    +
    +      expect(user).toEqual({ id: 'mockUserId', login: 'mockUserLogin' });
    +    });
    +
    +    it('should throw an error if the response is not ok', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.github.com/user',
    +          method: 'GET',
    +          response: {
    +            ok: false,
    +            statusText: 'Unauthorized',
    +          },
    +        },
    +      ]);
    +
    +      const accessToken = 'invalidAccessToken';
    +
    +      await expectAsync(adapter.getUserFromAccessToken(accessToken)).toBeRejectedWithError(
    +        'Failed to fetch GitHub user: Unauthorized'
    +      );
    +    });
    +
    +    it('should throw an error if user data is invalid', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.github.com/user',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve({}),
    +          },
    +        },
    +      ]);
    +
    +      const accessToken = 'validAccessToken';
    +
    +      await expectAsync(adapter.getUserFromAccessToken(accessToken)).toBeRejectedWithError(
    +        'Invalid GitHub user data received.'
    +      );
    +    });
    +  });
    +
    +  describe('GitHubAdapter E2E Test', function () {
    +    beforeEach(async function () {
    +      await reconfigureServer({
    +        auth: {
    +          github: {
    +            clientId: 'validClientId',
    +            clientSecret: 'validClientSecret',
    +          },
    +        },
    +      });
    +    });
    +
    +    it('should log in user using GitHub adapter successfully', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://github.com/login/oauth/access_token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                access_token: 'mockAccessToken123',
    +              }),
    +          },
    +        },
    +        {
    +          url: 'https://api.github.com/user',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                id: 'mockUserId',
    +                login: 'mockUserLogin',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { code: 'validCode' };
    +      const user = await Parse.User.logInWith('github', { authData });
    +
    +      expect(user.id).toBeDefined();
    +    });
    +
    +    it('should handle error when GitHub returns invalid code', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://github.com/login/oauth/access_token',
    +          method: 'POST',
    +          response: {
    +            ok: false,
    +            statusText: 'Invalid code',
    +          },
    +        },
    +      ]);
    +
    +      const authData = { code: 'invalidCode' };
    +
    +      await expectAsync(Parse.User.logInWith('github', { authData })).toBeRejectedWithError(
    +        'Failed to exchange code for token: Invalid code'
    +      );
    +    });
    +
    +    it('should handle error when GitHub returns invalid user data', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://github.com/login/oauth/access_token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                access_token: 'mockAccessToken123',
    +              }),
    +          },
    +        },
    +        {
    +          url: 'https://api.github.com/user',
    +          method: 'GET',
    +          response: {
    +            ok: false,
    +            statusText: 'Unauthorized',
    +          },
    +        },
    +      ]);
    +
    +      const authData = { code: 'validCode' };
    +
    +      await expectAsync(Parse.User.logInWith('github', { authData })).toBeRejectedWithError(
    +        'Failed to fetch GitHub user: Unauthorized'
    +      );
    +    });
    +
    +    it('e2e secure does not support insecure payload', async function () {
    +      mockFetch();
    +      const authData = { id: 'mockUserId', access_token: 'mockAccessToken123' };
    +      await expectAsync(Parse.User.logInWith('github', { authData })).toBeRejectedWithError(
    +        'GitHub code is required.'
    +      );
    +    });
    +
    +    it('e2e insecure does support secure payload', async function () {
    +      await reconfigureServer({
    +        auth: {
    +          github: {
    +            clientId: 'validClientId',
    +            clientSecret: 'validClientSecret',
    +            enableInsecureAuth: true,
    +          },
    +        },
    +      });
    +
    +      mockFetch([
    +        {
    +          url: 'https://github.com/login/oauth/access_token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                access_token: 'mockAccessToken123',
    +              }),
    +          },
    +        },
    +        {
    +          url: 'https://api.github.com/user',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                id: 'mockUserId',
    +                login: 'mockUserLogin',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { code: 'validCode' };
    +      const user = await Parse.User.logInWith('github', { authData });
    +
    +      expect(user.id).toBeDefined();
    +    });
    +  });
    +});
    
  • spec/Adapters/Auth/gpgames.spec.js+356 0 added
    @@ -0,0 +1,356 @@
    +const GooglePlayGamesServicesAdapter = require('../../../lib/Adapters/Auth/gpgames').default;
    +
    +describe('GooglePlayGamesServicesAdapter', function () {
    +  let adapter;
    +
    +  beforeEach(function () {
    +    adapter = new GooglePlayGamesServicesAdapter.constructor();
    +    adapter.clientId = 'validClientId';
    +    adapter.clientSecret = 'validClientSecret';
    +  });
    +
    +  describe('getAccessTokenFromCode', function () {
    +    it('should fetch an access token successfully', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://oauth2.googleapis.com/token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                access_token: 'mockAccessToken',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const code = 'validCode';
    +      const authData = { redirectUri: 'http://example.com' };
    +      const token = await adapter.getAccessTokenFromCode(code, authData);
    +
    +      expect(token).toBe('mockAccessToken');
    +    });
    +
    +    it('should throw an error if the response is not ok', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://oauth2.googleapis.com/token',
    +          method: 'POST',
    +          response: {
    +            ok: false,
    +            statusText: 'Bad Request',
    +          },
    +        },
    +      ]);
    +
    +      const code = 'invalidCode';
    +      const authData = { redirectUri: 'http://example.com' };
    +
    +      await expectAsync(adapter.getAccessTokenFromCode(code, authData)).toBeRejectedWithError(
    +        'Failed to exchange code for token: Bad Request'
    +      );
    +    });
    +
    +    it('should throw an error if the response contains an error', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://oauth2.googleapis.com/token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                error: 'invalid_grant',
    +                error_description: 'Code is invalid',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const code = 'invalidCode';
    +      const authData = { redirectUri: 'http://example.com' };
    +
    +      await expectAsync(adapter.getAccessTokenFromCode(code, authData)).toBeRejectedWithError(
    +        'Code is invalid'
    +      );
    +    });
    +  });
    +
    +  describe('getUserFromAccessToken', function () {
    +    it('should fetch user data successfully', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://www.googleapis.com/games/v1/players/mockUserId',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                playerId: 'mockUserId',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const accessToken = 'validAccessToken';
    +      const authData = { id: 'mockUserId' };
    +      const user = await adapter.getUserFromAccessToken(accessToken, authData);
    +
    +      expect(user).toEqual({ id: 'mockUserId' });
    +    });
    +
    +    it('should throw an error if the response is not ok', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://www.googleapis.com/games/v1/players/mockUserId',
    +          method: 'GET',
    +          response: {
    +            ok: false,
    +            statusText: 'Unauthorized',
    +          },
    +        },
    +      ]);
    +
    +      const accessToken = 'invalidAccessToken';
    +      const authData = { id: 'mockUserId' };
    +
    +      await expectAsync(adapter.getUserFromAccessToken(accessToken, authData)).toBeRejectedWithError(
    +        'Failed to fetch Google Play Games Services user: Unauthorized'
    +      );
    +    });
    +
    +    it('should throw an error if user data is invalid', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://www.googleapis.com/games/v1/players/mockUserId',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve({}),
    +          },
    +        },
    +      ]);
    +
    +      const accessToken = 'validAccessToken';
    +      const authData = { id: 'mockUserId' };
    +
    +      await expectAsync(adapter.getUserFromAccessToken(accessToken, authData)).toBeRejectedWithError(
    +        'Invalid Google Play Games Services user data received.'
    +      );
    +    });
    +
    +    it('should throw an error if playerId does not match the provided user ID', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://www.googleapis.com/games/v1/players/mockUserId',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                playerId: 'anotherUserId',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const accessToken = 'validAccessToken';
    +      const authData = { id: 'mockUserId' };
    +
    +      await expectAsync(adapter.getUserFromAccessToken(accessToken, authData)).toBeRejectedWithError(
    +        'Invalid Google Play Games Services user data received.'
    +      );
    +    });
    +  });
    +
    +  describe('GooglePlayGamesServicesAdapter E2E Test', function () {
    +    beforeEach(async function () {
    +      await reconfigureServer({
    +        auth: {
    +          gpgames: {
    +            clientId: 'validClientId',
    +            clientSecret: 'validClientSecret',
    +          },
    +        },
    +      });
    +    });
    +
    +    it('should log in user successfully with valid code', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://oauth2.googleapis.com/token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                access_token: 'mockAccessToken123',
    +              }),
    +          },
    +        },
    +        {
    +          url: 'https://www.googleapis.com/games/v1/players/mockUserId',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                playerId: 'mockUserId',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = {
    +        code: 'validCode',
    +        id: 'mockUserId',
    +        redirectUri: 'http://example.com',
    +      };
    +
    +      const user = await Parse.User.logInWith('gpgames', { authData });
    +
    +      expect(user.id).toBeDefined();
    +      expect(global.fetch).toHaveBeenCalledWith(
    +        'https://oauth2.googleapis.com/token',
    +        jasmine.any(Object)
    +      );
    +      expect(global.fetch).toHaveBeenCalledWith(
    +        'https://www.googleapis.com/games/v1/players/mockUserId',
    +        jasmine.any(Object)
    +      );
    +    });
    +
    +    it('should handle error when the token exchange fails', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://oauth2.googleapis.com/token',
    +          method: 'POST',
    +          response: {
    +            ok: false,
    +            statusText: 'Invalid code',
    +          },
    +        },
    +      ]);
    +
    +      const authData = {
    +        code: 'invalidCode',
    +        redirectUri: 'http://example.com',
    +      };
    +
    +      await expectAsync(Parse.User.logInWith('gpgames', { authData })).toBeRejectedWithError(
    +        'Failed to exchange code for token: Invalid code'
    +      );
    +
    +      expect(global.fetch).toHaveBeenCalledWith(
    +        'https://oauth2.googleapis.com/token',
    +        jasmine.any(Object)
    +      );
    +    });
    +
    +    it('should handle error when user data fetch fails', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://oauth2.googleapis.com/token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                access_token: 'mockAccessToken123',
    +              }),
    +          },
    +        },
    +        {
    +          url: 'https://www.googleapis.com/games/v1/players/mockUserId',
    +          method: 'GET',
    +          response: {
    +            ok: false,
    +            statusText: 'Unauthorized',
    +          },
    +        },
    +      ]);
    +
    +      const authData = {
    +        code: 'validCode',
    +        id: 'mockUserId',
    +        redirectUri: 'http://example.com',
    +      };
    +
    +      await expectAsync(Parse.User.logInWith('gpgames', { authData })).toBeRejectedWithError(
    +        'Failed to fetch Google Play Games Services user: Unauthorized'
    +      );
    +
    +      expect(global.fetch).toHaveBeenCalledWith(
    +        'https://oauth2.googleapis.com/token',
    +        jasmine.any(Object)
    +      );
    +      expect(global.fetch).toHaveBeenCalledWith(
    +        'https://www.googleapis.com/games/v1/players/mockUserId',
    +        jasmine.any(Object)
    +      );
    +    });
    +
    +    it('should handle error when user data is invalid', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://oauth2.googleapis.com/token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                access_token: 'mockAccessToken123',
    +              }),
    +          },
    +        },
    +        {
    +          url: 'https://www.googleapis.com/games/v1/players/mockUserId',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                playerId: 'anotherUserId',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = {
    +        code: 'validCode',
    +        id: 'mockUserId',
    +        redirectUri: 'http://example.com',
    +      };
    +
    +      await expectAsync(Parse.User.logInWith('gpgames', { authData })).toBeRejectedWithError(
    +        'Invalid Google Play Games Services user data received.'
    +      );
    +
    +      expect(global.fetch).toHaveBeenCalledWith(
    +        'https://oauth2.googleapis.com/token',
    +        jasmine.any(Object)
    +      );
    +      expect(global.fetch).toHaveBeenCalledWith(
    +        'https://www.googleapis.com/games/v1/players/mockUserId',
    +        jasmine.any(Object)
    +      );
    +    });
    +
    +    it('should handle error when no code or access token is provided', async function () {
    +      mockFetch();
    +
    +      const authData = {
    +        id: 'mockUserId',
    +      };
    +
    +      await expectAsync(Parse.User.logInWith('gpgames', { authData })).toBeRejectedWithError(
    +        'gpgames code is required.'
    +      );
    +
    +      expect(global.fetch).not.toHaveBeenCalled();
    +    });
    +  });
    +
    +});
    +
    
  • spec/Adapters/Auth/instagram.spec.js+258 0 added
    @@ -0,0 +1,258 @@
    +const InstagramAdapter = require('../../../lib/Adapters/Auth/instagram').default;
    +
    +describe('InstagramAdapter', function () {
    +  let adapter;
    +
    +  beforeEach(function () {
    +    adapter = new InstagramAdapter.constructor();
    +    adapter.clientId = 'validClientId';
    +    adapter.clientSecret = 'validClientSecret';
    +    adapter.redirectUri = 'https://example.com/callback';
    +  });
    +
    +  describe('getAccessTokenFromCode', function () {
    +    it('should fetch an access token successfully', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.instagram.com/oauth/access_token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                access_token: 'mockAccessToken',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { code: 'validCode' };
    +      const token = await adapter.getAccessTokenFromCode(authData);
    +
    +      expect(token).toBe('mockAccessToken');
    +    });
    +
    +    it('should throw an error if the response contains an error', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.instagram.com/oauth/access_token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                error: 'invalid_grant',
    +                error_description: 'Code is invalid',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { code: 'invalidCode' };
    +
    +      await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWithError(
    +        'Code is invalid'
    +      );
    +    });
    +  });
    +
    +  describe('getUserFromAccessToken', function () {
    +    it('should fetch user data successfully', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://graph.instagram.com/me?fields=id&access_token=mockAccessToken',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                id: 'mockUserId',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const accessToken = 'mockAccessToken';
    +      const authData = { id: 'mockUserId' };
    +      const user = await adapter.getUserFromAccessToken(accessToken, authData);
    +
    +      expect(user).toEqual({ id: 'mockUserId' });
    +    });
    +
    +    it('should throw an error if user ID does not match authData', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://graph.instagram.com/me?fields=id&access_token=mockAccessToken',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                id: 'differentUserId',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const accessToken = 'mockAccessToken';
    +      const authData = { id: 'mockUserId' };
    +
    +      await expectAsync(adapter.getUserFromAccessToken(accessToken, authData)).toBeRejectedWithError(
    +        'Instagram auth is invalid for this user.'
    +      );
    +    });
    +  });
    +
    +  describe('InstagramAdapter E2E Test', function () {
    +    beforeEach(async function () {
    +      await reconfigureServer({
    +        auth: {
    +          instagram: {
    +            clientId: 'validClientId',
    +            clientSecret: 'validClientSecret',
    +            redirectUri: 'https://example.com/callback',
    +          },
    +        },
    +      });
    +    });
    +
    +    it('should log in user successfully with valid code', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.instagram.com/oauth/access_token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                access_token: 'mockAccessToken123',
    +              }),
    +          },
    +        },
    +        {
    +          url: 'https://graph.instagram.com/me?fields=id&access_token=mockAccessToken123',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                id: 'mockUserId',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = {
    +        code: 'validCode',
    +        id: 'mockUserId',
    +      };
    +
    +      const user = await Parse.User.logInWith('instagram', { authData });
    +
    +      expect(user.id).toBeDefined();
    +    });
    +
    +    it('should handle error when access token exchange fails', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.instagram.com/oauth/access_token',
    +          method: 'POST',
    +          response: {
    +            ok: false,
    +            statusText: 'Invalid code',
    +          },
    +        },
    +      ]);
    +
    +      const authData = {
    +        code: 'invalidCode',
    +      };
    +
    +      await expectAsync(Parse.User.logInWith('instagram', { authData })).toBeRejectedWith(
    +        new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Instagram API request failed.')
    +      );
    +    });
    +
    +    it('should handle error when user data fetch fails', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.instagram.com/oauth/access_token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                access_token: 'mockAccessToken123',
    +              }),
    +          },
    +        },
    +        {
    +          url: 'https://graph.instagram.com/me?fields=id&access_token=mockAccessToken123',
    +          method: 'GET',
    +          response: {
    +            ok: false,
    +            statusText: 'Unauthorized',
    +          },
    +        },
    +      ]);
    +
    +      const authData = {
    +        code: 'validCode',
    +        id: 'mockUserId',
    +      };
    +
    +      await expectAsync(Parse.User.logInWith('instagram', { authData })).toBeRejectedWith(
    +        new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Instagram API request failed.')
    +      );
    +    });
    +
    +    it('should handle error when user data is invalid', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.instagram.com/oauth/access_token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                access_token: 'mockAccessToken123',
    +              }),
    +          },
    +        },
    +        {
    +          url: 'https://graph.instagram.com/me?fields=id&access_token=mockAccessToken123',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                id: 'differentUserId',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = {
    +        code: 'validCode',
    +        id: 'mockUserId',
    +      };
    +
    +      await expectAsync(Parse.User.logInWith('instagram', { authData })).toBeRejectedWithError(
    +        'Instagram auth is invalid for this user.'
    +      );
    +    });
    +
    +    it('should handle error when no code or access token is provided', async function () {
    +      mockFetch();
    +
    +      const authData = {
    +        id: 'mockUserId',
    +      };
    +
    +      await expectAsync(Parse.User.logInWith('instagram', { authData })).toBeRejectedWithError(
    +        'Instagram code is required.'
    +      );
    +    });
    +  });
    +
    +});
    
  • spec/Adapters/Auth/line.spec.js+309 0 added
    @@ -0,0 +1,309 @@
    +const LineAdapter = require('../../../lib/Adapters/Auth/line').default;
    +describe('LineAdapter', function () {
    +  let adapter;
    +
    +  beforeEach(function () {
    +    adapter = new LineAdapter.constructor();
    +    adapter.clientId = 'validClientId';
    +    adapter.clientSecret = 'validClientSecret';
    +  });
    +
    +  describe('getAccessTokenFromCode', function () {
    +    it('should throw an error if code is missing in authData', async function () {
    +      const authData = { redirect_uri: 'http://example.com' };
    +
    +      await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWithError(
    +        'Line auth is invalid for this user.'
    +      );
    +    });
    +
    +    it('should fetch an access token successfully', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.line.me/oauth2/v2.1/token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                access_token: 'mockAccessToken',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = {
    +        code: 'validCode',
    +        redirect_uri: 'http://example.com',
    +      };
    +
    +      const token = await adapter.getAccessTokenFromCode(authData);
    +
    +      expect(token).toBe('mockAccessToken');
    +    });
    +
    +    it('should throw an error if response is not ok', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.line.me/oauth2/v2.1/token',
    +          method: 'POST',
    +          response: {
    +            ok: false,
    +            statusText: 'Bad Request',
    +          },
    +        },
    +      ]);
    +
    +      const authData = {
    +        code: 'invalidCode',
    +        redirect_uri: 'http://example.com',
    +      };
    +
    +      await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWithError(
    +        'Failed to exchange code for token: Bad Request'
    +      );
    +    });
    +
    +    it('should throw an error if response contains an error object', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.line.me/oauth2/v2.1/token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                error: 'invalid_grant',
    +                error_description: 'Code is invalid',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = {
    +        code: 'invalidCode',
    +        redirect_uri: 'http://example.com',
    +      };
    +
    +      await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWithError(
    +        'Code is invalid'
    +      );
    +    });
    +  });
    +
    +  describe('getUserFromAccessToken', function () {
    +    it('should fetch user data successfully', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.line.me/v2/profile',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                userId: 'mockUserId',
    +                displayName: 'mockDisplayName',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const accessToken = 'validAccessToken';
    +      const user = await adapter.getUserFromAccessToken(accessToken);
    +
    +      expect(user).toEqual({
    +        userId: 'mockUserId',
    +        displayName: 'mockDisplayName',
    +      });
    +    });
    +
    +    it('should throw an error if response is not ok', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.line.me/v2/profile',
    +          method: 'GET',
    +          response: {
    +            ok: false,
    +            statusText: 'Unauthorized',
    +          },
    +        },
    +      ]);
    +
    +      const accessToken = 'invalidAccessToken';
    +
    +      await expectAsync(adapter.getUserFromAccessToken(accessToken)).toBeRejectedWithError(
    +        'Failed to fetch Line user: Unauthorized'
    +      );
    +    });
    +
    +    it('should throw an error if user data is invalid', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.line.me/v2/profile',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve({}),
    +          },
    +        },
    +      ]);
    +
    +      const accessToken = 'validAccessToken';
    +
    +      await expectAsync(adapter.getUserFromAccessToken(accessToken)).toBeRejectedWithError(
    +        'Invalid Line user data received.'
    +      );
    +    });
    +  });
    +
    +  describe('LineAdapter E2E Test', function () {
    +    beforeEach(async function () {
    +      await reconfigureServer({
    +        auth: {
    +          line: {
    +            clientId: 'validClientId',
    +            clientSecret: 'validClientSecret',
    +          },
    +        },
    +      });
    +    });
    +
    +    it('should log in user successfully with valid code', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.line.me/oauth2/v2.1/token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                access_token: 'mockAccessToken123',
    +              }),
    +          },
    +        },
    +        {
    +          url: 'https://api.line.me/v2/profile',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                userId: 'mockUserId',
    +                displayName: 'mockDisplayName',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = {
    +        code: 'validCode',
    +        redirect_uri: 'http://example.com',
    +      };
    +
    +      const user = await Parse.User.logInWith('line', { authData });
    +
    +      expect(user.id).toBeDefined();
    +    });
    +
    +    it('should handle error when token exchange fails', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.line.me/oauth2/v2.1/token',
    +          method: 'POST',
    +          response: {
    +            ok: false,
    +            statusText: 'Invalid code',
    +          },
    +        },
    +      ]);
    +
    +      const authData = {
    +        code: 'invalidCode',
    +        redirect_uri: 'http://example.com',
    +      };
    +
    +      await expectAsync(Parse.User.logInWith('line', { authData })).toBeRejectedWithError(
    +        'Failed to exchange code for token: Invalid code'
    +      );
    +    });
    +
    +    it('should handle error when user data fetch fails', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.line.me/oauth2/v2.1/token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                access_token: 'mockAccessToken123',
    +              }),
    +          },
    +        },
    +        {
    +          url: 'https://api.line.me/v2/profile',
    +          method: 'GET',
    +          response: {
    +            ok: false,
    +            statusText: 'Unauthorized',
    +          },
    +        },
    +      ]);
    +
    +      const authData = {
    +        code: 'validCode',
    +        redirect_uri: 'http://example.com',
    +      };
    +
    +      await expectAsync(Parse.User.logInWith('line', { authData })).toBeRejectedWithError(
    +        'Failed to fetch Line user: Unauthorized'
    +      );
    +    });
    +
    +    it('should handle error when user data is invalid', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.line.me/oauth2/v2.1/token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                access_token: 'mockAccessToken123',
    +              }),
    +          },
    +        },
    +        {
    +          url: 'https://api.line.me/v2/profile',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve({}),
    +          },
    +        },
    +      ]);
    +
    +      const authData = {
    +        code: 'validCode',
    +        redirect_uri: 'http://example.com',
    +      };
    +
    +      await expectAsync(Parse.User.logInWith('line', { authData })).toBeRejectedWithError(
    +        'Invalid Line user data received.'
    +      );
    +    });
    +
    +    it('should handle error when no code is provided', async function () {
    +      mockFetch();
    +
    +      const authData = {
    +        redirect_uri: 'http://example.com',
    +      };
    +
    +      await expectAsync(Parse.User.logInWith('line', { authData })).toBeRejectedWithError(
    +        'Line code is required.'
    +      );
    +    });
    +  });
    +
    +});
    
  • spec/Adapters/Auth/linkedIn.spec.js+312 0 added
    @@ -0,0 +1,312 @@
    +
    +const LinkedInAdapter = require('../../../lib/Adapters/Auth/linkedin').default;
    +describe('LinkedInAdapter', function () {
    +  let adapter;
    +  const validOptions = {
    +    clientId: 'validClientId',
    +    clientSecret: 'validClientSecret',
    +    enableInsecureAuth: false,
    +  };
    +
    +  beforeEach(function () {
    +    adapter = new LinkedInAdapter.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 beforeFind', function () {
    +    it('should throw error for invalid payload', async function () {
    +      adapter.enableInsecureAuth = true;
    +
    +      const payloads = [{}, { access_token: null }];
    +
    +      for (const payload of payloads) {
    +        await expectAsync(adapter.beforeFind(payload)).toBeRejectedWith(
    +          new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'LinkedIn auth is invalid for this user.')
    +        );
    +      }
    +    });
    +
    +    it('should process secure payload and set auth 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', is_mobile_sdk: false };
    +
    +      await adapter.beforeFind(authData);
    +
    +      expect(authData.access_token).toBe('validToken');
    +      expect(authData.id).toBe('validUserId');
    +    });
    +
    +    it('should validate insecure auth and match user id', async function () {
    +      adapter.enableInsecureAuth = true;
    +      spyOn(adapter, 'getUserFromAccessToken').and.returnValue(
    +        Promise.resolve({ id: 'validUserId' })
    +      );
    +
    +      const authData = { access_token: 'validToken', id: 'validUserId', is_mobile_sdk: false };
    +
    +      await expectAsync(adapter.beforeFind(authData)).toBeResolved();
    +    });
    +
    +    it('should throw error if insecure auth user id does not match', async function () {
    +      adapter.enableInsecureAuth = true;
    +      spyOn(adapter, 'getUserFromAccessToken').and.returnValue(
    +        Promise.resolve({ id: 'invalidUserId' })
    +      );
    +
    +      const authData = { access_token: 'validToken', id: 'validUserId', is_mobile_sdk: false };
    +
    +      await expectAsync(adapter.beforeFind(authData)).toBeRejectedWith(
    +        new Error('LinkedIn auth is invalid for this user.')
    +      );
    +    });
    +  });
    +
    +  describe('Test getUserFromAccessToken', function () {
    +    it('should fetch user successfully', async function () {
    +      global.fetch = jasmine.createSpy().and.returnValue(
    +        Promise.resolve({
    +          ok: true,
    +          json: () => Promise.resolve({ id: 'validUserId' }),
    +        })
    +      );
    +
    +      const user = await adapter.getUserFromAccessToken('validToken', false);
    +
    +      expect(global.fetch).toHaveBeenCalledWith('https://api.linkedin.com/v2/me', {
    +        headers: {
    +          Authorization: `Bearer validToken`,
    +          'x-li-format': 'json',
    +          'x-li-src': undefined,
    +        },
    +      });
    +      expect(user).toEqual({ id: 'validUserId' });
    +    });
    +
    +    it('should throw error for invalid response', async function () {
    +      global.fetch = jasmine.createSpy().and.returnValue(
    +        Promise.resolve({ ok: false })
    +      );
    +
    +      await expectAsync(adapter.getUserFromAccessToken('invalidToken', false)).toBeRejectedWith(
    +        new Error('LinkedIn API request failed.')
    +      );
    +    });
    +  });
    +
    +  describe('Test getAccessTokenFromCode', function () {
    +    it('should fetch token successfully', async function () {
    +      global.fetch = jasmine.createSpy().and.returnValue(
    +        Promise.resolve({
    +          ok: true,
    +          json: () => Promise.resolve({ access_token: 'validToken' }),
    +        })
    +      );
    +
    +      const tokenResponse = await adapter.getAccessTokenFromCode('validCode', 'http://example.com');
    +
    +      expect(global.fetch).toHaveBeenCalledWith('https://www.linkedin.com/oauth/v2/accessToken', {
    +        method: 'POST',
    +        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    +        body: jasmine.any(URLSearchParams),
    +      });
    +      expect(tokenResponse).toEqual('validToken');
    +    });
    +
    +    it('should throw error for invalid response', async function () {
    +      global.fetch = jasmine.createSpy().and.returnValue(
    +        Promise.resolve({ ok: false })
    +      );
    +
    +      await expectAsync(
    +        adapter.getAccessTokenFromCode('invalidCode', 'http://example.com')
    +      ).toBeRejectedWith(new Error('LinkedIn API request failed.'));
    +    });
    +  });
    +
    +  describe('Test validate methods', function () {
    +    const authData = { id: 'validUserId', access_token: 'validToken' };
    +
    +    it('validateLogin should return user id', function () {
    +      const result = adapter.validateLogin(authData);
    +      expect(result).toEqual({ id: 'validUserId' });
    +    });
    +
    +    it('validateSetUp should return user id', function () {
    +      const result = adapter.validateSetUp(authData);
    +      expect(result).toEqual({ id: 'validUserId' });
    +    });
    +
    +    it('validateUpdate should return user id', function () {
    +      const result = adapter.validateUpdate(authData);
    +      expect(result).toEqual({ id: 'validUserId' });
    +    });
    +
    +    it('afterFind should return user id', function () {
    +      const result = adapter.afterFind(authData);
    +      expect(result).toEqual({ id: 'validUserId' });
    +    });
    +  });
    +
    +  describe('LinkedInAdapter E2E Test', function () {
    +    beforeEach(async function () {
    +      await reconfigureServer({
    +        auth: {
    +          linkedin: {
    +            clientId: 'validClientId',
    +            clientSecret: 'validClientSecret',
    +          },
    +        },
    +      });
    +    });
    +
    +    it('should log in user using LinkedIn adapter successfully (secure)', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://www.linkedin.com/oauth/v2/accessToken',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                access_token: 'mockAccessToken123',
    +              }),
    +          },
    +        },
    +        {
    +          url: 'https://api.linkedin.com/v2/me',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                id: 'mockUserId',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { code: 'validCode', redirect_uri: 'https://example.com/callback' };
    +      const user = await Parse.User.logInWith('linkedin', { authData });
    +
    +      expect(user.id).toBeDefined();
    +      expect(global.fetch).toHaveBeenCalledWith(
    +        'https://www.linkedin.com/oauth/v2/accessToken',
    +        jasmine.any(Object)
    +      );
    +      expect(global.fetch).toHaveBeenCalledWith(
    +        'https://api.linkedin.com/v2/me',
    +        jasmine.any(Object)
    +      );
    +    });
    +
    +    it('should handle error when LinkedIn returns invalid user data', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://www.linkedin.com/oauth/v2/accessToken',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                access_token: 'mockAccessToken123',
    +              }),
    +          },
    +        },
    +        {
    +          url: 'https://api.linkedin.com/v2/me',
    +          method: 'GET',
    +          response: {
    +            ok: false,
    +            statusText: 'Unauthorized',
    +          },
    +        },
    +      ]);
    +
    +      const authData = { code: 'validCode', redirect_uri: 'https://example.com/callback' };
    +
    +      await expectAsync(Parse.User.logInWith('linkedin', { authData })).toBeRejectedWithError(
    +        'LinkedIn API request failed.'
    +      );
    +
    +      expect(global.fetch).toHaveBeenCalledWith(
    +        'https://www.linkedin.com/oauth/v2/accessToken',
    +        jasmine.any(Object)
    +      );
    +      expect(global.fetch).toHaveBeenCalledWith(
    +        'https://api.linkedin.com/v2/me',
    +        jasmine.any(Object)
    +      );
    +    });
    +
    +    it('secure does not support insecure payload if not enabled', async function () {
    +      mockFetch();
    +      const authData = { id: 'mockUserId', access_token: 'mockAccessToken123' };
    +      await expectAsync(Parse.User.logInWith('linkedin', { authData })).toBeRejectedWithError(
    +        'LinkedIn code is required.'
    +      );
    +
    +      expect(global.fetch).not.toHaveBeenCalled();
    +    });
    +
    +    it('insecure mode supports insecure payload if enabled', async function () {
    +      await reconfigureServer({
    +        auth: {
    +          linkedin: {
    +            clientId: 'validClientId',
    +            clientSecret: 'validClientSecret',
    +            enableInsecureAuth: true,
    +          },
    +        },
    +      });
    +
    +      mockFetch([
    +        {
    +          url: 'https://api.linkedin.com/v2/me',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () =>
    +              Promise.resolve({
    +                id: 'mockUserId',
    +              }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { id: 'mockUserId', access_token: 'mockAccessToken123' };
    +      const user = await Parse.User.logInWith('linkedin', { authData });
    +
    +      expect(user.id).toBeDefined();
    +      expect(global.fetch).toHaveBeenCalledWith(
    +        'https://api.linkedin.com/v2/me',
    +        jasmine.any(Object)
    +      );
    +    });
    +  });
    +});
    
  • spec/Adapters/Auth/microsoft.spec.js+307 0 added
    @@ -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.'
    +      );
    +    });
    +  });
    +
    +});
    
  • spec/Adapters/Auth/oauth2.spec.js+305 0 added
    @@ -0,0 +1,305 @@
    +const OAuth2Adapter = require('../../../lib/Adapters/Auth/oauth2').default;
    +
    +describe('OAuth2Adapter', () => {
    +  let adapter;
    +
    +  const validOptions = {
    +    tokenIntrospectionEndpointUrl: 'https://provider.com/introspect',
    +    useridField: 'sub',
    +    appidField: 'aud',
    +    appIds: ['valid-app-id'],
    +    authorizationHeader: 'Bearer validAuthToken',
    +  };
    +
    +  beforeEach(() => {
    +    adapter = new OAuth2Adapter.constructor();
    +    adapter.validateOptions(validOptions);
    +  });
    +
    +  describe('validateAppId', () => {
    +    it('should validate app ID successfully', async () => {
    +      const authData = { access_token: 'validAccessToken' };
    +      const mockResponse = {
    +        [validOptions.appidField]: 'valid-app-id',
    +      };
    +
    +      mockFetch([
    +        {
    +          url: validOptions.tokenIntrospectionEndpointUrl,
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve(mockResponse),
    +          },
    +        },
    +      ]);
    +
    +      await expectAsync(
    +        adapter.validateAppId(validOptions.appIds, authData, validOptions)
    +      ).toBeResolved();
    +    });
    +
    +    it('should throw an error if app ID is invalid', async () => {
    +      const authData = { access_token: 'validAccessToken' };
    +      const mockResponse = {
    +        [validOptions.appidField]: 'invalid-app-id',
    +      };
    +
    +      mockFetch([
    +        {
    +          url: validOptions.tokenIntrospectionEndpointUrl,
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve(mockResponse),
    +          },
    +        },
    +      ]);
    +
    +      await expectAsync(
    +        adapter.validateAppId(validOptions.appIds, authData, validOptions)
    +      ).toBeRejectedWithError('OAuth2: Invalid app ID.');
    +    });
    +  });
    +
    +  describe('validateAuthData', () => {
    +    it('should validate auth data successfully', async () => {
    +      const authData = { id: 'user-id', access_token: 'validAccessToken' };
    +      const mockResponse = {
    +        active: true,
    +        [validOptions.useridField]: 'user-id',
    +      };
    +
    +      mockFetch([
    +        {
    +          url: validOptions.tokenIntrospectionEndpointUrl,
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve(mockResponse),
    +          },
    +        },
    +      ]);
    +
    +      await expectAsync(
    +        adapter.validateAuthData(authData, null, validOptions)
    +      ).toBeResolvedTo({});
    +    });
    +
    +    it('should throw an error if the token is inactive', async () => {
    +      const authData = { id: 'user-id', access_token: 'validAccessToken' };
    +      const mockResponse = { active: false };
    +
    +      mockFetch([
    +        {
    +          url: validOptions.tokenIntrospectionEndpointUrl,
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve(mockResponse),
    +          },
    +        },
    +      ]);
    +
    +      await expectAsync(
    +        adapter.validateAuthData(authData, null, validOptions)
    +      ).toBeRejectedWith(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 access token is invalid for this user.'));
    +    });
    +
    +    it('should throw an error if user ID does not match', async () => {
    +      const authData = { id: 'user-id', access_token: 'validAccessToken' };
    +      const mockResponse = {
    +        active: true,
    +        [validOptions.useridField]: 'different-user-id',
    +      };
    +
    +      mockFetch([
    +        {
    +          url: validOptions.tokenIntrospectionEndpointUrl,
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve(mockResponse),
    +          },
    +        },
    +      ]);
    +
    +      await expectAsync(
    +        adapter.validateAuthData(authData, null, validOptions)
    +      ).toBeRejectedWithError('OAuth2 access token is invalid for this user.');
    +    });
    +  });
    +
    +  describe('requestTokenInfo', () => {
    +    it('should fetch token info successfully', async () => {
    +      const mockResponse = { active: true };
    +
    +      mockFetch([
    +        {
    +          url: validOptions.tokenIntrospectionEndpointUrl,
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve(mockResponse),
    +          },
    +        },
    +      ]);
    +
    +      const result = await adapter.requestTokenInfo(
    +        'validAccessToken',
    +        validOptions
    +      );
    +
    +      expect(result).toEqual(mockResponse);
    +    });
    +
    +    it('should throw an error if the introspection endpoint URL is missing', async () => {
    +      const options = { ...validOptions, tokenIntrospectionEndpointUrl: null };
    +
    +      expect(
    +        () => adapter.validateOptions(options)
    +      ).toThrow(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 token introspection endpoint URL is missing.'));
    +    });
    +
    +    it('should throw an error if the response is not ok', async () => {
    +      mockFetch([
    +        {
    +          url: validOptions.tokenIntrospectionEndpointUrl,
    +          method: 'POST',
    +          response: {
    +            ok: false,
    +            statusText: 'Bad Request',
    +          },
    +        },
    +      ]);
    +
    +      await expectAsync(
    +        adapter.requestTokenInfo('invalidAccessToken')
    +      ).toBeRejectedWithError('OAuth2 token introspection request failed.');
    +    });
    +  });
    +
    +  describe('OAuth2Adapter E2E Tests', () => {
    +    beforeEach(async () => {
    +      // Simulate reconfiguring the server with OAuth2 auth options
    +      await reconfigureServer({
    +        auth: {
    +          mockOauth: {
    +            tokenIntrospectionEndpointUrl: 'https://provider.com/introspect',
    +            useridField: 'sub',
    +            appidField: 'aud',
    +            appIds: ['valid-app-id'],
    +            authorizationHeader: 'Bearer validAuthToken',
    +            oauth2: true
    +          },
    +        },
    +      });
    +    });
    +
    +    it('should validate and authenticate user successfully', async () => {
    +      mockFetch([
    +        {
    +          url: 'https://provider.com/introspect',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve({
    +              active: true,
    +              sub: 'user123',
    +              aud: 'valid-app-id',
    +            }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { access_token: 'validAccessToken', id: 'user123' };
    +      const user = await Parse.User.logInWith('mockOauth', { authData });
    +
    +      expect(user.id).toBeDefined();
    +      expect(user.get('authData').mockOauth.id).toEqual('user123');
    +    });
    +
    +    it('should reject authentication for inactive token', async () => {
    +      mockFetch([
    +        {
    +          url: 'https://provider.com/introspect',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve({ active: false, aud: ['valid-app-id'] }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { access_token: 'inactiveToken', id: 'user123' };
    +      await expectAsync(Parse.User.logInWith('mockOauth', { authData })).toBeRejectedWith(
    +        new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 access token is invalid for this user.')
    +      );
    +    });
    +
    +    it('should reject authentication for mismatched user ID', async () => {
    +      mockFetch([
    +        {
    +          url: 'https://provider.com/introspect',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve({
    +              active: true,
    +              sub: 'different-user',
    +              aud: 'valid-app-id',
    +            }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { access_token: 'validAccessToken', id: 'user123' };
    +      await expectAsync(Parse.User.logInWith('mockOauth', { authData })).toBeRejectedWith(
    +        new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 access token is invalid for this user.')
    +      );
    +    });
    +
    +    it('should reject authentication for invalid app ID', async () => {
    +      mockFetch([
    +        {
    +          url: 'https://provider.com/introspect',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve({
    +              active: true,
    +              sub: 'user123',
    +              aud: 'invalid-app-id',
    +            }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { access_token: 'validAccessToken', id: 'user123' };
    +      await expectAsync(Parse.User.logInWith('mockOauth', { authData })).toBeRejectedWithError(
    +        'OAuth2: Invalid app ID.'
    +      );
    +    });
    +
    +    it('should handle error when token introspection endpoint is missing', async () => {
    +      await reconfigureServer({
    +        auth: {
    +          mockOauth: {
    +            tokenIntrospectionEndpointUrl: null,
    +            useridField: 'sub',
    +            appidField: 'aud',
    +            appIds: ['valid-app-id'],
    +            authorizationHeader: 'Bearer validAuthToken',
    +            oauth2: true
    +          },
    +        },
    +      });
    +
    +      const authData = { access_token: 'validAccessToken', id: 'user123' };
    +      await expectAsync(Parse.User.logInWith('mockOauth', { authData })).toBeRejectedWith(
    +        new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 token introspection endpoint URL is missing.')
    +      );
    +    });
    +  });
    +
    +});
    
  • spec/Adapters/Auth/qq.spec.js+252 0 added
    @@ -0,0 +1,252 @@
    +const QqAdapter = require('../../../lib/Adapters/Auth/qq').default;
    +
    +describe('QqAdapter', () => {
    +  let adapter;
    +
    +  beforeEach(() => {
    +    adapter = new QqAdapter.constructor();
    +  });
    +
    +  describe('getUserFromAccessToken', () => {
    +    it('should fetch user data successfully', async () => {
    +      const mockResponse = `callback({"client_id":"validAppId","openid":"user123"})`;
    +
    +      mockFetch([
    +        {
    +          url: 'https://graph.qq.com/oauth2.0/me',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            text: () => Promise.resolve(mockResponse),
    +          },
    +        },
    +      ]);
    +
    +      const result = await adapter.getUserFromAccessToken('validAccessToken');
    +
    +      expect(result).toEqual({ client_id: 'validAppId', openid: 'user123' });
    +    });
    +
    +    it('should throw an error if the API request fails', async () => {
    +      mockFetch([
    +        {
    +          url: 'https://graph.qq.com/oauth2.0/me',
    +          method: 'GET',
    +          response: {
    +            ok: false,
    +            statusText: 'Unauthorized',
    +          },
    +        },
    +      ]);
    +
    +      await expectAsync(
    +        adapter.getUserFromAccessToken('invalidAccessToken')
    +      ).toBeRejectedWithError('qq API request failed.');
    +    });
    +  });
    +
    +  describe('getAccessTokenFromCode', () => {
    +    it('should fetch access token successfully', async () => {
    +      const mockResponse = `callback({"access_token":"validAccessToken","expires_in":3600,"refresh_token":"refreshToken"})`;
    +
    +      mockFetch([
    +        {
    +          url: 'https://graph.qq.com/oauth2.0/token',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            text: () => Promise.resolve(mockResponse),
    +          },
    +        },
    +      ]);
    +
    +      const result = await adapter.getAccessTokenFromCode({
    +        code: 'validCode',
    +        redirect_uri: 'https://your-redirect-uri.com/callback',
    +      });
    +
    +      expect(result).toBe('validAccessToken');
    +    });
    +
    +    it('should throw an error if the API request fails', async () => {
    +      mockFetch([
    +        {
    +          url: 'https://graph.qq.com/oauth2.0/token',
    +          method: 'GET',
    +          response: {
    +            ok: false,
    +            statusText: 'Bad Request',
    +          },
    +        },
    +      ]);
    +
    +      await expectAsync(
    +        adapter.getAccessTokenFromCode({
    +          code: 'invalidCode',
    +          redirect_uri: 'https://your-redirect-uri.com/callback',
    +        })
    +      ).toBeRejectedWithError('qq API request failed.');
    +    });
    +  });
    +
    +  describe('parseResponseData', () => {
    +    it('should parse valid callback response data', () => {
    +      const response = `callback({"key":"value"})`;
    +      const result = adapter.parseResponseData(response);
    +
    +      expect(result).toEqual({ key: 'value' });
    +    });
    +
    +    it('should throw an error if the response data is invalid', () => {
    +      const response = 'invalid response';
    +
    +      expect(() => adapter.parseResponseData(response)).toThrowError(
    +        'qq auth is invalid for this user.'
    +      );
    +    });
    +  });
    +
    +  describe('QqAdapter E2E Test', () => {
    +    beforeEach(async () => {
    +      await reconfigureServer({
    +        auth: {
    +          qq: {
    +            clientId: 'validAppId',
    +            clientSecret: 'validAppSecret',
    +          },
    +        },
    +      });
    +    });
    +
    +    it('should log in user using Qq adapter successfully', async () => {
    +      mockFetch([
    +        {
    +          url: 'https://graph.qq.com/oauth2.0/token',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            text: () =>
    +              Promise.resolve(
    +                `callback({"access_token":"mockAccessToken","expires_in":3600})`
    +              ),
    +          },
    +        },
    +        {
    +          url: 'https://graph.qq.com/oauth2.0/me',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            text: () =>
    +              Promise.resolve(
    +                `callback({"client_id":"validAppId","openid":"user123"})`
    +              ),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { code: 'validCode', redirect_uri: 'https://your-redirect-uri.com/callback' };
    +      const user = await Parse.User.logInWith('qq', { authData });
    +
    +      expect(user.id).toBeDefined();
    +    });
    +
    +    it('should handle error when Qq returns invalid code', async () => {
    +      mockFetch([
    +        {
    +          url: 'https://graph.qq.com/oauth2.0/token',
    +          method: 'GET',
    +          response: {
    +            ok: false,
    +            statusText: 'Invalid code',
    +          },
    +        },
    +      ]);
    +
    +      const authData = { code: 'invalidCode', redirect_uri: 'https://your-redirect-uri.com/callback' };
    +
    +      await expectAsync(Parse.User.logInWith('qq', { authData })).toBeRejectedWithError(
    +        'qq API request failed.'
    +      );
    +    });
    +
    +    it('should handle error when Qq returns invalid user data', async () => {
    +      mockFetch([
    +        {
    +          url: 'https://graph.qq.com/oauth2.0/token',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            text: () =>
    +              Promise.resolve(
    +                `callback({"access_token":"mockAccessToken","expires_in":3600})`
    +              ),
    +          },
    +        },
    +        {
    +          url: 'https://graph.qq.com/oauth2.0/me',
    +          method: 'GET',
    +          response: {
    +            ok: false,
    +            statusText: 'Unauthorized',
    +          },
    +        },
    +      ]);
    +
    +      const authData = { code: 'validCode', redirect_uri: 'https://your-redirect-uri.com/callback' };
    +
    +      await expectAsync(Parse.User.logInWith('qq', { authData })).toBeRejectedWithError(
    +        'qq API request failed.'
    +      );
    +    });
    +
    +    it('e2e secure does not support insecure payload', async () => {
    +      mockFetch();
    +      const authData = { id: 'mockUserId', access_token: 'mockAccessToken' };
    +      await expectAsync(Parse.User.logInWith('qq', { authData })).toBeRejectedWithError(
    +        'qq code is required.'
    +      );
    +    });
    +
    +    it('e2e insecure does support secure payload', async () => {
    +      await reconfigureServer({
    +        auth: {
    +          qq: {
    +            appId: 'validAppId',
    +            appSecret: 'validAppSecret',
    +            enableInsecureAuth: true,
    +          },
    +        },
    +      });
    +
    +      mockFetch([
    +        {
    +          url: 'https://graph.qq.com/oauth2.0/token',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            text: () =>
    +              Promise.resolve(
    +                `callback({"access_token":"mockAccessToken","expires_in":3600})`
    +              ),
    +          },
    +        },
    +        {
    +          url: 'https://graph.qq.com/oauth2.0/me',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            text: () =>
    +              Promise.resolve(
    +                `callback({"client_id":"validAppId","openid":"user123"})`
    +              ),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { code: 'validCode', redirect_uri: 'https://your-redirect-uri.com/callback' };
    +      const user = await Parse.User.logInWith('qq', { authData });
    +
    +      expect(user.id).toBeDefined();
    +    });
    +  });
    +});
    
  • spec/Adapters/Auth/spotify.spec.js+113 0 added
    @@ -0,0 +1,113 @@
    +const SpotifyAdapter = require('../../../lib/Adapters/Auth/spotify').default;
    +
    +describe('SpotifyAdapter', () => {
    +  let adapter;
    +
    +  beforeEach(() => {
    +    adapter = new SpotifyAdapter.constructor();
    +  });
    +
    +  describe('getUserFromAccessToken', () => {
    +    it('should fetch user data successfully', async () => {
    +      const mockResponse = {
    +        id: 'spotifyUser123',
    +      };
    +
    +      mockFetch([
    +        {
    +          url: 'https://api.spotify.com/v1/me',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve(mockResponse),
    +          },
    +        },
    +      ]);
    +
    +      const result = await adapter.getUserFromAccessToken('validAccessToken');
    +
    +      expect(result).toEqual({ id: 'spotifyUser123' });
    +    });
    +
    +    it('should throw an error if the API request fails', async () => {
    +      mockFetch([
    +        {
    +          url: 'https://api.spotify.com/v1/me',
    +          method: 'GET',
    +          response: {
    +            ok: false,
    +            statusText: 'Unauthorized',
    +          },
    +        },
    +      ]);
    +
    +      await expectAsync(adapter.getUserFromAccessToken('invalidAccessToken')).toBeRejectedWithError(
    +        'Spotify API request failed.'
    +      );
    +    });
    +  });
    +
    +  describe('getAccessTokenFromCode', () => {
    +    it('should fetch access token successfully', async () => {
    +      const mockResponse = {
    +        access_token: 'validAccessToken',
    +        expires_in: 3600,
    +        refresh_token: 'refreshToken',
    +      };
    +
    +      mockFetch([
    +        {
    +          url: 'https://accounts.spotify.com/api/token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve(mockResponse),
    +          },
    +        },
    +      ]);
    +
    +      const authData = {
    +        code: 'validCode',
    +        redirect_uri: 'https://your-redirect-uri.com/callback',
    +        code_verifier: 'validCodeVerifier',
    +      };
    +
    +      const result = await adapter.getAccessTokenFromCode(authData);
    +
    +      expect(result).toEqual(mockResponse);
    +    });
    +
    +    it('should throw an error if authData is missing required fields', async () => {
    +      const authData = {
    +        redirect_uri: 'https://your-redirect-uri.com/callback',
    +      };
    +
    +      await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWithError(
    +        'Spotify auth configuration authData.code and/or authData.redirect_uri and/or authData.code_verifier.'
    +      );
    +    });
    +
    +    it('should throw an error if the API request fails', async () => {
    +      mockFetch([
    +        {
    +          url: 'https://accounts.spotify.com/api/token',
    +          method: 'POST',
    +          response: {
    +            ok: false,
    +            statusText: 'Bad Request',
    +          },
    +        },
    +      ]);
    +
    +      const authData = {
    +        code: 'invalidCode',
    +        redirect_uri: 'https://your-redirect-uri.com/callback',
    +        code_verifier: 'invalidCodeVerifier',
    +      };
    +
    +      await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWithError(
    +        'Spotify API request failed.'
    +      );
    +    });
    +  });
    +});
    
  • spec/Adapters/Auth/twitter.spec.js+120 0 added
    @@ -0,0 +1,120 @@
    +const TwitterAuthAdapter = require('../../../lib/Adapters/Auth/twitter').default;
    +
    +describe('TwitterAuthAdapter', function () {
    +  let adapter;
    +  const validOptions = {
    +    consumer_key: 'validConsumerKey',
    +    consumer_secret: 'validConsumerSecret',
    +  };
    +
    +  beforeEach(function () {
    +    adapter = new TwitterAuthAdapter.constructor();
    +  });
    +
    +  describe('Test configuration errors', function () {
    +    it('should throw an error when options are missing', function () {
    +      expect(() => adapter.validateOptions()).toThrowError('Twitter auth options are required.');
    +    });
    +
    +    it('should throw an error when consumer_key and consumer_secret are missing for secure auth', function () {
    +      const options = { enableInsecureAuth: false };
    +      expect(() => adapter.validateOptions(options)).toThrowError(
    +        'Consumer key and secret are required for secure Twitter auth.'
    +      );
    +    });
    +
    +    it('should not throw an error when valid options are provided', function () {
    +      expect(() => adapter.validateOptions(validOptions)).not.toThrow();
    +    });
    +  });
    +
    +  describe('Validate Insecure Auth', function () {
    +    it('should throw an error if oauth_token or oauth_token_secret are missing', async function () {
    +      const authData = { oauth_token: 'validToken' }; // Missing oauth_token_secret
    +      await expectAsync(adapter.validateInsecureAuth(authData, validOptions)).toBeRejectedWithError(
    +        'Twitter insecure auth requires oauth_token and oauth_token_secret.'
    +      );
    +    });
    +
    +    it('should validate insecure auth successfully when data matches', async function () {
    +      spyOn(adapter, 'request').and.returnValue(
    +        Promise.resolve({
    +          json: () => Promise.resolve({ id: 'validUserId' }),
    +        })
    +      );
    +
    +      const authData = {
    +        id: 'validUserId',
    +        oauth_token: 'validToken',
    +        oauth_token_secret: 'validSecret',
    +      };
    +      await expectAsync(adapter.validateInsecureAuth(authData, validOptions)).toBeResolved();
    +    });
    +
    +    it('should throw an error when user ID does not match', async function () {
    +      spyOn(adapter, 'request').and.returnValue(
    +        Promise.resolve({
    +          json: () => Promise.resolve({ id: 'invalidUserId' }),
    +        })
    +      );
    +
    +      const authData = {
    +        id: 'validUserId',
    +        oauth_token: 'validToken',
    +        oauth_token_secret: 'validSecret',
    +      };
    +      await expectAsync(adapter.validateInsecureAuth(authData, validOptions)).toBeRejectedWithError(
    +        'Twitter auth is invalid for this user.'
    +      );
    +    });
    +  });
    +
    +  describe('End-to-End Tests', function () {
    +    beforeEach(async function () {
    +      await reconfigureServer({
    +        auth: {
    +          twitter: validOptions,
    +        }
    +      })
    +    });
    +
    +    it('should authenticate user successfully using validateAuthData', async function () {
    +      spyOn(adapter, 'exchangeAccessToken').and.returnValue(
    +        Promise.resolve({ oauth_token: 'validToken', user_id: 'validUserId' })
    +      );
    +
    +      const authData = {
    +        oauth_token: 'validToken',
    +        oauth_verifier: 'validVerifier',
    +      };
    +      await expectAsync(adapter.validateAuthData(authData, validOptions)).toBeResolved();
    +      expect(authData.id).toBe('validUserId');
    +      expect(authData.auth_token).toBe('validToken');
    +    });
    +
    +    it('should handle multiple configurations and validate successfully', async function () {
    +      const authData = {
    +        consumer_key: 'validConsumerKey',
    +        oauth_token: 'validToken',
    +        oauth_token_secret: 'validSecret',
    +      };
    +
    +      const optionsArray = [
    +        { consumer_key: 'invalidKey', consumer_secret: 'invalidSecret' },
    +        validOptions,
    +      ];
    +
    +      const selectedOption = adapter.handleMultipleConfigurations(authData, optionsArray);
    +      expect(selectedOption).toEqual(validOptions);
    +    });
    +
    +    it('should throw an error when no matching configuration is found', function () {
    +      const authData = { consumer_key: 'missingKey' };
    +      const optionsArray = [validOptions];
    +
    +      expect(() => adapter.handleMultipleConfigurations(authData, optionsArray)).toThrowError(
    +        'Twitter auth is invalid for this user.'
    +      );
    +    });
    +  });
    +});
    
  • spec/Adapters/Auth/wechat.spec.js+234 0 added
    @@ -0,0 +1,234 @@
    +const WeChatAdapter = require('../../../lib/Adapters/Auth/wechat').default;
    +
    +describe('WeChatAdapter', function () {
    +  let adapter;
    +
    +  beforeEach(function () {
    +    adapter = new WeChatAdapter.constructor();
    +  });
    +
    +  describe('Test getUserFromAccessToken', function () {
    +    it('should fetch user successfully', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.weixin.qq.com/sns/auth?access_token=validToken&openid=validOpenId',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve({ errcode: 0, id: 'validUserId' }),
    +          },
    +        },
    +      ]);
    +
    +      const user = await adapter.getUserFromAccessToken('validToken', { id: 'validOpenId' });
    +
    +      expect(global.fetch).toHaveBeenCalledWith(
    +        'https://api.weixin.qq.com/sns/auth?access_token=validToken&openid=validOpenId'
    +      );
    +      expect(user).toEqual({ errcode: 0, id: 'validUserId' });
    +    });
    +
    +    it('should throw error for invalid response', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.weixin.qq.com/sns/auth?access_token=invalidToken&openid=undefined',
    +          method: 'GET',
    +          response: {
    +            ok: false,
    +            json: () => Promise.resolve({ errcode: 40013, errmsg: 'Invalid token' }),
    +          },
    +        },
    +      ]);
    +
    +      await expectAsync(adapter.getUserFromAccessToken('invalidToken', 'invalidOpenId')).toBeRejectedWith(
    +        jasmine.objectContaining({ message: 'WeChat auth is invalid for this user.' })
    +      );
    +    });
    +  });
    +
    +  describe('Test getAccessTokenFromCode', function () {
    +    it('should fetch access token successfully', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=validCode&grant_type=authorization_code',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve({ access_token: 'validToken', errcode: 0 }),
    +          },
    +        },
    +      ]);
    +
    +      adapter.validateOptions({ clientId: 'validAppId', clientSecret: 'validAppSecret' });
    +      const authData = { code: 'validCode' };
    +      const token = await adapter.getAccessTokenFromCode(authData);
    +
    +      expect(global.fetch).toHaveBeenCalledWith(
    +        'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=validCode&grant_type=authorization_code'
    +      );
    +      expect(token).toEqual('validToken');
    +    });
    +
    +    it('should throw error for invalid response', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=invalidCode&grant_type=authorization_code',
    +          method: 'GET',
    +          response: {
    +            ok: false,
    +            json: () => Promise.resolve({ errcode: 40029, errmsg: 'Invalid code' }),
    +          },
    +        },
    +      ]);
    +      adapter.validateOptions({ clientId: 'validAppId', clientSecret: 'validAppSecret' });
    +
    +      const authData = { code: 'invalidCode' };
    +
    +      await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWith(
    +        jasmine.objectContaining({ message: 'WeChat auth is invalid for this user.' })
    +      );
    +    });
    +  });
    +
    +  describe('WeChatAdapter E2E Tests', function () {
    +    beforeEach(async () => {
    +      await reconfigureServer({
    +        auth: {
    +          wechat: {
    +            clientId: 'validAppId',
    +            clientSecret: 'validAppSecret',
    +            enableInsecureAuth: false,
    +          },
    +        },
    +      });
    +    });
    +
    +    it('should authenticate user successfully using WeChatAdapter', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=validCode&grant_type=authorization_code',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve({ access_token: 'validAccessToken', openid: 'user123', errcode: 0 }),
    +          },
    +        },
    +        {
    +          url: 'https://api.weixin.qq.com/sns/auth?access_token=validAccessToken&openid=user123',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve({ errcode: 0, id: 'user123' }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
    +      const user = await Parse.User.logInWith('wechat', { authData });
    +
    +      expect(user.id).toBeDefined();
    +    });
    +
    +    it('should handle invalid code error gracefully', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=invalidCode&grant_type=authorization_code',
    +          method: 'GET',
    +          response: {
    +            ok: false,
    +            json: () => Promise.resolve({ errcode: 40029, errmsg: 'Invalid code' }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { code: 'invalidCode', redirect_uri: 'http://example.com/callback' };
    +
    +      await expectAsync(Parse.User.logInWith('wechat', { authData })).toBeRejectedWith(
    +        jasmine.objectContaining({ message: 'WeChat auth is invalid for this user.' })
    +      );
    +    });
    +
    +    it('should handle error when fetching user data fails', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=validCode&grant_type=authorization_code',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve({ access_token: 'validAccessToken', openid: 'user123', errcode: 0 }),
    +          },
    +        },
    +        {
    +          url: 'https://api.weixin.qq.com/sns/auth?access_token=validAccessToken&openid=user123',
    +          method: 'GET',
    +          response: {
    +            ok: false,
    +            json: () => Promise.resolve({ errcode: 40013, errmsg: 'Invalid token' }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
    +
    +      await expectAsync(Parse.User.logInWith('wechat', { authData })).toBeRejectedWith(
    +        jasmine.objectContaining({ message: 'WeChat auth is invalid for this user.' })
    +      );
    +    });
    +
    +    it('should allow insecure auth when enabled', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.weixin.qq.com/sns/auth?access_token=validAccessToken&openid=user123',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve({ errcode: 0, id: 'user123' }),
    +          },
    +        },
    +      ]);
    +
    +      await reconfigureServer({
    +        auth: {
    +          wechat: {
    +            appId: 'validAppId',
    +            appSecret: 'validAppSecret',
    +            enableInsecureAuth: true,
    +          },
    +        },
    +      });
    +
    +      const authData = { access_token: 'validAccessToken', id: 'user123' };
    +      const user = await Parse.User.logInWith('wechat', { authData });
    +
    +      expect(user.id).toBeDefined();
    +    });
    +
    +    it('should reject insecure auth when user id does not match', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.weixin.qq.com/sns/auth?access_token=validAccessToken&openid=incorrectUserId',
    +          method: 'GET',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve({ errcode: 0, id: 'incorrectUser' }),
    +          },
    +        },
    +      ]);
    +
    +      await reconfigureServer({
    +        auth: {
    +          wechat: {
    +            appId: 'validAppId',
    +            appSecret: 'validAppSecret',
    +            enableInsecureAuth: true,
    +          },
    +        },
    +      });
    +
    +      const authData = { access_token: 'validAccessToken', id: 'incorrectUserId' };
    +      await expectAsync(Parse.User.logInWith('wechat', { authData })).toBeRejectedWith(
    +        jasmine.objectContaining({ message: 'WeChat auth is invalid for this user.' })
    +      );
    +    });
    +  });
    +});
    
  • spec/Adapters/Auth/weibo.spec.js+204 0 added
    @@ -0,0 +1,204 @@
    +const WeiboAdapter = require('../../../lib/Adapters/Auth/weibo').default;
    +
    +describe('WeiboAdapter', function () {
    +  let adapter;
    +
    +  beforeEach(function () {
    +    adapter = new WeiboAdapter.constructor();
    +  });
    +
    +  describe('Test configuration errors', function () {
    +    it('should throw error if code or redirect_uri is missing', async function () {
    +      const invalidAuthData = [
    +        {},
    +        { code: 'validCode' },
    +        { redirect_uri: 'http://example.com/callback' },
    +      ];
    +
    +      for (const authData of invalidAuthData) {
    +        await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWith(
    +          jasmine.objectContaining({
    +            message: 'Weibo auth requires code and redirect_uri to be sent.',
    +          })
    +        );
    +      }
    +    });
    +  });
    +
    +  describe('Test getUserFromAccessToken', function () {
    +    it('should fetch user successfully', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.weibo.com/oauth2/get_token_info',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve({ uid: 'validUserId' }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { id: 'validUserId' };
    +      const user = await adapter.getUserFromAccessToken('validToken', authData);
    +
    +      expect(global.fetch).toHaveBeenCalledWith(
    +        'https://api.weibo.com/oauth2/get_token_info',
    +        jasmine.objectContaining({
    +          method: 'POST',
    +          headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    +        })
    +      );
    +      expect(user).toEqual({ id: 'validUserId' });
    +    });
    +
    +    it('should throw error for invalid response', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.weibo.com/oauth2/get_token_info',
    +          method: 'POST',
    +          response: {
    +            ok: false,
    +            json: () => Promise.resolve({}),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { id: 'invalidUserId' };
    +      await expectAsync(adapter.getUserFromAccessToken('invalidToken', authData)).toBeRejectedWith(
    +        jasmine.objectContaining({
    +          message: 'Weibo auth is invalid for this user.',
    +        })
    +      );
    +    });
    +  });
    +
    +  describe('Test getAccessTokenFromCode', function () {
    +    it('should fetch access token successfully', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.weibo.com/oauth2/access_token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve({ access_token: 'validToken', uid: 'validUserId' }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
    +      const token = await adapter.getAccessTokenFromCode(authData);
    +
    +      expect(global.fetch).toHaveBeenCalledWith(
    +        'https://api.weibo.com/oauth2/access_token',
    +        jasmine.objectContaining({
    +          method: 'POST',
    +          headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    +        })
    +      );
    +      expect(token).toEqual('validToken');
    +    });
    +
    +    it('should throw error for invalid response', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.weibo.com/oauth2/access_token',
    +          method: 'POST',
    +          response: {
    +            ok: false,
    +            json: () => Promise.resolve({ errcode: 40029 }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { code: 'invalidCode', redirect_uri: 'http://example.com/callback' };
    +      await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWith(
    +        jasmine.objectContaining({
    +          message: 'Weibo auth is invalid for this user.',
    +        })
    +      );
    +    });
    +  });
    +
    +  describe('WeiboAdapter E2E Tests', function () {
    +    beforeEach(async () => {
    +      await reconfigureServer({
    +        auth: {
    +          weibo: {
    +            clientId: 'validAppId',
    +            clientSecret: 'validAppSecret',
    +          },
    +        }
    +      });
    +    });
    +
    +    it('should authenticate user successfully using WeiboAdapter', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.weibo.com/oauth2/access_token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve({ access_token: 'validAccessToken', uid: 'user123' }),
    +          },
    +        },
    +        {
    +          url: 'https://api.weibo.com/oauth2/get_token_info',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve({ uid: 'user123' }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
    +      const user = await Parse.User.logInWith('weibo', { authData });
    +
    +      expect(user.id).toBeDefined();
    +    });
    +
    +    it('should handle invalid code error gracefully', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.weibo.com/oauth2/access_token',
    +          method: 'POST',
    +          response: {
    +            ok: false,
    +            json: () => Promise.resolve({ errcode: 40029 }),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { code: 'invalidCode', redirect_uri: 'http://example.com/callback' };
    +      await expectAsync(Parse.User.logInWith('weibo', { authData })).toBeRejectedWith(
    +        jasmine.objectContaining({ message: 'Weibo auth is invalid for this user.' })
    +      );
    +    });
    +
    +    it('should handle error when fetching user data fails', async function () {
    +      mockFetch([
    +        {
    +          url: 'https://api.weibo.com/oauth2/access_token',
    +          method: 'POST',
    +          response: {
    +            ok: true,
    +            json: () => Promise.resolve({ access_token: 'validAccessToken', uid: 'user123' }),
    +          },
    +        },
    +        {
    +          url: 'https://api.weibo.com/oauth2/get_token_info',
    +          method: 'POST',
    +          response: {
    +            ok: false,
    +            json: () => Promise.resolve({}),
    +          },
    +        },
    +      ]);
    +
    +      const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
    +      await expectAsync(Parse.User.logInWith('weibo', { authData })).toBeRejectedWith(
    +        jasmine.objectContaining({ message: 'Weibo auth is invalid for this user.' })
    +      );
    +    });
    +  });
    +});
    
  • spec/AuthenticationAdapters.spec.js+7 811 modified
    @@ -3,99 +3,8 @@ const Config = require('../lib/Config');
     const defaultColumns = require('../lib/Controllers/SchemaController').defaultColumns;
     const authenticationLoader = require('../lib/Adapters/Auth');
     const path = require('path');
    -const responses = {
    -  gpgames: { playerId: 'userId' },
    -  instagram: { id: 'userId' },
    -  janrainengage: { stat: 'ok', profile: { identifier: 'userId' } },
    -  janraincapture: { stat: 'ok', result: 'userId' },
    -  line: { userId: 'userId' },
    -  vkontakte: { response: [{ id: 'userId' }] },
    -  google: { sub: 'userId' },
    -  wechat: { errcode: 0 },
    -  weibo: { uid: 'userId' },
    -  qq: 'callback( {"openid":"userId"} );', // yes it's like that, run eval in the client :P
    -  phantauth: { sub: 'userId' },
    -  microsoft: { id: 'userId', mail: 'userMail' },
    -};
     
     describe('AuthenticationProviders', function () {
    -  [
    -    'apple',
    -    'gcenter',
    -    'gpgames',
    -    'facebook',
    -    'github',
    -    'instagram',
    -    'google',
    -    'linkedin',
    -    'meetup',
    -    'twitter',
    -    'janrainengage',
    -    'janraincapture',
    -    'line',
    -    'vkontakte',
    -    'qq',
    -    'spotify',
    -    'wechat',
    -    'weibo',
    -    'phantauth',
    -    'microsoft',
    -    'keycloak',
    -  ].map(function (providerName) {
    -    it('Should validate structure of ' + providerName, done => {
    -      const provider = require('../lib/Adapters/Auth/' + providerName);
    -      jequal(typeof provider.validateAuthData, 'function');
    -      jequal(typeof provider.validateAppId, 'function');
    -      const validateAuthDataPromise = provider.validateAuthData({}, {});
    -      const validateAppIdPromise = provider.validateAppId('app', 'key', {});
    -      jequal(validateAuthDataPromise.constructor, Promise.prototype.constructor);
    -      jequal(validateAppIdPromise.constructor, Promise.prototype.constructor);
    -      validateAuthDataPromise.then(
    -        () => {},
    -        () => {}
    -      );
    -      validateAppIdPromise.then(
    -        () => {},
    -        () => {}
    -      );
    -      done();
    -    });
    -
    -    it(`should provide the right responses for adapter ${providerName}`, async () => {
    -      const noResponse = ['twitter', 'apple', 'gcenter', 'google', 'keycloak'];
    -      if (noResponse.includes(providerName)) {
    -        return;
    -      }
    -      spyOn(require('../lib/Adapters/Auth/httpsRequest'), 'get').and.callFake(options => {
    -        if (
    -          options ===
    -            'https://oauth.vk.com/access_token?client_id=appId&client_secret=appSecret&v=5.123&grant_type=client_credentials' ||
    -          options ===
    -            'https://oauth.vk.com/access_token?client_id=appId&client_secret=appSecret&v=5.124&grant_type=client_credentials'
    -        ) {
    -          return {
    -            access_token: 'access_token',
    -          };
    -        }
    -        return Promise.resolve(responses[providerName] || { id: 'userId' });
    -      });
    -      spyOn(require('../lib/Adapters/Auth/httpsRequest'), 'request').and.callFake(() => {
    -        return Promise.resolve(responses[providerName] || { id: 'userId' });
    -      });
    -      const provider = require('../lib/Adapters/Auth/' + providerName);
    -      let params = {};
    -      if (providerName === 'vkontakte') {
    -        params = {
    -          appIds: 'appId',
    -          appSecret: 'appSecret',
    -        };
    -        await provider.validateAuthData({ id: 'userId' }, params);
    -        params.appVersion = '5.123';
    -      }
    -      await provider.validateAuthData({ id: 'userId' }, params);
    -    });
    -  });
    -
       const getMockMyOauthProvider = function () {
         return {
           authData: {
    @@ -568,46 +477,6 @@ describe('AuthenticationProviders', function () {
       });
     });
     
    -describe('instagram auth adapter', () => {
    -  const instagram = require('../lib/Adapters/Auth/instagram');
    -  const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
    -
    -  it('should use default api', async () => {
    -    spyOn(httpsRequest, 'get').and.callFake(() => {
    -      return Promise.resolve({ data: { id: 'userId' } });
    -    });
    -    await instagram.validateAuthData({ id: 'userId', access_token: 'the_token' }, {});
    -    expect(httpsRequest.get).toHaveBeenCalledWith(
    -      'https://graph.instagram.com/me?fields=id&access_token=the_token'
    -    );
    -  });
    -  it('response object without data child', async () => {
    -    spyOn(httpsRequest, 'get').and.callFake(() => {
    -      return Promise.resolve({ id: 'userId' });
    -    });
    -    await instagram.validateAuthData({ id: 'userId', access_token: 'the_token' }, {});
    -    expect(httpsRequest.get).toHaveBeenCalledWith(
    -      'https://graph.instagram.com/me?fields=id&access_token=the_token'
    -    );
    -  });
    -  it('should pass in api url', async () => {
    -    spyOn(httpsRequest, 'get').and.callFake(() => {
    -      return Promise.resolve({ data: { id: 'userId' } });
    -    });
    -    await instagram.validateAuthData(
    -      {
    -        id: 'userId',
    -        access_token: 'the_token',
    -        apiURL: 'https://new-api.instagram.com/v1/',
    -      },
    -      {}
    -    );
    -    expect(httpsRequest.get).toHaveBeenCalledWith(
    -      'https://new-api.instagram.com/v1/me?fields=id&access_token=the_token'
    -    );
    -  });
    -});
    -
     describe('google auth adapter', () => {
       const google = require('../lib/Adapters/Auth/google');
       const jwt = require('jsonwebtoken');
    @@ -730,35 +599,6 @@ describe('google auth adapter', () => {
       });
     });
     
    -describe('google play games service auth', () => {
    -  const gpgames = require('../lib/Adapters/Auth/gpgames');
    -  const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
    -
    -  it('validateAuthData should pass validation', async () => {
    -    spyOn(httpsRequest, 'get').and.callFake(() => {
    -      return Promise.resolve({ playerId: 'userId' });
    -    });
    -    await gpgames.validateAuthData({
    -      id: 'userId',
    -      access_token: 'access_token',
    -    });
    -  });
    -
    -  it('validateAuthData should throw error', async () => {
    -    spyOn(httpsRequest, 'get').and.callFake(() => {
    -      return Promise.resolve({ playerId: 'invalid' });
    -    });
    -    try {
    -      await gpgames.validateAuthData({
    -        id: 'userId',
    -        access_token: 'access_token',
    -      });
    -    } catch (e) {
    -      expect(e.message).toBe('Google Play Games Services - authData is invalid for this user.');
    -    }
    -  });
    -});
    -
     describe('keycloak auth adapter', () => {
       const keycloak = require('../lib/Adapters/Auth/keycloak');
       const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
    @@ -987,433 +827,6 @@ describe('keycloak auth adapter', () => {
       });
     });
     
    -describe('oauth2 auth adapter', () => {
    -  const oauth2 = require('../lib/Adapters/Auth/oauth2');
    -  const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
    -
    -  it('properly loads OAuth2 adapter via the "oauth2" option', () => {
    -    const options = {
    -      oauth2Authentication: {
    -        oauth2: true,
    -      },
    -    };
    -    const loadedAuthAdapter = authenticationLoader.loadAuthAdapter('oauth2Authentication', options);
    -    expect(loadedAuthAdapter.adapter).toEqual(oauth2);
    -  });
    -
    -  it('properly loads OAuth2 adapter with options', () => {
    -    const options = {
    -      oauth2Authentication: {
    -        oauth2: true,
    -        tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
    -        useridField: 'sub',
    -        appidField: 'appId',
    -        appIds: ['a', 'b'],
    -        authorizationHeader: 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=',
    -        debug: true,
    -      },
    -    };
    -    const loadedAuthAdapter = authenticationLoader.loadAuthAdapter('oauth2Authentication', options);
    -    const appIds = loadedAuthAdapter.appIds;
    -    const providerOptions = loadedAuthAdapter.providerOptions;
    -    expect(providerOptions.tokenIntrospectionEndpointUrl).toEqual('https://example.com/introspect');
    -    expect(providerOptions.useridField).toEqual('sub');
    -    expect(providerOptions.appidField).toEqual('appId');
    -    expect(appIds).toEqual(['a', 'b']);
    -    expect(providerOptions.authorizationHeader).toEqual('Basic dXNlcm5hbWU6cGFzc3dvcmQ=');
    -    expect(providerOptions.debug).toEqual(true);
    -  });
    -
    -  it('validateAppId should fail if OAuth2 tokenIntrospectionEndpointUrl is not configured properly', async () => {
    -    const options = {
    -      oauth2Authentication: {
    -        oauth2: true,
    -        appIds: ['a', 'b'],
    -        appidField: 'appId',
    -      },
    -    };
    -    const authData = {
    -      id: 'fakeid',
    -      access_token: 'sometoken',
    -    };
    -    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
    -      'oauth2Authentication',
    -      options
    -    );
    -    try {
    -      await adapter.validateAppId(appIds, authData, providerOptions);
    -    } catch (e) {
    -      expect(e.message).toBe(
    -        'OAuth2 token introspection endpoint URL is missing from configuration!'
    -      );
    -    }
    -  });
    -
    -  it('validateAppId appidField optional', async () => {
    -    const options = {
    -      oauth2Authentication: {
    -        oauth2: true,
    -        tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
    -      },
    -    };
    -    const authData = {
    -      id: 'fakeid',
    -      access_token: 'sometoken',
    -    };
    -    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
    -      'oauth2Authentication',
    -      options
    -    );
    -    try {
    -      await adapter.validateAppId(appIds, authData, providerOptions);
    -    } catch (e) {
    -      // Should not reach here
    -      fail(e);
    -    }
    -  });
    -
    -  it('validateAppId should fail without appIds', async () => {
    -    const options = {
    -      oauth2Authentication: {
    -        oauth2: true,
    -        tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
    -        appidField: 'appId',
    -      },
    -    };
    -    const authData = {
    -      id: 'fakeid',
    -      access_token: 'sometoken',
    -    };
    -    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
    -      'oauth2Authentication',
    -      options
    -    );
    -    try {
    -      await adapter.validateAppId(appIds, authData, providerOptions);
    -    } catch (e) {
    -      expect(e.message).toBe(
    -        'OAuth2 configuration is missing the client app IDs ("appIds" config parameter).'
    -      );
    -    }
    -  });
    -
    -  it('validateAppId should fail empty appIds', async () => {
    -    const options = {
    -      oauth2Authentication: {
    -        oauth2: true,
    -        tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
    -        appidField: 'appId',
    -        appIds: [],
    -      },
    -    };
    -    const authData = {
    -      id: 'fakeid',
    -      access_token: 'sometoken',
    -    };
    -    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
    -      'oauth2Authentication',
    -      options
    -    );
    -    try {
    -      await adapter.validateAppId(appIds, authData, providerOptions);
    -    } catch (e) {
    -      expect(e.message).toBe(
    -        'OAuth2 configuration is missing the client app IDs ("appIds" config parameter).'
    -      );
    -    }
    -  });
    -
    -  it('validateAppId invalid accessToken', async () => {
    -    const options = {
    -      oauth2Authentication: {
    -        oauth2: true,
    -        tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
    -        appidField: 'appId',
    -        appIds: ['a', 'b'],
    -      },
    -    };
    -    const authData = {
    -      id: 'fakeid',
    -      access_token: 'sometoken',
    -    };
    -    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
    -      'oauth2Authentication',
    -      options
    -    );
    -    spyOn(httpsRequest, 'request').and.callFake(() => {
    -      return Promise.resolve({});
    -    });
    -    try {
    -      await adapter.validateAppId(appIds, authData, providerOptions);
    -    } catch (e) {
    -      expect(e.message).toBe('OAuth2 access token is invalid for this user.');
    -    }
    -  });
    -
    -  it('validateAppId invalid accessToken appId', async () => {
    -    const options = {
    -      oauth2Authentication: {
    -        oauth2: true,
    -        tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
    -        appidField: 'appId',
    -        appIds: ['a', 'b'],
    -      },
    -    };
    -    const authData = {
    -      id: 'fakeid',
    -      access_token: 'sometoken',
    -    };
    -    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
    -      'oauth2Authentication',
    -      options
    -    );
    -    spyOn(httpsRequest, 'request').and.callFake(() => {
    -      return Promise.resolve({ active: true });
    -    });
    -    try {
    -      await adapter.validateAppId(appIds, authData, providerOptions);
    -    } catch (e) {
    -      expect(e.message).toBe(
    -        "OAuth2: the access_token's appID is empty or is not in the list of permitted appIDs in the auth configuration."
    -      );
    -    }
    -  });
    -
    -  it('validateAppId valid accessToken appId', async () => {
    -    const options = {
    -      oauth2Authentication: {
    -        oauth2: true,
    -        tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
    -        appidField: 'appId',
    -        appIds: ['a', 'b'],
    -      },
    -    };
    -    const authData = {
    -      id: 'fakeid',
    -      access_token: 'sometoken',
    -    };
    -    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
    -      'oauth2Authentication',
    -      options
    -    );
    -    spyOn(httpsRequest, 'request').and.callFake(() => {
    -      return Promise.resolve({
    -        active: true,
    -        appId: 'a',
    -      });
    -    });
    -    try {
    -      await adapter.validateAppId(appIds, authData, providerOptions);
    -    } catch (e) {
    -      // Should not enter here
    -      fail(e);
    -    }
    -  });
    -
    -  it('validateAppId valid accessToken appId array', async () => {
    -    const options = {
    -      oauth2Authentication: {
    -        oauth2: true,
    -        tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
    -        appidField: 'appId',
    -        appIds: ['a', 'b'],
    -      },
    -    };
    -    const authData = {
    -      id: 'fakeid',
    -      access_token: 'sometoken',
    -    };
    -    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
    -      'oauth2Authentication',
    -      options
    -    );
    -    spyOn(httpsRequest, 'request').and.callFake(() => {
    -      return Promise.resolve({
    -        active: true,
    -        appId: ['a'],
    -      });
    -    });
    -    try {
    -      await adapter.validateAppId(appIds, authData, providerOptions);
    -    } catch (e) {
    -      // Should not enter here
    -      fail(e);
    -    }
    -  });
    -
    -  it('validateAppId valid accessToken invalid appId', async () => {
    -    const options = {
    -      oauth2Authentication: {
    -        oauth2: true,
    -        tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
    -        appidField: 'appId',
    -        appIds: ['a', 'b'],
    -      },
    -    };
    -    const authData = {
    -      id: 'fakeid',
    -      access_token: 'sometoken',
    -    };
    -    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
    -      'oauth2Authentication',
    -      options
    -    );
    -    spyOn(httpsRequest, 'request').and.callFake(() => {
    -      return Promise.resolve({
    -        active: true,
    -        appId: 'unknown',
    -      });
    -    });
    -    try {
    -      await adapter.validateAppId(appIds, authData, providerOptions);
    -    } catch (e) {
    -      expect(e.message).toBe(
    -        "OAuth2: the access_token's appID is empty or is not in the list of permitted appIDs in the auth configuration."
    -      );
    -    }
    -  });
    -
    -  it('validateAuthData should fail if OAuth2 tokenIntrospectionEndpointUrl is not configured properly', async () => {
    -    const options = {
    -      oauth2Authentication: {
    -        oauth2: true,
    -      },
    -    };
    -    const authData = {
    -      id: 'fakeid',
    -      access_token: 'sometoken',
    -    };
    -    const { adapter, providerOptions } = authenticationLoader.loadAuthAdapter(
    -      'oauth2Authentication',
    -      options
    -    );
    -    try {
    -      await adapter.validateAuthData(authData, providerOptions);
    -    } catch (e) {
    -      expect(e.message).toBe(
    -        'OAuth2 token introspection endpoint URL is missing from configuration!'
    -      );
    -    }
    -  });
    -
    -  it('validateAuthData invalid accessToken', async () => {
    -    const options = {
    -      oauth2Authentication: {
    -        oauth2: true,
    -        tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
    -        useridField: 'sub',
    -        appidField: 'appId',
    -        appIds: ['a', 'b'],
    -        authorizationHeader: 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=',
    -      },
    -    };
    -    const authData = {
    -      id: 'fakeid',
    -      access_token: 'sometoken',
    -    };
    -    const { adapter, providerOptions } = authenticationLoader.loadAuthAdapter(
    -      'oauth2Authentication',
    -      options
    -    );
    -    spyOn(httpsRequest, 'request').and.callFake(() => {
    -      return Promise.resolve({});
    -    });
    -    try {
    -      await adapter.validateAuthData(authData, providerOptions);
    -    } catch (e) {
    -      expect(e.message).toBe('OAuth2 access token is invalid for this user.');
    -    }
    -    expect(httpsRequest.request).toHaveBeenCalledWith(
    -      {
    -        hostname: 'example.com',
    -        path: '/introspect',
    -        method: 'POST',
    -        headers: {
    -          'Content-Type': 'application/x-www-form-urlencoded',
    -          'Content-Length': 15,
    -          Authorization: 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=',
    -        },
    -      },
    -      'token=sometoken'
    -    );
    -  });
    -
    -  it('validateAuthData valid accessToken', async () => {
    -    const options = {
    -      oauth2Authentication: {
    -        oauth2: true,
    -        tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
    -        useridField: 'sub',
    -        appidField: 'appId',
    -        appIds: ['a', 'b'],
    -      },
    -    };
    -    const authData = {
    -      id: 'fakeid',
    -      access_token: 'sometoken',
    -    };
    -    const { adapter, providerOptions } = authenticationLoader.loadAuthAdapter(
    -      'oauth2Authentication',
    -      options
    -    );
    -    spyOn(httpsRequest, 'request').and.callFake(() => {
    -      return Promise.resolve({
    -        active: true,
    -        sub: 'fakeid',
    -      });
    -    });
    -    try {
    -      await adapter.validateAuthData(authData, providerOptions);
    -    } catch (e) {
    -      // Should not enter here
    -      fail(e);
    -    }
    -    expect(httpsRequest.request).toHaveBeenCalledWith(
    -      {
    -        hostname: 'example.com',
    -        path: '/introspect',
    -        method: 'POST',
    -        headers: {
    -          'Content-Type': 'application/x-www-form-urlencoded',
    -          'Content-Length': 15,
    -        },
    -      },
    -      'token=sometoken'
    -    );
    -  });
    -
    -  it('validateAuthData valid accessToken without useridField', async () => {
    -    const options = {
    -      oauth2Authentication: {
    -        oauth2: true,
    -        tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
    -        appidField: 'appId',
    -        appIds: ['a', 'b'],
    -      },
    -    };
    -    const authData = {
    -      id: 'fakeid',
    -      access_token: 'sometoken',
    -    };
    -    const { adapter, providerOptions } = authenticationLoader.loadAuthAdapter(
    -      'oauth2Authentication',
    -      options
    -    );
    -    spyOn(httpsRequest, 'request').and.callFake(() => {
    -      return Promise.resolve({
    -        active: true,
    -        sub: 'fakeid',
    -      });
    -    });
    -    try {
    -      await adapter.validateAuthData(authData, providerOptions);
    -    } catch (e) {
    -      // Should not enter here
    -      fail(e);
    -    }
    -  });
    -});
    -
     describe('apple signin auth adapter', () => {
       const apple = require('../lib/Adapters/Auth/apple');
       const jwt = require('jsonwebtoken');
    @@ -1722,206 +1135,17 @@ describe('apple signin auth adapter', () => {
       });
     });
     
    -describe('Apple Game Center Auth adapter', () => {
    -  const gcenter = require('../lib/Adapters/Auth/gcenter');
    -  const fs = require('fs');
    -  const testCert = fs.readFileSync(__dirname + '/support/cert/game_center.pem');
    -  const testCert2 = fs.readFileSync(__dirname + '/support/cert/game_center.pem');
    -
    -  it('can load adapter', async () => {
    -    const options = {
    -      gcenter: {
    -        rootCertificateUrl:
    -          'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
    -      },
    -    };
    -    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
    -      'gcenter',
    -      options
    -    );
    -    await adapter.validateAppId(
    -      appIds,
    -      { publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
    -      providerOptions
    -    );
    -  });
    -
    -  it('validateAuthData should validate', async () => {
    -    const options = {
    -      gcenter: {
    -        rootCertificateUrl:
    -          'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
    -      },
    -    };
    -    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
    -      'gcenter',
    -      options
    -    );
    -    await adapter.validateAppId(
    -      appIds,
    -      { publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
    -      providerOptions
    -    );
    -    // real token is used
    -    const authData = {
    -      id: 'G:1965586982',
    -      publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
    -      timestamp: 1565257031287,
    -      signature:
    -        'uqLBTr9Uex8zCpc1UQ1MIDMitb+HUat2Mah4Kw6AVLSGe0gGNJXlih2i5X+0ZwVY0S9zY2NHWi2gFjmhjt/4kxWGMkupqXX5H/qhE2m7hzox6lZJpH98ZEUbouWRfZX2ZhUlCkAX09oRNi7fI7mWL1/o88MaI/y6k6tLr14JTzmlxgdyhw+QRLxRPA6NuvUlRSJpyJ4aGtNH5/wHdKQWL8nUnFYiYmaY8R7IjzNxPfy8UJTUWmeZvMSgND4u8EjADPsz7ZtZyWAPi8kYcAb6M8k0jwLD3vrYCB8XXyO2RQb/FY2TM4zJuI7PzLlvvgOJXbbfVtHx7Evnm5NYoyzgzw==',
    -      salt: 'DzqqrQ==',
    -      bundleId: 'cloud.xtralife.gamecenterauth',
    -    };
    -    gcenter.cache['https://static.gc.apple.com/public-key/gc-prod-4.cer'] = testCert;
    -    await gcenter.validateAuthData(authData);
    -  });
    -
    -  it('validateAuthData invalid signature id', async () => {
    -    gcenter.cache['https://static.gc.apple.com/public-key/gc-prod-4.cer'] = testCert;
    -    gcenter.cache['https://static.gc.apple.com/public-key/gc-prod-6.cer'] = testCert2;
    -    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
    -      'gcenter',
    -      {}
    -    );
    -    await adapter.validateAppId(
    -      appIds,
    -      { publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
    -      providerOptions
    -    );
    -    const authData = {
    -      id: 'G:1965586982',
    -      publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-6.cer',
    -      timestamp: 1565257031287,
    -      signature: '1234',
    -      salt: 'DzqqrQ==',
    -      bundleId: 'com.example.com',
    -    };
    -    await expectAsync(gcenter.validateAuthData(authData)).toBeRejectedWith(
    -      new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Apple Game Center - invalid signature')
    -    );
    -  });
    -
    -  it('validateAuthData invalid public key http url', async () => {
    -    const options = {
    -      gcenter: {
    -        rootCertificateUrl:
    -          'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
    -      },
    -    };
    -    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
    -      'gcenter',
    -      options
    -    );
    -    await adapter.validateAppId(
    -      appIds,
    -      { publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
    -      providerOptions
    -    );
    -    const publicKeyUrls = [
    -      'example.com',
    -      'http://static.gc.apple.com/public-key/gc-prod-4.cer',
    -      'https://developer.apple.com/assets/elements/badges/download-on-the-app-store.svg',
    -      'https://example.com/ \\.apple.com/public_key.cer',
    -      'https://example.com/ &.apple.com/public_key.cer',
    -    ];
    -    await Promise.all(
    -      publicKeyUrls.map(publicKeyUrl =>
    -        expectAsync(
    -          gcenter.validateAuthData({
    -            id: 'G:1965586982',
    -            timestamp: 1565257031287,
    -            publicKeyUrl,
    -            signature: '1234',
    -            salt: 'DzqqrQ==',
    -            bundleId: 'com.example.com',
    -          })
    -        ).toBeRejectedWith(
    -          new Parse.Error(
    -            Parse.Error.SCRIPT_FAILED,
    -            `Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}`
    -          )
    -        )
    -      )
    -    );
    -  });
    -
    -  it('should not validate Symantec Cert', async () => {
    -    const options = {
    -      gcenter: {
    -        rootCertificateUrl:
    -          'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
    -      },
    -    };
    -    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
    -      'gcenter',
    -      options
    -    );
    -    await adapter.validateAppId(
    -      appIds,
    -      { publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
    -      providerOptions
    -    );
    -    expect(() =>
    -      gcenter.verifyPublicKeyIssuer(
    -        testCert,
    -        'https://static.gc.apple.com/public-key/gc-prod-4.cer'
    -      )
    -    );
    -  });
    -
    -  it('adapter should load default cert', async () => {
    -    const options = {
    -      gcenter: {},
    -    };
    -    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
    -      'gcenter',
    -      options
    -    );
    -    await adapter.validateAppId(
    -      appIds,
    -      { publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
    -      providerOptions
    -    );
    -    const previous = new Date();
    -    await adapter.validateAppId(
    -      appIds,
    -      { publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
    -      providerOptions
    -    );
    -
    -    const duration = new Date().getTime() - previous.getTime();
    -    expect(duration <= 1).toBe(true);
    -  });
    -
    -  it('adapter should throw', async () => {
    -    const options = {
    -      gcenter: {
    -        rootCertificateUrl: 'https://example.com',
    -      },
    -    };
    -    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
    -      'gcenter',
    -      options
    -    );
    -    await expectAsync(
    -      adapter.validateAppId(
    -        appIds,
    -        { publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
    -        providerOptions
    -      )
    -    ).toBeRejectedWith(
    -      new Parse.Error(
    -        Parse.Error.OBJECT_NOT_FOUND,
    -        'Apple Game Center auth adapter parameter `rootCertificateURL` is invalid.'
    -      )
    -    );
    -  });
    -});
    -
     describe('phant auth adapter', () => {
       const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
     
       it('validateAuthData should throw for invalid auth', async () => {
    +    await reconfigureServer({
    +      auth: {
    +        phantauth: {
    +          enableInsecureAuth: true,
    +        }
    +      }
    +    })
         const authData = {
           id: 'fakeid',
           access_token: 'sometoken',
    @@ -1938,34 +1162,6 @@ describe('phant auth adapter', () => {
       });
     });
     
    -describe('microsoft graph auth adapter', () => {
    -  const microsoft = require('../lib/Adapters/Auth/microsoft');
    -  const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
    -
    -  it('should use access_token for validation is passed and responds with id and mail', async () => {
    -    spyOn(httpsRequest, 'get').and.callFake(() => {
    -      return Promise.resolve({ id: 'userId', mail: 'userMail' });
    -    });
    -    await microsoft.validateAuthData({
    -      id: 'userId',
    -      access_token: 'the_token',
    -    });
    -  });
    -
    -  it('should fail to validate Microsoft Graph auth with bad token', done => {
    -    const authData = {
    -      id: 'fake-id',
    -      mail: 'fake@mail.com',
    -      access_token: 'very.long.bad.token',
    -    };
    -    microsoft.validateAuthData(authData).then(done.fail, err => {
    -      expect(err.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
    -      expect(err.message).toBe('Microsoft Graph auth is invalid for this user.');
    -      done();
    -    });
    -  });
    -});
    -
     describe('facebook limited auth adapter', () => {
       const facebook = require('../lib/Adapters/Auth/facebook');
       const jwt = require('jsonwebtoken');
    
  • spec/AuthenticationAdaptersV2.spec.js+3 3 modified
    @@ -355,16 +355,16 @@ describe('Auth Adapter features', () => {
         const authData = user.get('authData').modernAdapter3;
         expect(authData).toEqual({ foo: 'bar' });
         for (const call of afterSpy.calls.all()) {
    -      const args = call.args[0];
    +      const args = call.args[2];
           if (args.user) {
             user._objCount = args.user._objCount;
             break;
           }
         }
         expect(afterSpy).toHaveBeenCalledWith(
    -      { ip: '127.0.0.1', user, master: false },
           { id: 'modernAdapter3Data' },
    -      undefined
    +      undefined,
    +      { ip: '127.0.0.1', user, master: false },
         );
         expect(spy).toHaveBeenCalled();
       });
    
  • spec/eslint.config.js+1 0 modified
    @@ -9,6 +9,7 @@ module.exports = [
           globals: {
             ...globals.node,
             ...globals.jasmine,
    +        mockFetch: "readonly",
             Parse: "readonly",
             reconfigureServer: "readonly",
             createTestUser: "readonly",
    
  • spec/helper.js+20 0 modified
    @@ -414,6 +414,25 @@ function mockShortLivedAuth() {
       return auth;
     }
     
    +function mockFetch(mockResponses) {
    +  global.fetch = jasmine.createSpy('fetch').and.callFake((url, options = { }) => {
    +    options.method ||= 'GET';
    +    const mockResponse = mockResponses.find(
    +      (mock) => mock.url === url && mock.method === options.method
    +    );
    +
    +    if (mockResponse) {
    +      return Promise.resolve(mockResponse.response);
    +    }
    +
    +    return Promise.resolve({
    +      ok: false,
    +      statusText: 'Unknown URL or method',
    +    });
    +  });
    +}
    +
    +
     // This is polluting, but, it makes it way easier to directly port old tests.
     global.Parse = Parse;
     global.TestObject = TestObject;
    @@ -429,6 +448,7 @@ global.arrayContains = arrayContains;
     global.jequal = jequal;
     global.range = range;
     global.reconfigureServer = reconfigureServer;
    +global.mockFetch = mockFetch;
     global.defaultConfiguration = defaultConfiguration;
     global.mockCustomAuthenticator = mockCustomAuthenticator;
     global.mockFacebookAuthenticator = mockFacebookAuthenticator;
    
  • spec/SecurityCheckGroups.spec.js+3 0 modified
    @@ -32,13 +32,15 @@ describe('Security Check Groups', () => {
           config.masterKey = 'aMoreSecur3Passwor7!';
           config.security.enableCheckLog = false;
           config.allowClientClassCreation = false;
    +      config.enableInsecureAuthAdapters = false;
           await reconfigureServer(config);
     
           const group = new CheckGroupServerConfig();
           await group.run();
           expect(group.checks()[0].checkState()).toBe(CheckState.success);
           expect(group.checks()[1].checkState()).toBe(CheckState.success);
           expect(group.checks()[2].checkState()).toBe(CheckState.success);
    +      expect(group.checks()[4].checkState()).toBe(CheckState.success);
         });
     
         it('checks fail correctly', async () => {
    @@ -52,6 +54,7 @@ describe('Security Check Groups', () => {
           expect(group.checks()[0].checkState()).toBe(CheckState.fail);
           expect(group.checks()[1].checkState()).toBe(CheckState.fail);
           expect(group.checks()[2].checkState()).toBe(CheckState.fail);
    +      expect(group.checks()[4].checkState()).toBe(CheckState.fail);
         });
       });
     
    
  • spec/support/cert/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem+38 0 added
    @@ -0,0 +1,38 @@
    +-----BEGIN CERTIFICATE-----
    +MIIGsDCCBJigAwIBAgIQCK1AsmDSnEyfXs2pvZOu2TANBgkqhkiG9w0BAQwFADBi
    +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
    +d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg
    +RzQwHhcNMjEwNDI5MDAwMDAwWhcNMzYwNDI4MjM1OTU5WjBpMQswCQYDVQQGEwJV
    +UzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRy
    +dXN0ZWQgRzQgQ29kZSBTaWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMIIC
    +IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1bQvQtAorXi3XdU5WRuxiEL1
    +M4zrPYGXcMW7xIUmMJ+kjmjYXPXrNCQH4UtP03hD9BfXHtr50tVnGlJPDqFX/IiZ
    +wZHMgQM+TXAkZLON4gh9NH1MgFcSa0OamfLFOx/y78tHWhOmTLMBICXzENOLsvsI
    +8IrgnQnAZaf6mIBJNYc9URnokCF4RS6hnyzhGMIazMXuk0lwQjKP+8bqHPNlaJGi
    +TUyCEUhSaN4QvRRXXegYE2XFf7JPhSxIpFaENdb5LpyqABXRN/4aBpTCfMjqGzLm
    +ysL0p6MDDnSlrzm2q2AS4+jWufcx4dyt5Big2MEjR0ezoQ9uo6ttmAaDG7dqZy3S
    +vUQakhCBj7A7CdfHmzJawv9qYFSLScGT7eG0XOBv6yb5jNWy+TgQ5urOkfW+0/tv
    +k2E0XLyTRSiDNipmKF+wc86LJiUGsoPUXPYVGUztYuBeM/Lo6OwKp7ADK5GyNnm+
    +960IHnWmZcy740hQ83eRGv7bUKJGyGFYmPV8AhY8gyitOYbs1LcNU9D4R+Z1MI3s
    +MJN2FKZbS110YU0/EpF23r9Yy3IQKUHw1cVtJnZoEUETWJrcJisB9IlNWdt4z4FK
    +PkBHX8mBUHOFECMhWWCKZFTBzCEa6DgZfGYczXg4RTCZT/9jT0y7qg0IU0F8WD1H
    +s/q27IwyCQLMbDwMVhECAwEAAaOCAVkwggFVMBIGA1UdEwEB/wQIMAYBAf8CAQAw
    +HQYDVR0OBBYEFGg34Ou2O/hfEYb7/mF7CIhl9E5CMB8GA1UdIwQYMBaAFOzX44LS
    +cV1kTN8uZz/nupiuHA9PMA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEF
    +BQcDAzB3BggrBgEFBQcBAQRrMGkwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRp
    +Z2ljZXJ0LmNvbTBBBggrBgEFBQcwAoY1aHR0cDovL2NhY2VydHMuZGlnaWNlcnQu
    +Y29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcnQwQwYDVR0fBDwwOjA4oDagNIYy
    +aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5j
    +cmwwHAYDVR0gBBUwEzAHBgVngQwBAzAIBgZngQwBBAEwDQYJKoZIhvcNAQEMBQAD
    +ggIBADojRD2NCHbuj7w6mdNW4AIapfhINPMstuZ0ZveUcrEAyq9sMCcTEp6QRJ9L
    +/Z6jfCbVN7w6XUhtldU/SfQnuxaBRVD9nL22heB2fjdxyyL3WqqQz/WTauPrINHV
    +UHmImoqKwba9oUgYftzYgBoRGRjNYZmBVvbJ43bnxOQbX0P4PpT/djk9ntSZz0rd
    +KOtfJqGVWEjVGv7XJz/9kNF2ht0csGBc8w2o7uCJob054ThO2m67Np375SFTWsPK
    +6Wrxoj7bQ7gzyE84FJKZ9d3OVG3ZXQIUH0AzfAPilbLCIXVzUstG2MQ0HKKlS43N
    +b3Y3LIU/Gs4m6Ri+kAewQ3+ViCCCcPDMyu/9KTVcH4k4Vfc3iosJocsL6TEa/y4Z
    +XDlx4b6cpwoG1iZnt5LmTl/eeqxJzy6kdJKt2zyknIYf48FWGysj/4+16oh7cGvm
    +oLr9Oj9FpsToFpFSi0HASIRLlk2rREDjjfAVKM7t8RhWByovEMQMCGQ8M4+uKIw8
    +y4+ICw2/O/TOHnuO77Xry7fwdxPm5yg/rBKupS8ibEH5glwVZsxsDsrFhsP2JjMM
    +B0ug0wcCampAMEhLNKhRILutG4UI4lkNbcoFUCvqShyepf2gpx8GdOfy1lKQ/a+F
    +SCH5Vzu0nAPthkX0tGFuv2jiJmCG6sivqf6UHedjGzqGVnhO
    +-----END CERTIFICATE-----
    
  • spec/support/cert/gc-prod-4.cer+0 0 added
  • spec/support/jasmine.json+1 1 modified
    @@ -1,6 +1,6 @@
     {
       "spec_dir": "spec",
    -  "spec_files": ["*spec.js"],
    +  "spec_files": ["**/*.[sS]pec.js"],
       "helpers": ["helper.js"],
       "random": true
     }
    
  • spec/TwitterAuth.spec.js+0 97 removed
    @@ -1,97 +0,0 @@
    -const twitter = require('../lib/Adapters/Auth/twitter');
    -
    -describe('Twitter Auth', () => {
    -  it('should use the proper configuration', () => {
    -    // Multiple options, consumer_key found
    -    expect(
    -      twitter.handleMultipleConfigurations(
    -        {
    -          consumer_key: 'hello',
    -        },
    -        [
    -          {
    -            consumer_key: 'hello',
    -          },
    -          {
    -            consumer_key: 'world',
    -          },
    -        ]
    -      ).consumer_key
    -    ).toEqual('hello');
    -
    -    // Multiple options, consumer_key not found
    -    expect(function () {
    -      twitter.handleMultipleConfigurations(
    -        {
    -          consumer_key: 'some',
    -        },
    -        [
    -          {
    -            consumer_key: 'hello',
    -          },
    -          {
    -            consumer_key: 'world',
    -          },
    -        ]
    -      );
    -    }).toThrow();
    -
    -    // Multiple options, consumer_key not found
    -    expect(function () {
    -      twitter.handleMultipleConfigurations(
    -        {
    -          auth_token: 'token',
    -        },
    -        [
    -          {
    -            consumer_key: 'hello',
    -          },
    -          {
    -            consumer_key: 'world',
    -          },
    -        ]
    -      );
    -    }).toThrow();
    -
    -    // Single configuration and consumer_key set
    -    expect(
    -      twitter.handleMultipleConfigurations(
    -        {
    -          consumer_key: 'hello',
    -        },
    -        {
    -          consumer_key: 'hello',
    -        }
    -      ).consumer_key
    -    ).toEqual('hello');
    -
    -    // General case, only 1 config, no consumer_key set
    -    expect(
    -      twitter.handleMultipleConfigurations(
    -        {
    -          auth_token: 'token',
    -        },
    -        {
    -          consumer_key: 'hello',
    -        }
    -      ).consumer_key
    -    ).toEqual('hello');
    -  });
    -
    -  it('Should fail with missing options', done => {
    -    try {
    -      twitter.validateAuthData(
    -        {
    -          consumer_key: 'key',
    -          consumer_secret: 'secret',
    -          auth_token: 'token',
    -          auth_token_secret: 'secret',
    -        },
    -        undefined
    -      );
    -    } catch (error) {
    -      jequal(error.message, 'Twitter auth configuration missing');
    -      done();
    -    }
    -  });
    -});
    
  • src/Adapters/Auth/apple.js+44 0 modified
    @@ -1,3 +1,47 @@
    +/**
    + * Parse Server authentication adapter for Apple.
    + *
    + * @class AppleAdapter
    + * @param {Object} options - Configuration options for the adapter.
    + * @param {string} options.clientId - Your Apple App ID.
    + *
    + * @param {Object} authData - The authentication data provided by the client.
    + * @param {string} authData.id - The user ID obtained from Apple.
    + * @param {string} authData.token - The token obtained from Apple.
    + *
    + * @description
    + * ## Parse Server Configuration
    + * To configure Parse Server for Apple authentication, use the following structure:
    + * ```json
    + * {
    + *   "auth": {
    + *     "apple": {
    + *       "clientId": "12345"
    + *     }
    + *   }
    + * }
    + * ```
    + *
    + * ## Expected `authData` from the Client
    + * The adapter expects the client to provide the following `authData` payload:
    + * - `authData.id` (**string**, required): The user ID obtained from Apple.
    + * - `authData.token` (**string**, required): The token obtained from Apple.
    + *
    + * Parse Server stores the required authentication data in the database.
    + *
    + * ### Example AuthData from Apple
    + * ```json
    + * {
    + *   "apple": {
    + *     "id": "1234567",
    + *     "token": "xxxxx.yyyyy.zzzzz"
    + *   }
    + * }
    + * ```
    + *
    + * @see {@link https://developer.apple.com/documentation/signinwithapplerestapi Sign in with Apple REST API Documentation}
    + */
    +
     // Apple SignIn Auth
     // https://developer.apple.com/documentation/signinwithapplerestapi
     
    
  • src/Adapters/Auth/AuthAdapter.js+17 8 modified
    @@ -40,35 +40,35 @@ export class AuthAdapter {
        * Legacy usage, if provided it will be triggered when authData related to this provider is touched (signup/update/login)
        * otherwise you should implement validateSetup, validateLogin and validateUpdate
        * @param {Object} authData The client provided authData
    -   * @param {Parse.Cloud.TriggerRequest} request
        * @param {Object} options additional adapter options
    +   * @param {Parse.Cloud.TriggerRequest} request
        * @returns {Promise<ParseAuthResponse|void|undefined>}
        */
    -  validateAuthData(authData, request, options) {
    +  validateAuthData(authData, options, request) {
         return Promise.resolve({});
       }
     
       /**
        * Triggered when user provide for the first time this auth provider
        * could be a register or the user adding a new auth service
        * @param {Object} authData The client provided authData
    -   * @param {Parse.Cloud.TriggerRequest} request
        * @param {Object} options additional adapter options
    +   * @param {Parse.Cloud.TriggerRequest} request
        * @returns {Promise<ParseAuthResponse|void|undefined>}
        */
    -  validateSetUp(authData, req, options) {
    +  validateSetUp(authData, options, req) {
         return Promise.resolve({});
       }
     
       /**
        * Triggered when user provide authData related to this provider
        * The user is not logged in and has already set this provider before
        * @param {Object} authData The client provided authData
    -   * @param {Parse.Cloud.TriggerRequest} request
        * @param {Object} options additional adapter options
    +   * @param {Parse.Cloud.TriggerRequest} request
        * @returns {Promise<ParseAuthResponse|void|undefined>}
        */
    -  validateLogin(authData, req, options) {
    +  validateLogin(authData, options, req) {
         return Promise.resolve({});
       }
     
    @@ -80,10 +80,18 @@ export class AuthAdapter {
        * @param {Parse.Cloud.TriggerRequest} request
        * @returns {Promise<ParseAuthResponse|void|undefined>}
        */
    -  validateUpdate(authData, req, options) {
    +  validateUpdate(authData, options, req) {
         return Promise.resolve({});
       }
     
    +  /**
    +   * Triggered when user is looked up by authData with this provider. Override the `id` field if needed.
    +   * @param {Object} authData The client provided authData
    +   */
    +  beforeFind(authData) {
    +
    +  }
    +
       /**
        * Triggered in pre authentication process if needed (like webauthn, SMS OTP)
        * @param {Object} challengeData Data provided by the client
    @@ -100,9 +108,10 @@ export class AuthAdapter {
        * Triggered when auth data is fetched
        * @param {Object} authData authData
        * @param {Object} options additional adapter options
    +   * @param {Parse.Cloud.TriggerRequest} request
        * @returns {Promise<Object>} Any overrides required to authData
        */
    -  afterFind(authData, options) {
    +  afterFind(authData, options, request) {
         return Promise.resolve({});
       }
     
    
  • src/Adapters/Auth/BaseCodeAuthAdapter.js+112 0 added
    @@ -0,0 +1,112 @@
    +// abstract class for auth code adapters
    +import AuthAdapter from './AuthAdapter';
    +export default class BaseAuthCodeAdapter extends AuthAdapter {
    +  constructor(adapterName) {
    +    super();
    +    this.adapterName = adapterName;
    +  }
    +  validateOptions(options) {
    +
    +    if (!options) {
    +      throw new Error(`${this.adapterName} options are required.`);
    +    }
    +
    +    this.enableInsecureAuth = options.enableInsecureAuth;
    +    if (this.enableInsecureAuth) {
    +      return;
    +    }
    +
    +    this.clientId = options.clientId;
    +    this.clientSecret = options.clientSecret;
    +
    +    if (!this.clientId) {
    +      throw new Error(`${this.adapterName} clientId is required.`);
    +    }
    +
    +    if (!this.clientSecret) {
    +      throw new Error(`${this.adapterName} clientSecret is required.`);
    +    }
    +  }
    +
    +  async beforeFind(authData) {
    +    if (this.enableInsecureAuth && !authData?.code) {
    +      if (!authData?.access_token) {
    +        throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `${this.adapterName} auth is invalid for this user.`);
    +      }
    +
    +      const user = await this.getUserFromAccessToken(authData.access_token, authData);
    +
    +      if (user.id !== authData.id) {
    +        throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `${this.adapterName} auth is invalid for this user.`);
    +      }
    +
    +      return;
    +    }
    +
    +    if (!authData?.code) {
    +      throw new Parse.Error(Parse.Error.VALIDATION_ERROR, `${this.adapterName} code is required.`);
    +    }
    +
    +    const access_token = await this.getAccessTokenFromCode(authData);
    +    const user = await this.getUserFromAccessToken(access_token, authData);
    +
    +    if (authData.id && user.id !== authData.id) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `${this.adapterName} auth is invalid for this user.`);
    +    }
    +
    +    authData.access_token = access_token;
    +    authData.id = user.id;
    +
    +    delete authData.code;
    +    delete authData.redirect_uri;
    +
    +  }
    +
    +  async getUserFromAccessToken() {
    +    // abstract method
    +    throw new Error('getUserFromAccessToken is not implemented');
    +  }
    +
    +  async getAccessTokenFromCode() {
    +    // abstract method
    +    throw new Error('getAccessTokenFromCode is not implemented');
    +  }
    +
    +  validateLogin(authData) {
    +    // User validation is already done in beforeFind
    +    return {
    +      id: authData.id,
    +    }
    +  }
    +
    +  validateSetUp(authData) {
    +    // User validation is already done in beforeFind
    +    return {
    +      id: authData.id,
    +    }
    +  }
    +
    +  afterFind(authData) {
    +    return {
    +      id: authData.id,
    +    }
    +  }
    +
    +  validateUpdate(authData) {
    +    // User validation is already done in beforeFind
    +    return {
    +      id: authData.id,
    +    }
    +
    +  }
    +
    +  parseResponseData(data) {
    +    const startPos = data.indexOf('(');
    +    const endPos = data.indexOf(')');
    +    if (startPos === -1 || endPos === -1) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `${this.adapterName} auth is invalid for this user.`);
    +    }
    +    const jsonData = data.substring(startPos + 1, endPos);
    +    return JSON.parse(jsonData);
    +  }
    +}
    
  • src/Adapters/Auth/facebook.js+60 0 modified
    @@ -1,3 +1,63 @@
    +/**
    + * Parse Server authentication adapter for Facebook.
    + *
    + * @class FacebookAdapter
    + * @param {Object} options - The adapter configuration options.
    + * @param {string} options.appSecret - Your Facebook App Secret. Required for secure authentication.
    + * @param {string[]} options.appIds - An array of Facebook App IDs. Required for validating the app.
    + *
    + * @description
    + * ## Parse Server Configuration
    + * To configure Parse Server for Facebook authentication, use the following structure:
    + * ```json
    + * {
    + *   "auth": {
    + *     "facebook": {
    + *       "appSecret": "your-app-secret",
    + *       "appIds": ["your-app-id"]
    + *     }
    + *   }
    + * }
    + * ```
    + *
    + * The adapter supports the following authentication methods:
    + * - **Standard Login**: Requires `id` and `access_token`.
    + * - **Limited Login**: Requires `id` and `token`.
    + *
    + * ## Auth Payloads
    + * ### Standard Login Payload
    + * ```json
    + * {
    + *   "facebook": {
    + *     "id": "1234567",
    + *     "access_token": "abc123def456ghi789"
    + *   }
    + * }
    + * ```
    + *
    + * ### Limited Login Payload
    + * ```json
    + * {
    + *   "facebook": {
    + *     "id": "1234567",
    + *     "token": "xxxxx.yyyyy.zzzzz"
    + *   }
    + * }
    + * ```
    + *
    + * ## Notes
    + * - **Standard Login**: Use `id` and `access_token` for full functionality.
    + * - **Limited Login**: Use `id` and `token` (JWT) when tracking is opted out (e.g., via Apple's App Tracking Transparency).
    + * - Supported Parse Server versions:
    + *   - `>= 6.5.6 < 7`
    + *   - `>= 7.0.1`
    + *
    + * Secure authentication is recommended to ensure proper data protection and compliance with Facebook's guidelines.
    + *
    + * @see {@link https://developers.facebook.com/docs/facebook-login/limited-login/ Facebook Limited Login}
    + * @see {@link https://developers.facebook.com/docs/facebook-login/facebook-login-for-business/ Facebook Login for Business}
    + */
    +
     // Helper functions for accessing the Facebook Graph API.
     const Parse = require('parse/node').Parse;
     const crypto = require('crypto');
    
  • src/Adapters/Auth/gcenter.js+209 165 modified
    @@ -1,195 +1,239 @@
    -/* Apple Game Center Auth
    -https://developer.apple.com/documentation/gamekit/gklocalplayer/1515407-generateidentityverificationsign#discussion
    -
    -const authData = {
    -  publicKeyUrl: 'https://valid.apple.com/public/timeout.cer',
    -  timestamp: 1460981421303,
    -  signature: 'PoDwf39DCN464B49jJCU0d9Y0J',
    -  salt: 'saltST==',
    -  bundleId: 'com.valid.app'
    -  id: 'playerId',
    -};
    -*/
    -
    -const { Parse } = require('parse/node');
    -const crypto = require('crypto');
    -const https = require('https');
    -const { pki } = require('node-forge');
    -const ca = { cert: null, url: null };
    -const cache = {}; // (publicKey -> cert) cache
    -
    -function verifyPublicKeyUrl(publicKeyUrl) {
    -  try {
    -    const regex = /^https:\/\/(?:[-_A-Za-z0-9]+\.){0,}apple\.com\/.*\.cer$/;
    -    return regex.test(publicKeyUrl);
    -  } catch (error) {
    -    return false;
    +/**
    + * Parse Server authentication adapter for Apple Game Center.
    + *
    + * @class AppleGameCenterAdapter
    + * @param {Object} options - Configuration options for the adapter.
    + * @param {string} options.bundleId - Your Apple Game Center bundle ID. Required for secure authentication.
    + * @param {boolean} [options.enableInsecureAuth=false] - **[DEPRECATED]** Enable insecure authentication (not recommended).
    + *
    + * @param {Object} authData - The authentication data provided by the client.
    + * @param {string} authData.id - The user ID obtained from Apple Game Center.
    + * @param {string} authData.publicKeyUrl - The public key URL obtained from Apple Game Center.
    + * @param {string} authData.timestamp - The timestamp obtained from Apple Game Center.
    + * @param {string} authData.signature - The signature obtained from Apple Game Center.
    + * @param {string} authData.salt - The salt obtained from Apple Game Center.
    + * @param {string} [authData.bundleId] - **[DEPRECATED]** The bundle ID obtained from Apple Game Center (required for insecure authentication).
    + *
    + * @description
    + * ## Parse Server Configuration
    + * The following `authData` fields are required:
    + * `id`, `publicKeyUrl`, `timestamp`, `signature`, and `salt`. These fields are validated against the configured `bundleId` for additional security.
    + *
    + * To configure Parse Server for Apple Game Center authentication, use the following structure:
    + * ```json
    + * {
    + *  "auth": {
    + *    "gcenter": {
    + *     "bundleId": "com.valid.app"
    + *  }
    + * }
    + * ```
    + *
    + * ## Insecure Authentication (Not Recommended)
    + * The following `authData` fields are required for insecure authentication:
    + * `id`, `publicKeyUrl`, `timestamp`, `signature`, `salt`, and `bundleId` (**[DEPRECATED]**). This flow is insecure and poses potential security risks.
    + *
    + * To configure Parse Server for insecure authentication, use the following structure:
    + * ```json
    + * {
    + *   "auth": {
    + *    "gcenter": {
    + *      "enableInsecureAuth": true
    + *   }
    + * }
    + * ```
    + *
    + * ### Deprecation Notice
    + * The `enableInsecureAuth` option and `authData.bundleId` parameter are deprecated and may be removed in future releases. Use secure authentication with the `bundleId` configured in the `options` object instead.
    + *
    + *
    + * @example <caption>Secure Authentication Example</caption>
    + * // Example authData for secure authentication:
    + * const authData = {
    + *   gcenter: {
    + *     id: "1234567",
    + *     publicKeyUrl: "https://valid.apple.com/public/timeout.cer",
    + *     timestamp: 1460981421303,
    + *     salt: "saltST==",
    + *     signature: "PoDwf39DCN464B49jJCU0d9Y0J"
    + *   }
    + * };
    + *
    + * @example <caption>Insecure Authentication Example (Not Recommended)</caption>
    + * // Example authData for insecure authentication:
    + * const authData = {
    + *   gcenter: {
    + *     id: "1234567",
    + *     publicKeyUrl: "https://valid.apple.com/public/timeout.cer",
    + *     timestamp: 1460981421303,
    + *     salt: "saltST==",
    + *     signature: "PoDwf39DCN464B49jJCU0d9Y0J",
    + *     bundleId: "com.valid.app" // Deprecated.
    + *   }
    + * };
    + *
    + * @see {@link https://developer.apple.com/documentation/gamekit/gklocalplayer/3516283-fetchitems Apple Game Center Documentation}
    + */
    +/* global BigInt */
    +
    +import crypto from 'crypto';
    +import { asn1, pki } from 'node-forge';
    +import AuthAdapter from './AuthAdapter';
    +class GameCenterAuth extends AuthAdapter {
    +  constructor() {
    +    super();
    +    this.ca = { cert: null, url: null };
    +    this.cache = {};
    +    this.bundleId = '';
       }
    -}
     
    -function convertX509CertToPEM(X509Cert) {
    -  const pemPreFix = '-----BEGIN CERTIFICATE-----\n';
    -  const pemPostFix = '-----END CERTIFICATE-----';
    +  validateOptions(options) {
    +    if (!options) {
    +      throw new Error('Game center auth options are required.');
    +    }
     
    -  const base64 = X509Cert;
    -  const certBody = base64.match(new RegExp('.{0,64}', 'g')).join('\n');
    +    if (!this.loadingPromise) {
    +      this.loadingPromise = this.loadCertificate(options);
    +    }
     
    -  return pemPreFix + certBody + pemPostFix;
    -}
    +    this.enableInsecureAuth = options.enableInsecureAuth;
    +    this.bundleId = options.bundleId;
     
    -async function getAppleCertificate(publicKeyUrl) {
    -  if (!verifyPublicKeyUrl(publicKeyUrl)) {
    -    throw new Parse.Error(
    -      Parse.Error.OBJECT_NOT_FOUND,
    -      `Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}`
    -    );
    -  }
    -  if (cache[publicKeyUrl]) {
    -    return cache[publicKeyUrl];
    -  }
    -  const url = new URL(publicKeyUrl);
    -  const headOptions = {
    -    hostname: url.hostname,
    -    path: url.pathname,
    -    method: 'HEAD',
    -  };
    -  const cert_headers = await new Promise((resolve, reject) =>
    -    https.get(headOptions, res => resolve(res.headers)).on('error', reject)
    -  );
    -  const validContentTypes = ['application/x-x509-ca-cert', 'application/pkix-cert'];
    -  if (
    -    !validContentTypes.includes(cert_headers['content-type']) ||
    -    cert_headers['content-length'] == null ||
    -    cert_headers['content-length'] > 10000
    -  ) {
    -    throw new Parse.Error(
    -      Parse.Error.OBJECT_NOT_FOUND,
    -      `Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}`
    -    );
    +    if (!this.enableInsecureAuth && !this.bundleId) {
    +      throw new Error('bundleId is required for secure auth.');
    +    }
       }
    -  const { certificate, headers } = await getCertificate(publicKeyUrl);
    -  if (headers['cache-control']) {
    -    const expire = headers['cache-control'].match(/max-age=([0-9]+)/);
    -    if (expire) {
    -      cache[publicKeyUrl] = certificate;
    -      // we'll expire the cache entry later, as per max-age
    -      setTimeout(() => {
    -        delete cache[publicKeyUrl];
    -      }, parseInt(expire[1], 10) * 1000);
    +
    +  async loadCertificate(options) {
    +    const rootCertificateUrl =
    +      options.rootCertificateUrl ||
    +      'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem';
    +
    +    if (this.ca.url === rootCertificateUrl) {
    +      return rootCertificateUrl;
         }
    +
    +    const { certificate, headers } = await this.fetchCertificate(rootCertificateUrl);
    +
    +    if (
    +      headers.get('content-type') !== 'application/x-pem-file' ||
    +      !headers.get('content-length') ||
    +      parseInt(headers.get('content-length'), 10) > 10000
    +    ) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid rootCertificateURL.');
    +    }
    +
    +    this.ca.cert = pki.certificateFromPem(certificate);
    +    this.ca.url = rootCertificateUrl;
    +
    +    return rootCertificateUrl;
       }
    -  return verifyPublicKeyIssuer(certificate, publicKeyUrl);
    -}
     
    -function getCertificate(url, buffer) {
    -  return new Promise((resolve, reject) => {
    -    https
    -      .get(url, res => {
    -        const data = [];
    -        res.on('data', chunk => {
    -          data.push(chunk);
    -        });
    -        res.on('end', () => {
    -          if (buffer) {
    -            resolve({ certificate: Buffer.concat(data), headers: res.headers });
    -            return;
    -          }
    -          let cert = '';
    -          for (const chunk of data) {
    -            cert += chunk.toString('base64');
    -          }
    -          const certificate = convertX509CertToPEM(cert);
    -          resolve({ certificate, headers: res.headers });
    -        });
    -      })
    -      .on('error', reject);
    -  });
    -}
    +  verifyPublicKeyUrl(publicKeyUrl) {
    +    const regex = /^https:\/\/(?:[-_A-Za-z0-9]+\.){0,}apple\.com\/.*\.cer$/;
    +    return regex.test(publicKeyUrl);
    +  }
     
    -function convertTimestampToBigEndian(timestamp) {
    -  const buffer = Buffer.alloc(8);
    +  async fetchCertificate(url) {
    +    const response = await fetch(url);
    +    if (!response.ok) {
    +      throw new Error(`Failed to fetch certificate: ${url}`);
    +    }
     
    -  const high = ~~(timestamp / 0xffffffff);
    -  const low = timestamp % (0xffffffff + 0x1);
    +    const contentType = response.headers.get('content-type');
    +    const isPem = contentType?.includes('application/x-pem-file');
     
    -  buffer.writeUInt32BE(parseInt(high, 10), 0);
    -  buffer.writeUInt32BE(parseInt(low, 10), 4);
    +    if (isPem) {
    +      const certificate = await response.text();
    +      return { certificate, headers: response.headers };
    +    }
     
    -  return buffer;
    -}
    +    const data = await response.arrayBuffer();
    +    const binaryData = Buffer.from(data);
     
    -function verifySignature(publicKey, authData) {
    -  const verifier = crypto.createVerify('sha256');
    -  verifier.update(authData.playerId, 'utf8');
    -  verifier.update(authData.bundleId, 'utf8');
    -  verifier.update(convertTimestampToBigEndian(authData.timestamp));
    -  verifier.update(authData.salt, 'base64');
    +    const asn1Cert = asn1.fromDer(binaryData.toString('binary'));
    +    const forgeCert = pki.certificateFromAsn1(asn1Cert);
    +    const certificate = pki.certificateToPem(forgeCert);
     
    -  if (!verifier.verify(publicKey, authData.signature, 'base64')) {
    -    throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Apple Game Center - invalid signature');
    +    return { certificate, headers: response.headers };
       }
    -}
     
    -function verifyPublicKeyIssuer(cert, publicKeyUrl) {
    -  const publicKeyCert = pki.certificateFromPem(cert);
    -  if (!ca.cert) {
    -    throw new Parse.Error(
    -      Parse.Error.OBJECT_NOT_FOUND,
    -      'Apple Game Center auth adapter parameter `rootCertificateURL` is invalid.'
    -    );
    +  async getAppleCertificate(publicKeyUrl) {
    +    if (!this.verifyPublicKeyUrl(publicKeyUrl)) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `Invalid publicKeyUrl: ${publicKeyUrl}`);
    +    }
    +
    +    if (this.cache[publicKeyUrl]) {
    +      return this.cache[publicKeyUrl];
    +    }
    +
    +    const { certificate, headers } = await this.fetchCertificate(publicKeyUrl);
    +    const cacheControl = headers.get('cache-control');
    +    const expire = cacheControl?.match(/max-age=([0-9]+)/);
    +
    +    this.verifyPublicKeyIssuer(certificate, publicKeyUrl);
    +
    +    if (expire) {
    +      this.cache[publicKeyUrl] = certificate;
    +      setTimeout(() => delete this.cache[publicKeyUrl], parseInt(expire[1], 10) * 1000);
    +    }
    +
    +    return certificate;
       }
    -  try {
    -    if (!ca.cert.verify(publicKeyCert)) {
    +
    +  verifyPublicKeyIssuer(cert, publicKeyUrl) {
    +    const publicKeyCert = pki.certificateFromPem(cert);
    +
    +    if (!this.ca.cert) {
           throw new Parse.Error(
             Parse.Error.OBJECT_NOT_FOUND,
    -        `Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}`
    +        'Root certificate is invalid or missing.'
           );
         }
    -  } catch (e) {
    -    throw new Parse.Error(
    -      Parse.Error.OBJECT_NOT_FOUND,
    -      `Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}`
    -    );
    -  }
    -  return cert;
    -}
     
    -// Returns a promise that fulfills if this user id is valid.
    -async function validateAuthData(authData) {
    -  if (!authData.id) {
    -    throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Apple Game Center - authData id missing');
    +    if (!this.ca.cert.verify(publicKeyCert)) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `Invalid publicKeyUrl: ${publicKeyUrl}`);
    +    }
       }
    -  authData.playerId = authData.id;
    -  const publicKey = await getAppleCertificate(authData.publicKeyUrl);
    -  return verifySignature(publicKey, authData);
    -}
     
    -// Returns a promise that fulfills if this app id is valid.
    -async function validateAppId(appIds, authData, options = {}) {
    -  if (!options.rootCertificateUrl) {
    -    options.rootCertificateUrl =
    -      'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem';
    +  verifySignature(publicKey, authData) {
    +    const bundleId = this.bundleId || (this.enableInsecureAuth && authData.bundleId);
    +
    +    const verifier = crypto.createVerify('sha256');
    +    verifier.update(Buffer.from(authData.id, 'utf8'));
    +    verifier.update(Buffer.from(bundleId, 'utf8'));
    +    verifier.update(this.convertTimestampToBigEndian(authData.timestamp));
    +    verifier.update(Buffer.from(authData.salt, 'base64'));
    +
    +    if (!verifier.verify(publicKey, authData.signature, 'base64')) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid signature.');
    +    }
       }
    -  if (ca.url === options.rootCertificateUrl) {
    -    return;
    +
    +  async validateAuthData(authData) {
    +
    +    const requiredKeys = ['id', 'publicKeyUrl', 'timestamp', 'signature', 'salt'];
    +    if (this.enableInsecureAuth) {
    +      requiredKeys.push('bundleId');
    +    }
    +
    +    for (const key of requiredKeys) {
    +      if (!authData[key]) {
    +        throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `AuthData ${key} is missing.`);
    +      }
    +    }
    +
    +    await this.loadingPromise;
    +
    +    const publicKey = await this.getAppleCertificate(authData.publicKeyUrl);
    +    this.verifySignature(publicKey, authData);
       }
    -  const { certificate, headers } = await getCertificate(options.rootCertificateUrl, true);
    -  if (
    -    headers['content-type'] !== 'application/x-pem-file' ||
    -    headers['content-length'] == null ||
    -    headers['content-length'] > 10000
    -  ) {
    -    throw new Parse.Error(
    -      Parse.Error.OBJECT_NOT_FOUND,
    -      'Apple Game Center auth adapter parameter `rootCertificateURL` is invalid.'
    -    );
    +
    +  convertTimestampToBigEndian(timestamp) {
    +    const buffer = Buffer.alloc(8);
    +    buffer.writeBigUInt64BE(BigInt(timestamp));
    +    return buffer;
       }
    -  ca.cert = pki.certificateFromPem(certificate);
    -  ca.url = options.rootCertificateUrl;
     }
     
    -module.exports = {
    -  validateAppId,
    -  validateAuthData,
    -  cache,
    -};
    +export default new GameCenterAuth();
    
  • src/Adapters/Auth/github.js+122 30 modified
    @@ -1,35 +1,127 @@
    -// Helper functions for accessing the github API.
    -var Parse = require('parse/node').Parse;
    -const httpsRequest = require('./httpsRequest');
    -
    -// Returns a promise that fulfills iff this user id is valid.
    -function validateAuthData(authData) {
    -  return request('user', authData.access_token).then(data => {
    -    if (data && data.id == authData.id) {
    -      return;
    +/**
    + * Parse Server authentication adapter for GitHub.
    + * @class GitHubAdapter
    + * @param {Object} options - The adapter configuration options.
    + * @param {string} options.clientId - The GitHub App Client ID. Required for secure authentication.
    + * @param {string} options.clientSecret - The GitHub App Client Secret. Required for secure authentication.
    + * @param {boolean} [options.enableInsecureAuth=false] - **[DEPRECATED]** Enable insecure authentication (not recommended).
    + *
    + * @param {Object} authData - The authentication data provided by the client.
    + * @param {string} authData.code - The authorization code from GitHub. Required for secure authentication.
    + * @param {string} [authData.id] - **[DEPRECATED]** The GitHub user ID (required for insecure authentication).
    + * @param {string} [authData.access_token] - **[DEPRECATED]** The GitHub access token (required for insecure authentication).
    + *
    + * @description
    + * ## Parse Server Configuration
    + * * To configure Parse Server for GitHub authentication, use the following structure:
    + * ```json
    + * {
    + *  "auth": {
    + *   "github": {
    + *     "clientId": "12345",
    + *     "clientSecret": "abcde"
    + *   }
    + * }
    + * ```
    + *
    + * The GitHub adapter exchanges the `authData.code` provided by the client for an access token using GitHub's OAuth API. The following `authData` field is required:
    + * - `code`
    + *
    + * ## Insecure Authentication (Not Recommended)
    + * Insecure authentication uses the `authData.id` and `authData.access_token` provided by the client. This flow is insecure, deprecated, and poses potential security risks. The following `authData` fields are required:
    + * - `id` (**[DEPRECATED]**): The GitHub user ID.
    + * - `access_token` (**[DEPRECATED]**): The GitHub access token.
    + * To configure Parse Server for insecure authentication, use the following structure:
    + * ```json
    + * {
    + *  "auth": {
    + *    "github": {
    + *    "enableInsecureAuth": true
    + *  }
    + * }
    + * ```
    + *
    + * ### Deprecation Notice
    + * The `enableInsecureAuth` option and insecure `authData` fields (`id`, `access_token`) are deprecated and will be removed in future versions. Use secure authentication with `clientId` and `clientSecret`.
    + *
    + * @example <caption>Secure Authentication Example</caption>
    + * // Example authData for secure authentication:
    + * const authData = {
    + *   github: {
    + *     code: "abc123def456ghi789"
    + *   }
    + * };
    + *
    + * @example <caption>Insecure Authentication Example (Not Recommended)</caption>
    + * // Example authData for insecure authentication:
    + * const authData = {
    + *   github: {
    + *     id: "1234567",
    + *     access_token: "abc123def456ghi789" // Deprecated.
    + *   }
    + * };
    + *
    + * @note `enableInsecureAuth` will be removed in future versions. Use secure authentication with `clientId` and `clientSecret`.
    + * @note Secure authentication exchanges the `code` provided by the client for an access token using GitHub's OAuth API.
    + *
    + * @see {@link https://docs.github.com/en/developers/apps/authorizing-oauth-apps GitHub OAuth Documentation}
    + */
    +
    +import BaseCodeAuthAdapter from './BaseCodeAuthAdapter';
    +class GitHubAdapter extends BaseCodeAuthAdapter {
    +  constructor() {
    +    super('GitHub');
    +  }
    +  async getAccessTokenFromCode(authData) {
    +    const tokenUrl = 'https://github.com/login/oauth/access_token';
    +    const response = await fetch(tokenUrl, {
    +      method: 'POST',
    +      headers: {
    +        'Content-Type': 'application/json',
    +        Accept: 'application/json',
    +      },
    +      body: JSON.stringify({
    +        client_id: this.clientId,
    +        client_secret: this.clientSecret,
    +        code: authData.code,
    +      }),
    +    });
    +
    +    if (!response.ok) {
    +      throw new Parse.Error(Parse.Error.VALIDATION_ERROR, `Failed to exchange code for token: ${response.statusText}`);
         }
    -    throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Github auth is invalid for this user.');
    -  });
    -}
     
    -// Returns a promise that fulfills iff this app id is valid.
    -function validateAppId() {
    -  return Promise.resolve();
    -}
    +    const data = await response.json();
    +    if (data.error) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, data.error_description || data.error);
    +    }
    +
    +    return data.access_token;
    +  }
    +
    +  async getUserFromAccessToken(accessToken) {
    +    const userApiUrl = 'https://api.github.com/user';
    +    const response = await fetch(userApiUrl, {
    +      method: 'GET',
    +      headers: {
    +        Authorization: `Bearer ${accessToken}`,
    +        Accept: 'application/json',
    +      },
    +    });
    +
    +    if (!response.ok) {
    +      throw new Parse.Error(Parse.Error.VALIDATION_ERROR, `Failed to fetch GitHub user: ${response.statusText}`);
    +    }
    +
    +    const userData = await response.json();
    +    if (!userData.id || !userData.login) {
    +      throw new Parse.Error(Parse.Error.VALIDATION_ERROR, 'Invalid GitHub user data received.');
    +    }
    +
    +    return userData;
    +  }
     
    -// A promisey wrapper for api requests
    -function request(path, access_token) {
    -  return httpsRequest.get({
    -    host: 'api.github.com',
    -    path: '/' + path,
    -    headers: {
    -      Authorization: 'bearer ' + access_token,
    -      'User-Agent': 'parse-server',
    -    },
    -  });
     }
     
    -module.exports = {
    -  validateAppId: validateAppId,
    -  validateAuthData: validateAuthData,
    -};
    +export default new GitHubAdapter();
    +
    
  • src/Adapters/Auth/google.js+44 0 modified
    @@ -1,3 +1,47 @@
    +/**
    + * Parse Server authentication adapter for Google.
    + *
    + * @class GoogleAdapter
    + * @param {Object} options - The adapter configuration options.
    + * @param {string} options.clientId - Your Google application Client ID. Required for authentication.
    + *
    + * @description
    + * ## Parse Server Configuration
    + * To configure Parse Server for Google authentication, use the following structure:
    + * ```json
    + * {
    + *   "auth": {
    + *     "google": {
    + *       "clientId": "your-client-id"
    + *     }
    + *   }
    + * }
    + * ```
    + *
    + * The adapter requires the following `authData` fields:
    + * - **id**: The Google user ID.
    + * - **id_token**: The Google ID token.
    + * - **access_token**: The Google access token.
    + *
    + * ## Auth Payload
    + * ### Example Auth Data Payload
    + * ```json
    + * {
    + *   "google": {
    + *     "id": "1234567",
    + *     "id_token": "xxxxx.yyyyy.zzzzz",
    + *     "access_token": "abc123def456ghi789"
    + *   }
    + * }
    + * ```
    + *
    + * ## Notes
    + * - Ensure your Google Client ID is configured properly in the Parse Server configuration.
    + * - The `id_token` and `access_token` are validated against Google's authentication services.
    + *
    + * @see {@link https://developers.google.com/identity/sign-in/web/backend-auth Google Authentication Documentation}
    + */
    +
     'use strict';
     
     // Helper functions for accessing the google API.
    
  • src/Adapters/Auth/gpgames.js+135 29 modified
    @@ -1,33 +1,139 @@
    -/* Google Play Game Services
    -https://developers.google.com/games/services/web/api/players/get
    -
    -const authData = {
    -  id: 'playerId',
    -  access_token: 'token',
    -};
    -*/
    -const { Parse } = require('parse/node');
    -const httpsRequest = require('./httpsRequest');
    -
    -// Returns a promise that fulfills if this user id is valid.
    -async function validateAuthData(authData) {
    -  const response = await httpsRequest.get(
    -    `https://www.googleapis.com/games/v1/players/${authData.id}?access_token=${authData.access_token}`
    -  );
    -  if (!(response && response.playerId === authData.id)) {
    -    throw new Parse.Error(
    -      Parse.Error.OBJECT_NOT_FOUND,
    -      'Google Play Games Services - authData is invalid for this user.'
    -    );
    +/**
    + * Parse Server authentication adapter for Google Play Games Services.
    + *
    + * @class GooglePlayGamesServicesAdapter
    + * @param {Object} options - The adapter configuration options.
    + * @param {string} options.clientId - Your Google Play Games Services App Client ID. Required for secure authentication.
    + * @param {string} options.clientSecret - Your Google Play Games Services App Client Secret. Required for secure authentication.
    + * @param {boolean} [options.enableInsecureAuth=false] - **[DEPRECATED]** Enable insecure authentication (not recommended).
    + *
    + * @description
    + * ## Parse Server Configuration
    + * To configure Parse Server for Google Play Games Services authentication, use the following structure:
    + * ```json
    + * {
    + *   "auth": {
    + *     "gpgames": {
    + *       "clientId": "your-client-id",
    + *       "clientSecret": "your-client-secret"
    + *     }
    + *   }
    + * }
    + * ```
    + * ### Insecure Configuration (Not Recommended)
    + * ```json
    + * {
    + *   "auth": {
    + *     "gpgames": {
    + *       "enableInsecureAuth": true
    + *     }
    + *   }
    + * }
    + * ```
    + *
    + * The adapter requires the following `authData` fields:
    + * - **Secure Authentication**: `code`, `redirect_uri`.
    + * - **Insecure Authentication (Not Recommended)**: `id`, `access_token`.
    + *
    + * ## Auth Payloads
    + * ### Secure Authentication Payload
    + * ```json
    + * {
    + *   "gpgames": {
    + *     "code": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    + *     "redirect_uri": "https://example.com/callback"
    + *   }
    + * }
    + * ```
    + *
    + * ### Insecure Authentication Payload (Not Recommended)
    + * ```json
    + * {
    + *   "gpgames": {
    + *     "id": "123456789",
    + *     "access_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    + *   }
    + * }
    + * ```
    + *
    + * ## Notes
    + * - `enableInsecureAuth` is **not recommended** and may be removed in future versions. Use secure authentication with `code` and `redirect_uri`.
    + * - Secure authentication exchanges the `code` provided by the client for an access token using Google Play Games Services' OAuth API.
    + *
    + * @see {@link https://developers.google.com/games/services/console/enabling Google Play Games Services Authentication Documentation}
    + */
    +
    +import BaseCodeAuthAdapter from './BaseCodeAuthAdapter';
    +class GooglePlayGamesServicesAdapter extends BaseCodeAuthAdapter {
    +  constructor() {
    +    super("gpgames");
    +  }
    +
    +  async getAccessTokenFromCode(authData) {
    +    const tokenUrl = 'https://oauth2.googleapis.com/token';
    +    const response = await fetch(tokenUrl, {
    +      method: 'POST',
    +      headers: {
    +        'Content-Type': 'application/json',
    +        Accept: 'application/json',
    +      },
    +      body: JSON.stringify({
    +        client_id: this.clientId,
    +        client_secret: this.clientSecret,
    +        code: authData.code,
    +        redirect_uri: authData.redirectUri,
    +        grant_type: 'authorization_code',
    +      }),
    +    });
    +
    +    if (!response.ok) {
    +      throw new Parse.Error(
    +        Parse.Error.VALIDATION_ERROR,
    +        `Failed to exchange code for token: ${response.statusText}`
    +      );
    +    }
    +
    +    const data = await response.json();
    +    if (data.error) {
    +      throw new Parse.Error(
    +        Parse.Error.OBJECT_NOT_FOUND,
    +        data.error_description || data.error
    +      );
    +    }
    +
    +    return data.access_token;
    +  }
    +
    +  async getUserFromAccessToken(accessToken, authData) {
    +    const userApiUrl = `https://www.googleapis.com/games/v1/players/${authData.id}`;
    +    const response = await fetch(userApiUrl, {
    +      method: 'GET',
    +      headers: {
    +        Authorization: `Bearer ${accessToken}`,
    +        Accept: 'application/json',
    +      },
    +    });
    +
    +    if (!response.ok) {
    +      throw new Parse.Error(
    +        Parse.Error.VALIDATION_ERROR,
    +        `Failed to fetch Google Play Games Services user: ${response.statusText}`
    +      );
    +    }
    +
    +    const userData = await response.json();
    +    if (!userData.playerId || userData.playerId !== authData.id) {
    +      throw new Parse.Error(
    +        Parse.Error.VALIDATION_ERROR,
    +        'Invalid Google Play Games Services user data received.'
    +      );
    +    }
    +
    +    return {
    +      id: userData.playerId
    +    };
       }
    -}
     
    -// Returns a promise that fulfills if this app id is valid.
    -function validateAppId() {
    -  return Promise.resolve();
     }
     
    -module.exports = {
    -  validateAppId,
    -  validateAuthData,
    -};
    +export default new GooglePlayGamesServicesAdapter();
    
  • src/Adapters/Auth/index.js+22 21 modified
    @@ -3,30 +3,31 @@ import Parse from 'parse/node';
     import AuthAdapter from './AuthAdapter';
     
     const apple = require('./apple');
    -const gcenter = require('./gcenter');
    -const gpgames = require('./gpgames');
    +const digits = require('./twitter'); // digits tokens are validated by twitter
     const facebook = require('./facebook');
    -const instagram = require('./instagram');
    -const linkedin = require('./linkedin');
    -const meetup = require('./meetup');
    -import mfa from './mfa';
    +import gcenter from './gcenter';
    +import github from './github';
     const google = require('./google');
    -const github = require('./github');
    -const twitter = require('./twitter');
    -const spotify = require('./spotify');
    -const digits = require('./twitter'); // digits tokens are validated by twitter
    -const janrainengage = require('./janrainengage');
    +import gpgames from './gpgames';
    +import instagram from './instagram';
     const janraincapture = require('./janraincapture');
    -const line = require('./line');
    -const vkontakte = require('./vkontakte');
    -const qq = require('./qq');
    -const wechat = require('./wechat');
    -const weibo = require('./weibo');
    -const oauth2 = require('./oauth2');
    -const phantauth = require('./phantauth');
    -const microsoft = require('./microsoft');
    +const janrainengage = require('./janrainengage');
     const keycloak = require('./keycloak');
     const ldap = require('./ldap');
    +import line from './line';
    +import linkedin from './linkedin';
    +const meetup = require('./meetup');
    +import mfa from './mfa';
    +import microsoft from './microsoft';
    +import oauth2 from './oauth2';
    +const phantauth = require('./phantauth');
    +import qq from './qq';
    +import spotify from './spotify';
    +import twitter from './twitter';
    +const vkontakte = require('./vkontakte');
    +import wechat from './wechat';
    +import weibo from './weibo';
    +
     
     const anonymous = {
       validateAuthData: () => {
    @@ -241,9 +242,9 @@ module.exports = function (authOptions = {}, enableAnonymousUsers = true) {
               };
               const result = afterFind.call(
                 adapter,
    -            requestObject,
                 authData[provider],
    -            providerOptions
    +            providerOptions,
    +            requestObject,
               );
               if (result) {
                 authData[provider] = result;
    
  • src/Adapters/Auth/instagram.js+117 23 modified
    @@ -1,27 +1,121 @@
    -// Helper functions for accessing the instagram API.
    -var Parse = require('parse/node').Parse;
    -const httpsRequest = require('./httpsRequest');
    -const defaultURL = 'https://graph.instagram.com/';
    -
    -// Returns a promise that fulfills if this user id is valid.
    -function validateAuthData(authData) {
    -  const apiURL = authData.apiURL || defaultURL;
    -  const path = `${apiURL}me?fields=id&access_token=${authData.access_token}`;
    -  return httpsRequest.get(path).then(response => {
    -    const user = response.data ? response.data : response;
    -    if (user && user.id == authData.id) {
    -      return;
    +/**
    + * Parse Server authentication adapter for Instagram.
    + *
    + * @class InstagramAdapter
    + * @param {Object} options - The adapter configuration options.
    + * @param {string} options.clientId - Your Instagram App Client ID. Required for secure authentication.
    + * @param {string} options.clientSecret - Your Instagram App Client Secret. Required for secure authentication.
    + * @param {boolean} [options.enableInsecureAuth=false] - **[DEPRECATED]** Enable insecure authentication (not recommended).
    + *
    + * @description
    + * ## Parse Server Configuration
    + * To configure Parse Server for Instagram authentication, use the following structure:
    + * ```json
    + * {
    + *   "auth": {
    + *     "instagram": {
    + *       "clientId": "your-client-id",
    + *       "clientSecret": "your-client-secret"
    + *     }
    + *   }
    + * }
    + * ```
    + * ### Insecure Configuration (Not Recommended)
    + * ```json
    + * {
    + *   "auth": {
    + *     "instagram": {
    + *       "enableInsecureAuth": true
    + *     }
    + *   }
    + * }
    + * ```
    + *
    + * The adapter requires the following `authData` fields:
    + * - **Secure Authentication**: `code`, `redirect_uri`.
    + * - **Insecure Authentication (Deprecated)**: `id`, `access_token`.
    + *
    + * ## Auth Payloads
    + * ### Secure Authentication Payload
    + * ```json
    + * {
    + *   "instagram": {
    + *     "code": "lmn789opq012rst345uvw",
    + *     "redirect_uri": "https://example.com/callback"
    + *   }
    + * }
    + * ```
    + *
    + * ### Insecure Authentication Payload (Deprecated)
    + * ```json
    + * {
    + *   "instagram": {
    + *     "id": "1234567",
    + *     "access_token": "AQXNnd2hIT6z9bHFzZz2Kp1ghiMz_RtyuvwXYZ123abc"
    + *   }
    + * }
    + * ```
    + *
    + * ## Notes
    + * - `enableInsecureAuth` is **deprecated** and will be removed in future versions. Use secure authentication with `code` and `redirect_uri`.
    + * - Secure authentication exchanges the `code` and `redirect_uri` provided by the client for an access token using Instagram's OAuth flow.
    + *
    + * @see {@link https://developers.facebook.com/docs/instagram-basic-display-api/getting-started Instagram Basic Display API - Getting Started}
    + */
    +
    +
    +import BaseAuthCodeAdapter from './BaseCodeAuthAdapter';
    +class InstagramAdapter extends BaseAuthCodeAdapter {
    +  constructor() {
    +    super('Instagram');
    +  }
    +
    +  async getAccessTokenFromCode(authData) {
    +    const response = await fetch('https://api.instagram.com/oauth/access_token', {
    +      method: 'POST',
    +      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    +      body: new URLSearchParams({
    +        client_id: this.clientId,
    +        client_secret: this.clientSecret,
    +        grant_type: 'authorization_code',
    +        redirect_uri: this.redirectUri,
    +        code: authData.code
    +      })
    +    });
    +
    +    if (!response.ok) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Instagram API request failed.');
    +    }
    +
    +    const data = await response.json();
    +    if (data.error) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, data.error_description || data.error);
    +    }
    +
    +    return data.access_token;
    +  }
    +
    +  async getUserFromAccessToken(accessToken, authData) {
    +    const defaultURL = 'https://graph.instagram.com/';
    +    const apiURL = authData.apiURL || defaultURL;
    +    const path = `${apiURL}me?fields=id&access_token=${accessToken}`;
    +
    +    const response = await fetch(path);
    +
    +    if (!response.ok) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Instagram API request failed.');
    +    }
    +
    +    const user = await response.json();
    +    if (user?.id !== authData.id) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Instagram auth is invalid for this user.');
    +    }
    +
    +    return {
    +      id: user.id,
         }
    -    throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Instagram auth is invalid for this user.');
    -  });
    -}
     
    -// Returns a promise that fulfills iff this app id is valid.
    -function validateAppId() {
    -  return Promise.resolve();
    +  }
     }
     
    -module.exports = {
    -  validateAppId,
    -  validateAuthData,
    -};
    +export default new InstagramAdapter();
    
  • src/Adapters/Auth/janraincapture.js+45 0 modified
    @@ -1,3 +1,48 @@
    +/**
    + * Parse Server authentication adapter for Janrain Capture API.
    + *
    + * @class JanrainCapture
    + * @param {Object} options - The adapter configuration options.
    + * @param {String} options.janrain_capture_host - The Janrain Capture API host.
    + *
    + * @param {Object} authData - The authentication data provided by the client.
    + * @param {String} authData.id - The Janrain Capture user ID.
    + * @param {String} authData.access_token - The Janrain Capture access token.
    + *
    + * @description
    + * ## Parse Server Configuration
    + * To configure Parse Server for Janrain Capture authentication, use the following structure:
    + * ```json
    + * {
    + *   "auth": {
    + *     "janrain": {
    + *       "janrain_capture_host": "your-janrain-capture-host"
    + *     }
    + *   }
    + * }
    + * ```
    + *
    + * The adapter requires the following `authData` fields:
    + * - `id`: The Janrain Capture user ID.
    + * - `access_token`: An authorized Janrain Capture access token for the user.
    + *
    + * ## Auth Payload Example
    + * ```json
    + * {
    + *   "janrain": {
    + *     "id": "user's Janrain Capture ID as a string",
    + *     "access_token": "an authorized Janrain Capture access token for the user"
    + *   }
    + * }
    + * ```
    + *
    + * ## Notes
    + * Parse Server validates the provided `authData` using the Janrain Capture API.
    + *
    + * @see {@link https://docs.janrain.com/api/registration/entity/#entity Janrain Capture API Documentation}
    + */
    +
    +
     // Helper functions for accessing the Janrain Capture API.
     var Parse = require('parse/node').Parse;
     var querystring = require('querystring');
    
  • src/Adapters/Auth/janrainengage.js+9 0 modified
    @@ -2,9 +2,18 @@
     var httpsRequest = require('./httpsRequest');
     var Parse = require('parse/node').Parse;
     var querystring = require('querystring');
    +import Config from '../../Config';
    +import Deprecator from '../../Deprecator/Deprecator';
     
     // Returns a promise that fulfills iff this user id is valid.
     function validateAuthData(authData, options) {
    +  const config = Config.get(Parse.applicationId);
    +
    +  Deprecator.logRuntimeDeprecation({ usage: 'janrainengage adapter' });
    +  if (!config?.auth?.janrainengage?.enableInsecureAuth || !config.enableInsecureAuthAdapters) {
    +    throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'janrainengage adapter only works with enableInsecureAuth: true');
    +  }
    +
       return apiRequest(options.api_key, authData.auth_token).then(data => {
         //successful response will have a "stat" (status) of 'ok' and a profile node with an identifier
         //see: http://developers.janrain.com/overview/social-login/identity-providers/user-profile-data/#normalized-user-profile-data
    
  • src/Adapters/Auth/keycloak.js+67 34 modified
    @@ -1,37 +1,70 @@
    -/*
    -  # Parse Server Keycloak Authentication
    -
    -  ## Keycloak `authData`
    -
    -  ```
    -    {
    -      "keycloak": {
    -        "access_token": "access token you got from keycloak JS client authentication",
    -        "id": "the id retrieved from client authentication in Keycloak",
    -        "roles": ["the roles retrieved from client authentication in Keycloak"],
    -        "groups": ["the groups retrieved from client authentication in Keycloak"]
    -      }
    -    }
    -  ```
    -
    -  The authentication module will test if the authData is the same as the
    -  userinfo oauth call, comparing the attributes
    -
    -  Copy the JSON config file generated on Keycloak (https://www.keycloak.org/docs/latest/securing_apps/index.html#_javascript_adapter)
    -  and paste it inside of a folder (Ex.: `auth/keycloak.json`) in your server.
    -
    -  The options passed to Parse server:
    -
    -  ```
    -    {
    -      auth: {
    -        keycloak: {
    -          config: require(`./auth/keycloak.json`)
    -        }
    -      }
    -    }
    -  ```
    -*/
    +/**
    + * Parse Server authentication adapter for Keycloak.
    + *
    + * @class KeycloakAdapter
    + * @param {Object} options - The adapter configuration options.
    + * @param {Object} options.config - The Keycloak configuration object, typically loaded from a JSON file.
    + * @param {String} options.config.auth-server-url - The Keycloak authentication server URL.
    + * @param {String} options.config.realm - The Keycloak realm name.
    + * @param {String} options.config.client-id - The Keycloak client ID.
    + *
    + * @param {Object} authData - The authentication data provided by the client.
    + * @param {String} authData.access_token - The Keycloak access token retrieved during client authentication.
    + * @param {String} authData.id - The user ID retrieved from Keycloak during client authentication.
    + * @param {Array} [authData.roles] - The roles assigned to the user in Keycloak (optional).
    + * @param {Array} [authData.groups] - The groups assigned to the user in Keycloak (optional).
    + *
    + * @description
    + * ## Parse Server Configuration
    + * To configure Parse Server for Keycloak authentication, use the following structure:
    + * ```javascript
    + * {
    + *   "auth": {
    + *     "keycloak": {
    + *       "config": require('./auth/keycloak.json')
    + *     }
    + *   }
    + * }
    + * ```
    + * Ensure the `keycloak.json` configuration file is generated from Keycloak's setup guide and includes:
    + * - `auth-server-url`: The Keycloak authentication server URL.
    + * - `realm`: The Keycloak realm name.
    + * - `client-id`: The Keycloak client ID.
    + *
    + * ## Auth Data
    + * The adapter requires the following `authData` fields:
    + * - `access_token`: The Keycloak access token retrieved during client authentication.
    + * - `id`: The user ID retrieved from Keycloak during client authentication.
    + * - `roles` (optional): The roles assigned to the user in Keycloak.
    + * - `groups` (optional): The groups assigned to the user in Keycloak.
    + *
    + * ## Auth Payload Example
    + * ### Example Auth Data
    + * ```json
    + * {
    + *   "keycloak": {
    + *     "access_token": "an authorized Keycloak access token for the user",
    + *     "id": "user's Keycloak ID as a string",
    + *     "roles": ["admin", "user"],
    + *     "groups": ["group1", "group2"]
    + *   }
    + * }
    + * ```
    + *
    + * ## Notes
    + * - Parse Server validates the provided `authData` by making a `userinfo` call to Keycloak and ensures the attributes match those returned by Keycloak.
    + *
    + * ## Keycloak Configuration
    + * To configure Keycloak, copy the JSON configuration file generated from Keycloak's setup guide:
    + * - [Keycloak Securing Apps Documentation](https://www.keycloak.org/docs/latest/securing_apps/index.html#_javascript_adapter)
    + *
    + * Place the configuration file on your server, for example:
    + * - `auth/keycloak.json`
    + *
    + * For more information on Keycloak authentication, see:
    + * - [Securing Apps Documentation](https://www.keycloak.org/docs/latest/securing_apps/)
    + * - [Server Administration Documentation](https://www.keycloak.org/docs/latest/server_admin/)
    + */
     
     const { Parse } = require('parse/node');
     const httpsRequest = require('./httpsRequest');
    
  • src/Adapters/Auth/ldap.js+75 0 modified
    @@ -1,3 +1,78 @@
    +/**
    + * Parse Server authentication adapter for LDAP.
    + *
    + * @class LDAP
    + * @param {Object} options - The adapter configuration options.
    + * @param {String} options.url - The LDAP server URL. Must start with `ldap://` or `ldaps://`.
    + * @param {String} options.suffix - The LDAP suffix for user distinguished names (DN).
    + * @param {String} [options.dn] - The distinguished name (DN) template for user authentication. Replace `{{id}}` with the username.
    + * @param {Object} [options.tlsOptions] - Options for LDAPS TLS connections.
    + * @param {String} [options.groupCn] - The common name (CN) of the group to verify user membership.
    + * @param {String} [options.groupFilter] - The LDAP search filter for groups, with `{{id}}` replaced by the username.
    + *
    + * @param {Object} authData - The authentication data provided by the client.
    + * @param {String} authData.id - The user's LDAP username.
    + * @param {String} authData.password - The user's LDAP password.
    + *
    + * @description
    + * ## Parse Server Configuration
    + * To configure Parse Server for LDAP authentication, use the following structure:
    + * ```javascript
    + * {
    + *   auth: {
    + *     ldap: {
    + *       url: 'ldaps://ldap.example.com',
    + *       suffix: 'ou=users,dc=example,dc=com',
    + *       groupCn: 'admins',
    + *       groupFilter: '(memberUid={{id}})',
    + *       tlsOptions: {
    + *         rejectUnauthorized: false
    + *       }
    + *     }
    + *   }
    + * }
    + * ```
    + *
    + * ## Authentication Process
    + * 1. Validates the provided `authData` using an LDAP bind operation.
    + * 2. Optionally, verifies that the user belongs to a specific group by performing an LDAP search using the provided `groupCn` or `groupFilter`.
    + *
    + * ## Auth Payload
    + * The adapter requires the following `authData` fields:
    + * - `id`: The user's LDAP username.
    + * - `password`: The user's LDAP password.
    + *
    + * ### Example Auth Payload
    + * ```json
    + * {
    + *   "ldap": {
    + *     "id": "jdoe",
    + *     "password": "password123"
    + *   }
    + * }
    + * ```
    + *
    + * @example <caption>Configuration Example</caption>
    + * // Example Parse Server configuration:
    + * const config = {
    + *   auth: {
    + *     ldap: {
    + *       url: 'ldaps://ldap.example.com',
    + *       suffix: 'ou=users,dc=example,dc=com',
    + *       groupCn: 'admins',
    + *       groupFilter: '(memberUid={{id}})',
    + *       tlsOptions: {
    + *         rejectUnauthorized: false
    + *       }
    + *     }
    + *   }
    + * };
    + *
    + * @see {@link https://ldap.com/ LDAP Basics}
    + * @see {@link https://ldap.com/ldap-filters/ LDAP Filters}
    + */
    +
    +
     const ldapjs = require('ldapjs');
     const Parse = require('parse/node').Parse;
     
    
  • src/Adapters/Auth/line.js+138 31 modified
    @@ -1,36 +1,143 @@
    -// Helper functions for accessing the line API.
    -var Parse = require('parse/node').Parse;
    -const httpsRequest = require('./httpsRequest');
    -
    -// Returns a promise that fulfills if this user id is valid.
    -function validateAuthData(authData) {
    -  return request('profile', authData.access_token).then(response => {
    -    if (response && response.userId && response.userId === authData.id) {
    -      return;
    +/**
    + * Parse Server authentication adapter for Line.
    + *
    + * @class LineAdapter
    + * @param {Object} options - The adapter configuration options.
    + * @param {string} options.clientId - Your Line App Client ID. Required for secure authentication.
    + * @param {string} options.clientSecret - Your Line App Client Secret. Required for secure authentication.
    + * @param {boolean} [options.enableInsecureAuth=false] - **[DEPRECATED]** Enable insecure authentication (not recommended).
    + *
    + * @description
    + * ## Parse Server Configuration
    + * To configure Parse Server for Line authentication, use the following structure:
    + * ### Secure Configuration
    + * ```json
    + * {
    + *   "auth": {
    + *     "line": {
    + *       "clientId": "your-client-id",
    + *       "clientSecret": "your-client-secret"
    + *     }
    + *   }
    + * }
    + * ```
    + * ### Insecure Configuration (Not Recommended)
    + * ```json
    + * {
    + *   "auth": {
    + *     "line": {
    + *       "enableInsecureAuth": true
    + *     }
    + *   }
    + * }
    + * ```
    + *
    + * The adapter requires the following `authData` fields:
    + * - **Secure Authentication**: `code`, `redirect_uri`.
    + * - **Insecure Authentication (Not Recommended)**: `id`, `access_token`.
    + *
    + * ## Auth Payloads
    + * ### Secure Authentication Payload
    + * ```json
    + * {
    + *   "line": {
    + *     "code": "xxxxxxxxx",
    + *     "redirect_uri": "https://example.com/callback"
    + *   }
    + * }
    + * ```
    + *
    + * ### Insecure Authentication Payload (Not Recommended)
    + * ```json
    + * {
    + *   "line": {
    + *     "id": "1234567",
    + *     "access_token": "xxxxxxxxx"
    + *   }
    + * }
    + * ```
    + *
    + * ## Notes
    + * - `enableInsecureAuth` is **not recommended** and will be removed in future versions. Use secure authentication with `clientId` and `clientSecret`.
    + * - Secure authentication exchanges the `code` and `redirect_uri` provided by the client for an access token using Line's OAuth flow.
    + *
    + * @see {@link https://developers.line.biz/en/docs/line-login/integrate-line-login/ Line Login Documentation}
    + */
    +
    +import BaseCodeAuthAdapter from './BaseCodeAuthAdapter';
    +
    +class LineAdapter extends BaseCodeAuthAdapter {
    +  constructor() {
    +    super('Line');
    +  }
    +
    +  async getAccessTokenFromCode(authData) {
    +    if (!authData.code) {
    +      throw new Parse.Error(
    +        Parse.Error.OBJECT_NOT_FOUND,
    +        'Line auth is invalid for this user.'
    +      );
         }
    -    throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Line auth is invalid for this user.');
    -  });
    -}
     
    -// Returns a promise that fulfills iff this app id is valid.
    -function validateAppId() {
    -  return Promise.resolve();
    -}
    +    const tokenUrl = 'https://api.line.me/oauth2/v2.1/token';
    +    const response = await fetch(tokenUrl, {
    +      method: 'POST',
    +      headers: {
    +        'Content-Type': 'application/x-www-form-urlencoded',
    +      },
    +      body: new URLSearchParams({
    +        client_id: this.clientId,
    +        client_secret: this.clientSecret,
    +        grant_type: 'authorization_code',
    +        redirect_uri: authData.redirect_uri,
    +        code: authData.code,
    +      }),
    +    });
    +
    +    if (!response.ok) {
    +      throw new Parse.Error(
    +        Parse.Error.OBJECT_NOT_FOUND,
    +        `Failed to exchange code for token: ${response.statusText}`
    +      );
    +    }
    +
    +    const data = await response.json();
    +    if (data.error) {
    +      throw new Parse.Error(
    +        Parse.Error.OBJECT_NOT_FOUND,
    +        data.error_description || data.error
    +      );
    +    }
    +
    +    return data.access_token;
    +  }
    +
    +  async getUserFromAccessToken(accessToken) {
    +    const userApiUrl = 'https://api.line.me/v2/profile';
    +    const response = await fetch(userApiUrl, {
    +      method: 'GET',
    +      headers: {
    +        Authorization: `Bearer ${accessToken}`,
    +      },
    +    });
    +
    +    if (!response.ok) {
    +      throw new Parse.Error(
    +        Parse.Error.OBJECT_NOT_FOUND,
    +        `Failed to fetch Line user: ${response.statusText}`
    +      );
    +    }
    +
    +    const userData = await response.json();
    +    if (!userData?.userId) {
    +      throw new Parse.Error(
    +        Parse.Error.VALIDATION_ERROR,
    +        'Invalid Line user data received.'
    +      );
    +    }
     
    -// A promisey wrapper for api requests
    -function request(path, access_token) {
    -  var options = {
    -    host: 'api.line.me',
    -    path: '/v2/' + path,
    -    method: 'GET',
    -    headers: {
    -      Authorization: 'Bearer ' + access_token,
    -    },
    -  };
    -  return httpsRequest.get(options);
    +    return userData;
    +  }
     }
     
    -module.exports = {
    -  validateAppId: validateAppId,
    -  validateAuthData: validateAuthData,
    -};
    +export default new LineAdapter();
    
  • src/Adapters/Auth/linkedin.js+107 32 modified
    @@ -1,40 +1,115 @@
    -// Helper functions for accessing the linkedin API.
    -var Parse = require('parse/node').Parse;
    -const httpsRequest = require('./httpsRequest');
    +/**
    + * Parse Server authentication adapter for LinkedIn.
    + *
    + * @class LinkedInAdapter
    + * @param {Object} options - The adapter configuration options.
    + * @param {string} options.clientId - Your LinkedIn App Client ID. Required for secure authentication.
    + * @param {string} options.clientSecret - Your LinkedIn App Client Secret. Required for secure authentication.
    + * @param {boolean} [options.enableInsecureAuth=false] - **[DEPRECATED]** Enable insecure authentication (not recommended).
    + *
    + * @description
    + * ## Parse Server Configuration
    + * To configure Parse Server for LinkedIn authentication, use the following structure:
    + * ### Secure Configuration
    + * ```json
    + * {
    + *   "auth": {
    + *     "linkedin": {
    + *       "clientId": "your-client-id",
    + *       "clientSecret": "your-client-secret"
    + *     }
    + *   }
    + * }
    + * ```
    + * ### Insecure Configuration (Not Recommended)
    + * ```json
    + * {
    + *   "auth": {
    + *     "linkedin": {
    + *       "enableInsecureAuth": true
    + *     }
    + *   }
    + * }
    + * ```
    + *
    + * The adapter requires the following `authData` fields:
    + * - **Secure Authentication**: `code`, `redirect_uri`, and optionally `is_mobile_sdk`.
    + * - **Insecure Authentication (Not Recommended)**: `id`, `access_token`, and optionally `is_mobile_sdk`.
    + *
    + * ## Auth Payloads
    + * ### Secure Authentication Payload
    + * ```json
    + * {
    + *   "linkedin": {
    + *     "code": "lmn789opq012rst345uvw",
    + *     "redirect_uri": "https://your-redirect-uri.com/callback",
    + *     "is_mobile_sdk": true
    + *   }
    + * }
    + * ```
    + *
    + * ### Insecure Authentication Payload (Not Recommended)
    + * ```json
    + * {
    + *   "linkedin": {
    + *     "id": "7654321",
    + *     "access_token": "AQXNnd2hIT6z9bHFzZz2Kp1ghiMz_RtyuvwXYZ123abc",
    + *     "is_mobile_sdk": true
    + *   }
    + * }
    + * ```
    + *
    + * ## Notes
    + * - Secure authentication exchanges the `code` and `redirect_uri` provided by the client for an access token using LinkedIn's OAuth API.
    + * - Insecure authentication validates the user ID and access token directly, bypassing OAuth flows. This method is **not recommended** and may introduce security vulnerabilities.
    + * - `enableInsecureAuth` is **deprecated** and may be removed in future versions.
    + *
    + * @see {@link https://learn.microsoft.com/en-us/linkedin/shared/authentication/authentication LinkedIn Authentication Documentation}
    + */
     
    -// Returns a promise that fulfills iff this user id is valid.
    -function validateAuthData(authData) {
    -  return request('me', authData.access_token, authData.is_mobile_sdk).then(data => {
    -    if (data && data.id == authData.id) {
    -      return;
    +import BaseAuthCodeAdapter from './BaseCodeAuthAdapter';
    +class LinkedInAdapter extends BaseAuthCodeAdapter {
    +  constructor() {
    +    super('LinkedIn');
    +  }
    +  async getUserFromAccessToken(access_token, authData) {
    +    const response = await fetch('https://api.linkedin.com/v2/me', {
    +      headers: {
    +        Authorization: `Bearer ${access_token}`,
    +        'x-li-format': 'json',
    +        'x-li-src': authData?.is_mobile_sdk ? 'msdk' : undefined,
    +      },
    +    });
    +
    +    if (!response.ok) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'LinkedIn API request failed.');
         }
    -    throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Linkedin auth is invalid for this user.');
    -  });
    -}
     
    -// Returns a promise that fulfills iff this app id is valid.
    -function validateAppId() {
    -  return Promise.resolve();
    -}
    +    return response.json();
    +  }
    +
    +  async getAccessTokenFromCode(authData) {
    +    const response = await fetch('https://www.linkedin.com/oauth/v2/accessToken', {
    +      method: 'POST',
    +      headers: {
    +        'Content-Type': 'application/x-www-form-urlencoded',
    +      },
    +      body: new URLSearchParams({
    +        grant_type: 'authorization_code',
    +        code: authData.code,
    +        redirect_uri: authData.redirect_uri,
    +        client_id: this.clientId,
    +        client_secret: this.clientSecret,
    +      }),
    +    });
     
    -// A promisey wrapper for api requests
    -function request(path, access_token, is_mobile_sdk) {
    -  var headers = {
    -    Authorization: 'Bearer ' + access_token,
    -    'x-li-format': 'json',
    -  };
    +    if (!response.ok) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'LinkedIn API request failed.');
    +    }
     
    -  if (is_mobile_sdk) {
    -    headers['x-li-src'] = 'msdk';
    +    const json = await response.json();
    +    return json.access_token;
       }
    -  return httpsRequest.get({
    -    host: 'api.linkedin.com',
    -    path: '/v2/' + path,
    -    headers: headers,
    -  });
     }
     
    -module.exports = {
    -  validateAppId: validateAppId,
    -  validateAuthData: validateAuthData,
    -};
    +export default new LinkedInAdapter();
    
  • src/Adapters/Auth/meetup.js+15 6 modified
    @@ -1,15 +1,24 @@
     // Helper functions for accessing the meetup API.
     var Parse = require('parse/node').Parse;
     const httpsRequest = require('./httpsRequest');
    +import Config from '../../Config';
    +import Deprecator from '../../Deprecator/Deprecator';
     
     // Returns a promise that fulfills iff this user id is valid.
    -function validateAuthData(authData) {
    -  return request('member/self', authData.access_token).then(data => {
    -    if (data && data.id == authData.id) {
    -      return;
    -    }
    +async function validateAuthData(authData) {
    +  const config = Config.get(Parse.applicationId);
    +  const meetupConfig = config.auth.meetup;
    +
    +  Deprecator.logRuntimeDeprecation({ usage: 'meetup adapter' });
    +
    +  if (!meetupConfig?.enableInsecureAuth) {
    +    throw new Parse.Error('Meetup only works with enableInsecureAuth: true');
    +  }
    +
    +  const data = await request('member/self', authData.access_token);
    +  if (data?.id !== authData.id) {
         throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Meetup auth is invalid for this user.');
    -  });
    +  }
     }
     
     // Returns a promise that fulfills iff this app id is valid.
    
  • src/Adapters/Auth/mfa.js+79 1 modified
    @@ -1,3 +1,81 @@
    +/**
    + * Parse Server authentication adapter for Multi-Factor Authentication (MFA).
    + *
    + * @class MFAAdapter
    + * @param {Object} options - The adapter options.
    + * @param {Array<String>} options.options - Supported MFA methods. Must include `"SMS"` or `"TOTP"`.
    + * @param {Number} [options.digits=6] - The number of digits for the one-time password (OTP). Must be between 4 and 10.
    + * @param {Number} [options.period=30] - The validity period of the OTP in seconds. Must be greater than 10.
    + * @param {String} [options.algorithm="SHA1"] - The algorithm used for TOTP generation. Defaults to `"SHA1"`.
    + * @param {Function} [options.sendSMS] - A callback function for sending SMS OTPs. Required if `"SMS"` is included in `options`.
    + *
    + * @description
    + * ## Parse Server Configuration
    + * To configure Parse Server for MFA, use the following structure:
    + * ```javascript
    + * {
    + *   auth: {
    + *     mfa: {
    + *       options: ["SMS", "TOTP"],
    + *       digits: 6,
    + *       period: 30,
    + *       algorithm: "SHA1",
    + *       sendSMS: (token, mobile) => {
    + *         // Send the SMS using your preferred SMS provider.
    + *         console.log(`Sending SMS to ${mobile} with token: ${token}`);
    + *       }
    + *     }
    + *   }
    + * }
    + * ```
    + *
    + * ## MFA Methods
    + * - **SMS**:
    + *   - Requires a valid mobile number.
    + *   - Sends a one-time password (OTP) via SMS for login or verification.
    + *   - Uses the `sendSMS` callback for sending the OTP.
    + *
    + * - **TOTP**:
    + *   - Requires a secret key for setup.
    + *   - Validates the user's OTP against a time-based one-time password (TOTP) generated using the secret key.
    + *   - Supports configurable digits, period, and algorithm for TOTP generation.
    + *
    + * ## MFA Payload
    + * The adapter requires the following `authData` fields:
    + * - **For SMS-based MFA**:
    + *   - `mobile`: The user's mobile number (required for setup).
    + *   - `token`: The OTP provided by the user for login or verification.
    + * - **For TOTP-based MFA**:
    + *   - `secret`: The TOTP secret key for the user (required for setup).
    + *   - `token`: The OTP provided by the user for login or verification.
    + *
    + * ## Example Payloads
    + * ### SMS Setup Payload
    + * ```json
    + * {
    + *   "mobile": "+1234567890"
    + * }
    + * ```
    + *
    + * ### TOTP Setup Payload
    + * ```json
    + * {
    + *   "secret": "BASE32ENCODEDSECRET",
    + *   "token": "123456"
    + * }
    + * ```
    + *
    + * ### Login Payload
    + * ```json
    + * {
    + *   "token": "123456"
    + * }
    + * ```
    + *
    + * @see {@link https://en.wikipedia.org/wiki/Time-based_One-Time_Password_algorithm Time-based One-Time Password Algorithm (TOTP)}
    + * @see {@link https://tools.ietf.org/html/rfc6238 RFC 6238: TOTP: Time-Based One-Time Password Algorithm}
    + */
    +
     import { TOTP, Secret } from 'otpauth';
     import { randomString } from '../../cryptoUtils';
     import AuthAdapter from './AuthAdapter';
    @@ -113,7 +191,7 @@ class MFAAdapter extends AuthAdapter {
         }
         throw 'Invalid MFA data';
       }
    -  afterFind(req, authData) {
    +  afterFind(authData, options, req) {
         if (req.master) {
           return;
         }
    
  • src/Adapters/Auth/microsoft.js+103 31 modified
    @@ -1,37 +1,109 @@
    -// Helper functions for accessing the microsoft graph API.
    -var Parse = require('parse/node').Parse;
    -const httpsRequest = require('./httpsRequest');
    +/**
    + * Parse Server authentication adapter for Microsoft.
    + *
    + * @class MicrosoftAdapter
    + * @param {Object} options - The adapter configuration options.
    + * @param {string} options.clientId - Your Microsoft App Client ID. Required for secure authentication.
    + * @param {string} options.clientSecret - Your Microsoft App Client Secret. Required for secure authentication.
    + * @param {boolean} [options.enableInsecureAuth=false] - **[DEPRECATED]** Enable insecure authentication (not recommended).
    + *
    + * @description
    + * ## Parse Server Configuration
    + * To configure Parse Server for Microsoft authentication, use the following structure:
    + * ### Secure Configuration
    + * ```json
    + * {
    + *   "auth": {
    + *     "microsoft": {
    + *       "clientId": "your-client-id",
    + *       "clientSecret": "your-client-secret"
    + *     }
    + *   }
    + * }
    + * ```
    + * ### Insecure Configuration (Not Recommended)
    + * ```json
    + * {
    + *   "auth": {
    + *     "microsoft": {
    + *       "enableInsecureAuth": true
    + *     }
    + *   }
    + * }
    + * ```
    + *
    + * The adapter requires the following `authData` fields:
    + * - **Secure Authentication**: `code`, `redirect_uri`.
    + * - **Insecure Authentication (Not Recommended)**: `id`, `access_token`.
    + *
    + * ## Auth Payloads
    + * ### Secure Authentication Payload
    + * ```json
    + * {
    + *   "microsoft": {
    + *     "code": "lmn789opq012rst345uvw",
    + *     "redirect_uri": "https://your-redirect-uri.com/callback"
    + *   }
    + * }
    + * ```
    + * ### Insecure Authentication Payload (Not Recommended)
    + * ```json
    + * {
    + *   "microsoft": {
    + *     "id": "7654321",
    + *     "access_token": "AQXNnd2hIT6z9bHFzZz2Kp1ghiMz_RtyuvwXYZ123abc"
    + *   }
    + * }
    + * ```
    + *
    + * ## Notes
    + * - Secure authentication exchanges the `code` and `redirect_uri` provided by the client for an access token using Microsoft's OAuth API.
    + * - **Insecure authentication** validates the user ID and access token directly, bypassing OAuth flows (not recommended). This method is deprecated and may be removed in future versions.
    + *
    + * @see {@link https://docs.microsoft.com/en-us/graph/auth/auth-concepts Microsoft Authentication Documentation}
    + */
     
    -// Returns a promise that fulfills if this user mail is valid.
    -function validateAuthData(authData) {
    -  return request('me', authData.access_token).then(response => {
    -    if (response && response.id && response.id == authData.id) {
    -      return;
    +import BaseAuthCodeAdapter from './BaseCodeAuthAdapter';
    +class MicrosoftAdapter extends BaseAuthCodeAdapter {
    +  constructor() {
    +    super('Microsoft');
    +  }
    +  async getUserFromAccessToken(access_token) {
    +    const userResponse = await fetch('https://graph.microsoft.com/v1.0/me', {
    +      headers: {
    +        Authorization: 'Bearer ' + access_token,
    +      },
    +    });
    +
    +    if (!userResponse.ok) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Microsoft API request failed.');
         }
    -    throw new Parse.Error(
    -      Parse.Error.OBJECT_NOT_FOUND,
    -      'Microsoft Graph auth is invalid for this user.'
    -    );
    -  });
    -}
     
    -// Returns a promise that fulfills if this app id is valid.
    -function validateAppId() {
    -  return Promise.resolve();
    -}
    +    return userResponse.json();
    +  }
    +
    +  async getAccessTokenFromCode(authData) {
    +    const response = await fetch('https://login.microsoftonline.com/common/oauth2/v2.0/token', {
    +      method: 'POST',
    +      headers: {
    +        'Content-Type': 'application/x-www-form-urlencoded',
    +      },
    +      body: new URLSearchParams({
    +        client_id: this.clientId,
    +        client_secret: this.clientSecret,
    +        grant_type: 'authorization_code',
    +        redirect_uri: authData.redirect_uri,
    +        code: authData.code,
    +      }),
    +    });
    +
    +    if (!response.ok) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Microsoft API request failed.');
    +    }
     
    -// A promisey wrapper for api requests
    -function request(path, access_token) {
    -  return httpsRequest.get({
    -    host: 'graph.microsoft.com',
    -    path: '/v1.0/' + path,
    -    headers: {
    -      Authorization: 'Bearer ' + access_token,
    -    },
    -  });
    +    const json = await response.json();
    +    return json.access_token;
    +  }
     }
     
    -module.exports = {
    -  validateAppId: validateAppId,
    -  validateAuthData: validateAuthData,
    -};
    +export default new MicrosoftAdapter();
    
  • src/Adapters/Auth/oauth2.js+101 117 modified
    @@ -1,137 +1,121 @@
    -/*
    - * This auth adapter is based on the OAuth 2.0 Token Introspection specification.
    - * See RFC 7662 for details (https://tools.ietf.org/html/rfc7662).
    - * It's purpose is to validate OAuth2 access tokens using the OAuth2 provider's
    - * token introspection endpoint (if implemented by the provider).
    +/**
    + * Parse Server authentication adapter for OAuth2 Token Introspection.
      *
    - * The adapter accepts the following config parameters:
    + * @class OAuth2Adapter
    + * @param {Object} options - The adapter configuration options.
    + * @param {string} options.tokenIntrospectionEndpointUrl - The URL of the token introspection endpoint. Required.
    + * @param {boolean} options.oauth2 - Indicates that the request should be handled by the OAuth2 adapter. Required.
    + * @param {string} [options.useridField] - The field in the introspection response that contains the user ID. Optional.
    + * @param {string} [options.appidField] - The field in the introspection response that contains the app ID. Optional.
    + * @param {string[]} [options.appIds] - List of allowed app IDs. Required if `appidField` is defined.
    + * @param {string} [options.authorizationHeader] - The Authorization header value for the introspection request. Optional.
      *
    - * 1. "tokenIntrospectionEndpointUrl" (string, required)
    - *      The URL of the token introspection endpoint of the OAuth2 provider that
    - *      issued the access token to the client that is to be validated.
    - *
    - * 2. "useridField" (string, optional)
    - *      The name of the field in the token introspection response that contains
    - *      the userid. If specified, it will be used to verify the value of the "id"
    - *      field in the "authData" JSON that is coming from the client.
    - *      This can be the "aud" (i.e. audience), the "sub" (i.e. subject) or the
    - *      "username" field in the introspection response, but since only the
    - *      "active" field is required and all other reponse fields are optional
    - *      in the RFC, it has to be optional in this adapter as well.
    - *      Default: - (undefined)
    - *
    - * 3. "appidField" (string, optional)
    - *      The name of the field in the token introspection response that contains
    - *      the appId of the client. If specified, it will be used to verify it's
    - *      value against the set of appIds in the adapter config. The concept of
    - *      appIds comes from the two major social login providers
    - *      (Google and Facebook). They have not yet implemented the token
    - *      introspection endpoint, but the concept can be valid for any OAuth2
    - *      provider.
    - *      Default: - (undefined)
    - *
    - * 4. "appIds" (array of strings, required if appidField is defined)
    - *      A set of appIds that are used to restrict accepted access tokens based
    - *      on a specific field's value in the token introspection response.
    - *      Default: - (undefined)
    - *
    - * 5. "authorizationHeader" (string, optional)
    - *      The value of the "Authorization" HTTP header in requests sent to the
    - *      introspection endpoint. It must contain the raw value.
    - *      Thus if HTTP Basic authorization is to be used, it must contain the
    - *      "Basic" string, followed by whitespace, then by the base64 encoded
    - *      version of the concatenated <username> + ":" + <password> string.
    - *      Eg. "Basic dXNlcm5hbWU6cGFzc3dvcmQ="
    + * @description
    + * ## Parse Server Configuration
    + * To configure Parse Server for OAuth2 Token Introspection, use the following structure:
    + * ```json
    + * {
    + *   "auth": {
    + *     "oauth2Provider": {
    + *       "tokenIntrospectionEndpointUrl": "https://provider.com/introspect",
    + *       "useridField": "sub",
    + *       "appidField": "aud",
    + *       "appIds": ["my-app-id"],
    + *       "authorizationHeader": "Basic dXNlcm5hbWU6cGFzc3dvcmQ=",
    + *       "oauth2": true
    + *     }
    + *   }
    + * }
    + * ```
      *
    - * The adapter expects requests with the following authData JSON:
    + * The adapter requires the following `authData` fields:
    + * - `id`: The user ID provided by the client.
    + * - `access_token`: The access token provided by the client.
      *
    + * ## Auth Payload
    + * ### Example Auth Payload
    + * ```json
      * {
    - *   "someadapter": {
    - *     "id": "user's OAuth2 provider-specific id as a string",
    - *     "access_token": "an authorized OAuth2 access token for the user",
    + *   "oauth2": {
    + *     "id": "user-id",
    + *     "access_token": "access-token"
      *   }
      * }
    + * ```
    + *
    + * ## Notes
    + * - `tokenIntrospectionEndpointUrl` is mandatory and should point to a valid OAuth2 provider's introspection endpoint.
    + * - If `appidField` is defined, `appIds` must also be specified to validate the app ID in the introspection response.
    + * - `authorizationHeader` can be used to authenticate requests to the token introspection endpoint.
    + *
    + * @see {@link https://datatracker.ietf.org/doc/html/rfc7662 OAuth 2.0 Token Introspection Specification}
      */
     
    -const Parse = require('parse/node').Parse;
    -const querystring = require('querystring');
    -const httpsRequest = require('./httpsRequest');
    -
    -const INVALID_ACCESS = 'OAuth2 access token is invalid for this user.';
    -const INVALID_ACCESS_APPID =
    -  "OAuth2: the access_token's appID is empty or is not in the list of permitted appIDs in the auth configuration.";
    -const MISSING_APPIDS =
    -  'OAuth2 configuration is missing the client app IDs ("appIds" config parameter).';
    -const MISSING_URL = 'OAuth2 token introspection endpoint URL is missing from configuration!';
    -
    -// Returns a promise that fulfills if this user id is valid.
    -function validateAuthData(authData, options) {
    -  return requestTokenInfo(options, authData.access_token).then(response => {
    -    if (
    -      !response ||
    -      !response.active ||
    -      (options.useridField && authData.id !== response[options.useridField])
    -    ) {
    -      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, INVALID_ACCESS);
    +
    +import AuthAdapter from './AuthAdapter';
    +
    +class OAuth2Adapter extends AuthAdapter {
    +  validateOptions(options) {
    +    super.validateOptions(options);
    +
    +    if (!options.tokenIntrospectionEndpointUrl) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 token introspection endpoint URL is missing.');
    +    }
    +    if (options.appidField && !options.appIds?.length) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 configuration is missing app IDs.');
         }
    -  });
    -}
     
    -function validateAppId(appIds, authData, options) {
    -  if (!options || !options.appidField) {
    -    return Promise.resolve();
    +    this.tokenIntrospectionEndpointUrl = options.tokenIntrospectionEndpointUrl;
    +    this.useridField = options.useridField;
    +    this.appidField = options.appidField;
    +    this.appIds = options.appIds;
    +    this.authorizationHeader = options.authorizationHeader;
       }
    -  if (!appIds || appIds.length === 0) {
    -    throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, MISSING_APPIDS);
    -  }
    -  return requestTokenInfo(options, authData.access_token).then(response => {
    -    if (!response || !response.active) {
    -      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, INVALID_ACCESS);
    +
    +  async validateAppId(authData) {
    +    if (!this.appidField) {
    +      return;
         }
    -    const appidField = options.appidField;
    -    if (!response[appidField]) {
    -      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, INVALID_ACCESS_APPID);
    +
    +    const response = await this.requestTokenInfo(authData.access_token);
    +
    +    const appIdFieldValue = response[this.appidField];
    +    const isValidAppId = Array.isArray(appIdFieldValue)
    +      ? appIdFieldValue.some(appId => this.appIds.includes(appId))
    +      : this.appIds.includes(appIdFieldValue);
    +
    +    if (!isValidAppId) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2: Invalid app ID.');
         }
    -    const responseValue = response[appidField];
    -    if (!Array.isArray(responseValue) && appIds.includes(responseValue)) {
    -      return;
    -    } else if (
    -      Array.isArray(responseValue) &&
    -      responseValue.some(appId => appIds.includes(appId))
    -    ) {
    -      return;
    -    } else {
    -      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, INVALID_ACCESS_APPID);
    +  }
    +
    +  async validateAuthData(authData) {
    +    const response = await this.requestTokenInfo(authData.access_token);
    +
    +    if (!response.active || (this.useridField && authData.id !== response[this.useridField])) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 access token is invalid for this user.');
         }
    -  });
    -}
     
    -// A promise wrapper for requests to the OAuth2 token introspection endpoint.
    -function requestTokenInfo(options, access_token) {
    -  if (!options || !options.tokenIntrospectionEndpointUrl) {
    -    throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, MISSING_URL);
    +    return {};
       }
    -  const parsedUrl = new URL(options.tokenIntrospectionEndpointUrl);
    -  const postData = querystring.stringify({
    -    token: access_token,
    -  });
    -  const headers = {
    -    'Content-Type': 'application/x-www-form-urlencoded',
    -    'Content-Length': Buffer.byteLength(postData),
    -  };
    -  if (options.authorizationHeader) {
    -    headers['Authorization'] = options.authorizationHeader;
    +
    +  async requestTokenInfo(accessToken) {
    +    const response = await fetch(this.tokenIntrospectionEndpointUrl, {
    +      method: 'POST',
    +      headers: {
    +        'Content-Type': 'application/x-www-form-urlencoded',
    +        ...(this.authorizationHeader && { Authorization: this.authorizationHeader })
    +      },
    +      body: new URLSearchParams({ token: accessToken })
    +    });
    +
    +    if (!response.ok) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 token introspection request failed.');
    +    }
    +
    +    return response.json();
       }
    -  const postOptions = {
    -    hostname: parsedUrl.hostname,
    -    path: parsedUrl.pathname,
    -    method: 'POST',
    -    headers: headers,
    -  };
    -  return httpsRequest.request(postOptions, postData);
     }
     
    -module.exports = {
    -  validateAppId: validateAppId,
    -  validateAuthData: validateAuthData,
    -};
    +export default new OAuth2Adapter();
    +
    
  • src/Adapters/Auth/phantauth.js+15 6 modified
    @@ -7,15 +7,24 @@
     
     const { Parse } = require('parse/node');
     const httpsRequest = require('./httpsRequest');
    +import Config from '../../Config';
    +import Deprecator from '../../Deprecator/Deprecator';
     
     // Returns a promise that fulfills if this user id is valid.
    -function validateAuthData(authData) {
    -  return request('auth/userinfo', authData.access_token).then(data => {
    -    if (data && data.sub == authData.id) {
    -      return;
    -    }
    +async function validateAuthData(authData) {
    +  const config = Config.get(Parse.applicationId);
    +
    +  Deprecator.logRuntimeDeprecation({ usage: 'phantauth adapter' });
    +
    +  const phantauthConfig = config.auth.phantauth;
    +  if (!phantauthConfig?.enableInsecureAuth) {
    +    throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'PhantAuth only works with enableInsecureAuth: true');
    +  }
    +
    +  const data = await request('auth/userinfo', authData.access_token);
    +  if (data?.sub !== authData.id) {
         throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'PhantAuth auth is invalid for this user.');
    -  });
    +  }
     }
     
     // Returns a promise that fulfills if this app id is valid.
    
  • src/Adapters/Auth/qq.js+105 34 modified
    @@ -1,41 +1,112 @@
    -// Helper functions for accessing the qq Graph API.
    -const httpsRequest = require('./httpsRequest');
    -var Parse = require('parse/node').Parse;
    -
    -// Returns a promise that fulfills iff this user id is valid.
    -function validateAuthData(authData) {
    -  return graphRequest('me?access_token=' + authData.access_token).then(function (data) {
    -    if (data && data.openid == authData.id) {
    -      return;
    +/**
    + * Parse Server authentication adapter for QQ.
    + *
    + * @class QqAdapter
    + * @param {Object} options - The adapter configuration options.
    + * @param {string} options.clientId - Your QQ App ID. Required for secure authentication.
    + * @param {string} options.clientSecret - Your QQ App Secret. Required for secure authentication.
    + * @param {boolean} [options.enableInsecureAuth=false] - **[DEPRECATED]** Enable insecure authentication (not recommended).
    + *
    + * @description
    + * ## Parse Server Configuration
    + * To configure Parse Server for QQ authentication, use the following structure:
    + * ### Secure Configuration
    + * ```json
    + * {
    + *   "auth": {
    + *     "qq": {
    + *       "clientId": "your-app-id",
    + *       "clientSecret": "your-app-secret"
    + *     }
    + *   }
    + * }
    + * ```
    + * ### Insecure Configuration (Not Recommended)
    + * ```json
    + * {
    + *   "auth": {
    + *     "qq": {
    + *       "enableInsecureAuth": true
    + *     }
    + *   }
    + * }
    + * ```
    + *
    + * The adapter requires the following `authData` fields:
    + * - **Secure Authentication**: `code`, `redirect_uri`.
    + * - **Insecure Authentication (Not Recommended)**: `id`, `access_token`.
    + *
    + * ## Auth Payloads
    + * ### Secure Authentication Payload
    + * ```json
    + * {
    + *   "qq": {
    + *     "code": "abcd1234",
    + *     "redirect_uri": "https://your-redirect-uri.com/callback"
    + *   }
    + * }
    + * ```
    + * ### Insecure Authentication Payload (Not Recommended)
    + * ```json
    + * {
    + *   "qq": {
    + *     "id": "1234567",
    + *     "access_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    + *   }
    + * }
    + * ```
    + *
    + * ## Notes
    + * - Secure authentication exchanges the `code` and `redirect_uri` provided by the client for an access token using QQ's OAuth API.
    + * - **Insecure authentication** validates the `id` and `access_token` directly, bypassing OAuth flows. This approach is not recommended and may be deprecated in future versions.
    + *
    + * @see {@link https://wiki.connect.qq.com/ QQ Authentication Documentation}
    + */
    +
    +import BaseAuthCodeAdapter from './BaseCodeAuthAdapter';
    +class QqAdapter extends BaseAuthCodeAdapter {
    +  constructor() {
    +    super('qq');
    +  }
    +
    +  async getUserFromAccessToken(access_token) {
    +    const response = await fetch('https://graph.qq.com/oauth2.0/me', {
    +      headers: {
    +        Authorization: `Bearer ${access_token}`,
    +      },
    +    });
    +
    +    if (!response.ok) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'qq API request failed.');
         }
    -    throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'qq auth is invalid for this user.');
    -  });
    -}
     
    -// Returns a promise that fulfills if this app id is valid.
    -function validateAppId() {
    -  return Promise.resolve();
    -}
    +    const data = await response.text();
    +    return this.parseResponseData(data);
    +  }
     
    -// A promisey wrapper for qq graph requests.
    -function graphRequest(path) {
    -  return httpsRequest.get('https://graph.qq.com/oauth2.0/' + path, true).then(data => {
    -    return parseResponseData(data);
    -  });
    -}
    +  async getAccessTokenFromCode(authData) {
    +    const response = await fetch('https://graph.qq.com/oauth2.0/token', {
    +      method: 'GET',
    +      headers: {
    +        'Content-Type': 'application/x-www-form-urlencoded',
    +      },
    +      body: new URLSearchParams({
    +        grant_type: 'authorization_code',
    +        client_id: this.clientId,
    +        client_secret: this.clientSecret,
    +        redirect_uri: authData.redirect_uri,
    +        code: authData.code,
    +      }).toString(),
    +    });
    +
    +    if (!response.ok) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'qq API request failed.');
    +    }
     
    -function parseResponseData(data) {
    -  const starPos = data.indexOf('(');
    -  const endPos = data.indexOf(')');
    -  if (starPos == -1 || endPos == -1) {
    -    throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'qq auth is invalid for this user.');
    +    const text = await response.text();
    +    const data = this.parseResponseData(text);
    +    return data.access_token;
       }
    -  data = data.substring(starPos + 1, endPos - 1);
    -  return JSON.parse(data);
     }
     
    -module.exports = {
    -  validateAppId,
    -  validateAuthData,
    -  parseResponseData,
    -};
    +export default new QqAdapter();
    
  • src/Adapters/Auth/spotify.js+112 38 modified
    @@ -1,44 +1,118 @@
    -// Helper functions for accessing the Spotify API.
    -const httpsRequest = require('./httpsRequest');
    -var Parse = require('parse/node').Parse;
    -
    -// Returns a promise that fulfills iff this user id is valid.
    -function validateAuthData(authData) {
    -  return request('me', authData.access_token).then(data => {
    -    if (data && data.id == authData.id) {
    -      return;
    -    }
    -    throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Spotify auth is invalid for this user.');
    -  });
    -}
    +/**
    + * Parse Server authentication adapter for Spotify.
    + *
    + * @class SpotifyAdapter
    + * @param {Object} options - The adapter configuration options.
    + * @param {string} options.clientId - Your Spotify application's Client ID. Required for secure authentication.
    + * @param {boolean} [options.enableInsecureAuth=false] - **[DEPRECATED]** Enable insecure authentication (not recommended).
    + *
    + * @description
    + * ## Parse Server Configuration
    + * To configure Parse Server for Spotify authentication, use the following structure:
    + * ### Secure Configuration
    + * ```json
    + * {
    + *   "auth": {
    + *     "spotify": {
    + *       "clientId": "your-client-id"
    + *     }
    + *   }
    + * }
    + * ```
    + * ### Insecure Configuration (Not Recommended)
    + * ```json
    + * {
    + *   "auth": {
    + *     "spotify": {
    + *       "enableInsecureAuth": true
    + *     }
    + *   }
    + * }
    + * ```
    + *
    + * The adapter requires the following `authData` fields:
    + * - **Secure Authentication**: `code`, `redirect_uri`, and `code_verifier`.
    + * - **Insecure Authentication (Not Recommended)**: `id`, `access_token`.
    + *
    + * ## Auth Payloads
    + * ### Secure Authentication Payload
    + * ```json
    + * {
    + *   "spotify": {
    + *     "code": "abc123def456ghi789",
    + *     "redirect_uri": "https://example.com/callback",
    + *     "code_verifier": "secure-code-verifier"
    + *   }
    + * }
    + * ```
    + * ### Insecure Authentication Payload (Not Recommended)
    + * ```json
    + * {
    + *   "spotify": {
    + *     "id": "1234567",
    + *     "access_token": "abc123def456ghi789"
    + *   }
    + * }
    + * ```
    + *
    + * ## Notes
    + * - `enableInsecureAuth` is **not recommended** and bypasses secure flows by validating the user ID and access token directly. This method is not suitable for production environments and may be removed in future versions.
    + * - Secure authentication exchanges the `code` provided by the client for an access token using Spotify's OAuth API. This method ensures greater security and is the recommended approach.
    + *
    + * @see {@link https://developer.spotify.com/documentation/web-api/tutorials/getting-started Spotify OAuth Documentation}
    + */
     
    -// Returns a promise that fulfills if this app id is valid.
    -async function validateAppId(appIds, authData) {
    -  const access_token = authData.access_token;
    -  if (!Array.isArray(appIds)) {
    -    throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'appIds must be an array.');
    +import BaseAuthCodeAdapter from './BaseCodeAuthAdapter';
    +class SpotifyAdapter extends BaseAuthCodeAdapter {
    +  constructor() {
    +    super('spotify');
       }
    -  if (!appIds.length) {
    -    throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Spotify auth is not configured.');
    -  }
    -  const data = await request('me', access_token);
    -  if (!data || !appIds.includes(data.id)) {
    -    throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Spotify auth is invalid for this user.');
    +
    +  async getUserFromAccessToken(access_token) {
    +    const response = await fetch('https://api.spotify.com/v1/me', {
    +      headers: {
    +        Authorization: 'Bearer ' + access_token,
    +      },
    +    });
    +
    +    if (!response.ok) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Spotify API request failed.');
    +    }
    +
    +    const user = await response.json();
    +    return {
    +      id: user.id,
    +    };
       }
    -}
     
    -// A promisey wrapper for Spotify API requests.
    -function request(path, access_token) {
    -  return httpsRequest.get({
    -    host: 'api.spotify.com',
    -    path: '/v1/' + path,
    -    headers: {
    -      Authorization: 'Bearer ' + access_token,
    -    },
    -  });
    +  async getAccessTokenFromCode(authData) {
    +    if (!authData.code || !authData.redirect_uri || !authData.code_verifier) {
    +      throw new Parse.Error(
    +        Parse.Error.OBJECT_NOT_FOUND,
    +        'Spotify auth configuration authData.code and/or authData.redirect_uri and/or authData.code_verifier.'
    +      );
    +    }
    +
    +    const response = await fetch('https://accounts.spotify.com/api/token', {
    +      method: 'POST',
    +      headers: {
    +        'Content-Type': 'application/x-www-form-urlencoded',
    +      },
    +      body: new URLSearchParams({
    +        grant_type: 'authorization_code',
    +        code: authData.code,
    +        redirect_uri: authData.redirect_uri,
    +        code_verifier: authData.code_verifier,
    +        client_id: this.clientId,
    +      }),
    +    });
    +
    +    if (!response.ok) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Spotify API request failed.');
    +    }
    +
    +    return response.json();
    +  }
     }
     
    -module.exports = {
    -  validateAppId: validateAppId,
    -  validateAuthData: validateAuthData,
    -};
    +export default new SpotifyAdapter();
    
  • src/Adapters/Auth/twitter.js+233 40 modified
    @@ -1,51 +1,244 @@
    -// Helper functions for accessing the twitter API.
    -var OAuth = require('./OAuth1Client');
    -var Parse = require('parse/node').Parse;
    -
    -// Returns a promise that fulfills iff this user id is valid.
    -function validateAuthData(authData, options) {
    -  if (!options) {
    -    throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Twitter auth configuration missing');
    -  }
    -  options = handleMultipleConfigurations(authData, options);
    -  var client = new OAuth(options);
    -  client.host = 'api.twitter.com';
    -  client.auth_token = authData.auth_token;
    -  client.auth_token_secret = authData.auth_token_secret;
    -
    -  return client.get('/1.1/account/verify_credentials.json').then(data => {
    -    if (data && data.id_str == '' + authData.id) {
    +/**
    + * Parse Server authentication adapter for Twitter.
    + *
    + * @class TwitterAdapter
    + * @param {Object} options - The adapter configuration options.
    + * @param {string} options.consumerKey - The Twitter App Consumer Key. Required for secure authentication.
    + * @param {string} options.consumerSecret - The Twitter App Consumer Secret. Required for secure authentication.
    + * @param {boolean} [options.enableInsecureAuth=false] - **[DEPRECATED]** Enable insecure authentication (not recommended).
    + *
    + * @description
    + * ## Parse Server Configuration
    + * To configure Parse Server for Twitter authentication, use the following structure:
    + * ### Secure Configuration
    + * ```json
    + * {
    + *   "auth": {
    + *     "twitter": {
    + *       "consumerKey": "your-consumer-key",
    + *       "consumerSecret": "your-consumer-secret"
    + *     }
    + *   }
    + * }
    + * ```
    + * ### Insecure Configuration (Not Recommended)
    + * ```json
    + * {
    + *   "auth": {
    + *     "twitter": {
    + *       "enableInsecureAuth": true
    + *     }
    + *   }
    + * }
    + * ```
    + *
    + * The adapter requires the following `authData` fields:
    + * - **Secure Authentication**: `oauth_token`, `oauth_verifier`.
    + * - **Insecure Authentication (Not Recommended)**: `id`, `oauth_token`, `oauth_token_secret`.
    + *
    + * ## Auth Payloads
    + * ### Secure Authentication Payload
    + * ```json
    + * {
    + *   "twitter": {
    + *     "oauth_token": "1234567890-abc123def456",
    + *     "oauth_verifier": "abc123def456"
    + *   }
    + * }
    + * ```
    + *
    + * ### Insecure Authentication Payload (Not Recommended)
    + * ```json
    + * {
    + *   "twitter": {
    + *     "id": "1234567890",
    + *     "oauth_token": "1234567890-abc123def456",
    + *     "oauth_token_secret": "1234567890-abc123def456"
    + *   }
    + * }
    + * ```
    + *
    + * ## Notes
    + * - **Deprecation Notice**: `enableInsecureAuth` and insecure fields (`id`, `oauth_token_secret`) are **deprecated** and may be removed in future versions. Use secure authentication with `consumerKey` and `consumerSecret`.
    + * - Secure authentication exchanges the `oauth_token` and `oauth_verifier` provided by the client for an access token using Twitter's OAuth API.
    + *
    + * @see {@link https://developer.twitter.com/en/docs/authentication/oauth-1-0a Twitter OAuth Documentation}
    + */
    +
    +import Config from '../../Config';
    +import querystring from 'querystring';
    +import AuthAdapter from './AuthAdapter';
    +
    +class TwitterAuthAdapter extends AuthAdapter {
    +  validateOptions(options) {
    +    if (!options) {
    +      throw new Error('Twitter auth options are required.');
    +    }
    +
    +    this.enableInsecureAuth = options.enableInsecureAuth;
    +
    +    if (!this.enableInsecureAuth && (!options.consumer_key || !options.consumer_secret)) {
    +      throw new Error('Consumer key and secret are required for secure Twitter auth.');
    +    }
    +  }
    +
    +  async validateAuthData(authData, options) {
    +    const config = Config.get(Parse.applicationId);
    +    const twitterConfig = config.auth.twitter;
    +
    +    if (this.enableInsecureAuth && twitterConfig && config.enableInsecureAuthAdapters) {
    +      return this.validateInsecureAuth(authData, options);
    +    }
    +
    +    if (!options.consumer_key || !options.consumer_secret) {
    +      throw new Parse.Error(
    +        Parse.Error.OBJECT_NOT_FOUND,
    +        'Twitter auth configuration missing consumer_key and/or consumer_secret.'
    +      );
    +    }
    +
    +    const accessTokenData = await this.exchangeAccessToken(authData);
    +
    +    if (accessTokenData?.oauth_token && accessTokenData?.user_id) {
    +      authData.id = accessTokenData.user_id;
    +      authData.auth_token = accessTokenData.oauth_token;
           return;
         }
    -    throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Twitter auth is invalid for this user.');
    -  });
    -}
     
    -// Returns a promise that fulfills iff this app id is valid.
    -function validateAppId() {
    -  return Promise.resolve();
    -}
    +    throw new Parse.Error(
    +      Parse.Error.OBJECT_NOT_FOUND,
    +      'Twitter auth is invalid for this user.'
    +    );
    +  }
    +
    +  async validateInsecureAuth(authData, options) {
    +    if (!authData.oauth_token || !authData.oauth_token_secret) {
    +      throw new Parse.Error(
    +        Parse.Error.OBJECT_NOT_FOUND,
    +        'Twitter insecure auth requires oauth_token and oauth_token_secret.'
    +      );
    +    }
    +
    +    options = this.handleMultipleConfigurations(authData, options);
    +
    +    const data = await this.request(authData, options);
    +    const parsedData = await data.json();
    +
    +    if (parsedData?.id === authData.id) {
    +      return;
    +    }
    +
    +    throw new Parse.Error(
    +      Parse.Error.OBJECT_NOT_FOUND,
    +      'Twitter auth is invalid for this user.'
    +    );
    +  }
     
    -function handleMultipleConfigurations(authData, options) {
    -  if (Array.isArray(options)) {
    -    const consumer_key = authData.consumer_key;
    -    if (!consumer_key) {
    -      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Twitter auth is invalid for this user.');
    +  async exchangeAccessToken(authData) {
    +    const accessTokenRequestOptions = {
    +      method: 'POST',
    +      headers: {
    +        'Content-Type': 'application/x-www-form-urlencoded',
    +      },
    +      body: querystring.stringify({
    +        oauth_token: authData.oauth_token,
    +        oauth_verifier: authData.oauth_verifier,
    +      }),
    +    };
    +
    +    const response = await fetch('https://api.twitter.com/oauth/access_token', accessTokenRequestOptions);
    +    if (!response.ok) {
    +      throw new Error('Failed to exchange access token.');
         }
    -    options = options.filter(option => {
    -      return option.consumer_key == consumer_key;
    +
    +    return response.json();
    +  }
    +
    +  handleMultipleConfigurations(authData, options) {
    +    if (Array.isArray(options)) {
    +      const consumer_key = authData.consumer_key;
    +
    +      if (!consumer_key) {
    +        throw new Parse.Error(
    +          Parse.Error.OBJECT_NOT_FOUND,
    +          'Twitter auth is invalid for this user.'
    +        );
    +      }
    +
    +      options = options.filter(option => option.consumer_key === consumer_key);
    +
    +      if (options.length === 0) {
    +        throw new Parse.Error(
    +          Parse.Error.OBJECT_NOT_FOUND,
    +          'Twitter auth is invalid for this user.'
    +        );
    +      }
    +
    +      return options[0];
    +    }
    +
    +    return options;
    +  }
    +
    +  async request(authData, options) {
    +    const { consumer_key, consumer_secret } = options;
    +
    +    const oauth = {
    +      consumer_key,
    +      consumer_secret,
    +      auth_token: authData.oauth_token,
    +      auth_token_secret: authData.oauth_token_secret,
    +    };
    +
    +    const url = new URL('https://api.twitter.com/2/users/me');
    +
    +    const response = await fetch(url, {
    +      headers: {
    +        Authorization: 'Bearer ' + oauth.auth_token,
    +      },
    +      body: JSON.stringify(oauth),
         });
     
    -    if (options.length == 0) {
    -      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Twitter auth is invalid for this user.');
    +    if (!response.ok) {
    +      throw new Error('Failed to fetch user data.');
    +    }
    +
    +    return response;
    +  }
    +
    +  async beforeFind(authData) {
    +    if (this.enableInsecureAuth && !authData?.code) {
    +      if (!authData?.access_token) {
    +        throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Twitter auth is invalid for this user.');
    +      }
    +
    +      const user = await this.getUserFromAccessToken(authData.access_token, authData);
    +
    +      if (user.id !== authData.id) {
    +        throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Twitter auth is invalid for this user.');
    +      }
    +
    +      return;
    +    }
    +
    +    if (!authData?.code) {
    +      throw new Parse.Error(Parse.Error.VALIDATION_ERROR, 'Twitter code is required.');
         }
    -    options = options[0];
    +
    +    const access_token = await this.exchangeAccessToken(authData);
    +    const user = await this.getUserFromAccessToken(access_token, authData);
    +
    +
    +    authData.access_token = access_token;
    +    authData.id = user.id;
    +
    +    delete authData.code;
    +    delete authData.redirect_uri;
    +  }
    +
    +  validateAppId() {
    +    return Promise.resolve();
       }
    -  return options;
     }
     
    -module.exports = {
    -  validateAppId,
    -  validateAuthData,
    -  handleMultipleConfigurations,
    -};
    +export default new TwitterAuthAdapter();
    
  • src/Adapters/Auth/vkontakte.js+23 19 modified
    @@ -4,28 +4,32 @@
     
     const httpsRequest = require('./httpsRequest');
     var Parse = require('parse/node').Parse;
    +import Config from '../../Config';
    +import Deprecator from '../../Deprecator/Deprecator';
     
     // Returns a promise that fulfills iff this user id is valid.
    -function validateAuthData(authData, params) {
    -  return vkOAuth2Request(params).then(function (response) {
    -    if (response && response.access_token) {
    -      return request(
    -        'api.vk.com',
    -        'method/users.get?access_token=' + authData.access_token + '&v=' + params.apiVersion
    -      ).then(function (response) {
    -        if (
    -          response &&
    -          response.response &&
    -          response.response.length &&
    -          response.response[0].id == authData.id
    -        ) {
    -          return;
    -        }
    -        throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Vk auth is invalid for this user.');
    -      });
    -    }
    +async function validateAuthData(authData, params) {
    +  const config = Config.get(Parse.applicationId);
    +  Deprecator.logRuntimeDeprecation({ usage: 'vkontakte adapter' });
    +
    +  const vkConfig = config.auth.vkontakte;
    +  if (!vkConfig?.enableInsecureAuth || !config.enableInsecureAuthAdapters) {
    +    throw new Parse.Error('Vk only works with enableInsecureAuth: true');
    +  }
    +
    +  const response = await vkOAuth2Request(params);
    +  if (!response?.access_token) {
         throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Vk appIds or appSecret is incorrect.');
    -  });
    +  }
    +
    +  const vkUser = await request(
    +    'api.vk.com',
    +    `method/users.get?access_token=${authData.access_token}&v=${params.apiVersion}`
    +  );
    +
    +  if (!vkUser?.response?.length || vkUser.response[0].id !== authData.id) {
    +    throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Vk auth is invalid for this user.');
    +  }
     }
     
     function vkOAuth2Request(params) {
    
  • src/Adapters/Auth/wechat.js+115 25 modified
    @@ -1,30 +1,120 @@
    -// Helper functions for accessing the WeChat Graph API.
    -const httpsRequest = require('./httpsRequest');
    -var Parse = require('parse/node').Parse;
    -
    -// Returns a promise that fulfills iff this user id is valid.
    -function validateAuthData(authData) {
    -  return graphRequest('auth?access_token=' + authData.access_token + '&openid=' + authData.id).then(
    -    function (data) {
    -      if (data.errcode == 0) {
    -        return;
    -      }
    -      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'wechat auth is invalid for this user.');
    +/**
    + * Parse Server authentication adapter for WeChat.
    + *
    + * @class WeChatAdapter
    + * @param {Object} options - The adapter options object.
    + * @param {boolean} [options.enableInsecureAuth=false] - **[DEPRECATED]** Enable insecure authentication (not recommended).
    + * @param {string} options.clientId - Your WeChat App ID.
    + * @param {string} options.clientSecret - Your WeChat App Secret.
    + *
    + * @description
    + * ## Parse Server Configuration
    + * To configure Parse Server for WeChat authentication, use the following structure:
    + * ### Secure Configuration (Recommended)
    + * ```json
    + * {
    + *   "auth": {
    + *     "wechat": {
    + *       "clientId": "your-client-id",
    + *       "clientSecret": "your-client-secret"
    + *     }
    + *   }
    + * }
    + * ```
    + * ### Insecure Configuration (Not Recommended)
    + * ```json
    + * {
    + *   "auth": {
    + *     "wechat": {
    + *       "enableInsecureAuth": true
    + *     }
    + *   }
    + * }
    + * ```
    + *
    + * The adapter requires the following `authData` fields:
    + * - **With `enableInsecureAuth` (Not Recommended)**: `id`, `access_token`.
    + * - **Without `enableInsecureAuth`**: `code`.
    + *
    + * ## Auth Payloads
    + * ### Secure Authentication Payload (Recommended)
    + * ```json
    + * {
    + *   "wechat": {
    + *     "code": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    + *   }
    + * }
    + * ```
    + * ### Insecure Authentication Payload (Not Recommended)
    + * ```json
    + * {
    + *   "wechat": {
    + *     "id": "1234567",
    + *     "access_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    + *   }
    + * }
    + * ```
    + *
    + * ## Notes
    + * - With `enableInsecureAuth`, the adapter directly validates the `id` and `access_token` sent by the client.
    + * - Without `enableInsecureAuth`, the adapter uses the `code` provided by the client to exchange for an access token via WeChat's OAuth API.
    + * - The `enableInsecureAuth` flag is **deprecated** and may be removed in future versions. Use secure authentication with the `code` field instead.
    + *
    + * @example <caption>Auth Data Example</caption>
    + * // Example authData provided by the client:
    + * const authData = {
    + *   wechat: {
    + *     code: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    + *   }
    + * };
    + *
    + * @see {@link https://developers.weixin.qq.com/doc/offiaccount/en/OA_Web_Apps/Wechat_webpage_authorization.html WeChat Authentication Documentation}
    + */
    +
    +import BaseAuthCodeAdapter from './BaseCodeAuthAdapter';
    +
    +class WeChatAdapter extends BaseAuthCodeAdapter {
    +  constructor() {
    +    super('WeChat');
    +  }
    +
    +  async getUserFromAccessToken(access_token, authData) {
    +    const response = await fetch(
    +      `https://api.weixin.qq.com/sns/auth?access_token=${access_token}&openid=${authData.id}`
    +    );
    +
    +    const data = await response.json();
    +
    +    if (!response.ok || data.errcode !== 0) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'WeChat auth is invalid for this user.');
         }
    -  );
    -}
     
    -// Returns a promise that fulfills if this app id is valid.
    -function validateAppId() {
    -  return Promise.resolve();
    -}
    +    return data;
    +  }
    +
    +  async getAccessTokenFromCode(authData) {
    +    if (!authData.code) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'WeChat auth requires a code to be sent.');
    +    }
    +
    +    const appId = this.clientId;
    +    const appSecret = this.clientSecret
    +
    +
    +    const response = await fetch(
    +      `https://api.weixin.qq.com/sns/oauth2/access_token?appid=${appId}&secret=${appSecret}&code=${authData.code}&grant_type=authorization_code`
    +    );
    +
    +    const data = await response.json();
    +
    +    if (!response.ok || data.errcode) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'WeChat auth is invalid for this user.');
    +    }
    +
    +    authData.id = data.openid;
     
    -// A promisey wrapper for WeChat graph requests.
    -function graphRequest(path) {
    -  return httpsRequest.get('https://api.weixin.qq.com/sns/' + path);
    +    return data.access_token;
    +  }
     }
     
    -module.exports = {
    -  validateAppId,
    -  validateAuthData,
    -};
    +export default new WeChatAdapter();
    
  • src/Adapters/Auth/weibo.js+144 36 modified
    @@ -1,41 +1,149 @@
    -// Helper functions for accessing the weibo Graph API.
    -var httpsRequest = require('./httpsRequest');
    -var Parse = require('parse/node').Parse;
    -var querystring = require('querystring');
    -
    -// Returns a promise that fulfills iff this user id is valid.
    -function validateAuthData(authData) {
    -  return graphRequest(authData.access_token).then(function (data) {
    -    if (data && data.uid == authData.id) {
    -      return;
    +/**
    + * Parse Server authentication adapter for Weibo.
    + *
    + * @class WeiboAdapter
    + * @param {Object} options - The adapter configuration options.
    + * @param {boolean} [options.enableInsecureAuth=false] - **[DEPRECATED]** Enable insecure authentication (not recommended).
    + * @param {string} options.clientId - Your Weibo client ID.
    + * @param {string} options.clientSecret - Your Weibo client secret.
    + *
    + * @description
    + * ## Parse Server Configuration
    + * To configure Parse Server for Weibo authentication, use the following structure:
    + * ### Secure Configuration
    + * ```json
    + * {
    + *   "auth": {
    + *     "weibo": {
    + *       "clientId": "your-client-id",
    + *       "clientSecret": "your-client-secret"
    + *     }
    + *   }
    + * }
    + * ```
    + * ### Insecure Configuration (Not Recommended)
    + * ```json
    + * {
    + *   "auth": {
    + *     "weibo": {
    + *       "enableInsecureAuth": true
    + *     }
    + *   }
    + * }
    + * ```
    + *
    + * The adapter requires the following `authData` fields:
    + * - **Secure Authentication**: `code`, `redirect_uri`.
    + * - **Insecure Authentication (Not Recommended)**: `id`, `access_token`.
    + *
    + * ## Auth Payloads
    + * ### Secure Authentication Payload
    + * ```json
    + * {
    + *   "weibo": {
    + *     "code": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    + *     "redirect_uri": "https://example.com/callback"
    + *   }
    + * }
    + * ```
    + * ### Insecure Authentication Payload (Not Recommended)
    + * ```json
    + * {
    + *   "weibo": {
    + *     "id": "1234567",
    + *     "access_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    + *   }
    + * }
    + * ```
    + *
    + * ## Notes
    + * - **Insecure Authentication**: When `enableInsecureAuth` is enabled, the adapter directly validates the `id` and `access_token` provided by the client.
    + * - **Secure Authentication**: When `enableInsecureAuth` is disabled, the adapter exchanges the `code` and `redirect_uri` for an access token using Weibo's OAuth API.
    + * - `enableInsecureAuth` is **deprecated** and may be removed in future versions. Use secure authentication with `code` and `redirect_uri`.
    + *
    + * @example <caption>Auth Data Example (Secure)</caption>
    + * const authData = {
    + *   weibo: {
    + *     code: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    + *     redirect_uri: "https://example.com/callback"
    + *   }
    + * };
    + *
    + * @example <caption>Auth Data Example (Insecure - Not Recommended)</caption>
    + * const authData = {
    + *   weibo: {
    + *     id: "1234567",
    + *     access_token: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    + *   }
    + * };
    + *
    + * @see {@link https://open.weibo.com/wiki/Oauth2/access_token Weibo Authentication Documentation}
    + */
    +
    +import BaseAuthCodeAdapter from './BaseCodeAuthAdapter';
    +import querystring from 'querystring';
    +
    +class WeiboAdapter extends BaseAuthCodeAdapter {
    +  constructor() {
    +    super('Weibo');
    +  }
    +
    +  async getUserFromAccessToken(access_token) {
    +    const postData = querystring.stringify({
    +      access_token: access_token,
    +    });
    +
    +    const response = await fetch('https://api.weibo.com/oauth2/get_token_info', {
    +      method: 'POST',
    +      headers: {
    +        'Content-Type': 'application/x-www-form-urlencoded',
    +      },
    +      body: postData,
    +    });
    +
    +    const data = await response.json();
    +
    +    if (!response.ok) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Weibo auth is invalid for this user.');
         }
    -    throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'weibo auth is invalid for this user.');
    -  });
    -}
     
    -// Returns a promise that fulfills if this app id is valid.
    -function validateAppId() {
    -  return Promise.resolve();
    -}
    +    return {
    +      id: data.uid,
    +    }
    +  }
    +
    +  async getAccessTokenFromCode(authData) {
    +    if (!authData?.code || !authData?.redirect_uri) {
    +      throw new Parse.Error(
    +        Parse.Error.OBJECT_NOT_FOUND,
    +        'Weibo auth requires code and redirect_uri to be sent.'
    +      );
    +    }
    +
    +    const postData = querystring.stringify({
    +      client_id: this.clientId,
    +      client_secret: this.clientSecret,
    +      grant_type: 'authorization_code',
    +      code: authData.code,
    +      redirect_uri: authData.redirect_uri,
    +    });
    +
    +    const response = await fetch('https://api.weibo.com/oauth2/access_token', {
    +      method: 'POST',
    +      headers: {
    +        'Content-Type': 'application/x-www-form-urlencoded',
    +      },
    +      body: postData,
    +    });
    +
    +    const data = await response.json();
    +
    +    if (!response.ok || data.errcode) {
    +      throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Weibo auth is invalid for this user.');
    +    }
     
    -// A promisey wrapper for weibo graph requests.
    -function graphRequest(access_token) {
    -  var postData = querystring.stringify({
    -    access_token: access_token,
    -  });
    -  var options = {
    -    hostname: 'api.weibo.com',
    -    path: '/oauth2/get_token_info',
    -    method: 'POST',
    -    headers: {
    -      'Content-Type': 'application/x-www-form-urlencoded',
    -      'Content-Length': Buffer.byteLength(postData),
    -    },
    -  };
    -  return httpsRequest.request(options, postData);
    +    return data.access_token;
    +  }
     }
     
    -module.exports = {
    -  validateAppId,
    -  validateAuthData,
    -};
    +export default new WeiboAdapter();
    
  • src/Auth.js+27 18 modified
    @@ -417,26 +417,35 @@ Auth.prototype._getAllRolesNamesForRoleIds = function (roleIDs, names = [], quer
         });
     };
     
    -const findUsersWithAuthData = (config, authData) => {
    +const findUsersWithAuthData = async (config, authData, beforeFind) => {
       const providers = Object.keys(authData);
    -  const query = providers
    -    .reduce((memo, provider) => {
    -      if (!authData[provider] || (authData && !authData[provider].id)) {
    -        return memo;
    +
    +  const queries = await Promise.all(
    +    providers.map(async provider => {
    +      const providerAuthData = authData[provider];
    +
    +      const adapter = config.authDataManager.getValidatorForProvider(provider)?.adapter;
    +      if (beforeFind && typeof adapter?.beforeFind === 'function') {
    +        await adapter.beforeFind(providerAuthData);
           }
    -      const queryKey = `authData.${provider}.id`;
    -      const query = {};
    -      query[queryKey] = authData[provider].id;
    -      memo.push(query);
    -      return memo;
    -    }, [])
    -    .filter(q => {
    -      return typeof q !== 'undefined';
    -    });
     
    -  return query.length > 0
    -    ? config.database.find('_User', { $or: query }, { limit: 2 })
    -    : Promise.resolve([]);
    +      if (!providerAuthData?.id) {
    +        return null;
    +      }
    +
    +      return { [`authData.${provider}.id`]: providerAuthData.id };
    +    })
    +  );
    +
    +  // Filter out null queries
    +  const validQueries = queries.filter(query => query !== null);
    +
    +  if (!validQueries.length) {
    +    return [];
    +  }
    +
    +  // Perform database query
    +  return config.database.find('_User', { $or: validQueries }, { limit: 2 });
     };
     
     const hasMutatedAuthData = (authData, userAuthData) => {
    @@ -539,7 +548,7 @@ const handleAuthDataValidation = async (authData, req, foundUser) => {
             acc.authData[provider] = null;
             continue;
           }
    -      const { validator } = req.config.authDataManager.getValidatorForProvider(provider);
    +      const { validator } = req.config.authDataManager.getValidatorForProvider(provider) || {};
           const authProvider = (req.config.auth || {})[provider] || {};
           if (!validator || authProvider.enabled === false) {
             throw new Parse.Error(
    
  • src/cli/parse-server.js+1 0 modified
    @@ -32,6 +32,7 @@ runner({
       help,
       usage: '[options] <path/to/configuration.json>',
       start: function (program, options, logOptions) {
    +
         if (!options.appId || !options.masterKey) {
           program.outputHelp();
           console.error('');
    
  • src/Config.js+12 0 modified
    @@ -20,6 +20,7 @@ import {
       SecurityOptions,
     } from './Options/Definitions';
     import ParseServer from './cloud-code/Parse.Server';
    +import Deprecator from './Deprecator/Deprecator';
     
     function removeTrailingSlash(str) {
       if (!str) {
    @@ -84,6 +85,7 @@ export class Config {
         pages,
         security,
         enforcePrivateUsers,
    +    enableInsecureAuthAdapters,
         schema,
         requestKeywordDenylist,
         allowExpiredAuthDataToken,
    @@ -129,6 +131,7 @@ export class Config {
         this.validateSecurityOptions(security);
         this.validateSchemaOptions(schema);
         this.validateEnforcePrivateUsers(enforcePrivateUsers);
    +    this.validateEnableInsecureAuthAdapters(enableInsecureAuthAdapters);
         this.validateAllowExpiredAuthDataToken(allowExpiredAuthDataToken);
         this.validateRequestKeywordDenylist(requestKeywordDenylist);
         this.validateRateLimit(rateLimit);
    @@ -504,6 +507,15 @@ export class Config {
         }
       }
     
    +  static validateEnableInsecureAuthAdapters(enableInsecureAuthAdapters) {
    +    if (enableInsecureAuthAdapters && typeof enableInsecureAuthAdapters !== 'boolean') {
    +      throw 'Parse Server option enableInsecureAuthAdapters must be a boolean.';
    +    }
    +    if (enableInsecureAuthAdapters) {
    +      Deprecator.logRuntimeDeprecation({ usage: 'insecure adapter' });
    +    }
    +  }
    +
       get mount() {
         var mount = this._mount;
         if (this.publicServerURL) {
    
  • src/Deprecator/Deprecations.js+4 1 modified
    @@ -15,4 +15,7 @@
      *
      * If there are no deprecations, this must return an empty array.
      */
    -module.exports = [{ optionKey: 'encodeParseObjectInCloudFunction', changeNewKey: '' }]
    +module.exports = [
    +  { optionKey: 'encodeParseObjectInCloudFunction', changeNewDefault: 'true' },
    +  { optionKey: 'enableInsecureAuthAdapters', changeNewDefault: 'false' },
    +];
    
  • src/Options/Definitions.js+7 0 modified
    @@ -233,6 +233,13 @@ module.exports.ParseServerOptions = {
         action: parsers.booleanParser,
         default: false,
       },
    +  enableInsecureAuthAdapters: {
    +    env: 'PARSE_SERVER_ENABLE_INSECURE_AUTH_ADAPTERS',
    +    help:
    +      'Enable (or disable) insecure auth adapters, defaults to true. Insecure auth adapters are deprecated and it is recommended to disable them.',
    +    action: parsers.booleanParser,
    +    default: true,
    +  },
       encodeParseObjectInCloudFunction: {
         env: 'PARSE_SERVER_ENCODE_PARSE_OBJECT_IN_CLOUD_FUNCTION',
         help:
    
  • src/Options/docs.js+1 0 modified
    @@ -43,6 +43,7 @@
      * @property {Boolean} enableAnonymousUsers Enable (or disable) anonymous users, defaults to true
      * @property {Boolean} enableCollationCaseComparison Optional. If set to `true`, the collation rule of case comparison for queries and indexes is enabled. Enable this option to run Parse Server with MongoDB Atlas Serverless or AWS Amazon DocumentDB. If `false`, the collation rule of case comparison is disabled. Default is `false`.
      * @property {Boolean} enableExpressErrorHandler Enables the default express error handler for all errors
    + * @property {Boolean} enableInsecureAuthAdapters Enable (or disable) insecure auth adapters, defaults to true. Insecure auth adapters are deprecated and it is recommended to disable them.
      * @property {Boolean} encodeParseObjectInCloudFunction If set to `true`, a `Parse.Object` that is in the payload when calling a Cloud Function will be converted to an instance of `Parse.Object`. If `false`, the object will not be converted and instead be a plain JavaScript object, which contains the raw data of a `Parse.Object` but is not an actual instance of `Parse.Object`. Default is `false`. <br><br>ℹ️ The expected behavior would be that the object is converted to an instance of `Parse.Object`, so you would normally set this option to `true`. The default is `false` because this is a temporary option that has been introduced to avoid a breaking change when fixing a bug where JavaScript objects are not converted to actual instances of `Parse.Object`.
      * @property {String} encryptionKey Key for encrypting your files
      * @property {Boolean} enforcePrivateUsers Set to true if new users should be created without public read and write access.
    
  • src/Options/index.js+4 0 modified
    @@ -161,6 +161,10 @@ export interface ParseServerOptions {
       /* Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication
       :ENV: PARSE_SERVER_AUTH_PROVIDERS */
       auth: ?{ [string]: AuthAdapter };
    +  /* Enable (or disable) insecure auth adapters, defaults to true. Insecure auth adapters are deprecated and it is recommended to disable them.
    +  :ENV: PARSE_SERVER_ENABLE_INSECURE_AUTH_ADAPTERS
    +  :DEFAULT: true */
    +  enableInsecureAuthAdapters: ?boolean;
       /* Max file size for uploads, defaults to 20mb
       :DEFAULT: 20mb */
       maxUploadSize: ?string;
    
  • src/RestWrite.js+3 4 modified
    @@ -458,9 +458,8 @@ RestWrite.prototype.validateAuthData = function () {
       var providers = Object.keys(authData);
       if (providers.length > 0) {
         const canHandleAuthData = providers.some(provider => {
    -      var providerAuthData = authData[provider];
    -      var hasToken = providerAuthData && providerAuthData.id;
    -      return hasToken || providerAuthData === null;
    +      const providerAuthData = authData[provider] || {};
    +      return !!Object.keys(providerAuthData).length;
         });
         if (canHandleAuthData || hasUsernameAndPassword || this.auth.isMaster || this.getUserId()) {
           return this.handleAuthData(authData);
    @@ -520,7 +519,7 @@ RestWrite.prototype.ensureUniqueAuthDataId = async function () {
     };
     
     RestWrite.prototype.handleAuthData = async function (authData) {
    -  const r = await Auth.findUsersWithAuthData(this.config, authData);
    +  const r = await Auth.findUsersWithAuthData(this.config, authData, true);
       const results = this.filteredObjectsByACL(r);
     
       const userId = this.getUserId();
    
  • src/Security/CheckGroups/CheckGroupServerConfig.js+11 0 modified
    @@ -69,6 +69,17 @@ class CheckGroupServerConfig extends CheckGroup {
               }
             },
           }),
    +      new Check({
    +        title: 'Insecure auth adapters disabled',
    +        warning:
    +          "Attackers may explore insecure auth adapters' vulnerabilities and log in on behalf of another user.",
    +        solution: "Change Parse Server configuration to 'enableInsecureAuthAdapters: false'.",
    +        check: () => {
    +          if (config.enableInsecureAuthAdapters !== false) {
    +            throw 1;
    +          }
    +        },
    +      }),
         ];
       }
     }
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

8

News mentions

0

No linked articles in our index yet.