You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
319 lines
8.2 KiB
319 lines
8.2 KiB
![]()
9 years ago
|
/*globals OAuth*/
|
||
|
|
||
![]()
9 years ago
|
const logger = new Logger('CustomOAuth');
|
||
|
|
||
![]()
9 years ago
|
const Services = {};
|
||
![]()
9 years ago
|
const BeforeUpdateOrCreateUserFromExternalService = [];
|
||
![]()
9 years ago
|
|
||
|
export class CustomOAuth {
|
||
|
constructor(name, options) {
|
||
![]()
9 years ago
|
logger.debug('Init CustomOAuth', name, options);
|
||
|
|
||
![]()
9 years ago
|
this.name = name;
|
||
|
if (!Match.test(this.name, String)) {
|
||
|
throw new Meteor.Error('CustomOAuth: Name is required and must be String');
|
||
|
}
|
||
|
|
||
|
if (Services[this.name]) {
|
||
|
Services[this.name].configure(options);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
Services[this.name] = this;
|
||
|
|
||
|
this.configure(options);
|
||
|
|
||
|
this.userAgent = 'Meteor';
|
||
|
if (Meteor.release) {
|
||
![]()
8 years ago
|
this.userAgent += `/${ Meteor.release }`;
|
||
![]()
9 years ago
|
}
|
||
|
|
||
|
Accounts.oauth.registerService(this.name);
|
||
|
this.registerService();
|
||
![]()
9 years ago
|
this.addHookToProcessUser();
|
||
![]()
9 years ago
|
}
|
||
|
|
||
|
configure(options) {
|
||
|
if (!Match.test(options, Object)) {
|
||
|
throw new Meteor.Error('CustomOAuth: Options is required and must be Object');
|
||
|
}
|
||
|
|
||
|
if (!Match.test(options.serverURL, String)) {
|
||
|
throw new Meteor.Error('CustomOAuth: Options.serverURL is required and must be String');
|
||
|
}
|
||
|
|
||
|
if (!Match.test(options.tokenPath, String)) {
|
||
|
options.tokenPath = '/oauth/token';
|
||
|
}
|
||
|
|
||
|
if (!Match.test(options.identityPath, String)) {
|
||
|
options.identityPath = '/me';
|
||
|
}
|
||
|
|
||
|
this.serverURL = options.serverURL;
|
||
|
this.tokenPath = options.tokenPath;
|
||
|
this.identityPath = options.identityPath;
|
||
|
this.tokenSentVia = options.tokenSentVia;
|
||
![]()
9 years ago
|
this.usernameField = (options.usernameField || '').trim();
|
||
|
this.mergeUsers = options.mergeUsers;
|
||
![]()
9 years ago
|
|
||
|
if (!/^https?:\/\/.+/.test(this.tokenPath)) {
|
||
|
this.tokenPath = this.serverURL + this.tokenPath;
|
||
|
}
|
||
|
|
||
|
if (!/^https?:\/\/.+/.test(this.identityPath)) {
|
||
|
this.identityPath = this.serverURL + this.identityPath;
|
||
|
}
|
||
|
|
||
|
if (Match.test(options.addAutopublishFields, Object)) {
|
||
|
Accounts.addAutopublishFields(options.addAutopublishFields);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
getAccessToken(query) {
|
||
|
const config = ServiceConfiguration.configurations.findOne({service: this.name});
|
||
|
if (!config) {
|
||
|
throw new ServiceConfiguration.ConfigError();
|
||
|
}
|
||
|
|
||
|
let response = undefined;
|
||
![]()
8 years ago
|
|
||
|
const allOptions = {
|
||
|
headers: {
|
||
|
'User-Agent': this.userAgent, // http://doc.gitlab.com/ce/api/users.html#Current-user
|
||
|
Accept: 'application/json'
|
||
|
},
|
||
|
params: {
|
||
|
code: query.code,
|
||
|
redirect_uri: OAuth._redirectUri(this.name, config),
|
||
|
grant_type: 'authorization_code',
|
||
|
state: query.state
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// Only send clientID / secret once on header or payload.
|
||
|
if (this.tokenSentVia === 'header') {
|
||
|
allOptions['auth'] = `${ config.clientId }:${ OAuth.openSecret(config.secret) }`;
|
||
|
} else {
|
||
|
allOptions['params']['client_secret'] = OAuth.openSecret(config.secret);
|
||
|
allOptions['params']['client_id'] = config.clientId;
|
||
|
}
|
||
|
|
||
![]()
9 years ago
|
try {
|
||
![]()
8 years ago
|
response = HTTP.post(this.tokenPath, allOptions);
|
||
![]()
9 years ago
|
} catch (err) {
|
||
![]()
8 years ago
|
const error = new Error(`Failed to complete OAuth handshake with ${ this.name } at ${ this.tokenPath }. ${ err.message }`);
|
||
![]()
9 years ago
|
throw _.extend(error, {response: err.response});
|
||
|
}
|
||
|
|
||
![]()
8 years ago
|
let data;
|
||
![]()
8 years ago
|
if (response.data) {
|
||
![]()
8 years ago
|
data = response.data;
|
||
![]()
9 years ago
|
} else {
|
||
![]()
8 years ago
|
data = JSON.parse(response.content);
|
||
|
}
|
||
|
|
||
|
if (data.error) { //if the http response was a json object with an error attribute
|
||
![]()
8 years ago
|
throw new Error(`Failed to complete OAuth handshake with ${ this.name } at ${ this.tokenPath }. ${ data.error }`);
|
||
![]()
8 years ago
|
} else {
|
||
|
return data.access_token;
|
||
![]()
9 years ago
|
}
|
||
|
}
|
||
|
|
||
|
getIdentity(accessToken) {
|
||
|
const params = {};
|
||
|
const headers = {
|
||
|
'User-Agent': this.userAgent // http://doc.gitlab.com/ce/api/users.html#Current-user
|
||
|
};
|
||
|
|
||
|
if (this.tokenSentVia === 'header') {
|
||
![]()
8 years ago
|
headers['Authorization'] = `Bearer ${ accessToken }`;
|
||
![]()
9 years ago
|
} else {
|
||
|
params['access_token'] = accessToken;
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
const response = HTTP.get(this.identityPath, {
|
||
![]()
8 years ago
|
headers,
|
||
|
params
|
||
![]()
9 years ago
|
});
|
||
|
|
||
![]()
9 years ago
|
let data;
|
||
|
|
||
![]()
9 years ago
|
if (response.data) {
|
||
![]()
9 years ago
|
data = response.data;
|
||
![]()
9 years ago
|
} else {
|
||
![]()
9 years ago
|
data = JSON.parse(response.content);
|
||
![]()
9 years ago
|
}
|
||
![]()
9 years ago
|
|
||
|
logger.debug('Identity response', JSON.stringify(data, null, 2));
|
||
|
|
||
|
return data;
|
||
![]()
9 years ago
|
} catch (err) {
|
||
![]()
8 years ago
|
const error = new Error(`Failed to fetch identity from ${ this.name } at ${ this.identityPath }. ${ err.message }`);
|
||
![]()
9 years ago
|
throw _.extend(error, {response: err.response});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
registerService() {
|
||
|
const self = this;
|
||
|
OAuth.registerService(this.name, 2, null, (query) => {
|
||
|
const accessToken = self.getAccessToken(query);
|
||
|
// console.log 'at:', accessToken
|
||
|
|
||
|
let identity = self.getIdentity(accessToken);
|
||
|
|
||
|
if (identity) {
|
||
![]()
8 years ago
|
// Set 'id' to '_id' for any sources that provide it
|
||
|
if (identity._id && !identity.id) {
|
||
|
identity.id = identity._id;
|
||
|
}
|
||
|
|
||
![]()
9 years ago
|
// Fix for Reddit
|
||
|
if (identity.result) {
|
||
|
identity = identity.result;
|
||
|
}
|
||
|
|
||
|
// Fix WordPress-like identities having 'ID' instead of 'id'
|
||
|
if (identity.ID && !identity.id) {
|
||
|
identity.id = identity.ID;
|
||
|
}
|
||
|
|
||
|
// Fix Auth0-like identities having 'user_id' instead of 'id'
|
||
|
if (identity.user_id && !identity.id) {
|
||
|
identity.id = identity.user_id;
|
||
|
}
|
||
|
|
||
|
if (identity.CharacterID && !identity.id) {
|
||
|
identity.id = identity.CharacterID;
|
||
|
}
|
||
|
|
||
|
// Fix Dataporten having 'user.userid' instead of 'id'
|
||
|
if (identity.user && identity.user.userid && !identity.id) {
|
||
|
identity.id = identity.user.userid;
|
||
|
identity.email = identity.user.email;
|
||
|
}
|
||
|
|
||
|
// Fix general 'phid' instead of 'id' from phabricator
|
||
|
if (identity.phid && !identity.id) {
|
||
|
identity.id = identity.phid;
|
||
|
}
|
||
|
|
||
|
// Fix Keycloak-like identities having 'sub' instead of 'id'
|
||
|
if (identity.sub && !identity.id) {
|
||
|
identity.id = identity.sub;
|
||
|
}
|
||
|
|
||
|
// Fix general 'userid' instead of 'id' from provider
|
||
|
if (identity.userid && !identity.id) {
|
||
|
identity.id = identity.userid;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// console.log 'id:', JSON.stringify identity, null, ' '
|
||
|
|
||
|
const serviceData = {
|
||
|
_OAuthCustom: true,
|
||
![]()
8 years ago
|
accessToken
|
||
![]()
9 years ago
|
};
|
||
|
|
||
|
_.extend(serviceData, identity);
|
||
|
|
||
|
const data = {
|
||
![]()
8 years ago
|
serviceData,
|
||
![]()
9 years ago
|
options: {
|
||
|
profile: {
|
||
|
name: identity.name || identity.username || identity.nickname || identity.CharacterName || identity.userName || identity.preferred_username || (identity.user && identity.user.name)
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// console.log data
|
||
|
|
||
|
return data;
|
||
|
});
|
||
|
}
|
||
|
|
||
|
retrieveCredential(credentialToken, credentialSecret) {
|
||
|
return OAuth.retrieveCredential(credentialToken, credentialSecret);
|
||
|
}
|
||
![]()
9 years ago
|
|
||
![]()
9 years ago
|
getUsername(data) {
|
||
|
let username = '';
|
||
|
|
||
|
if (this.usernameField.indexOf('#{') > -1) {
|
||
|
username = this.usernameField.replace(/#{(.+?)}/g, function(match, field) {
|
||
|
if (!data[field]) {
|
||
![]()
8 years ago
|
throw new Meteor.Error('field_not_found', `Username template item "${ field }" not found in data`, data);
|
||
![]()
9 years ago
|
}
|
||
|
return data[field];
|
||
|
});
|
||
|
} else {
|
||
|
username = data[this.usernameField];
|
||
|
if (!username) {
|
||
![]()
8 years ago
|
throw new Meteor.Error('field_not_found', `Username field "${ this.usernameField }" not found in data`, data);
|
||
![]()
9 years ago
|
}
|
||
|
}
|
||
|
|
||
|
return username;
|
||
|
}
|
||
|
|
||
![]()
9 years ago
|
addHookToProcessUser() {
|
||
|
BeforeUpdateOrCreateUserFromExternalService.push((serviceName, serviceData/*, options*/) => {
|
||
|
if (serviceName !== this.name) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (this.usernameField) {
|
||
![]()
9 years ago
|
const username = this.getUsername(serviceData);
|
||
![]()
9 years ago
|
|
||
![]()
9 years ago
|
const user = RocketChat.models.Users.findOneByUsername(username);
|
||
![]()
9 years ago
|
if (!user) {
|
||
![]()
9 years ago
|
return;
|
||
|
}
|
||
|
|
||
|
// User already created or merged
|
||
|
if (user.services && user.services[serviceName] && user.services[serviceName].id === serviceData.id) {
|
||
![]()
9 years ago
|
return;
|
||
|
}
|
||
|
|
||
|
if (this.mergeUsers !== true) {
|
||
![]()
8 years ago
|
throw new Meteor.Error('CustomOAuth', `User with username ${ user.username } already exists`);
|
||
![]()
9 years ago
|
}
|
||
|
|
||
![]()
8 years ago
|
const serviceIdKey = `services.${ serviceName }.id`;
|
||
![]()
9 years ago
|
const update = {
|
||
|
$set: {
|
||
|
[serviceIdKey]: serviceData.id
|
||
|
}
|
||
|
};
|
||
|
|
||
![]()
9 years ago
|
RocketChat.models.Users.update({_id: user._id}, update);
|
||
![]()
9 years ago
|
}
|
||
|
});
|
||
![]()
9 years ago
|
|
||
|
Accounts.validateNewUser((user) => {
|
||
|
if (!user.services || !user.services[this.name] || !user.services[this.name].id) {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
if (this.usernameField) {
|
||
|
user.username = this.getUsername(user.services[this.name]);
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
});
|
||
|
|
||
![]()
9 years ago
|
}
|
||
![]()
9 years ago
|
}
|
||
![]()
9 years ago
|
|
||
|
|
||
|
const updateOrCreateUserFromExternalService = Accounts.updateOrCreateUserFromExternalService;
|
||
|
Accounts.updateOrCreateUserFromExternalService = function(/*serviceName, serviceData, options*/) {
|
||
|
for (const hook of BeforeUpdateOrCreateUserFromExternalService) {
|
||
|
hook.apply(this, arguments);
|
||
|
}
|
||
|
|
||
|
return updateOrCreateUserFromExternalService.apply(this, arguments);
|
||
|
};
|