Skip to content

Commit 63f6e52

Browse files
committed
[Feature] Custom Oauth login with provider access Token
Closes #14108
1 parent c0fab2b commit 63f6e52

File tree

3 files changed

+123
-5
lines changed

3 files changed

+123
-5
lines changed

app/lib/server/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import './oauth/facebook';
2121
import './oauth/google';
2222
import './oauth/proxy';
2323
import './oauth/twitter';
24+
import './oauth/custom';
2425
import './methods/addOAuthService';
2526
import './methods/addUsersToRoom';
2627
import './methods/addUserToRoom';

app/lib/server/oauth/custom.js

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { Match, check } from 'meteor/check';
2+
import _ from 'underscore';
3+
import { HTTP } from 'meteor/http';
4+
import { ServiceConfiguration } from 'meteor/service-configuration';
5+
import { OAuth } from 'meteor/oauth';
6+
import { registerAccessTokenService } from './oauth';
7+
8+
function getIdentity(accessToken, config) {
9+
try {
10+
return HTTP.get(
11+
config.serverURL + config.identityPath,
12+
{
13+
headers: {
14+
Authorization: `Bearer ${ accessToken }`,
15+
Accept: 'application/json',
16+
},
17+
}).data;
18+
} catch (err) {
19+
throw _.extend(new Error(`Failed to fetch identity from custom OAuth ${ config.service }. ${ err.message }`), { response: err.response });
20+
}
21+
}
22+
23+
// use RFC7662 OAuth 2.0 Token Introspection
24+
function getTokeninfo(idToken, config) {
25+
try {
26+
const introspectPath = '/introspect'; // not yet configurable in Rocket.Chat, thought RFC defines that path
27+
return HTTP.post(
28+
config.serverURL + introspectPath,
29+
{
30+
auth: `${ config.clientId }:${ OAuth.openSecret(config.secret) }`,
31+
headers: {
32+
Accept: 'application/json',
33+
},
34+
params: {
35+
token: idToken,
36+
token_type_hint: 'access_token',
37+
},
38+
}).data;
39+
} catch (err) {
40+
throw _.extend(new Error(`Failed to fetch tokeninfo from custom OAuth ${ config.service }. ${ err.message }`), { response: err.response });
41+
}
42+
}
43+
44+
registerAccessTokenService('custom', function(options) {
45+
check(options, Match.ObjectIncluding({
46+
accessToken: String,
47+
expiresIn: Match.Maybe(Match.Integer),
48+
scope: Match.Maybe(String),
49+
identity: Match.Maybe(Object),
50+
}));
51+
52+
const config = ServiceConfiguration.configurations.findOne({ service: options.serviceName });
53+
let tokeninfo;
54+
if (!options.expiresIn || !options.scope) {
55+
try {
56+
tokeninfo = getTokeninfo(options.accessToken, config);
57+
// console.log('app/lib/server/oauth/custom.js: tokeninfo=', tokeninfo);
58+
} catch (err) {
59+
// ignore tokeninfo failures, as getIdentity still validates the token, we just dont know how long it's valid
60+
console.log(err);
61+
}
62+
}
63+
const identity = options.identity || getIdentity(options.accessToken, config);
64+
65+
// support OpenID Connect /userinfo names
66+
if (typeof identity.profile_image_url === 'undefined' && identity.picture) {
67+
identity.profile_image_url = identity.picture;
68+
}
69+
if (typeof identity.lang === 'undefined' && identity.locale) {
70+
identity.profile_image_url = identity.picture;
71+
}
72+
73+
const serviceData = {
74+
_OAuthCustom: true,
75+
accessToken: options.accessToken,
76+
expiresAt: tokeninfo.exp || (+new Date) + (1000 * parseInt(options.expiresIn, 10)),
77+
scope: options.scope || tokeninfo.scope || config.scope.split(/ /),
78+
id: identity[config.usernameField],
79+
};
80+
81+
// only set the token in serviceData if it's there. this ensures
82+
// that we don't lose old ones (since we only get this on the first
83+
// log in attempt)
84+
if (options.refreshToken) {
85+
serviceData.refreshToken = options.refreshToken;
86+
}
87+
88+
const whitelistedFields = [
89+
'name',
90+
'description',
91+
'profile_image_url',
92+
'profile_image_url_https',
93+
'lang',
94+
'email',
95+
];
96+
if (!whitelistedFields.includes(config.usernameField) && typeof serviceData[config.usernameField] === 'undefined') {
97+
whitelistedFields.push(config.usernameField);
98+
}
99+
const fields = _.pick(identity, whitelistedFields);
100+
_.extend(serviceData, fields);
101+
102+
return {
103+
serviceData,
104+
options: {
105+
profile: {
106+
name: identity.name,
107+
},
108+
},
109+
};
110+
});

app/lib/server/oauth/oauth.js

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,34 +24,41 @@ Accounts.registerLoginHandler(function(options) {
2424
serviceName: String,
2525
}));
2626

27-
const service = AccessTokenServices[options.serviceName];
27+
// Check if service is configured and therefore a custom OAuth
28+
const config = ServiceConfiguration.configurations.findOne({ service: options.serviceName });
29+
30+
let service = AccessTokenServices[options.serviceName];
31+
32+
if (!service && config) {
33+
service = AccessTokenServices.custom;
34+
}
2835

2936
// Skip everything if there's no service set by the oauth middleware
3037
if (!service) {
3138
throw new Error(`Unexpected AccessToken service ${ options.serviceName }`);
3239
}
3340

3441
// Make sure we're configured
35-
if (!ServiceConfiguration.configurations.findOne({ service: service.serviceName })) {
42+
if (!config) {
3643
throw new ServiceConfiguration.ConfigError();
3744
}
3845

39-
if (!_.contains(Accounts.oauth.serviceNames(), service.serviceName)) {
46+
if (!_.contains(Accounts.oauth.serviceNames(), options.serviceName)) {
4047
// serviceName was not found in the registered services list.
4148
// This could happen because the service never registered itself or
4249
// unregisterService was called on it.
4350
return {
4451
type: 'oauth',
4552
error: new Meteor.Error(
4653
Accounts.LoginCancelledError.numericError,
47-
`No registered oauth service found for: ${ service.serviceName }`
54+
`No registered oauth service found for: ${ options.serviceName }`
4855
),
4956
};
5057
}
5158

5259
const oauthResult = service.handleAccessTokenRequest(options);
5360

54-
return Accounts.updateOrCreateUserFromExternalService(service.serviceName, oauthResult.serviceData, oauthResult.options);
61+
return Accounts.updateOrCreateUserFromExternalService(options.serviceName, oauthResult.serviceData, oauthResult.options);
5562
});
5663

5764

0 commit comments

Comments
 (0)