Add production Google Auth Adapter instead of using the development url (#6734)
* Add the production Google Auth Adapter instead of using the development url * Update tests to the new google auth * lint
This commit is contained in:
@@ -19,7 +19,7 @@ const responses = {
|
|||||||
microsoft: { id: 'userId', mail: 'userMail' },
|
microsoft: { id: 'userId', mail: 'userMail' },
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('AuthenticationProviders', function() {
|
describe('AuthenticationProviders', function () {
|
||||||
[
|
[
|
||||||
'apple',
|
'apple',
|
||||||
'gcenter',
|
'gcenter',
|
||||||
@@ -42,8 +42,8 @@ describe('AuthenticationProviders', function() {
|
|||||||
'weibo',
|
'weibo',
|
||||||
'phantauth',
|
'phantauth',
|
||||||
'microsoft',
|
'microsoft',
|
||||||
].map(function(providerName) {
|
].map(function (providerName) {
|
||||||
it('Should validate structure of ' + providerName, done => {
|
it('Should validate structure of ' + providerName, (done) => {
|
||||||
const provider = require('../lib/Adapters/Auth/' + providerName);
|
const provider = require('../lib/Adapters/Auth/' + providerName);
|
||||||
jequal(typeof provider.validateAuthData, 'function');
|
jequal(typeof provider.validateAuthData, 'function');
|
||||||
jequal(typeof provider.validateAppId, 'function');
|
jequal(typeof provider.validateAppId, 'function');
|
||||||
@@ -66,12 +66,12 @@ describe('AuthenticationProviders', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it(`should provide the right responses for adapter ${providerName}`, async () => {
|
it(`should provide the right responses for adapter ${providerName}`, async () => {
|
||||||
const noResponse = ['twitter', 'apple', 'gcenter'];
|
const noResponse = ['twitter', 'apple', 'gcenter', 'google'];
|
||||||
if (noResponse.includes(providerName)) {
|
if (noResponse.includes(providerName)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
spyOn(require('../lib/Adapters/Auth/httpsRequest'), 'get').and.callFake(
|
spyOn(require('../lib/Adapters/Auth/httpsRequest'), 'get').and.callFake(
|
||||||
options => {
|
(options) => {
|
||||||
if (
|
if (
|
||||||
options ===
|
options ===
|
||||||
'https://oauth.vk.com/access_token?client_id=appId&client_secret=appSecret&v=5.59&grant_type=client_credentials'
|
'https://oauth.vk.com/access_token?client_id=appId&client_secret=appSecret&v=5.59&grant_type=client_credentials'
|
||||||
@@ -101,7 +101,7 @@ describe('AuthenticationProviders', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const getMockMyOauthProvider = function() {
|
const getMockMyOauthProvider = function () {
|
||||||
return {
|
return {
|
||||||
authData: {
|
authData: {
|
||||||
id: '12345',
|
id: '12345',
|
||||||
@@ -114,7 +114,7 @@ describe('AuthenticationProviders', function() {
|
|||||||
synchronizedAuthToken: null,
|
synchronizedAuthToken: null,
|
||||||
synchronizedExpiration: null,
|
synchronizedExpiration: null,
|
||||||
|
|
||||||
authenticate: function(options) {
|
authenticate: function (options) {
|
||||||
if (this.shouldError) {
|
if (this.shouldError) {
|
||||||
options.error(this, 'An error occurred');
|
options.error(this, 'An error occurred');
|
||||||
} else if (this.shouldCancel) {
|
} else if (this.shouldCancel) {
|
||||||
@@ -123,7 +123,7 @@ describe('AuthenticationProviders', function() {
|
|||||||
options.success(this, this.authData);
|
options.success(this, this.authData);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
restoreAuthentication: function(authData) {
|
restoreAuthentication: function (authData) {
|
||||||
if (!authData) {
|
if (!authData) {
|
||||||
this.synchronizedUserId = null;
|
this.synchronizedUserId = null;
|
||||||
this.synchronizedAuthToken = null;
|
this.synchronizedAuthToken = null;
|
||||||
@@ -135,10 +135,10 @@ describe('AuthenticationProviders', function() {
|
|||||||
this.synchronizedExpiration = authData.expiration_date;
|
this.synchronizedExpiration = authData.expiration_date;
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
getAuthType: function() {
|
getAuthType: function () {
|
||||||
return 'myoauth';
|
return 'myoauth';
|
||||||
},
|
},
|
||||||
deauthenticate: function() {
|
deauthenticate: function () {
|
||||||
this.loggedOut = true;
|
this.loggedOut = true;
|
||||||
this.restoreAuthentication(null);
|
this.restoreAuthentication(null);
|
||||||
},
|
},
|
||||||
@@ -146,16 +146,16 @@ describe('AuthenticationProviders', function() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Parse.User.extend({
|
Parse.User.extend({
|
||||||
extended: function() {
|
extended: function () {
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const createOAuthUser = function(callback) {
|
const createOAuthUser = function (callback) {
|
||||||
return createOAuthUserWithSessionToken(undefined, callback);
|
return createOAuthUserWithSessionToken(undefined, callback);
|
||||||
};
|
};
|
||||||
|
|
||||||
const createOAuthUserWithSessionToken = function(token, callback) {
|
const createOAuthUserWithSessionToken = function (token, callback) {
|
||||||
const jsonBody = {
|
const jsonBody = {
|
||||||
authData: {
|
authData: {
|
||||||
myoauth: getMockMyOauthProvider().authData,
|
myoauth: getMockMyOauthProvider().authData,
|
||||||
@@ -175,7 +175,7 @@ describe('AuthenticationProviders', function() {
|
|||||||
body: jsonBody,
|
body: jsonBody,
|
||||||
};
|
};
|
||||||
return request(options)
|
return request(options)
|
||||||
.then(response => {
|
.then((response) => {
|
||||||
if (callback) {
|
if (callback) {
|
||||||
callback(null, response, response.data);
|
callback(null, response, response.data);
|
||||||
}
|
}
|
||||||
@@ -184,7 +184,7 @@ describe('AuthenticationProviders', function() {
|
|||||||
body: response.data,
|
body: response.data,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
if (callback) {
|
if (callback) {
|
||||||
callback(error);
|
callback(error);
|
||||||
}
|
}
|
||||||
@@ -192,7 +192,7 @@ describe('AuthenticationProviders', function() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
it('should create user with REST API', done => {
|
it('should create user with REST API', (done) => {
|
||||||
createOAuthUser((error, response, body) => {
|
createOAuthUser((error, response, body) => {
|
||||||
expect(error).toBe(null);
|
expect(error).toBe(null);
|
||||||
const b = body;
|
const b = body;
|
||||||
@@ -203,7 +203,7 @@ describe('AuthenticationProviders', function() {
|
|||||||
const q = new Parse.Query('_Session');
|
const q = new Parse.Query('_Session');
|
||||||
q.equalTo('sessionToken', sessionToken);
|
q.equalTo('sessionToken', sessionToken);
|
||||||
q.first({ useMasterKey: true })
|
q.first({ useMasterKey: true })
|
||||||
.then(res => {
|
.then((res) => {
|
||||||
if (!res) {
|
if (!res) {
|
||||||
fail('should not fail fetching the session');
|
fail('should not fail fetching the session');
|
||||||
done();
|
done();
|
||||||
@@ -219,7 +219,7 @@ describe('AuthenticationProviders', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should only create a single user with REST API', done => {
|
it('should only create a single user with REST API', (done) => {
|
||||||
let objectId;
|
let objectId;
|
||||||
createOAuthUser((error, response, body) => {
|
createOAuthUser((error, response, body) => {
|
||||||
expect(error).toBe(null);
|
expect(error).toBe(null);
|
||||||
@@ -239,9 +239,9 @@ describe('AuthenticationProviders', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should fail to link if session token don't match user", done => {
|
it("should fail to link if session token don't match user", (done) => {
|
||||||
Parse.User.signUp('myUser', 'password')
|
Parse.User.signUp('myUser', 'password')
|
||||||
.then(user => {
|
.then((user) => {
|
||||||
return createOAuthUserWithSessionToken(user.getSessionToken());
|
return createOAuthUserWithSessionToken(user.getSessionToken());
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -250,7 +250,7 @@ describe('AuthenticationProviders', function() {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
return Parse.User.signUp('myUser2', 'password');
|
return Parse.User.signUp('myUser2', 'password');
|
||||||
})
|
})
|
||||||
.then(user => {
|
.then((user) => {
|
||||||
return createOAuthUserWithSessionToken(user.getSessionToken());
|
return createOAuthUserWithSessionToken(user.getSessionToken());
|
||||||
})
|
})
|
||||||
.then(fail, ({ data }) => {
|
.then(fail, ({ data }) => {
|
||||||
@@ -330,16 +330,16 @@ describe('AuthenticationProviders', function() {
|
|||||||
expect(typeof authAdapter.validateAppId).toBe('function');
|
expect(typeof authAdapter.validateAppId).toBe('function');
|
||||||
}
|
}
|
||||||
|
|
||||||
it('properly loads custom adapter', done => {
|
it('properly loads custom adapter', (done) => {
|
||||||
const validAuthData = {
|
const validAuthData = {
|
||||||
id: 'hello',
|
id: 'hello',
|
||||||
token: 'world',
|
token: 'world',
|
||||||
};
|
};
|
||||||
const adapter = {
|
const adapter = {
|
||||||
validateAppId: function() {
|
validateAppId: function () {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
validateAuthData: function(authData) {
|
validateAuthData: function (authData) {
|
||||||
if (
|
if (
|
||||||
authData.id == validAuthData.id &&
|
authData.id == validAuthData.id &&
|
||||||
authData.token == validAuthData.token
|
authData.token == validAuthData.token
|
||||||
@@ -370,14 +370,14 @@ describe('AuthenticationProviders', function() {
|
|||||||
expect(appIdSpy).not.toHaveBeenCalled();
|
expect(appIdSpy).not.toHaveBeenCalled();
|
||||||
done();
|
done();
|
||||||
},
|
},
|
||||||
err => {
|
(err) => {
|
||||||
jfail(err);
|
jfail(err);
|
||||||
done();
|
done();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('properly loads custom adapter module object', done => {
|
it('properly loads custom adapter module object', (done) => {
|
||||||
const authenticationHandler = authenticationLoader({
|
const authenticationHandler = authenticationLoader({
|
||||||
customAuthentication: path.resolve('./spec/support/CustomAuth.js'),
|
customAuthentication: path.resolve('./spec/support/CustomAuth.js'),
|
||||||
});
|
});
|
||||||
@@ -394,14 +394,14 @@ describe('AuthenticationProviders', function() {
|
|||||||
() => {
|
() => {
|
||||||
done();
|
done();
|
||||||
},
|
},
|
||||||
err => {
|
(err) => {
|
||||||
jfail(err);
|
jfail(err);
|
||||||
done();
|
done();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('properly loads custom adapter module object (again)', done => {
|
it('properly loads custom adapter module object (again)', (done) => {
|
||||||
const authenticationHandler = authenticationLoader({
|
const authenticationHandler = authenticationLoader({
|
||||||
customAuthentication: {
|
customAuthentication: {
|
||||||
module: path.resolve('./spec/support/CustomAuthFunction.js'),
|
module: path.resolve('./spec/support/CustomAuthFunction.js'),
|
||||||
@@ -421,7 +421,7 @@ describe('AuthenticationProviders', function() {
|
|||||||
() => {
|
() => {
|
||||||
done();
|
done();
|
||||||
},
|
},
|
||||||
err => {
|
(err) => {
|
||||||
jfail(err);
|
jfail(err);
|
||||||
done();
|
done();
|
||||||
}
|
}
|
||||||
@@ -530,7 +530,7 @@ describe('AuthenticationProviders', function() {
|
|||||||
expect(providerOptions.appSecret).toEqual('secret');
|
expect(providerOptions.appSecret).toEqual('secret');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail if Facebook appIds is not configured properly', done => {
|
it('should fail if Facebook appIds is not configured properly', (done) => {
|
||||||
const options = {
|
const options = {
|
||||||
facebookaccountkit: {
|
facebookaccountkit: {
|
||||||
appIds: [],
|
appIds: [],
|
||||||
@@ -540,13 +540,13 @@ describe('AuthenticationProviders', function() {
|
|||||||
'facebookaccountkit',
|
'facebookaccountkit',
|
||||||
options
|
options
|
||||||
);
|
);
|
||||||
adapter.validateAppId(appIds).then(done.fail, err => {
|
adapter.validateAppId(appIds).then(done.fail, (err) => {
|
||||||
expect(err.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
|
expect(err.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail to validate Facebook accountkit auth with bad token', done => {
|
it('should fail to validate Facebook accountkit auth with bad token', (done) => {
|
||||||
const options = {
|
const options = {
|
||||||
facebookaccountkit: {
|
facebookaccountkit: {
|
||||||
appIds: ['a', 'b'],
|
appIds: ['a', 'b'],
|
||||||
@@ -560,14 +560,14 @@ describe('AuthenticationProviders', function() {
|
|||||||
'facebookaccountkit',
|
'facebookaccountkit',
|
||||||
options
|
options
|
||||||
);
|
);
|
||||||
adapter.validateAuthData(authData).then(done.fail, err => {
|
adapter.validateAuthData(authData).then(done.fail, (err) => {
|
||||||
expect(err.code).toBe(190);
|
expect(err.code).toBe(190);
|
||||||
expect(err.type).toBe('OAuthException');
|
expect(err.type).toBe('OAuthException');
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail to validate Facebook accountkit auth with bad token regardless of app secret proof', done => {
|
it('should fail to validate Facebook accountkit auth with bad token regardless of app secret proof', (done) => {
|
||||||
const options = {
|
const options = {
|
||||||
facebookaccountkit: {
|
facebookaccountkit: {
|
||||||
appIds: ['a', 'b'],
|
appIds: ['a', 'b'],
|
||||||
@@ -582,7 +582,9 @@ describe('AuthenticationProviders', function() {
|
|||||||
'facebookaccountkit',
|
'facebookaccountkit',
|
||||||
options
|
options
|
||||||
);
|
);
|
||||||
adapter.validateAuthData(authData, providerOptions).then(done.fail, err => {
|
adapter
|
||||||
|
.validateAuthData(authData, providerOptions)
|
||||||
|
.then(done.fail, (err) => {
|
||||||
expect(err.code).toBe(190);
|
expect(err.code).toBe(190);
|
||||||
expect(err.type).toBe('OAuthException');
|
expect(err.type).toBe('OAuthException');
|
||||||
done();
|
done();
|
||||||
@@ -627,66 +629,124 @@ describe('instagram auth adapter', () => {
|
|||||||
|
|
||||||
describe('google auth adapter', () => {
|
describe('google auth adapter', () => {
|
||||||
const google = require('../lib/Adapters/Auth/google');
|
const google = require('../lib/Adapters/Auth/google');
|
||||||
const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
|
const jwt = require('jsonwebtoken');
|
||||||
|
|
||||||
it('should use id_token for validation is passed', async () => {
|
it('should throw error with missing id_token', async () => {
|
||||||
spyOn(httpsRequest, 'get').and.callFake(() => {
|
|
||||||
return Promise.resolve({ sub: 'userId' });
|
|
||||||
});
|
|
||||||
await google.validateAuthData({ id: 'userId', id_token: 'the_token' }, {});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should use id_token for validation is passed and responds with user_id', async () => {
|
|
||||||
spyOn(httpsRequest, 'get').and.callFake(() => {
|
|
||||||
return Promise.resolve({ user_id: 'userId' });
|
|
||||||
});
|
|
||||||
await google.validateAuthData({ id: 'userId', id_token: 'the_token' }, {});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should use access_token for validation is passed and responds with user_id', async () => {
|
|
||||||
spyOn(httpsRequest, 'get').and.callFake(() => {
|
|
||||||
return Promise.resolve({ user_id: 'userId' });
|
|
||||||
});
|
|
||||||
await google.validateAuthData(
|
|
||||||
{ id: 'userId', access_token: 'the_token' },
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should use access_token for validation is passed with sub', async () => {
|
|
||||||
spyOn(httpsRequest, 'get').and.callFake(() => {
|
|
||||||
return Promise.resolve({ sub: 'userId' });
|
|
||||||
});
|
|
||||||
await google.validateAuthData({ id: 'userId', id_token: 'the_token' }, {});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should fail when the id_token is invalid', async () => {
|
|
||||||
spyOn(httpsRequest, 'get').and.callFake(() => {
|
|
||||||
return Promise.resolve({ sub: 'badId' });
|
|
||||||
});
|
|
||||||
try {
|
try {
|
||||||
await google.validateAuthData(
|
await google.validateAuthData({}, {});
|
||||||
{ id: 'userId', id_token: 'the_token' },
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
fail();
|
fail();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
expect(e.message).toBe('Google auth is invalid for this user.');
|
expect(e.message).toBe('id token is invalid for this user.');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail when the access_token is invalid', async () => {
|
it('should not decode invalid id_token', async () => {
|
||||||
spyOn(httpsRequest, 'get').and.callFake(() => {
|
|
||||||
return Promise.resolve({ sub: 'badId' });
|
|
||||||
});
|
|
||||||
try {
|
try {
|
||||||
await google.validateAuthData(
|
await google.validateAuthData(
|
||||||
{ id: 'userId', access_token: 'the_token' },
|
{ id: 'the_user_id', id_token: 'the_token' },
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
fail();
|
fail();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
expect(e.message).toBe('Google auth is invalid for this user.');
|
expect(e.message).toBe('provided token does not decode as JWT');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// it('should throw error if public key used to encode token is not available', async () => {
|
||||||
|
// const fakeDecodedToken = { header: { kid: '789', alg: 'RS256' } };
|
||||||
|
// try {
|
||||||
|
// spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||||
|
|
||||||
|
// await google.validateAuthData({ id: 'the_user_id', id_token: 'the_token' }, {});
|
||||||
|
// fail();
|
||||||
|
// } catch (e) {
|
||||||
|
// expect(e.message).toBe(
|
||||||
|
// `Unable to find matching key for Key ID: ${fakeDecodedToken.header.kid}`
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
it('(using client id as string) should verify id_token', async () => {
|
||||||
|
const fakeClaim = {
|
||||||
|
iss: 'https://accounts.google.com',
|
||||||
|
aud: 'secret',
|
||||||
|
exp: Date.now(),
|
||||||
|
sub: 'the_user_id',
|
||||||
|
};
|
||||||
|
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||||
|
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||||
|
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||||
|
|
||||||
|
const result = await google.validateAuthData(
|
||||||
|
{ id: 'the_user_id', id_token: 'the_token' },
|
||||||
|
{ clientId: 'secret' }
|
||||||
|
);
|
||||||
|
expect(result).toEqual(fakeClaim);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('(using client id as string) should throw error with with invalid jwt issuer', async () => {
|
||||||
|
const fakeClaim = {
|
||||||
|
iss: 'https://not.google.com',
|
||||||
|
sub: 'the_user_id',
|
||||||
|
};
|
||||||
|
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||||
|
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||||
|
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await google.validateAuthData(
|
||||||
|
{ id: 'the_user_id', id_token: 'the_token' },
|
||||||
|
{ clientId: 'secret' }
|
||||||
|
);
|
||||||
|
fail();
|
||||||
|
} catch (e) {
|
||||||
|
expect(e.message).toBe(
|
||||||
|
'id token not issued by correct provider - expected: https://accounts.google.com | from: https://not.google.com'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
xit('(using client id as string) should throw error with invalid jwt client_id', async () => {
|
||||||
|
const fakeClaim = {
|
||||||
|
iss: 'https://accounts.google.com',
|
||||||
|
aud: 'secret',
|
||||||
|
exp: Date.now(),
|
||||||
|
sub: 'the_user_id',
|
||||||
|
};
|
||||||
|
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||||
|
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||||
|
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await google.validateAuthData(
|
||||||
|
{ id: 'INSERT ID HERE', token: 'INSERT APPLE TOKEN HERE' },
|
||||||
|
{ clientId: 'secret' }
|
||||||
|
);
|
||||||
|
fail();
|
||||||
|
} catch (e) {
|
||||||
|
expect(e.message).toBe('jwt audience invalid. expected: secret');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
xit('should throw error with invalid user id', async () => {
|
||||||
|
const fakeClaim = {
|
||||||
|
iss: 'https://accounts.google.com',
|
||||||
|
aud: 'secret',
|
||||||
|
exp: Date.now(),
|
||||||
|
sub: 'the_user_id',
|
||||||
|
};
|
||||||
|
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||||
|
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||||
|
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await google.validateAuthData(
|
||||||
|
{ id: 'invalid user', token: 'INSERT APPLE TOKEN HERE' },
|
||||||
|
{ clientId: 'INSERT CLIENT ID HERE' }
|
||||||
|
);
|
||||||
|
fail();
|
||||||
|
} catch (e) {
|
||||||
|
expect(e.message).toBe('auth data is invalid for this user.');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1593,13 +1653,13 @@ describe('microsoft graph auth adapter', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail to validate Microsoft Graph auth with bad token', done => {
|
it('should fail to validate Microsoft Graph auth with bad token', (done) => {
|
||||||
const authData = {
|
const authData = {
|
||||||
id: 'fake-id',
|
id: 'fake-id',
|
||||||
mail: 'fake@mail.com',
|
mail: 'fake@mail.com',
|
||||||
access_token: 'very.long.bad.token',
|
access_token: 'very.long.bad.token',
|
||||||
};
|
};
|
||||||
microsoft.validateAuthData(authData).then(done.fail, err => {
|
microsoft.validateAuthData(authData).then(done.fail, (err) => {
|
||||||
expect(err.code).toBe(101);
|
expect(err.code).toBe(101);
|
||||||
expect(err.message).toBe(
|
expect(err.message).toBe(
|
||||||
'Microsoft Graph auth is invalid for this user.'
|
'Microsoft Graph auth is invalid for this user.'
|
||||||
|
|||||||
@@ -1,47 +1,90 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
// Helper functions for accessing the google API.
|
// Helper functions for accessing the google API.
|
||||||
var Parse = require('parse/node').Parse;
|
var Parse = require('parse/node').Parse;
|
||||||
const httpsRequest = require('./httpsRequest');
|
|
||||||
|
|
||||||
function validateIdToken(id, token) {
|
const https = require('https');
|
||||||
return googleRequest('tokeninfo?id_token=' + token).then(response => {
|
const jwt = require('jsonwebtoken');
|
||||||
if (response && (response.sub == id || response.user_id == id)) {
|
|
||||||
return;
|
const TOKEN_ISSUER = 'https://accounts.google.com';
|
||||||
|
|
||||||
|
let cache = {};
|
||||||
|
|
||||||
|
|
||||||
|
// Retrieve Google Signin Keys (with cache control)
|
||||||
|
function getGoogleKeyByKeyId(keyId) {
|
||||||
|
if (cache[keyId] && cache.expiresAt > new Date()) {
|
||||||
|
return cache[keyId];
|
||||||
}
|
}
|
||||||
throw new Parse.Error(
|
|
||||||
Parse.Error.OBJECT_NOT_FOUND,
|
return new Promise((resolve, reject) => {
|
||||||
'Google auth is invalid for this user.'
|
https.get(`https://www.googleapis.com/oauth2/v3/certs`, res => {
|
||||||
);
|
let data = '';
|
||||||
|
res.on('data', chunk => {
|
||||||
|
data += chunk.toString('utf8');
|
||||||
|
});
|
||||||
|
res.on('end', () => {
|
||||||
|
const {keys} = JSON.parse(data);
|
||||||
|
const pems = keys.reduce((pems, {n: modulus, e: exposant, kid}) => Object.assign(pems, {[kid]: rsaPublicKeyToPEM(modulus, exposant)}), {});
|
||||||
|
|
||||||
|
if (res.headers['cache-control']) {
|
||||||
|
var expire = res.headers['cache-control'].match(/max-age=([0-9]+)/);
|
||||||
|
|
||||||
|
if (expire) {
|
||||||
|
cache = Object.assign({}, pems, {expiresAt: new Date((new Date()).getTime() + Number(expire[1]) * 1000)});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(pems[keyId]);
|
||||||
|
});
|
||||||
|
}).on('error', reject);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateAuthToken(id, token) {
|
function getHeaderFromToken(token) {
|
||||||
return googleRequest('tokeninfo?access_token=' + token).then(response => {
|
const decodedToken = jwt.decode(token, {complete: true});
|
||||||
if (response && (response.sub == id || response.user_id == id)) {
|
|
||||||
return;
|
if (!decodedToken) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `provided token does not decode as JWT`);
|
||||||
}
|
}
|
||||||
throw new Parse.Error(
|
|
||||||
Parse.Error.OBJECT_NOT_FOUND,
|
return decodedToken.header;
|
||||||
'Google auth is invalid for this user.'
|
}
|
||||||
);
|
|
||||||
});
|
async function verifyIdToken({id_token: token, id}, {clientId}) {
|
||||||
|
if (!token) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `id token is invalid for this user.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { kid: keyId, alg: algorithm } = getHeaderFromToken(token);
|
||||||
|
let jwtClaims;
|
||||||
|
const googleKey = await getGoogleKeyByKeyId(keyId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
jwtClaims = jwt.verify(token, googleKey, { algorithms: algorithm, audience: clientId });
|
||||||
|
} catch (exception) {
|
||||||
|
const message = exception.message;
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jwtClaims.iss !== TOKEN_ISSUER) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `id token not issued by correct provider - expected: ${TOKEN_ISSUER} | from: ${jwtClaims.iss}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jwtClaims.sub !== id) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `auth data is invalid for this user.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clientId && jwtClaims.aud !== clientId) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `id token not authorized for this clientId.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return jwtClaims;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns a promise that fulfills if this user id is valid.
|
// Returns a promise that fulfills if this user id is valid.
|
||||||
function validateAuthData(authData) {
|
function validateAuthData(authData, options) {
|
||||||
if (authData.id_token) {
|
return verifyIdToken(authData, options);
|
||||||
return validateIdToken(authData.id, authData.id_token);
|
|
||||||
} else {
|
|
||||||
return validateAuthToken(authData.id, authData.access_token).then(
|
|
||||||
() => {
|
|
||||||
// Validation with auth token worked
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
// Try with the id_token param
|
|
||||||
return validateIdToken(authData.id, authData.access_token);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns a promise that fulfills if this app id is valid.
|
// Returns a promise that fulfills if this app id is valid.
|
||||||
@@ -49,12 +92,58 @@ function validateAppId() {
|
|||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
// A promisey wrapper for api requests
|
|
||||||
function googleRequest(path) {
|
|
||||||
return httpsRequest.get('https://www.googleapis.com/oauth2/v3/' + path);
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
validateAppId: validateAppId,
|
validateAppId: validateAppId,
|
||||||
validateAuthData: validateAuthData,
|
validateAuthData: validateAuthData
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Helpers functions to convert the RSA certs to PEM (from jwks-rsa)
|
||||||
|
function rsaPublicKeyToPEM(modulusB64, exponentB64) {
|
||||||
|
const modulus = new Buffer(modulusB64, 'base64');
|
||||||
|
const exponent = new Buffer(exponentB64, 'base64');
|
||||||
|
const modulusHex = prepadSigned(modulus.toString('hex'));
|
||||||
|
const exponentHex = prepadSigned(exponent.toString('hex'));
|
||||||
|
const modlen = modulusHex.length / 2;
|
||||||
|
const explen = exponentHex.length / 2;
|
||||||
|
|
||||||
|
const encodedModlen = encodeLengthHex(modlen);
|
||||||
|
const encodedExplen = encodeLengthHex(explen);
|
||||||
|
const encodedPubkey = '30' +
|
||||||
|
encodeLengthHex(modlen + explen + encodedModlen.length / 2 + encodedExplen.length / 2 + 2) +
|
||||||
|
'02' + encodedModlen + modulusHex +
|
||||||
|
'02' + encodedExplen + exponentHex;
|
||||||
|
|
||||||
|
const der = new Buffer(encodedPubkey, 'hex')
|
||||||
|
.toString('base64');
|
||||||
|
|
||||||
|
let pem = '-----BEGIN RSA PUBLIC KEY-----\n';
|
||||||
|
pem += `${der.match(/.{1,64}/g).join('\n')}`;
|
||||||
|
pem += '\n-----END RSA PUBLIC KEY-----\n';
|
||||||
|
return pem;
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepadSigned(hexStr) {
|
||||||
|
const msb = hexStr[0];
|
||||||
|
if (msb < '0' || msb > '7') {
|
||||||
|
return `00${hexStr}`;
|
||||||
|
}
|
||||||
|
return hexStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toHex(number) {
|
||||||
|
const nstr = number.toString(16);
|
||||||
|
if (nstr.length % 2) {
|
||||||
|
return `0${nstr}`;
|
||||||
|
}
|
||||||
|
return nstr;
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeLengthHex(n) {
|
||||||
|
if (n <= 127) {
|
||||||
|
return toHex(n);
|
||||||
|
}
|
||||||
|
const nHex = toHex(n);
|
||||||
|
const lengthOfLengthByte = 128 + nHex.length / 2;
|
||||||
|
return toHex(lengthOfLengthByte) + nHex;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user