@semapps/auth 0.3.21 → 0.4.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +4 -5
- package/middlewares/localLogout.js +6 -0
- package/middlewares/redirectToFront.js +14 -0
- package/middlewares/saveRedirectUrl.js +9 -0
- package/middlewares/sendToken.js +6 -0
- package/mixins/auth.js +115 -0
- package/mixins/auth.sso.js +81 -0
- package/package.json +6 -4
- package/services/account.js +84 -67
- package/services/auth.cas.js +45 -0
- package/services/auth.local.js +95 -0
- package/services/auth.oidc.js +65 -0
- package/services/migration.js +24 -0
- package/CasConnector.js +0 -40
- package/Connector.js +0 -174
- package/LocalConnector.js +0 -104
- package/OidcConnector.js +0 -57
- package/services/auth.js +0 -144
package/index.js
CHANGED
@@ -1,9 +1,8 @@
|
|
1
1
|
module.exports = {
|
2
|
-
|
2
|
+
AuthCASService: require('./services/auth.cas'),
|
3
|
+
AuthLocalService: require('./services/auth.local'),
|
4
|
+
AuthOIDCService: require('./services/auth.oidc'),
|
3
5
|
AuthAccountService: require('./services/account'),
|
4
6
|
AuthJWTService: require('./services/jwt'),
|
5
|
-
|
6
|
-
CasConnector: require('./CasConnector'),
|
7
|
-
LocalConnector: require('./LocalConnector'),
|
8
|
-
OidcConnector: require('./OidcConnector')
|
7
|
+
AuthMigrationService: require('./services/migration')
|
9
8
|
};
|
@@ -0,0 +1,14 @@
|
|
1
|
+
const redirectToFront = (req, res) => {
|
2
|
+
// Redirect browser to the redirect URL pushed in session
|
3
|
+
let redirectUrl = new URL(req.session.redirectUrl);
|
4
|
+
if (req.user) {
|
5
|
+
// If a token was stored, add it to the URL so that the client may use it
|
6
|
+
if (req.user.token) redirectUrl.searchParams.set('token', req.user.token);
|
7
|
+
redirectUrl.searchParams.set('new', req.user.newUser ? 'true' : 'false');
|
8
|
+
}
|
9
|
+
// Redirect using NodeJS HTTP
|
10
|
+
res.writeHead(302, { Location: redirectUrl.toString() });
|
11
|
+
res.end();
|
12
|
+
};
|
13
|
+
|
14
|
+
module.exports = redirectToFront;
|
@@ -0,0 +1,9 @@
|
|
1
|
+
const saveRedirectUrl = (req, res, next) => {
|
2
|
+
// Persist referer on the session to get it back after redirection
|
3
|
+
// If the redirectUrl is already in the session, use it as default value
|
4
|
+
req.session.redirectUrl =
|
5
|
+
req.query.redirectUrl || (req.session && req.session.redirectUrl) || req.headers.referer || '/';
|
6
|
+
next();
|
7
|
+
};
|
8
|
+
|
9
|
+
module.exports = saveRedirectUrl;
|
package/mixins/auth.js
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
const AuthAccountService = require('../services/account');
|
2
|
+
const AuthJWTService = require('../services/jwt');
|
3
|
+
const { Errors: E } = require('moleculer-web');
|
4
|
+
const passport = require('passport');
|
5
|
+
|
6
|
+
const AuthMixin = {
|
7
|
+
settings: {
|
8
|
+
baseUrl: null,
|
9
|
+
jwtPath: null,
|
10
|
+
registrationAllowed: true,
|
11
|
+
reservedUsernames: [],
|
12
|
+
webIdSelection: []
|
13
|
+
},
|
14
|
+
dependencies: ['api', 'webid'],
|
15
|
+
async created() {
|
16
|
+
const { jwtPath, reservedUsernames } = this.settings;
|
17
|
+
|
18
|
+
await this.broker.createService(AuthJWTService, {
|
19
|
+
settings: { jwtPath }
|
20
|
+
});
|
21
|
+
|
22
|
+
await this.broker.createService(AuthAccountService, {
|
23
|
+
settings: { reservedUsernames }
|
24
|
+
});
|
25
|
+
},
|
26
|
+
async started() {
|
27
|
+
if (!this.passportId) throw new Error('this.passportId must be set in the service creation.');
|
28
|
+
|
29
|
+
this.passport = passport;
|
30
|
+
this.passport.serializeUser((user, done) => {
|
31
|
+
done(null, user);
|
32
|
+
});
|
33
|
+
this.passport.deserializeUser((user, done) => {
|
34
|
+
done(null, user);
|
35
|
+
});
|
36
|
+
|
37
|
+
this.strategy = this.getStrategy();
|
38
|
+
|
39
|
+
this.passport.use(this.passportId, this.strategy);
|
40
|
+
|
41
|
+
for (let route of this.getApiRoutes()) {
|
42
|
+
await this.broker.call('api.addRoute', { route });
|
43
|
+
}
|
44
|
+
},
|
45
|
+
actions: {
|
46
|
+
// See https://moleculer.services/docs/0.13/moleculer-web.html#Authentication
|
47
|
+
async authenticate(ctx) {
|
48
|
+
const { route, req, res } = ctx.params;
|
49
|
+
// Extract token from authorization header (do not take the Bearer part)
|
50
|
+
const token = req.headers.authorization && req.headers.authorization.split(' ')[1];
|
51
|
+
if (token) {
|
52
|
+
const payload = await ctx.call('auth.jwt.verifyToken', { token });
|
53
|
+
if (payload) {
|
54
|
+
ctx.meta.tokenPayload = payload;
|
55
|
+
ctx.meta.webId = payload.webId;
|
56
|
+
return Promise.resolve(payload);
|
57
|
+
} else {
|
58
|
+
// Invalid token
|
59
|
+
// TODO make sure token is deleted client-side
|
60
|
+
ctx.meta.webId = 'anon';
|
61
|
+
return Promise.reject(new E.UnAuthorizedError(E.ERR_INVALID_TOKEN));
|
62
|
+
}
|
63
|
+
} else {
|
64
|
+
// No token, anonymous error
|
65
|
+
ctx.meta.webId = 'anon';
|
66
|
+
return Promise.resolve(null);
|
67
|
+
}
|
68
|
+
},
|
69
|
+
// See https://moleculer.services/docs/0.13/moleculer-web.html#Authorization
|
70
|
+
async authorize(ctx) {
|
71
|
+
const { route, req, res } = ctx.params;
|
72
|
+
// Extract token from authorization header (do not take the Bearer part)
|
73
|
+
const token = req.headers.authorization && req.headers.authorization.split(' ')[1];
|
74
|
+
if (token) {
|
75
|
+
const payload = await ctx.call('auth.jwt.verifyToken', { token });
|
76
|
+
if (payload) {
|
77
|
+
ctx.meta.tokenPayload = payload;
|
78
|
+
ctx.meta.webId = payload.webId;
|
79
|
+
return Promise.resolve(payload);
|
80
|
+
} else {
|
81
|
+
ctx.meta.webId = 'anon';
|
82
|
+
return Promise.reject(new E.UnAuthorizedError(E.ERR_INVALID_TOKEN));
|
83
|
+
}
|
84
|
+
} else {
|
85
|
+
ctx.meta.webId = 'anon';
|
86
|
+
return Promise.reject(new E.UnAuthorizedError(E.ERR_NO_TOKEN));
|
87
|
+
}
|
88
|
+
},
|
89
|
+
async impersonate(ctx) {
|
90
|
+
const { webId } = ctx.params;
|
91
|
+
return await ctx.call('auth.jwt.generateToken', {
|
92
|
+
payload: {
|
93
|
+
webId
|
94
|
+
}
|
95
|
+
});
|
96
|
+
}
|
97
|
+
},
|
98
|
+
methods: {
|
99
|
+
getStrategy() {
|
100
|
+
throw new Error('getStrategy must be implemented by the service');
|
101
|
+
},
|
102
|
+
getApiRoutes() {
|
103
|
+
throw new Error('getApiRoutes must be implemented by the service');
|
104
|
+
},
|
105
|
+
pickWebIdData(data) {
|
106
|
+
if (this.settings.webIdSelection.length > 0) {
|
107
|
+
return Object.fromEntries(this.settings.webIdSelection.filter(key => key in data).map(key => [key, data[key]]));
|
108
|
+
} else {
|
109
|
+
return data;
|
110
|
+
}
|
111
|
+
}
|
112
|
+
}
|
113
|
+
};
|
114
|
+
|
115
|
+
module.exports = AuthMixin;
|
@@ -0,0 +1,81 @@
|
|
1
|
+
const session = require('express-session');
|
2
|
+
const AuthMixin = require('./auth');
|
3
|
+
const saveRedirectUrl = require('../middlewares/saveRedirectUrl');
|
4
|
+
const redirectToFront = require('../middlewares/redirectToFront');
|
5
|
+
const localLogout = require('../middlewares/localLogout');
|
6
|
+
|
7
|
+
const AuthSSOMixin = {
|
8
|
+
mixins: [AuthMixin],
|
9
|
+
settings: {
|
10
|
+
baseUrl: null,
|
11
|
+
jwtPath: null,
|
12
|
+
registrationAllowed: true,
|
13
|
+
reservedUsernames: [],
|
14
|
+
webIdSelection: [],
|
15
|
+
// SSO-specific settings
|
16
|
+
sessionSecret: 's€m@pps',
|
17
|
+
selectSsoData: null
|
18
|
+
},
|
19
|
+
actions: {
|
20
|
+
async loginOrSignup(ctx) {
|
21
|
+
let { ssoData } = ctx.params;
|
22
|
+
|
23
|
+
const profileData = this.settings.selectSsoData ? await this.settings.selectSsoData(ssoData) : ssoData;
|
24
|
+
|
25
|
+
// TODO use UUID to identify unique accounts with SSO
|
26
|
+
const existingAccounts = await ctx.call('auth.account.find', { query: { email: profileData.email } });
|
27
|
+
|
28
|
+
let accountData, webId, newUser;
|
29
|
+
if (existingAccounts.length > 0) {
|
30
|
+
accountData = existingAccounts[0];
|
31
|
+
webId = accountData.webId;
|
32
|
+
newUser = false;
|
33
|
+
|
34
|
+
// TODO update account with recent information
|
35
|
+
// await this.broker.call('webid.edit', profileData, { meta: { webId } });
|
36
|
+
|
37
|
+
await this.broker.emit('auth.connected', { webId, accountData });
|
38
|
+
} else {
|
39
|
+
if (!this.settings.registrationAllowed) {
|
40
|
+
throw new Error('registration.not-allowed');
|
41
|
+
}
|
42
|
+
|
43
|
+
accountData = await ctx.call('auth.account.create', { uuid: profileData.uuid, email: profileData.email });
|
44
|
+
webId = await ctx.call('webid.create', { nick: accountData.username, ...profileData });
|
45
|
+
newUser = true;
|
46
|
+
|
47
|
+
// Link the webId with the account
|
48
|
+
await ctx.call('auth.account.attachWebId', { accountUri: accountData['@id'], webId });
|
49
|
+
|
50
|
+
ctx.emit('auth.registered', { webId, profileData, accountData }, { meta: { webId: null, dataset: null } });
|
51
|
+
}
|
52
|
+
|
53
|
+
const token = await ctx.call('auth.jwt.generateToken', { payload: { webId: accountData.webId } });
|
54
|
+
|
55
|
+
return { token, newUser };
|
56
|
+
}
|
57
|
+
},
|
58
|
+
methods: {
|
59
|
+
getApiRoutes() {
|
60
|
+
const sessionMiddleware = session({ secret: this.settings.sessionSecret, maxAge: null });
|
61
|
+
return [
|
62
|
+
{
|
63
|
+
path: '/auth',
|
64
|
+
use: [sessionMiddleware, this.passport.initialize(), this.passport.session()],
|
65
|
+
aliases: {
|
66
|
+
'GET /': [saveRedirectUrl, this.passport.authenticate(this.passportId, { session: false }), redirectToFront]
|
67
|
+
}
|
68
|
+
},
|
69
|
+
{
|
70
|
+
path: '/auth/logout',
|
71
|
+
use: [sessionMiddleware, this.passport.initialize(), this.passport.session()],
|
72
|
+
aliases: {
|
73
|
+
'GET /': [saveRedirectUrl, localLogout, redirectToFront]
|
74
|
+
}
|
75
|
+
}
|
76
|
+
];
|
77
|
+
}
|
78
|
+
}
|
79
|
+
};
|
80
|
+
|
81
|
+
module.exports = AuthSSOMixin;
|
package/package.json
CHANGED
@@ -1,15 +1,17 @@
|
|
1
1
|
{
|
2
2
|
"name": "@semapps/auth",
|
3
|
-
"version": "0.
|
3
|
+
"version": "0.4.0-alpha.0",
|
4
4
|
"description": "Authentification module for SemApps",
|
5
5
|
"license": "Apache-2.0",
|
6
6
|
"author": "Virtual Assembly",
|
7
7
|
"dependencies": {
|
8
|
-
"@semapps/mime-types": "0.
|
8
|
+
"@semapps/mime-types": "0.4.0-alpha.0",
|
9
|
+
"@semapps/triplestore": "0.4.0-alpha.0",
|
9
10
|
"bcrypt": "^5.0.1",
|
10
11
|
"express-session": "^1.17.0",
|
11
12
|
"jsonwebtoken": "^8.5.1",
|
12
|
-
"moleculer": "^0.14.
|
13
|
+
"moleculer": "^0.14.17",
|
14
|
+
"moleculer-db": "^0.8.16",
|
13
15
|
"moleculer-web": "^0.10.0-beta1",
|
14
16
|
"openid-client": "^4.7.4",
|
15
17
|
"passport": "^0.4.1",
|
@@ -20,5 +22,5 @@
|
|
20
22
|
"publishConfig": {
|
21
23
|
"access": "public"
|
22
24
|
},
|
23
|
-
"gitHead": "
|
25
|
+
"gitHead": "1c0962ddc0bc571bcbdd4af58efc72a2569889ef"
|
24
26
|
}
|
package/services/account.js
CHANGED
@@ -1,59 +1,69 @@
|
|
1
1
|
const bcrypt = require('bcrypt');
|
2
|
-
const
|
2
|
+
const DbService = require('moleculer-db');
|
3
|
+
const { TripleStoreAdapter } = require('@semapps/triplestore');
|
3
4
|
|
4
5
|
module.exports = {
|
5
6
|
name: 'auth.account',
|
7
|
+
mixins: [DbService],
|
8
|
+
adapter: new TripleStoreAdapter({ type: 'AuthAccount', dataset: 'settings' }),
|
6
9
|
settings: {
|
7
|
-
|
10
|
+
idField: '@id',
|
11
|
+
reservedUsernames: []
|
8
12
|
},
|
9
|
-
dependencies: ['
|
13
|
+
dependencies: ['triplestore'],
|
10
14
|
actions: {
|
11
15
|
async create(ctx) {
|
12
|
-
|
13
|
-
const hashedPassword = await this.hashPassword(password);
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
16
|
+
let { uuid, username, password, email, webId } = ctx.params;
|
17
|
+
const hashedPassword = password ? await this.hashPassword(password) : undefined;
|
18
|
+
|
19
|
+
const emailExists = await ctx.call('auth.account.emailExists', { email });
|
20
|
+
if (emailExists) {
|
21
|
+
throw new Error('email.already.exists');
|
22
|
+
}
|
23
|
+
|
24
|
+
if (username) {
|
25
|
+
await this.isValidUsername(ctx, username);
|
26
|
+
} else {
|
27
|
+
// If username is not provided, find an username based on the email
|
28
|
+
let usernameValid = false,
|
29
|
+
i = 1;
|
30
|
+
do {
|
31
|
+
i++;
|
32
|
+
username = email.split('@')[0].toLowerCase();
|
33
|
+
if (i > 2) username += i;
|
34
|
+
usernameValid = await this.isValidUsername(ctx, username);
|
35
|
+
} while (usernameValid);
|
36
|
+
}
|
37
|
+
|
38
|
+
return await this._create(ctx, {
|
39
|
+
uuid,
|
40
|
+
username,
|
41
|
+
email,
|
42
|
+
hashedPassword,
|
26
43
|
webId
|
27
44
|
});
|
45
|
+
},
|
46
|
+
async attachWebId(ctx) {
|
47
|
+
const { accountUri, webId } = ctx.params;
|
28
48
|
|
29
|
-
return await
|
30
|
-
|
31
|
-
accept: MIME_TYPES.JSON,
|
49
|
+
return await this._update(ctx, {
|
50
|
+
'@id': accountUri,
|
32
51
|
webId
|
33
52
|
});
|
34
53
|
},
|
35
54
|
async verify(ctx) {
|
36
|
-
const {
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
WHERE {
|
43
|
-
<${this.settings.containerUri}> ldp:contains ?accountUri .
|
44
|
-
?accountUri semapps:email '${email}' .
|
45
|
-
?accountUri semapps:password ?passwordHash .
|
46
|
-
?accountUri semapps:webId ?webId .
|
47
|
-
}
|
48
|
-
`,
|
49
|
-
accept: MIME_TYPES.JSON,
|
50
|
-
webId: 'system'
|
55
|
+
const { username, password } = ctx.params;
|
56
|
+
|
57
|
+
const accounts = await this._find(ctx, {
|
58
|
+
query: {
|
59
|
+
username
|
60
|
+
}
|
51
61
|
});
|
52
62
|
|
53
|
-
if (
|
54
|
-
const passwordMatch = await this.comparePassword(password,
|
63
|
+
if (accounts.length > 0) {
|
64
|
+
const passwordMatch = await this.comparePassword(password, accounts[0].hashedPassword);
|
55
65
|
if (passwordMatch) {
|
56
|
-
return
|
66
|
+
return accounts[0];
|
57
67
|
} else {
|
58
68
|
throw new Error('account.not-found');
|
59
69
|
}
|
@@ -61,43 +71,50 @@ module.exports = {
|
|
61
71
|
throw new Error('account.not-found');
|
62
72
|
}
|
63
73
|
},
|
64
|
-
async
|
74
|
+
async usernameExists(ctx) {
|
75
|
+
const { username } = ctx.params;
|
76
|
+
const accounts = await this._find(ctx, { query: { username } });
|
77
|
+
return accounts.length > 0;
|
78
|
+
},
|
79
|
+
async emailExists(ctx) {
|
65
80
|
const { email } = ctx.params;
|
66
|
-
const
|
67
|
-
|
68
|
-
PREFIX semapps: <http://semapps.org/ns/core#>
|
69
|
-
PREFIX ldp: <http://www.w3.org/ns/ldp#>
|
70
|
-
SELECT ?accountUri ?webId
|
71
|
-
WHERE {
|
72
|
-
<${this.settings.containerUri}> ldp:contains ?accountUri .
|
73
|
-
?accountUri semapps:email '${email}' .
|
74
|
-
?accountUri semapps:webId ?webId .
|
75
|
-
}
|
76
|
-
`,
|
77
|
-
accept: MIME_TYPES.JSON,
|
78
|
-
webId: 'system'
|
79
|
-
});
|
80
|
-
return results.length > 0 ? results[0].webId.value : null;
|
81
|
+
const accounts = await this._find(ctx, { query: { email } });
|
82
|
+
return accounts.length > 0;
|
81
83
|
},
|
82
84
|
async findByWebId(ctx) {
|
83
85
|
const { webId } = ctx.params;
|
84
|
-
const
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
webId: 'system'
|
86
|
+
const accounts = await this._find(ctx, { query: { webId } });
|
87
|
+
return accounts.length > 0 ? accounts[0] : null;
|
88
|
+
},
|
89
|
+
async setPassword(ctx) {
|
90
|
+
const { webId, password } = ctx.params;
|
91
|
+
const hashedPassword = await this.hashPassword(password);
|
92
|
+
const account = await ctx.call('auth.account.findByWebId', { webId });
|
93
|
+
|
94
|
+
return await this._update(ctx, {
|
95
|
+
'@id': account.id,
|
96
|
+
hashedPassword
|
96
97
|
});
|
97
|
-
return results.length > 0 ? results[0].accountUri.value : null;
|
98
98
|
}
|
99
99
|
},
|
100
100
|
methods: {
|
101
|
+
async isValidUsername(ctx, username) {
|
102
|
+
// Ensure the username has no space or special characters
|
103
|
+
if (!/^[a-zA-Z0-9\-_.]+$/.exec(username)) {
|
104
|
+
throw new Error('username.invalid');
|
105
|
+
}
|
106
|
+
|
107
|
+
// Ensure we don't use reservedUsernames
|
108
|
+
if (this.settings.reservedUsernames.includes(username)) {
|
109
|
+
throw new Error('username.already.exists');
|
110
|
+
}
|
111
|
+
|
112
|
+
// Ensure email or username doesn't already exist
|
113
|
+
const usernameExists = await ctx.call('auth.account.usernameExists', { username });
|
114
|
+
if (usernameExists) {
|
115
|
+
throw new Error('username.already.exists');
|
116
|
+
}
|
117
|
+
},
|
101
118
|
async hashPassword(password) {
|
102
119
|
return new Promise((resolve, reject) => {
|
103
120
|
bcrypt.hash(password, 10, (err, hash) => {
|
@@ -0,0 +1,45 @@
|
|
1
|
+
const { Strategy } = require('passport-cas2');
|
2
|
+
const AuthSSOMixin = require('../mixins/auth.sso');
|
3
|
+
const { Errors: E } = require('moleculer-web');
|
4
|
+
|
5
|
+
const AuthCASService = {
|
6
|
+
name: 'auth',
|
7
|
+
mixins: [AuthSSOMixin],
|
8
|
+
settings: {
|
9
|
+
baseUrl: null,
|
10
|
+
jwtPath: null,
|
11
|
+
registrationAllowed: true,
|
12
|
+
reservedUsernames: [],
|
13
|
+
webIdSelection: [],
|
14
|
+
// SSO-specific settings
|
15
|
+
sessionSecret: 's€m@pps',
|
16
|
+
selectSsoData: null,
|
17
|
+
// Cas-specific settings
|
18
|
+
casUrl: null
|
19
|
+
},
|
20
|
+
async created() {
|
21
|
+
this.passportId = 'cas';
|
22
|
+
},
|
23
|
+
methods: {
|
24
|
+
getStrategy() {
|
25
|
+
return new Strategy(
|
26
|
+
{
|
27
|
+
casURL: this.settings.casUrl,
|
28
|
+
passReqToCallback: true
|
29
|
+
},
|
30
|
+
(req, username, profile, done) => {
|
31
|
+
req.$ctx
|
32
|
+
.call('auth.loginOrSignup', { ssoData: { username, ...profile } })
|
33
|
+
.then(loginData => {
|
34
|
+
done(null, loginData);
|
35
|
+
})
|
36
|
+
.catch(e => {
|
37
|
+
done(new E.UnAuthorizedError(e.message), false);
|
38
|
+
});
|
39
|
+
}
|
40
|
+
);
|
41
|
+
}
|
42
|
+
}
|
43
|
+
};
|
44
|
+
|
45
|
+
module.exports = AuthCASService;
|
@@ -0,0 +1,95 @@
|
|
1
|
+
const { Strategy } = require('passport-local');
|
2
|
+
const AuthMixin = require('../mixins/auth');
|
3
|
+
const sendToken = require('../middlewares/sendToken');
|
4
|
+
const { MoleculerError } = require('moleculer').Errors;
|
5
|
+
|
6
|
+
const AuthLocalService = {
|
7
|
+
name: 'auth',
|
8
|
+
mixins: [AuthMixin],
|
9
|
+
settings: {
|
10
|
+
baseUrl: null,
|
11
|
+
jwtPath: null,
|
12
|
+
registrationAllowed: true,
|
13
|
+
reservedUsernames: [],
|
14
|
+
webIdSelection: []
|
15
|
+
},
|
16
|
+
created() {
|
17
|
+
this.passportId = 'local';
|
18
|
+
},
|
19
|
+
actions: {
|
20
|
+
async signup(ctx) {
|
21
|
+
const { username, email, password, ...otherData } = ctx.params;
|
22
|
+
|
23
|
+
let accountData = await ctx.call('auth.account.create', { username, email, password });
|
24
|
+
|
25
|
+
const profileData = { nick: username, email, ...otherData };
|
26
|
+
const webId = await ctx.call('webid.create', this.pickWebIdData(profileData));
|
27
|
+
|
28
|
+
// Link the webId with the account
|
29
|
+
accountData = await ctx.call('auth.account.attachWebId', { accountUri: accountData['@id'], webId });
|
30
|
+
|
31
|
+
ctx.emit('auth.registered', { webId, profileData, accountData }, { meta: { webId: null, dataset: null } });
|
32
|
+
|
33
|
+
const token = await ctx.call('auth.jwt.generateToken', { payload: { webId } });
|
34
|
+
|
35
|
+
return { token, webId, newUser: true };
|
36
|
+
},
|
37
|
+
async login(ctx) {
|
38
|
+
const { username, password } = ctx.params;
|
39
|
+
|
40
|
+
const accountData = await ctx.call('auth.account.verify', { username, password });
|
41
|
+
|
42
|
+
ctx.emit('auth.connected', { webId: accountData.webId, accountData }, { meta: { webId: null, dataset: null } });
|
43
|
+
|
44
|
+
const token = await ctx.call('auth.jwt.generateToken', { payload: { webId: accountData.webId } });
|
45
|
+
|
46
|
+
return { token, webId: accountData.webId, newUser: true };
|
47
|
+
}
|
48
|
+
},
|
49
|
+
methods: {
|
50
|
+
getStrategy() {
|
51
|
+
return new Strategy(
|
52
|
+
{
|
53
|
+
usernameField: 'username',
|
54
|
+
passwordField: 'password',
|
55
|
+
passReqToCallback: true // We want to have access to req below
|
56
|
+
},
|
57
|
+
(req, username, password, done) => {
|
58
|
+
req.$ctx
|
59
|
+
.call('auth.login', { username, password })
|
60
|
+
.then(returnedData => {
|
61
|
+
done(null, returnedData);
|
62
|
+
})
|
63
|
+
.catch(e => {
|
64
|
+
console.error(e);
|
65
|
+
done(new MoleculerError(e.message, 401), false);
|
66
|
+
});
|
67
|
+
}
|
68
|
+
);
|
69
|
+
},
|
70
|
+
getApiRoutes() {
|
71
|
+
const loginRoute = {
|
72
|
+
path: '/auth/login',
|
73
|
+
use: [this.passport.initialize()],
|
74
|
+
aliases: {
|
75
|
+
'POST /': [this.passport.authenticate(this.passportId, { session: false }), sendToken]
|
76
|
+
}
|
77
|
+
};
|
78
|
+
|
79
|
+
const signupRoute = {
|
80
|
+
path: '/auth/signup',
|
81
|
+
aliases: {
|
82
|
+
'POST /': 'auth.signup'
|
83
|
+
}
|
84
|
+
};
|
85
|
+
|
86
|
+
if (this.settings.registrationAllowed) {
|
87
|
+
return [loginRoute, signupRoute];
|
88
|
+
} else {
|
89
|
+
return [loginRoute];
|
90
|
+
}
|
91
|
+
}
|
92
|
+
}
|
93
|
+
};
|
94
|
+
|
95
|
+
module.exports = AuthLocalService;
|
@@ -0,0 +1,65 @@
|
|
1
|
+
const urlJoin = require('url-join');
|
2
|
+
const { Issuer, Strategy } = require('openid-client');
|
3
|
+
const AuthSSOMixin = require('../mixins/auth.sso');
|
4
|
+
|
5
|
+
const AuthOIDCService = {
|
6
|
+
name: 'auth',
|
7
|
+
mixins: [AuthSSOMixin],
|
8
|
+
settings: {
|
9
|
+
baseUrl: null,
|
10
|
+
jwtPath: null,
|
11
|
+
registrationAllowed: true,
|
12
|
+
reservedUsernames: [],
|
13
|
+
webIdSelection: [],
|
14
|
+
// SSO-specific settings
|
15
|
+
sessionSecret: 's€m@pps',
|
16
|
+
selectSsoData: null,
|
17
|
+
// OIDC-specific settings
|
18
|
+
issuer: null,
|
19
|
+
clientId: null,
|
20
|
+
clientSecret: null
|
21
|
+
},
|
22
|
+
async created() {
|
23
|
+
this.passportId = 'oidc';
|
24
|
+
this.issuer = await Issuer.discover(this.settings.issuer);
|
25
|
+
},
|
26
|
+
methods: {
|
27
|
+
getStrategy() {
|
28
|
+
const client = new this.issuer.Client({
|
29
|
+
client_id: this.settings.clientId,
|
30
|
+
client_secret: this.settings.clientSecret,
|
31
|
+
redirect_uri: urlJoin(this.settings.baseUrl, 'auth'),
|
32
|
+
token_endpoint_auth_method: this.settings.clientSecret ? undefined : 'none'
|
33
|
+
});
|
34
|
+
|
35
|
+
const params = {
|
36
|
+
// ... any authorization params override client properties
|
37
|
+
// client_id defaults to client.client_id
|
38
|
+
// redirect_uri defaults to client.redirect_uris[0]
|
39
|
+
// response type defaults to client.response_types[0], then 'code'
|
40
|
+
// scope defaults to 'openid'
|
41
|
+
};
|
42
|
+
|
43
|
+
return new Strategy(
|
44
|
+
{
|
45
|
+
client,
|
46
|
+
params,
|
47
|
+
passReqToCallback: true
|
48
|
+
},
|
49
|
+
(req, tokenset, userinfo, done) => {
|
50
|
+
req.$ctx
|
51
|
+
.call('auth.loginOrSignup', { ssoData: userinfo })
|
52
|
+
.then(loginData => {
|
53
|
+
done(null, loginData);
|
54
|
+
})
|
55
|
+
.catch(e => {
|
56
|
+
console.error(e);
|
57
|
+
done(null, false);
|
58
|
+
});
|
59
|
+
}
|
60
|
+
);
|
61
|
+
}
|
62
|
+
}
|
63
|
+
};
|
64
|
+
|
65
|
+
module.exports = AuthOIDCService;
|
@@ -0,0 +1,24 @@
|
|
1
|
+
const { MIME_TYPES } = require('@semapps/mime-types');
|
2
|
+
|
3
|
+
module.exports = {
|
4
|
+
name: 'auth.migration',
|
5
|
+
actions: {
|
6
|
+
async migrateUsersToAccounts(ctx) {
|
7
|
+
const { usersContainer, emailPredicate, usernamePredicate } = ctx.params;
|
8
|
+
|
9
|
+
const results = await ctx.call('ldp.container.get', { containerUri: usersContainer, accept: MIME_TYPES.JSON });
|
10
|
+
|
11
|
+
for (let user of results['ldp:contains']) {
|
12
|
+
if (user[emailPredicate]) {
|
13
|
+
await ctx.call('auth.account.create', {
|
14
|
+
email: user[emailPredicate],
|
15
|
+
username: user[usernamePredicate],
|
16
|
+
webId: user.id
|
17
|
+
});
|
18
|
+
} else {
|
19
|
+
console.log('No email found for user ' + user.id);
|
20
|
+
}
|
21
|
+
}
|
22
|
+
}
|
23
|
+
}
|
24
|
+
};
|
package/CasConnector.js
DELETED
@@ -1,40 +0,0 @@
|
|
1
|
-
const { Strategy } = require('passport-cas2');
|
2
|
-
const Connector = require('./Connector');
|
3
|
-
|
4
|
-
class CasConnector extends Connector {
|
5
|
-
constructor(settings) {
|
6
|
-
const { casUrl, ...otherSettings } = settings;
|
7
|
-
super('cas', {
|
8
|
-
casUrl,
|
9
|
-
...otherSettings
|
10
|
-
});
|
11
|
-
}
|
12
|
-
async initialize() {
|
13
|
-
this.passport.serializeUser(function(user, done) {
|
14
|
-
done(null, user);
|
15
|
-
});
|
16
|
-
|
17
|
-
this.passport.deserializeUser(function(user, done) {
|
18
|
-
done(null, user);
|
19
|
-
});
|
20
|
-
|
21
|
-
this.casStrategy = new Strategy(
|
22
|
-
{
|
23
|
-
casURL: this.settings.casUrl
|
24
|
-
},
|
25
|
-
(username, profile, done) => {
|
26
|
-
done(null, profile);
|
27
|
-
}
|
28
|
-
);
|
29
|
-
|
30
|
-
this.passport.use(this.casStrategy);
|
31
|
-
}
|
32
|
-
globalLogout(req, res, next) {
|
33
|
-
// We access directly the `cas` object in order to set the doRedirect parameter as false
|
34
|
-
// and redirect to /cas/logout?url={redirectUrl} instead of/cas/logout?service={redirectUrl}
|
35
|
-
// https://github.com/appdevdesigns/passport-cas/blob/master/lib/passport-cas.js#L264
|
36
|
-
this.casStrategy.cas.logout(req, res, req.session.redirectUrl, false);
|
37
|
-
}
|
38
|
-
}
|
39
|
-
|
40
|
-
module.exports = CasConnector;
|
package/Connector.js
DELETED
@@ -1,174 +0,0 @@
|
|
1
|
-
const passport = require('passport');
|
2
|
-
const session = require('express-session');
|
3
|
-
const E = require('moleculer-web').Errors;
|
4
|
-
|
5
|
-
class Connector {
|
6
|
-
constructor(passportId, settings) {
|
7
|
-
this.passport = passport;
|
8
|
-
this.passportId = passportId;
|
9
|
-
this.settings = {
|
10
|
-
sessionSecret: settings.sessionSecret || 's€m@pps',
|
11
|
-
selectProfileData: settings.selectProfileData,
|
12
|
-
findOrCreateProfile: settings.findOrCreateProfile,
|
13
|
-
redirectUri: settings.redirectUri,
|
14
|
-
...settings
|
15
|
-
};
|
16
|
-
}
|
17
|
-
saveRedirectUrl(req, res, next) {
|
18
|
-
// Persist referer on the session to get it back after redirection
|
19
|
-
// If the redirectUrl is already in the session, use it as default value
|
20
|
-
req.session.redirectUrl =
|
21
|
-
req.query.redirectUrl || (req.session && req.session.redirectUrl) || req.headers.referer || '/';
|
22
|
-
next();
|
23
|
-
}
|
24
|
-
async findOrCreateProfile(req, res, next) {
|
25
|
-
// Select profile data amongst all the data returned by the connector
|
26
|
-
const profileData = this.settings.selectProfileData ? await this.settings.selectProfileData(req.user) : req.user;
|
27
|
-
try {
|
28
|
-
const { webId, newUser } = await this.settings.findOrCreateProfile(profileData, req.user);
|
29
|
-
req.user.webId = webId;
|
30
|
-
req.user.newUser = newUser;
|
31
|
-
next();
|
32
|
-
} catch (e) {
|
33
|
-
this.redirectWithError(res, req, e);
|
34
|
-
}
|
35
|
-
}
|
36
|
-
async generateToken(req, res, next) {
|
37
|
-
// If token is already provided by the connector, skip this step.
|
38
|
-
if (!req.user.token) {
|
39
|
-
const profileData = this.settings.selectProfileData ? await this.settings.selectProfileData(req.user) : req.user;
|
40
|
-
const payload = { webId: req.user.webId, ...profileData };
|
41
|
-
req.user.token = await req.$ctx.call('auth.jwt.generateToken', { payload });
|
42
|
-
}
|
43
|
-
next();
|
44
|
-
}
|
45
|
-
localLogout(req, res, next) {
|
46
|
-
req.logout(); // Passport logout
|
47
|
-
next();
|
48
|
-
}
|
49
|
-
globalLogout(req, res, next) {
|
50
|
-
// Must be implemented in extended class
|
51
|
-
next();
|
52
|
-
}
|
53
|
-
redirectToFront(req, res, next) {
|
54
|
-
// Redirect browser to the redirect URL pushed in session
|
55
|
-
let redirectUrl = new URL(req.session.redirectUrl);
|
56
|
-
if (req.user) {
|
57
|
-
// If a token was stored, add it to the URL so that the client may use it
|
58
|
-
if (req.user.token) redirectUrl.searchParams.set('token', req.user.token);
|
59
|
-
redirectUrl.searchParams.set('new', req.user.newUser ? 'true' : 'false');
|
60
|
-
}
|
61
|
-
// Redirect using NodeJS HTTP
|
62
|
-
res.writeHead(302, { Location: redirectUrl.toString() });
|
63
|
-
res.end();
|
64
|
-
next();
|
65
|
-
}
|
66
|
-
redirectWithError(res, req, error) {
|
67
|
-
let redirectUrl = new URL(req.session.redirectUrl);
|
68
|
-
redirectUrl.searchParams.set('error', error.message);
|
69
|
-
res.writeHead(302, { Location: redirectUrl.toString() });
|
70
|
-
res.end();
|
71
|
-
}
|
72
|
-
login() {
|
73
|
-
return async (req, res) => {
|
74
|
-
const middlewares = [
|
75
|
-
this.saveRedirectUrl.bind(this),
|
76
|
-
this.passport.authenticate(this.passportId, {
|
77
|
-
session: false
|
78
|
-
}),
|
79
|
-
this.findOrCreateProfile.bind(this),
|
80
|
-
this.generateToken.bind(this),
|
81
|
-
this.redirectToFront.bind(this)
|
82
|
-
];
|
83
|
-
|
84
|
-
await this.runMiddlewares(middlewares, req, res);
|
85
|
-
};
|
86
|
-
}
|
87
|
-
signup() {
|
88
|
-
// By default, signup and login are the same.
|
89
|
-
return this.login();
|
90
|
-
}
|
91
|
-
logout() {
|
92
|
-
return async (req, res) => {
|
93
|
-
let middlewares = [
|
94
|
-
this.saveRedirectUrl.bind(this),
|
95
|
-
this.localLogout.bind(this),
|
96
|
-
req.query.global === 'true' ? this.globalLogout.bind(this) : this.redirectToFront.bind(this)
|
97
|
-
];
|
98
|
-
|
99
|
-
await this.runMiddlewares(middlewares, req, res);
|
100
|
-
};
|
101
|
-
}
|
102
|
-
async runMiddlewares(middlewares, req, res) {
|
103
|
-
for (const middleware of middlewares) {
|
104
|
-
let asyncRes;
|
105
|
-
let error = await new Promise(resolve => {
|
106
|
-
try {
|
107
|
-
asyncRes = middleware(req, res, resolve);
|
108
|
-
} catch (e) {
|
109
|
-
console.log(e);
|
110
|
-
resolve(e);
|
111
|
-
}
|
112
|
-
});
|
113
|
-
if (error) {
|
114
|
-
this.redirectWithError(res, req, error);
|
115
|
-
}
|
116
|
-
await asyncRes;
|
117
|
-
}
|
118
|
-
}
|
119
|
-
getRouteMiddlewares(passport) {
|
120
|
-
const sessionMiddleware = session({
|
121
|
-
secret: this.settings.sessionSecret,
|
122
|
-
maxAge: null
|
123
|
-
});
|
124
|
-
|
125
|
-
if (passport) {
|
126
|
-
return [sessionMiddleware, this.passport.initialize(), this.passport.session()];
|
127
|
-
} else {
|
128
|
-
return [sessionMiddleware];
|
129
|
-
}
|
130
|
-
}
|
131
|
-
// See https://moleculer.services/docs/0.13/moleculer-web.html#Authentication
|
132
|
-
async authenticate(ctx, route, req, res) {
|
133
|
-
// Extract token from authorization header (do not take the Bearer part)
|
134
|
-
const token = req.headers.authorization && req.headers.authorization.split(' ')[1];
|
135
|
-
if (token) {
|
136
|
-
const payload = await ctx.call('auth.jwt.verifyToken', { token });
|
137
|
-
if (payload) {
|
138
|
-
ctx.meta.tokenPayload = payload;
|
139
|
-
ctx.meta.webId = payload.webId;
|
140
|
-
return Promise.resolve(payload);
|
141
|
-
} else {
|
142
|
-
// Invalid token
|
143
|
-
// TODO make sure token is deleted client-side
|
144
|
-
ctx.meta.webId = 'anon';
|
145
|
-
return Promise.reject(new E.UnAuthorizedError(E.ERR_INVALID_TOKEN));
|
146
|
-
}
|
147
|
-
} else {
|
148
|
-
// No token, anonymous error
|
149
|
-
ctx.meta.webId = 'anon';
|
150
|
-
return Promise.resolve(null);
|
151
|
-
}
|
152
|
-
}
|
153
|
-
// See https://moleculer.services/docs/0.13/moleculer-web.html#Authorization
|
154
|
-
async authorize(ctx, route, req, res) {
|
155
|
-
// Extract token from authorization header (do not take the Bearer part)
|
156
|
-
const token = req.headers.authorization && req.headers.authorization.split(' ')[1];
|
157
|
-
if (token) {
|
158
|
-
const payload = await ctx.call('auth.jwt.verifyToken', { token });
|
159
|
-
if (payload) {
|
160
|
-
ctx.meta.tokenPayload = payload;
|
161
|
-
ctx.meta.webId = payload.webId;
|
162
|
-
return Promise.resolve(payload);
|
163
|
-
} else {
|
164
|
-
ctx.meta.webId = 'anon';
|
165
|
-
return Promise.reject(new E.UnAuthorizedError(E.ERR_INVALID_TOKEN));
|
166
|
-
}
|
167
|
-
} else {
|
168
|
-
ctx.meta.webId = 'anon';
|
169
|
-
return Promise.reject(new E.UnAuthorizedError(E.ERR_NO_TOKEN));
|
170
|
-
}
|
171
|
-
}
|
172
|
-
}
|
173
|
-
|
174
|
-
module.exports = Connector;
|
package/LocalConnector.js
DELETED
@@ -1,104 +0,0 @@
|
|
1
|
-
const { Strategy } = require('passport-local');
|
2
|
-
const Connector = require('./Connector');
|
3
|
-
const { MIME_TYPES } = require('@semapps/mime-types');
|
4
|
-
|
5
|
-
class LocalConnector extends Connector {
|
6
|
-
constructor(settings) {
|
7
|
-
super('local', settings || {});
|
8
|
-
}
|
9
|
-
async initialize() {
|
10
|
-
this.localStrategy = new Strategy(
|
11
|
-
{
|
12
|
-
usernameField: 'email',
|
13
|
-
passwordField: 'password',
|
14
|
-
passReqToCallback: true // We want to have access to req below
|
15
|
-
},
|
16
|
-
(req, email, password, done) => {
|
17
|
-
req.$ctx
|
18
|
-
.call('auth.account.verify', { email, password })
|
19
|
-
.then(({ webId }) =>
|
20
|
-
req.$ctx.call('ldp.resource.get', {
|
21
|
-
resourceUri: webId,
|
22
|
-
accept: MIME_TYPES.JSON,
|
23
|
-
webId: 'system'
|
24
|
-
})
|
25
|
-
)
|
26
|
-
.then(userData => {
|
27
|
-
req.$ctx.emit('auth.connected', { webId: userData.id, profileData: userData });
|
28
|
-
done(null, { ...userData, webId: userData.id });
|
29
|
-
})
|
30
|
-
.catch(e => {
|
31
|
-
console.error(e);
|
32
|
-
done(null, false);
|
33
|
-
});
|
34
|
-
}
|
35
|
-
);
|
36
|
-
|
37
|
-
this.passport.use(this.localStrategy);
|
38
|
-
}
|
39
|
-
login() {
|
40
|
-
return async (req, res) => {
|
41
|
-
const middlewares = [
|
42
|
-
this.passport.authenticate(this.passportId, {
|
43
|
-
session: false
|
44
|
-
}),
|
45
|
-
this.generateToken.bind(this),
|
46
|
-
this.sendToken.bind(this)
|
47
|
-
];
|
48
|
-
|
49
|
-
await this.runMiddlewares(middlewares, req, res);
|
50
|
-
};
|
51
|
-
}
|
52
|
-
signup() {
|
53
|
-
return async (req, res) => {
|
54
|
-
const middlewares = [this.createAccount.bind(this), this.generateToken.bind(this), this.sendToken.bind(this)];
|
55
|
-
|
56
|
-
await this.runMiddlewares(middlewares, req, res);
|
57
|
-
};
|
58
|
-
}
|
59
|
-
createAccount(req, res, next) {
|
60
|
-
const { email, password, ...profileData } = req.$params;
|
61
|
-
req.$ctx
|
62
|
-
.call('auth.account.findByEmail', { email })
|
63
|
-
.then(webId => {
|
64
|
-
if (!webId) {
|
65
|
-
return req.$ctx.call('webid.create', profileData);
|
66
|
-
} else {
|
67
|
-
throw new Error('email.already.exists');
|
68
|
-
}
|
69
|
-
})
|
70
|
-
.then(webId =>
|
71
|
-
req.$ctx.call('ldp.resource.get', {
|
72
|
-
resourceUri: webId,
|
73
|
-
accept: MIME_TYPES.JSON,
|
74
|
-
webId: 'system'
|
75
|
-
})
|
76
|
-
)
|
77
|
-
.then(userData => {
|
78
|
-
req.user = userData;
|
79
|
-
req.user.webId = userData.id;
|
80
|
-
req.user.newUser = true;
|
81
|
-
return req.$ctx.call('auth.account.create', {
|
82
|
-
email,
|
83
|
-
password,
|
84
|
-
webId: userData.id
|
85
|
-
});
|
86
|
-
})
|
87
|
-
.then(accountData => {
|
88
|
-
req.$ctx.emit('auth.registered', { webId: accountData['semapps:webId'], profileData, accountData });
|
89
|
-
next();
|
90
|
-
})
|
91
|
-
.catch(e => this.sendError(res, e.message));
|
92
|
-
}
|
93
|
-
sendToken(req, res, next) {
|
94
|
-
res.setHeader('Content-Type', 'application/json');
|
95
|
-
res.end(JSON.stringify({ token: req.user.token, newUser: req.user.newUser }));
|
96
|
-
next();
|
97
|
-
}
|
98
|
-
sendError(res, message, statusCode = 400) {
|
99
|
-
res.writeHead(statusCode, message);
|
100
|
-
res.end();
|
101
|
-
}
|
102
|
-
}
|
103
|
-
|
104
|
-
module.exports = LocalConnector;
|
package/OidcConnector.js
DELETED
@@ -1,57 +0,0 @@
|
|
1
|
-
const { Issuer, Strategy } = require('openid-client');
|
2
|
-
const Connector = require('./Connector');
|
3
|
-
|
4
|
-
class OidcConnector extends Connector {
|
5
|
-
constructor(settings) {
|
6
|
-
const { issuer, clientId, clientSecret, ...otherSettings } = settings;
|
7
|
-
super('oidc', {
|
8
|
-
issuer,
|
9
|
-
clientId,
|
10
|
-
clientSecret,
|
11
|
-
...otherSettings
|
12
|
-
});
|
13
|
-
}
|
14
|
-
async initialize() {
|
15
|
-
this.issuer = await Issuer.discover(this.settings.issuer);
|
16
|
-
let config = {
|
17
|
-
client_id: this.settings.clientId,
|
18
|
-
client_secret: this.settings.clientSecret,
|
19
|
-
redirect_uri: this.settings.redirectUri
|
20
|
-
};
|
21
|
-
if (!config.client_secret) {
|
22
|
-
config.token_endpoint_auth_method = 'none';
|
23
|
-
}
|
24
|
-
const client = new this.issuer.Client(config);
|
25
|
-
const params = {
|
26
|
-
// ... any authorization params override client properties
|
27
|
-
// client_id defaults to client.client_id
|
28
|
-
// redirect_uri defaults to client.redirect_uris[0]
|
29
|
-
// response type defaults to client.response_types[0], then 'code'
|
30
|
-
// scope defaults to 'openid'
|
31
|
-
};
|
32
|
-
|
33
|
-
this.passport.use(
|
34
|
-
'oidc',
|
35
|
-
new Strategy(
|
36
|
-
{
|
37
|
-
client,
|
38
|
-
params
|
39
|
-
},
|
40
|
-
(tokenset, userinfo, done) => {
|
41
|
-
done(null, userinfo);
|
42
|
-
}
|
43
|
-
)
|
44
|
-
);
|
45
|
-
}
|
46
|
-
globalLogout(req, res, next) {
|
47
|
-
// Redirect using NodeJS HTTP
|
48
|
-
res.writeHead(302, {
|
49
|
-
Location: `${this.issuer.end_session_endpoint}?post_logout_redirect_uri=${encodeURIComponent(
|
50
|
-
req.session.redirectUrl
|
51
|
-
)}`
|
52
|
-
});
|
53
|
-
res.end();
|
54
|
-
}
|
55
|
-
}
|
56
|
-
|
57
|
-
module.exports = OidcConnector;
|
package/services/auth.js
DELETED
@@ -1,144 +0,0 @@
|
|
1
|
-
const urlJoin = require('url-join');
|
2
|
-
const { MIME_TYPES } = require('@semapps/mime-types');
|
3
|
-
const AuthAccountService = require('./account');
|
4
|
-
const AuthJWTService = require('./jwt');
|
5
|
-
const OidcConnector = require('../OidcConnector');
|
6
|
-
const CasConnector = require('../CasConnector');
|
7
|
-
const LocalConnector = require('../LocalConnector');
|
8
|
-
|
9
|
-
module.exports = {
|
10
|
-
name: 'auth',
|
11
|
-
settings: {
|
12
|
-
baseUrl: null,
|
13
|
-
accountsContainer: null,
|
14
|
-
jwtPath: null,
|
15
|
-
oidc: {
|
16
|
-
issuer: null,
|
17
|
-
clientId: null,
|
18
|
-
clientSecret: null
|
19
|
-
},
|
20
|
-
cas: {
|
21
|
-
url: null
|
22
|
-
},
|
23
|
-
selectProfileData: null,
|
24
|
-
registrationAllowed: true
|
25
|
-
},
|
26
|
-
dependencies: ['api', 'webid'],
|
27
|
-
async created() {
|
28
|
-
const { baseUrl, accountsContainer, jwtPath, oidc, cas } = this.settings;
|
29
|
-
|
30
|
-
await this.broker.createService(AuthJWTService, {
|
31
|
-
settings: { jwtPath }
|
32
|
-
});
|
33
|
-
|
34
|
-
if (!oidc.issuer && !cas.url) {
|
35
|
-
await this.broker.createService(AuthAccountService, {
|
36
|
-
settings: {
|
37
|
-
containerUri: accountsContainer || urlJoin(baseUrl, 'accounts')
|
38
|
-
}
|
39
|
-
});
|
40
|
-
}
|
41
|
-
},
|
42
|
-
async started() {
|
43
|
-
const { baseUrl, selectProfileData, oidc, cas } = this.settings;
|
44
|
-
|
45
|
-
if (oidc.issuer) {
|
46
|
-
this.connector = new OidcConnector({
|
47
|
-
issuer: oidc.issuer,
|
48
|
-
clientId: oidc.clientId,
|
49
|
-
clientSecret: oidc.clientSecret,
|
50
|
-
redirectUri: urlJoin(baseUrl, 'auth'),
|
51
|
-
selectProfileData,
|
52
|
-
findOrCreateProfile: this.findOrCreateProfile
|
53
|
-
});
|
54
|
-
} else if (cas.url) {
|
55
|
-
this.connector = new CasConnector({
|
56
|
-
casUrl: cas.url,
|
57
|
-
selectProfileData,
|
58
|
-
findOrCreateProfile: this.findOrCreateProfile
|
59
|
-
});
|
60
|
-
} else {
|
61
|
-
this.connector = new LocalConnector();
|
62
|
-
}
|
63
|
-
|
64
|
-
await this.connector.initialize();
|
65
|
-
|
66
|
-
await this.broker.call('api.addRoute', {
|
67
|
-
route: {
|
68
|
-
path: '/auth',
|
69
|
-
use: this.connector.getRouteMiddlewares(true),
|
70
|
-
aliases: {
|
71
|
-
'GET /logout': this.connector.logout(),
|
72
|
-
'GET /': this.connector.login(),
|
73
|
-
'POST /': this.connector.login()
|
74
|
-
},
|
75
|
-
onError(req, res, err) {
|
76
|
-
console.error(err);
|
77
|
-
}
|
78
|
-
}
|
79
|
-
});
|
80
|
-
|
81
|
-
await this.broker.call('api.addRoute', {
|
82
|
-
route: {
|
83
|
-
path: '/auth/signup',
|
84
|
-
use: this.connector.getRouteMiddlewares(false),
|
85
|
-
aliases: {
|
86
|
-
'POST /': this.connector.signup()
|
87
|
-
},
|
88
|
-
onError(req, res, err) {
|
89
|
-
console.error(err);
|
90
|
-
}
|
91
|
-
}
|
92
|
-
});
|
93
|
-
},
|
94
|
-
methods: {
|
95
|
-
async findOrCreateProfile(profileData, authData) {
|
96
|
-
let webId = await this.broker.call(
|
97
|
-
'webid.findByEmail',
|
98
|
-
{ email: profileData.email },
|
99
|
-
{ meta: { webId: 'system' } }
|
100
|
-
);
|
101
|
-
|
102
|
-
const newUser = !webId;
|
103
|
-
|
104
|
-
if (newUser) {
|
105
|
-
if (!this.settings.registrationAllowed) {
|
106
|
-
throw new Error('registration.not-allowed');
|
107
|
-
}
|
108
|
-
webId = await this.broker.call('webid.create', profileData);
|
109
|
-
await this.broker.emit('auth.registered', { webId, profileData, authData });
|
110
|
-
} else {
|
111
|
-
await this.broker.call('webid.edit', profileData, { meta: { webId } });
|
112
|
-
await this.broker.emit('auth.connected', { webId, profileData, authData });
|
113
|
-
}
|
114
|
-
|
115
|
-
return { webId, newUser };
|
116
|
-
}
|
117
|
-
},
|
118
|
-
actions: {
|
119
|
-
async authenticate(ctx) {
|
120
|
-
const { route, req, res } = ctx.params;
|
121
|
-
return await this.connector.authenticate(ctx, route, req, res);
|
122
|
-
},
|
123
|
-
async authorize(ctx) {
|
124
|
-
const { route, req, res } = ctx.params;
|
125
|
-
return await this.connector.authorize(ctx, route, req, res);
|
126
|
-
},
|
127
|
-
async impersonate(ctx) {
|
128
|
-
const { webId } = ctx.params;
|
129
|
-
const userData = await ctx.call('ldp.resource.get', {
|
130
|
-
resourceUri: webId,
|
131
|
-
accept: MIME_TYPES.JSON,
|
132
|
-
webId: 'system'
|
133
|
-
});
|
134
|
-
return await ctx.call('auth.jwt.generateToken', {
|
135
|
-
payload: {
|
136
|
-
webId,
|
137
|
-
email: userData['foaf:email'],
|
138
|
-
name: userData['foaf:name'],
|
139
|
-
familyName: userData['foaf:familyName']
|
140
|
-
}
|
141
|
-
});
|
142
|
-
}
|
143
|
-
}
|
144
|
-
};
|