|
|
|
@ -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*/) { |
|
|
|
|