[FIX] Custom Oauth login not working with accessToken (#14113)

Support passing your custom OAuth providers access_token in with login to get a Rocket.Chat token to use with other api calls
pull/13761/head^2
Kautilya Tripathi 6 years ago committed by Aaron Ogle
parent 4ea7efcd99
commit ea8bb4b9b4
  1. 226
      app/custom-oauth/server/custom_oauth_server.js
  2. 1
      app/dolphin/lib/common.js
  3. 1
      app/drupal/lib/common.js
  4. 1
      app/gitlab/lib/common.js
  5. 1
      app/lib/server/methods/addOAuthService.js
  6. 1
      app/lib/server/methods/removeOAuthService.js
  7. 2
      app/lib/server/oauth/oauth.js
  8. 2
      app/lib/server/publications/settings.js
  9. 2
      app/lib/server/startup/oAuthServicesUpdate.js
  10. 1
      app/tokenpass/lib/common.js
  11. 1
      app/wordpress/lib/common.js
  12. 3
      packages/rocketchat-i18n/i18n/en.i18n.json
  13. 2
      server/configuration/accounts_meld.js

@ -1,5 +1,5 @@
import { Meteor } from 'meteor/meteor';
import { Match } from 'meteor/check';
import { Match, check } from 'meteor/check';
import { Accounts } from 'meteor/accounts-base';
import { OAuth } from 'meteor/oauth';
import { HTTP } from 'meteor/http';
@ -8,6 +8,7 @@ import { Logger } from '../../logger';
import { Users } from '../../models';
import _ from 'underscore';
import { isURL } from '../../utils/lib/isURL';
import { registerAccessTokenService } from '../../lib/server/oauth/oauth';
const logger = new Logger('CustomOAuth');
@ -40,6 +41,7 @@ export class CustomOAuth {
Accounts.oauth.registerService(this.name);
this.registerService();
this.addHookToProcessUser();
this.registerAccessTokenService(this.name, this.accessTokenParam);
}
configure(options) {
@ -59,6 +61,10 @@ export class CustomOAuth {
options.identityPath = '/me';
}
if (!Match.test(options.accessTokenParam, String)) {
options.accessTokenParam = 'access_token';
}
this.serverURL = options.serverURL;
this.tokenPath = options.tokenPath;
this.identityPath = options.identityPath;
@ -66,6 +72,7 @@ export class CustomOAuth {
this.identityTokenSentVia = options.identityTokenSentVia;
this.usernameField = (options.usernameField || '').trim();
this.mergeUsers = options.mergeUsers;
this.accessTokenParam = options.accessTokenParam;
if (this.identityTokenSentVia == null || this.identityTokenSentVia === 'default') {
this.identityTokenSentVia = this.tokenSentVia;
@ -134,7 +141,7 @@ export class CustomOAuth {
}
}
getIdentity(accessToken) {
getIdentity(accessToken, accessTokenParam) {
const params = {};
const headers = {
'User-Agent': this.userAgent, // http://doc.gitlab.com/ce/api/users.html#Current-user
@ -143,7 +150,7 @@ export class CustomOAuth {
if (this.identityTokenSentVia === 'header') {
headers.Authorization = `Bearer ${ accessToken }`;
} else {
params.access_token = accessToken;
params[accessTokenParam] = accessToken;
}
try {
@ -162,7 +169,7 @@ export class CustomOAuth {
logger.debug('Identity response', JSON.stringify(data, null, 2));
return data;
return this.normalizeIdentity(data);
} catch (err) {
const error = new Error(`Failed to fetch identity from ${ this.name } at ${ this.identityPath }. ${ err.message }`);
throw _.extend(error, { response: err.response });
@ -173,105 +180,110 @@ export class CustomOAuth {
const self = this;
OAuth.registerService(this.name, 2, null, (query) => {
const accessToken = self.getAccessToken(query);
// console.log 'at:', accessToken
let identity = self.getIdentity(accessToken);
const identity = self.getIdentity(accessToken, this.accessTokenParam);
if (identity) {
// Set 'id' to '_id' for any sources that provide it
if (identity._id && !identity.id) {
identity.id = identity._id;
}
const serviceData = {
_OAuthCustom: true,
accessToken,
};
// Fix for Reddit
if (identity.result) {
identity = identity.result;
}
_.extend(serviceData, identity);
// Fix WordPress-like identities having 'ID' instead of 'id'
if (identity.ID && !identity.id) {
identity.id = identity.ID;
}
const data = {
serviceData,
options: {
profile: {
name: identity.name,
},
},
};
// Fix Auth0-like identities having 'user_id' instead of 'id'
if (identity.user_id && !identity.id) {
identity.id = identity.user_id;
}
return data;
});
}
if (identity.CharacterID && !identity.id) {
identity.id = identity.CharacterID;
}
normalizeIdentity(identity) {
if (identity) {
// Set 'id' to '_id' for any sources that provide it
if (identity._id && !identity.id) {
identity.id = identity._id;
}
// Fix Dataporten having 'user.userid' instead of 'id'
if (identity.user && identity.user.userid && !identity.id) {
if (identity.user.userid_sec && identity.user.userid_sec[0]) {
identity.id = identity.user.userid_sec[0];
} else {
identity.id = identity.user.userid;
}
identity.email = identity.user.email;
}
// Fix for Xenforo [BD]API plugin for 'user.user_id; instead of 'id'
if (identity.user && identity.user.user_id && !identity.id) {
identity.id = identity.user.user_id;
identity.email = identity.user.user_email;
}
// Fix general 'phid' instead of 'id' from phabricator
if (identity.phid && !identity.id) {
identity.id = identity.phid;
}
// Fix for Reddit
if (identity.result) {
identity = identity.result;
}
// Fix Keycloak-like identities having 'sub' instead of 'id'
if (identity.sub && !identity.id) {
identity.id = identity.sub;
}
// Fix WordPress-like identities having 'ID' instead of 'id'
if (identity.ID && !identity.id) {
identity.id = identity.ID;
}
// Fix OpenShift identities where id is in 'metadata' object
if (!identity.id && identity.metadata && identity.metadata.uid) {
identity.id = identity.metadata.uid;
identity.name = identity.fullName;
}
// Fix Auth0-like identities having 'user_id' instead of 'id'
if (identity.user_id && !identity.id) {
identity.id = identity.user_id;
}
// Fix general 'userid' instead of 'id' from provider
if (identity.userid && !identity.id) {
identity.id = identity.userid;
}
if (identity.CharacterID && !identity.id) {
identity.id = identity.CharacterID;
}
// Fix Nextcloud provider
if (!identity.id && identity.ocs && identity.ocs.data && identity.ocs.data.id) {
identity.id = identity.ocs.data.id;
identity.name = identity.ocs.data.displayname;
identity.email = identity.ocs.data.email;
// Fix Dataporten having 'user.userid' instead of 'id'
if (identity.user && identity.user.userid && !identity.id) {
if (identity.user.userid_sec && identity.user.userid_sec[0]) {
identity.id = identity.user.userid_sec[0];
} else {
identity.id = identity.user.userid;
}
identity.email = identity.user.email;
}
// Fix for Xenforo [BD]API plugin for 'user.user_id; instead of 'id'
if (identity.user && identity.user.user_id && !identity.id) {
identity.id = identity.user.user_id;
identity.email = identity.user.user_email;
}
// Fix general 'phid' instead of 'id' from phabricator
if (identity.phid && !identity.id) {
identity.id = identity.phid;
}
// Fix when authenticating from a meteor app with 'emails' field
if (!identity.email && (identity.emails && Array.isArray(identity.emails) && identity.emails.length >= 1)) {
identity.email = identity.emails[0].address ? identity.emails[0].address : undefined;
}
// Fix Keycloak-like identities having 'sub' instead of 'id'
if (identity.sub && !identity.id) {
identity.id = identity.sub;
}
// console.log 'id:', JSON.stringify identity, null, ' '
// Fix OpenShift identities where id is in 'metadata' object
if (!identity.id && identity.metadata && identity.metadata.uid) {
identity.id = identity.metadata.uid;
identity.name = identity.fullName;
}
const serviceData = {
_OAuthCustom: true,
accessToken,
};
// Fix general 'userid' instead of 'id' from provider
if (identity.userid && !identity.id) {
identity.id = identity.userid;
}
_.extend(serviceData, identity);
// Fix Nextcloud provider
if (!identity.id && identity.ocs && identity.ocs.data && identity.ocs.data.id) {
identity.id = identity.ocs.data.id;
identity.name = identity.ocs.data.displayname;
identity.email = identity.ocs.data.email;
}
const data = {
serviceData,
options: {
profile: {
name: identity.name || identity.username || identity.nickname || identity.CharacterName || identity.userName || identity.preferred_username || (identity.user && identity.user.name),
},
},
};
// Fix when authenticating from a meteor app with 'emails' field
if (!identity.email && (identity.emails && Array.isArray(identity.emails) && identity.emails.length >= 1)) {
identity.email = identity.emails[0].address ? identity.emails[0].address : undefined;
}
}
if (this.usernameField) {
identity.username = this.getUsername(identity);
}
// console.log data
identity.name = this.getName(identity);
return data;
});
return identity;
}
retrieveCredential(credentialToken, credentialSecret) {
@ -290,16 +302,19 @@ export class CustomOAuth {
return username;
}
getName(identity) {
const name = identity.name || identity.username || identity.nickname || identity.CharacterName || identity.userName || identity.preferred_username || (identity.user && identity.user.name);
return name;
}
addHookToProcessUser() {
BeforeUpdateOrCreateUserFromExternalService.push((serviceName, serviceData/* , options*/) => {
if (serviceName !== this.name) {
return;
}
if (this.usernameField) {
const username = this.getUsername(serviceData);
const user = Users.findOneByUsername(username);
if (serviceData.username) {
const user = Users.findOneByUsername(serviceData.username);
if (!user) {
return;
}
@ -338,8 +353,43 @@ export class CustomOAuth {
});
}
}
registerAccessTokenService(name, accessTokenParam) {
const self = this;
const whitelisted = [
'id',
'email',
'username',
'name'];
registerAccessTokenService(name, function(options) {
check(options, Match.ObjectIncluding({
accessToken: String,
expiresIn: Match.Integer,
identity: Match.Maybe(Object),
}));
const identity = options.identity || self.getIdentity(options.accessToken, accessTokenParam);
const serviceData = {
accessToken: options.accessToken,
expiresAt: (+new Date) + (1000 * parseInt(options.expiresIn, 10)),
};
const fields = _.pick(identity, whitelisted);
_.extend(serviceData, fields);
return {
serviceData,
options: {
profile: {
name: identity.name,
},
},
};
});
}
}
const { updateOrCreateUserFromExternalService } = Accounts;
Accounts.updateOrCreateUserFromExternalService = function(...args /* serviceName, serviceData, options*/) {

@ -16,6 +16,7 @@ const config = {
forLoggedInUser: ['services.dolphin'],
forOtherUsers: ['services.dolphin.name'],
},
accessTokenParam: 'access_token',
};
const Dolphin = new CustomOAuth('dolphin', config);

@ -19,6 +19,7 @@ const config = {
forLoggedInUser: ['services.drupal'],
forOtherUsers: ['services.drupal.name'],
},
accessTokenParam: 'access_token',
};
const Drupal = new CustomOAuth('drupal', config);

@ -11,6 +11,7 @@ const config = {
forLoggedInUser: ['services.gitlab'],
forOtherUsers: ['services.gitlab.username'],
},
accessTokenParam: 'private_token',
};
const Gitlab = new CustomOAuth('gitlab', config);

@ -29,6 +29,7 @@ Meteor.methods({
settings.add(`Accounts_OAuth_Custom-${ name }-identity_path` , '/me' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Identity_Path', persistent: true });
settings.add(`Accounts_OAuth_Custom-${ name }-authorize_path` , '/oauth/authorize', { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Authorize_Path', persistent: true });
settings.add(`Accounts_OAuth_Custom-${ name }-scope` , 'openid' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Scope', persistent: true });
settings.add(`Accounts_OAuth_Custom-${ name }-access_token_param` , 'access_token' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Access_Token_Param', persistent: true });
settings.add(`Accounts_OAuth_Custom-${ name }-id` , '' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_id', persistent: true });
settings.add(`Accounts_OAuth_Custom-${ name }-secret` , '' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Secret', persistent: true });
settings.add(`Accounts_OAuth_Custom-${ name }-login_style` , 'popup' , { type: 'select' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Login_Style', persistent: true, values: [{ key: 'redirect', i18nLabel: 'Redirect' }, { key: 'popup', i18nLabel: 'Popup' }, { key: '', i18nLabel: 'Default' }] });

@ -25,6 +25,7 @@ Meteor.methods({
settings.removeById(`Accounts_OAuth_Custom-${ name }-identity_path`);
settings.removeById(`Accounts_OAuth_Custom-${ name }-authorize_path`);
settings.removeById(`Accounts_OAuth_Custom-${ name }-scope`);
settings.removeById(`Accounts_OAuth_Custom-${ name }-access_token_param`);
settings.removeById(`Accounts_OAuth_Custom-${ name }-token_sent_via`);
settings.removeById(`Accounts_OAuth_Custom-${ name }-identity_token_sent_via`);
settings.removeById(`Accounts_OAuth_Custom-${ name }-id`);

@ -32,7 +32,7 @@ Accounts.registerLoginHandler(function(options) {
}
// Make sure we're configured
if (!ServiceConfiguration.configurations.findOne({ service: service.serviceName })) {
if (!ServiceConfiguration.configurations.findOne({ service: options.serviceName })) {
throw new ServiceConfiguration.ConfigError();
}

@ -81,7 +81,7 @@ Settings.on('change', ({ clientAction, id, data, diff }) => {
case 'removed': {
const setting = data || Settings.findOneById(id, { fields: { public: 1 } });
if (setting.public === true) {
if (setting && setting.public === true) {
Notifications.notifyAllInThisInstance('public-settings-changed', clientAction, { _id: id });
}
Notifications.notifyLoggedInThisInstance('private-settings-changed', clientAction, { _id: id });

@ -38,6 +38,7 @@ function _OAuthServicesUpdate() {
data.identityPath = settings.get(`${ service.key }-identity_path`);
data.authorizePath = settings.get(`${ service.key }-authorize_path`);
data.scope = settings.get(`${ service.key }-scope`);
data.accessTokenParam = settings.get(`${ service.key }-access_token_param`);
data.buttonLabelText = settings.get(`${ service.key }-button_label_text`);
data.buttonLabelColor = settings.get(`${ service.key }-button_label_color`);
data.loginStyle = settings.get(`${ service.key }-login_style`);
@ -57,6 +58,7 @@ function _OAuthServicesUpdate() {
identityTokenSentVia: data.identityTokenSentVia,
usernameField: data.usernameField,
mergeUsers: data.mergeUsers,
accessTokenParam: data.accessTokenParam,
});
}
if (serviceName === 'Facebook') {

@ -16,6 +16,7 @@ const config = {
forLoggedInUser: ['services.tokenpass'],
forOtherUsers: ['services.tokenpass.name'],
},
accessTokenParam: 'access_token',
};
const Tokenpass = new CustomOAuth('tokenpass', config);

@ -13,6 +13,7 @@ const config = {
forLoggedInUser: ['services.wordpress'],
forOtherUsers: ['services.wordpress.user_login'],
},
accessTokenParam: 'access_token',
};
const WordPress = new CustomOAuth('wordpress', config);

@ -87,6 +87,7 @@
"Accounts_OAuth_Custom_Identity_Token_Sent_Via": "Identity Token Sent Via",
"Accounts_OAuth_Custom_Login_Style": "Login Style",
"Accounts_OAuth_Custom_Merge_Users": "Merge users",
"Accounts_OAuth_Custom_Access_Token_Param": "Param Name for access token",
"Accounts_OAuth_Custom_Scope": "Scope",
"Accounts_OAuth_Custom_Secret": "Secret",
"Accounts_OAuth_Custom_Token_Path": "Token Path",
@ -3199,4 +3200,4 @@
"Your_question": "Your question",
"Your_server_link": "Your server link",
"Your_workspace_is_ready": "Your workspace is ready to use 🎉"
}
}

@ -16,7 +16,7 @@ Accounts.updateOrCreateUserFromExternalService = function(serviceName, serviceDa
];
if (services.includes(serviceName) === false && serviceData._OAuthCustom !== true) {
return;
return orig_updateOrCreateUserFromExternalService.apply(this, [serviceName, serviceData, ...args]);
}
if (serviceName === 'meteor-developer') {

Loading…
Cancel
Save