@flusys/nestjs-auth 7.0.2 → 7.1.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/README.md +67 -0
- package/cjs/config/auth.constants.js +8 -0
- package/cjs/config/event-actions.js +4 -2
- package/cjs/config/message-keys.js +27 -0
- package/cjs/controllers/index.js +2 -0
- package/cjs/controllers/social-auth-config.controller.js +107 -0
- package/cjs/controllers/social-auth.controller.js +175 -0
- package/cjs/controllers/social-auth.controller.spec.js +79 -0
- package/cjs/docs/auth-swagger.config.js +9 -1
- package/cjs/dtos/index.js +1 -0
- package/cjs/dtos/social-auth.dto.js +318 -0
- package/cjs/entities/index.js +3 -1
- package/cjs/entities/index.spec.js +12 -1
- package/cjs/entities/social-auth-config.entity.js +102 -0
- package/cjs/enums/index.js +1 -0
- package/cjs/enums/social-provider-type.enum.js +17 -0
- package/cjs/index.js +1 -0
- package/cjs/interfaces/index.js +1 -0
- package/cjs/interfaces/social-auth.interface.js +4 -0
- package/cjs/modules/auth.module.js +6 -0
- package/cjs/modules/auth.module.spec.js +15 -0
- package/cjs/providers/facebook.social-provider.js +73 -0
- package/cjs/providers/google.social-provider.js +68 -0
- package/cjs/providers/index.js +18 -0
- package/cjs/providers/linkedin.social-provider.js +66 -0
- package/cjs/providers/microsoft.social-provider.js +78 -0
- package/cjs/providers/oauth-http.util.js +107 -0
- package/cjs/providers/social-provider.registry.js +24 -0
- package/cjs/providers/social-provider.registry.spec.js +135 -0
- package/cjs/services/auth-config.service.js +3 -0
- package/cjs/services/auth-config.service.spec.js +3 -0
- package/cjs/services/auth-datasource.provider.js +4 -0
- package/cjs/services/auth-datasource.provider.spec.js +30 -0
- package/cjs/services/authentication.service.js +38 -8
- package/cjs/services/authentication.service.spec.js +58 -0
- package/cjs/services/index.js +2 -0
- package/cjs/services/social-auth-config.service.js +103 -0
- package/cjs/services/social-auth-config.service.spec.js +71 -0
- package/cjs/services/social-auth.service.js +310 -0
- package/cjs/services/social-auth.service.spec.js +310 -0
- package/config/auth.constants.d.ts +2 -0
- package/config/event-actions.d.ts +2 -0
- package/config/message-keys.d.ts +21 -0
- package/controllers/index.d.ts +2 -0
- package/controllers/social-auth-config.controller.d.ts +22 -0
- package/controllers/social-auth.controller.d.ts +11 -0
- package/dtos/index.d.ts +1 -0
- package/dtos/social-auth.dto.d.ts +42 -0
- package/entities/index.d.ts +3 -1
- package/entities/social-auth-config.entity.d.ts +9 -0
- package/enums/index.d.ts +1 -0
- package/enums/social-provider-type.enum.d.ts +6 -0
- package/fesm/config/auth.constants.js +3 -0
- package/fesm/config/event-actions.js +4 -2
- package/fesm/config/message-keys.js +21 -0
- package/fesm/controllers/index.js +2 -0
- package/fesm/controllers/social-auth-config.controller.js +97 -0
- package/fesm/controllers/social-auth.controller.js +165 -0
- package/fesm/controllers/social-auth.controller.spec.js +75 -0
- package/fesm/docs/auth-swagger.config.js +9 -1
- package/fesm/dtos/index.js +1 -0
- package/fesm/dtos/social-auth.dto.js +282 -0
- package/fesm/entities/index.js +4 -1
- package/fesm/entities/index.spec.js +12 -1
- package/fesm/entities/social-auth-config.entity.js +92 -0
- package/fesm/enums/index.js +1 -0
- package/fesm/enums/social-provider-type.enum.js +7 -0
- package/fesm/index.js +1 -0
- package/fesm/interfaces/index.js +1 -0
- package/fesm/interfaces/social-auth.interface.js +1 -0
- package/fesm/modules/auth.module.js +8 -2
- package/fesm/modules/auth.module.spec.js +17 -2
- package/fesm/providers/facebook.social-provider.js +63 -0
- package/fesm/providers/google.social-provider.js +58 -0
- package/fesm/providers/index.js +1 -0
- package/fesm/providers/linkedin.social-provider.js +56 -0
- package/fesm/providers/microsoft.social-provider.js +68 -0
- package/fesm/providers/oauth-http.util.js +77 -0
- package/fesm/providers/social-provider.registry.js +14 -0
- package/fesm/providers/social-provider.registry.spec.js +131 -0
- package/fesm/services/auth-config.service.js +3 -0
- package/fesm/services/auth-config.service.spec.js +3 -0
- package/fesm/services/auth-datasource.provider.js +4 -0
- package/fesm/services/auth-datasource.provider.spec.js +30 -0
- package/fesm/services/authentication.service.js +38 -8
- package/fesm/services/authentication.service.spec.js +58 -0
- package/fesm/services/index.js +2 -0
- package/fesm/services/social-auth-config.service.js +93 -0
- package/fesm/services/social-auth-config.service.spec.js +67 -0
- package/fesm/services/social-auth.service.js +300 -0
- package/fesm/services/social-auth.service.spec.js +265 -0
- package/index.d.ts +1 -0
- package/interfaces/index.d.ts +1 -0
- package/interfaces/social-auth.interface.d.ts +43 -0
- package/package.json +3 -3
- package/providers/facebook.social-provider.d.ts +10 -0
- package/providers/google.social-provider.d.ts +10 -0
- package/providers/index.d.ts +1 -0
- package/providers/linkedin.social-provider.d.ts +10 -0
- package/providers/microsoft.social-provider.d.ts +10 -0
- package/providers/oauth-http.util.d.ts +10 -0
- package/providers/social-provider.registry.d.ts +3 -0
- package/services/auth-config.service.d.ts +1 -0
- package/services/auth-datasource.provider.d.ts +1 -0
- package/services/authentication.service.d.ts +4 -0
- package/services/index.d.ts +2 -0
- package/services/social-auth-config.service.d.ts +21 -0
- package/services/social-auth.service.d.ts +24 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
Object.defineProperty(exports, "googleSocialProvider", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: function() {
|
|
8
|
+
return googleSocialProvider;
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
const _socialprovidertypeenum = require("../enums/social-provider-type.enum");
|
|
12
|
+
const _oauthhttputil = require("./oauth-http.util");
|
|
13
|
+
function _define_property(obj, key, value) {
|
|
14
|
+
if (key in obj) {
|
|
15
|
+
Object.defineProperty(obj, key, {
|
|
16
|
+
value: value,
|
|
17
|
+
enumerable: true,
|
|
18
|
+
configurable: true,
|
|
19
|
+
writable: true
|
|
20
|
+
});
|
|
21
|
+
} else {
|
|
22
|
+
obj[key] = value;
|
|
23
|
+
}
|
|
24
|
+
return obj;
|
|
25
|
+
}
|
|
26
|
+
const AUTHORIZE_URL = 'https://accounts.google.com/o/oauth2/v2/auth';
|
|
27
|
+
const TOKEN_URL = 'https://oauth2.googleapis.com/token';
|
|
28
|
+
const USERINFO_URL = 'https://openidconnect.googleapis.com/v1/userinfo';
|
|
29
|
+
let GoogleSocialProvider = class GoogleSocialProvider {
|
|
30
|
+
buildAuthorizationUrl(credentials, state) {
|
|
31
|
+
const query = (0, _oauthhttputil.buildQuery)({
|
|
32
|
+
client_id: credentials.clientId,
|
|
33
|
+
redirect_uri: credentials.redirectUri,
|
|
34
|
+
response_type: 'code',
|
|
35
|
+
scope: (0, _oauthhttputil.resolveScopes)(credentials, this.defaultScopes),
|
|
36
|
+
state,
|
|
37
|
+
access_type: 'online',
|
|
38
|
+
prompt: 'select_account'
|
|
39
|
+
});
|
|
40
|
+
return `${AUTHORIZE_URL}?${query}`;
|
|
41
|
+
}
|
|
42
|
+
async fetchProfile(credentials, code) {
|
|
43
|
+
const token = await (0, _oauthhttputil.postForm)(this.provider, TOKEN_URL, {
|
|
44
|
+
code,
|
|
45
|
+
client_id: credentials.clientId,
|
|
46
|
+
client_secret: credentials.clientSecret,
|
|
47
|
+
redirect_uri: credentials.redirectUri,
|
|
48
|
+
grant_type: 'authorization_code'
|
|
49
|
+
});
|
|
50
|
+
const profile = await (0, _oauthhttputil.getJson)(this.provider, USERINFO_URL, (0, _oauthhttputil.readAccessToken)(this.provider, token));
|
|
51
|
+
return {
|
|
52
|
+
providerUserId: (0, _oauthhttputil.readString)(profile, 'sub') ?? '',
|
|
53
|
+
email: (0, _oauthhttputil.readString)(profile, 'email'),
|
|
54
|
+
emailVerified: profile['email_verified'] === true,
|
|
55
|
+
name: (0, _oauthhttputil.readString)(profile, 'name'),
|
|
56
|
+
avatarUrl: (0, _oauthhttputil.readString)(profile, 'picture')
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
constructor(){
|
|
60
|
+
_define_property(this, "provider", _socialprovidertypeenum.SocialProviderTypeEnum.GOOGLE);
|
|
61
|
+
_define_property(this, "defaultScopes", [
|
|
62
|
+
'openid',
|
|
63
|
+
'email',
|
|
64
|
+
'profile'
|
|
65
|
+
]);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
const googleSocialProvider = new GoogleSocialProvider();
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
_export_star(require("./social-provider.registry"), exports);
|
|
6
|
+
function _export_star(from, to) {
|
|
7
|
+
Object.keys(from).forEach(function(k) {
|
|
8
|
+
if (k !== "default" && !Object.prototype.hasOwnProperty.call(to, k)) {
|
|
9
|
+
Object.defineProperty(to, k, {
|
|
10
|
+
enumerable: true,
|
|
11
|
+
get: function() {
|
|
12
|
+
return from[k];
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
return from;
|
|
18
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
Object.defineProperty(exports, "linkedInSocialProvider", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: function() {
|
|
8
|
+
return linkedInSocialProvider;
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
const _socialprovidertypeenum = require("../enums/social-provider-type.enum");
|
|
12
|
+
const _oauthhttputil = require("./oauth-http.util");
|
|
13
|
+
function _define_property(obj, key, value) {
|
|
14
|
+
if (key in obj) {
|
|
15
|
+
Object.defineProperty(obj, key, {
|
|
16
|
+
value: value,
|
|
17
|
+
enumerable: true,
|
|
18
|
+
configurable: true,
|
|
19
|
+
writable: true
|
|
20
|
+
});
|
|
21
|
+
} else {
|
|
22
|
+
obj[key] = value;
|
|
23
|
+
}
|
|
24
|
+
return obj;
|
|
25
|
+
}
|
|
26
|
+
const AUTHORIZE_URL = 'https://www.linkedin.com/oauth/v2/authorization';
|
|
27
|
+
const TOKEN_URL = 'https://www.linkedin.com/oauth/v2/accessToken';
|
|
28
|
+
const USERINFO_URL = 'https://api.linkedin.com/v2/userinfo';
|
|
29
|
+
let LinkedInSocialProvider = class LinkedInSocialProvider {
|
|
30
|
+
buildAuthorizationUrl(credentials, state) {
|
|
31
|
+
const query = (0, _oauthhttputil.buildQuery)({
|
|
32
|
+
response_type: 'code',
|
|
33
|
+
client_id: credentials.clientId,
|
|
34
|
+
redirect_uri: credentials.redirectUri,
|
|
35
|
+
scope: (0, _oauthhttputil.resolveScopes)(credentials, this.defaultScopes),
|
|
36
|
+
state
|
|
37
|
+
});
|
|
38
|
+
return `${AUTHORIZE_URL}?${query}`;
|
|
39
|
+
}
|
|
40
|
+
async fetchProfile(credentials, code) {
|
|
41
|
+
const token = await (0, _oauthhttputil.postForm)(this.provider, TOKEN_URL, {
|
|
42
|
+
grant_type: 'authorization_code',
|
|
43
|
+
code,
|
|
44
|
+
client_id: credentials.clientId,
|
|
45
|
+
client_secret: credentials.clientSecret,
|
|
46
|
+
redirect_uri: credentials.redirectUri
|
|
47
|
+
});
|
|
48
|
+
const profile = await (0, _oauthhttputil.getJson)(this.provider, USERINFO_URL, (0, _oauthhttputil.readAccessToken)(this.provider, token));
|
|
49
|
+
return {
|
|
50
|
+
providerUserId: (0, _oauthhttputil.readString)(profile, 'sub') ?? '',
|
|
51
|
+
email: (0, _oauthhttputil.readString)(profile, 'email'),
|
|
52
|
+
emailVerified: profile['email_verified'] === true,
|
|
53
|
+
name: (0, _oauthhttputil.readString)(profile, 'name'),
|
|
54
|
+
avatarUrl: (0, _oauthhttputil.readString)(profile, 'picture')
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
constructor(){
|
|
58
|
+
_define_property(this, "provider", _socialprovidertypeenum.SocialProviderTypeEnum.LINKEDIN);
|
|
59
|
+
_define_property(this, "defaultScopes", [
|
|
60
|
+
'openid',
|
|
61
|
+
'profile',
|
|
62
|
+
'email'
|
|
63
|
+
]);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const linkedInSocialProvider = new LinkedInSocialProvider();
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
Object.defineProperty(exports, "microsoftSocialProvider", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: function() {
|
|
8
|
+
return microsoftSocialProvider;
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
const _socialprovidertypeenum = require("../enums/social-provider-type.enum");
|
|
12
|
+
const _oauthhttputil = require("./oauth-http.util");
|
|
13
|
+
function _define_property(obj, key, value) {
|
|
14
|
+
if (key in obj) {
|
|
15
|
+
Object.defineProperty(obj, key, {
|
|
16
|
+
value: value,
|
|
17
|
+
enumerable: true,
|
|
18
|
+
configurable: true,
|
|
19
|
+
writable: true
|
|
20
|
+
});
|
|
21
|
+
} else {
|
|
22
|
+
obj[key] = value;
|
|
23
|
+
}
|
|
24
|
+
return obj;
|
|
25
|
+
}
|
|
26
|
+
const GRAPH_ME_URL = 'https://graph.microsoft.com/v1.0/me';
|
|
27
|
+
const DEFAULT_TENANT = 'common';
|
|
28
|
+
function resolveTenant(credentials) {
|
|
29
|
+
const tenantId = credentials.extraConfig?.['tenantId'];
|
|
30
|
+
return typeof tenantId === 'string' && tenantId ? tenantId : DEFAULT_TENANT;
|
|
31
|
+
}
|
|
32
|
+
function endpoint(credentials, action) {
|
|
33
|
+
return `https://login.microsoftonline.com/${resolveTenant(credentials)}/oauth2/v2.0/${action}`;
|
|
34
|
+
}
|
|
35
|
+
let MicrosoftSocialProvider = class MicrosoftSocialProvider {
|
|
36
|
+
buildAuthorizationUrl(credentials, state) {
|
|
37
|
+
const query = (0, _oauthhttputil.buildQuery)({
|
|
38
|
+
client_id: credentials.clientId,
|
|
39
|
+
response_type: 'code',
|
|
40
|
+
redirect_uri: credentials.redirectUri,
|
|
41
|
+
response_mode: 'query',
|
|
42
|
+
scope: (0, _oauthhttputil.resolveScopes)(credentials, this.defaultScopes),
|
|
43
|
+
state
|
|
44
|
+
});
|
|
45
|
+
return `${endpoint(credentials, 'authorize')}?${query}`;
|
|
46
|
+
}
|
|
47
|
+
async fetchProfile(credentials, code) {
|
|
48
|
+
const token = await (0, _oauthhttputil.postForm)(this.provider, endpoint(credentials, 'token'), {
|
|
49
|
+
client_id: credentials.clientId,
|
|
50
|
+
client_secret: credentials.clientSecret,
|
|
51
|
+
code,
|
|
52
|
+
redirect_uri: credentials.redirectUri,
|
|
53
|
+
grant_type: 'authorization_code',
|
|
54
|
+
scope: (0, _oauthhttputil.resolveScopes)(credentials, this.defaultScopes)
|
|
55
|
+
});
|
|
56
|
+
const profile = await (0, _oauthhttputil.getJson)(this.provider, GRAPH_ME_URL, (0, _oauthhttputil.readAccessToken)(this.provider, token));
|
|
57
|
+
// `mail` is Microsoft's confirmed mailbox claim; `userPrincipalName` is a sign-in
|
|
58
|
+
// identifier that is not guaranteed to be a verified, owned mailbox address.
|
|
59
|
+
const verifiedEmail = (0, _oauthhttputil.readString)(profile, 'mail');
|
|
60
|
+
return {
|
|
61
|
+
providerUserId: (0, _oauthhttputil.readString)(profile, 'id') ?? '',
|
|
62
|
+
email: verifiedEmail ?? (0, _oauthhttputil.readString)(profile, 'userPrincipalName'),
|
|
63
|
+
emailVerified: !!verifiedEmail,
|
|
64
|
+
name: (0, _oauthhttputil.readString)(profile, 'displayName'),
|
|
65
|
+
avatarUrl: null
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
constructor(){
|
|
69
|
+
_define_property(this, "provider", _socialprovidertypeenum.SocialProviderTypeEnum.MICROSOFT);
|
|
70
|
+
_define_property(this, "defaultScopes", [
|
|
71
|
+
'openid',
|
|
72
|
+
'profile',
|
|
73
|
+
'email',
|
|
74
|
+
'User.Read'
|
|
75
|
+
]);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
const microsoftSocialProvider = new MicrosoftSocialProvider();
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
function _export(target, all) {
|
|
6
|
+
for(var name in all)Object.defineProperty(target, name, {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
get: Object.getOwnPropertyDescriptor(all, name).get
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
_export(exports, {
|
|
12
|
+
get buildQuery () {
|
|
13
|
+
return buildQuery;
|
|
14
|
+
},
|
|
15
|
+
get getJson () {
|
|
16
|
+
return getJson;
|
|
17
|
+
},
|
|
18
|
+
get postForm () {
|
|
19
|
+
return postForm;
|
|
20
|
+
},
|
|
21
|
+
get readAccessToken () {
|
|
22
|
+
return readAccessToken;
|
|
23
|
+
},
|
|
24
|
+
get readString () {
|
|
25
|
+
return readString;
|
|
26
|
+
},
|
|
27
|
+
get resolveScopes () {
|
|
28
|
+
return resolveScopes;
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
const _common = require("@nestjs/common");
|
|
32
|
+
const _messagekeys = require("../config/message-keys");
|
|
33
|
+
const OAUTH_HTTP_TIMEOUT_MS = 10_000;
|
|
34
|
+
function failExchange(provider, detail) {
|
|
35
|
+
throw new _common.UnauthorizedException({
|
|
36
|
+
message: `Social sign-in with ${provider} failed: ${detail}`,
|
|
37
|
+
messageKey: _messagekeys.SOCIAL_AUTH_MESSAGES.EXCHANGE_FAILED,
|
|
38
|
+
messageVariables: {
|
|
39
|
+
provider
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
async function fetchWithTimeout(provider, url, init) {
|
|
44
|
+
try {
|
|
45
|
+
return await fetch(url, {
|
|
46
|
+
...init,
|
|
47
|
+
signal: AbortSignal.timeout(OAUTH_HTTP_TIMEOUT_MS)
|
|
48
|
+
});
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (error instanceof Error && error.name === 'TimeoutError') {
|
|
51
|
+
failExchange(provider, 'request timed out');
|
|
52
|
+
}
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function readJson(provider, response) {
|
|
57
|
+
const body = await response.text();
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
failExchange(provider, `${response.status} ${body.slice(0, 200)}`);
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
return JSON.parse(body);
|
|
63
|
+
} catch {
|
|
64
|
+
return failExchange(provider, 'response was not JSON');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function postForm(provider, url, form) {
|
|
68
|
+
const response = await fetchWithTimeout(provider, url, {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
headers: {
|
|
71
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
72
|
+
Accept: 'application/json'
|
|
73
|
+
},
|
|
74
|
+
body: new URLSearchParams(form).toString()
|
|
75
|
+
});
|
|
76
|
+
return readJson(provider, response);
|
|
77
|
+
}
|
|
78
|
+
async function getJson(provider, url, accessToken) {
|
|
79
|
+
const response = await fetchWithTimeout(provider, url, {
|
|
80
|
+
headers: {
|
|
81
|
+
Authorization: `Bearer ${accessToken}`,
|
|
82
|
+
Accept: 'application/json'
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
return readJson(provider, response);
|
|
86
|
+
}
|
|
87
|
+
function readAccessToken(provider, token) {
|
|
88
|
+
const accessToken = token['access_token'];
|
|
89
|
+
if (typeof accessToken !== 'string' || !accessToken) {
|
|
90
|
+
failExchange(provider, 'no access token returned');
|
|
91
|
+
}
|
|
92
|
+
return accessToken;
|
|
93
|
+
}
|
|
94
|
+
function readString(source, key) {
|
|
95
|
+
const value = source[key];
|
|
96
|
+
return typeof value === 'string' && value ? value : null;
|
|
97
|
+
}
|
|
98
|
+
function buildQuery(params) {
|
|
99
|
+
const query = new URLSearchParams();
|
|
100
|
+
for (const [key, value] of Object.entries(params)){
|
|
101
|
+
if (value) query.append(key, value);
|
|
102
|
+
}
|
|
103
|
+
return query.toString();
|
|
104
|
+
}
|
|
105
|
+
function resolveScopes(credentials, defaults, separator = ' ') {
|
|
106
|
+
return (credentials.scopes?.length ? credentials.scopes : defaults).join(separator);
|
|
107
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
Object.defineProperty(exports, "getSocialProvider", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: function() {
|
|
8
|
+
return getSocialProvider;
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
const _socialprovidertypeenum = require("../enums/social-provider-type.enum");
|
|
12
|
+
const _facebooksocialprovider = require("./facebook.social-provider");
|
|
13
|
+
const _googlesocialprovider = require("./google.social-provider");
|
|
14
|
+
const _linkedinsocialprovider = require("./linkedin.social-provider");
|
|
15
|
+
const _microsoftsocialprovider = require("./microsoft.social-provider");
|
|
16
|
+
const PROVIDERS = {
|
|
17
|
+
[_socialprovidertypeenum.SocialProviderTypeEnum.GOOGLE]: _googlesocialprovider.googleSocialProvider,
|
|
18
|
+
[_socialprovidertypeenum.SocialProviderTypeEnum.FACEBOOK]: _facebooksocialprovider.facebookSocialProvider,
|
|
19
|
+
[_socialprovidertypeenum.SocialProviderTypeEnum.LINKEDIN]: _linkedinsocialprovider.linkedInSocialProvider,
|
|
20
|
+
[_socialprovidertypeenum.SocialProviderTypeEnum.MICROSOFT]: _microsoftsocialprovider.microsoftSocialProvider
|
|
21
|
+
};
|
|
22
|
+
function getSocialProvider(provider) {
|
|
23
|
+
return PROVIDERS[provider];
|
|
24
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
const _common = require("@nestjs/common");
|
|
6
|
+
const _socialprovidertypeenum = require("../enums/social-provider-type.enum");
|
|
7
|
+
const _socialproviderregistry = require("./social-provider.registry");
|
|
8
|
+
const CREDENTIALS = {
|
|
9
|
+
clientId: 'client-id',
|
|
10
|
+
clientSecret: 'client-secret',
|
|
11
|
+
redirectUri: 'http://localhost:2001/auth/social/callback'
|
|
12
|
+
};
|
|
13
|
+
function mockFetchSequence(...responses) {
|
|
14
|
+
const fetchMock = jest.fn();
|
|
15
|
+
for (const response of responses){
|
|
16
|
+
fetchMock.mockResolvedValueOnce({
|
|
17
|
+
ok: response.ok ?? true,
|
|
18
|
+
status: response.status ?? 200,
|
|
19
|
+
text: async ()=>JSON.stringify(response.body)
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
globalThis.fetch = fetchMock;
|
|
23
|
+
return fetchMock;
|
|
24
|
+
}
|
|
25
|
+
describe('social providers', ()=>{
|
|
26
|
+
afterEach(()=>jest.restoreAllMocks());
|
|
27
|
+
describe('buildAuthorizationUrl', ()=>{
|
|
28
|
+
it.each(Object.values(_socialprovidertypeenum.SocialProviderTypeEnum))('carries the client id, redirect URI and state for %s', (provider)=>{
|
|
29
|
+
const url = new URL((0, _socialproviderregistry.getSocialProvider)(provider).buildAuthorizationUrl(CREDENTIALS, 'state-token'));
|
|
30
|
+
expect(url.searchParams.get('client_id')).toBe('client-id');
|
|
31
|
+
expect(url.searchParams.get('redirect_uri')).toBe(CREDENTIALS.redirectUri);
|
|
32
|
+
expect(url.searchParams.get('state')).toBe('state-token');
|
|
33
|
+
expect(url.searchParams.get('scope')).toBeTruthy();
|
|
34
|
+
});
|
|
35
|
+
it('uses the configured scopes over the provider defaults', ()=>{
|
|
36
|
+
const url = new URL((0, _socialproviderregistry.getSocialProvider)(_socialprovidertypeenum.SocialProviderTypeEnum.GOOGLE).buildAuthorizationUrl({
|
|
37
|
+
...CREDENTIALS,
|
|
38
|
+
scopes: [
|
|
39
|
+
'openid',
|
|
40
|
+
'email'
|
|
41
|
+
]
|
|
42
|
+
}, 'state-token'));
|
|
43
|
+
expect(url.searchParams.get('scope')).toBe('openid email');
|
|
44
|
+
});
|
|
45
|
+
it('targets the common Microsoft directory unless a tenant is configured', ()=>{
|
|
46
|
+
const provider = (0, _socialproviderregistry.getSocialProvider)(_socialprovidertypeenum.SocialProviderTypeEnum.MICROSOFT);
|
|
47
|
+
expect(provider.buildAuthorizationUrl(CREDENTIALS, 'state-token')).toContain('login.microsoftonline.com/common/');
|
|
48
|
+
expect(provider.buildAuthorizationUrl({
|
|
49
|
+
...CREDENTIALS,
|
|
50
|
+
extraConfig: {
|
|
51
|
+
tenantId: 'contoso'
|
|
52
|
+
}
|
|
53
|
+
}, 'state-token')).toContain('login.microsoftonline.com/contoso/');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
describe('fetchProfile', ()=>{
|
|
57
|
+
it('normalises a Google OpenID profile', async ()=>{
|
|
58
|
+
mockFetchSequence({
|
|
59
|
+
body: {
|
|
60
|
+
access_token: 'token'
|
|
61
|
+
}
|
|
62
|
+
}, {
|
|
63
|
+
body: {
|
|
64
|
+
sub: 'google-1',
|
|
65
|
+
email: 'user@example.com',
|
|
66
|
+
email_verified: true,
|
|
67
|
+
name: 'Test User',
|
|
68
|
+
picture: 'https://cdn.test/avatar.png'
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
const profile = await (0, _socialproviderregistry.getSocialProvider)(_socialprovidertypeenum.SocialProviderTypeEnum.GOOGLE).fetchProfile(CREDENTIALS, 'code');
|
|
72
|
+
expect(profile).toEqual({
|
|
73
|
+
providerUserId: 'google-1',
|
|
74
|
+
email: 'user@example.com',
|
|
75
|
+
emailVerified: true,
|
|
76
|
+
name: 'Test User',
|
|
77
|
+
avatarUrl: 'https://cdn.test/avatar.png'
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
it('reads the nested avatar Facebook returns', async ()=>{
|
|
81
|
+
mockFetchSequence({
|
|
82
|
+
body: {
|
|
83
|
+
access_token: 'token'
|
|
84
|
+
}
|
|
85
|
+
}, {
|
|
86
|
+
body: {
|
|
87
|
+
id: 'fb-1',
|
|
88
|
+
name: 'Test User',
|
|
89
|
+
email: 'user@example.com',
|
|
90
|
+
picture: {
|
|
91
|
+
data: {
|
|
92
|
+
url: 'https://cdn.test/fb.png'
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
const profile = await (0, _socialproviderregistry.getSocialProvider)(_socialprovidertypeenum.SocialProviderTypeEnum.FACEBOOK).fetchProfile(CREDENTIALS, 'code');
|
|
98
|
+
expect(profile.avatarUrl).toBe('https://cdn.test/fb.png');
|
|
99
|
+
expect(profile.emailVerified).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
it('falls back to the Microsoft principal name when no mailbox is set', async ()=>{
|
|
102
|
+
mockFetchSequence({
|
|
103
|
+
body: {
|
|
104
|
+
access_token: 'token'
|
|
105
|
+
}
|
|
106
|
+
}, {
|
|
107
|
+
body: {
|
|
108
|
+
id: 'ms-1',
|
|
109
|
+
displayName: 'Test User',
|
|
110
|
+
userPrincipalName: 'user@contoso.com'
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
const profile = await (0, _socialproviderregistry.getSocialProvider)(_socialprovidertypeenum.SocialProviderTypeEnum.MICROSOFT).fetchProfile(CREDENTIALS, 'code');
|
|
114
|
+
expect(profile.email).toBe('user@contoso.com');
|
|
115
|
+
});
|
|
116
|
+
it('rejects when the token endpoint fails', async ()=>{
|
|
117
|
+
mockFetchSequence({
|
|
118
|
+
ok: false,
|
|
119
|
+
status: 400,
|
|
120
|
+
body: {
|
|
121
|
+
error: 'invalid_grant'
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
await expect((0, _socialproviderregistry.getSocialProvider)(_socialprovidertypeenum.SocialProviderTypeEnum.LINKEDIN).fetchProfile(CREDENTIALS, 'code')).rejects.toThrow(_common.UnauthorizedException);
|
|
125
|
+
});
|
|
126
|
+
it('rejects when the token response carries no access token', async ()=>{
|
|
127
|
+
mockFetchSequence({
|
|
128
|
+
body: {
|
|
129
|
+
token_type: 'bearer'
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
await expect((0, _socialproviderregistry.getSocialProvider)(_socialprovidertypeenum.SocialProviderTypeEnum.GOOGLE).fetchProfile(CREDENTIALS, 'code')).rejects.toThrow(_common.UnauthorizedException);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -55,6 +55,9 @@ let AuthConfigService = class AuthConfigService {
|
|
|
55
55
|
isEmailVerificationEnabled() {
|
|
56
56
|
return this.options.bootstrapAppConfig?.enableEmailVerification ?? true;
|
|
57
57
|
}
|
|
58
|
+
isSignUpEnabled() {
|
|
59
|
+
return this.options.bootstrapAppConfig?.enableSignUp ?? true;
|
|
60
|
+
}
|
|
58
61
|
isPermissionModeFound() {
|
|
59
62
|
return this.options.bootstrapAppConfig?.permissionMode !== undefined;
|
|
60
63
|
}
|
|
@@ -21,6 +21,7 @@ describe('AuthConfigService', ()=>{
|
|
|
21
21
|
expect(service.isMultiTenant()).toBe(false);
|
|
22
22
|
expect(service.isCompanyFeatureEnabled()).toBe(false);
|
|
23
23
|
expect(service.isEmailVerificationEnabled()).toBe(true);
|
|
24
|
+
expect(service.isSignUpEnabled()).toBe(true);
|
|
24
25
|
expect(service.isPermissionModeFound()).toBe(false);
|
|
25
26
|
});
|
|
26
27
|
it('throws when reading the JWT secret without configuration', ()=>{
|
|
@@ -53,6 +54,7 @@ describe('AuthConfigService', ()=>{
|
|
|
53
54
|
databaseMode: 'multi-tenant',
|
|
54
55
|
enableCompanyFeature: true,
|
|
55
56
|
enableEmailVerification: false,
|
|
57
|
+
enableSignUp: false,
|
|
56
58
|
permissionMode: 'RBAC'
|
|
57
59
|
},
|
|
58
60
|
config: {
|
|
@@ -74,6 +76,7 @@ describe('AuthConfigService', ()=>{
|
|
|
74
76
|
const service = new _authconfigservice.AuthConfigService(buildOptions());
|
|
75
77
|
expect(service.isCompanyFeatureEnabled()).toBe(true);
|
|
76
78
|
expect(service.isEmailVerificationEnabled()).toBe(false);
|
|
79
|
+
expect(service.isSignUpEnabled()).toBe(false);
|
|
77
80
|
expect(service.isPermissionModeFound()).toBe(true);
|
|
78
81
|
});
|
|
79
82
|
it('returns configured JWT and refresh token values', ()=>{
|
|
@@ -99,6 +99,10 @@ let AuthDataSourceProvider = class AuthDataSourceProvider extends _nestjsshared.
|
|
|
99
99
|
const effectiveTenant = tenant ?? this.getCurrentTenant();
|
|
100
100
|
return effectiveTenant?.enableEmailVerification ?? this.authOptions.bootstrapAppConfig?.enableEmailVerification ?? true;
|
|
101
101
|
}
|
|
102
|
+
isSignUpEnabled(tenant) {
|
|
103
|
+
const effectiveTenant = tenant ?? this.getCurrentTenant();
|
|
104
|
+
return effectiveTenant?.enableSignUp ?? this.authOptions.bootstrapAppConfig?.enableSignUp ?? true;
|
|
105
|
+
}
|
|
102
106
|
async getAuthEntities() {
|
|
103
107
|
const { getAuthEntitiesByConfig } = await Promise.resolve().then(()=>/*#__PURE__*/ _interop_require_wildcard(require("../entities")));
|
|
104
108
|
return getAuthEntitiesByConfig({
|
|
@@ -82,6 +82,36 @@ describe('AuthDataSourceProvider', ()=>{
|
|
|
82
82
|
})).toBe(false);
|
|
83
83
|
});
|
|
84
84
|
});
|
|
85
|
+
describe('isSignUpEnabled', ()=>{
|
|
86
|
+
it('returns true by default (sign-up defaults to enabled)', ()=>{
|
|
87
|
+
const provider = new _authdatasourceprovider.AuthDataSourceProvider(buildOptions({
|
|
88
|
+
bootstrapAppConfig: undefined
|
|
89
|
+
}));
|
|
90
|
+
expect(provider.isSignUpEnabled()).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
it('returns false when bootstrap config disables it', ()=>{
|
|
93
|
+
const provider = new _authdatasourceprovider.AuthDataSourceProvider(buildOptions({
|
|
94
|
+
bootstrapAppConfig: {
|
|
95
|
+
databaseMode: 'single',
|
|
96
|
+
enableCompanyFeature: false,
|
|
97
|
+
enableSignUp: false
|
|
98
|
+
}
|
|
99
|
+
}));
|
|
100
|
+
expect(provider.isSignUpEnabled()).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
it('prefers the tenant override over bootstrap config', ()=>{
|
|
103
|
+
const provider = new _authdatasourceprovider.AuthDataSourceProvider(buildOptions({
|
|
104
|
+
bootstrapAppConfig: {
|
|
105
|
+
databaseMode: 'multi-tenant',
|
|
106
|
+
enableCompanyFeature: true,
|
|
107
|
+
enableSignUp: true
|
|
108
|
+
}
|
|
109
|
+
}));
|
|
110
|
+
expect(provider.isSignUpEnabled({
|
|
111
|
+
enableSignUp: false
|
|
112
|
+
})).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
85
115
|
describe('getAuthEntities', ()=>{
|
|
86
116
|
it('returns only base entities when company feature and email verification are disabled', async ()=>{
|
|
87
117
|
const provider = new _authdatasourceprovider.AuthDataSourceProvider(buildOptions({
|
|
@@ -108,6 +108,9 @@ let AuthenticationService = class AuthenticationService {
|
|
|
108
108
|
const providerAvailable = this.authEmailService?.isEmailEnabled() ?? false;
|
|
109
109
|
return featureEnabled && providerAvailable;
|
|
110
110
|
}
|
|
111
|
+
isSignUpEnabled() {
|
|
112
|
+
return this.dataSourceProvider?.isSignUpEnabled() ?? this.authConfig.isSignUpEnabled();
|
|
113
|
+
}
|
|
111
114
|
async getUserRepository() {
|
|
112
115
|
if (!this.dataSourceProvider) {
|
|
113
116
|
throw new _common.InternalServerErrorException({
|
|
@@ -134,12 +137,7 @@ let AuthenticationService = class AuthenticationService {
|
|
|
134
137
|
};
|
|
135
138
|
}
|
|
136
139
|
const user = await this.validateUser(email, password);
|
|
137
|
-
|
|
138
|
-
throw new _common.ForbiddenException({
|
|
139
|
-
message: 'User account is deactivated',
|
|
140
|
-
messageKey: _config.AUTH_MESSAGES.LOGIN_ACCOUNT_DEACTIVATED
|
|
141
|
-
});
|
|
142
|
-
}
|
|
140
|
+
this.ensureUserActive(user);
|
|
143
141
|
if (this.isEmailVerificationEnabled() && !user.emailVerified) {
|
|
144
142
|
return {
|
|
145
143
|
requiresEmailVerification: true,
|
|
@@ -148,11 +146,23 @@ let AuthenticationService = class AuthenticationService {
|
|
|
148
146
|
messageKey: _config.AUTH_MESSAGES.LOGIN_EMAIL_NOT_VERIFIED
|
|
149
147
|
};
|
|
150
148
|
}
|
|
149
|
+
return this.issueLoginForUser(user);
|
|
150
|
+
}
|
|
151
|
+
async issueLoginForUser(user) {
|
|
152
|
+
this.ensureUserActive(user);
|
|
151
153
|
if (!this.isCompanyFeatureEnabled()) {
|
|
152
154
|
return this.generateLoginResponse(user);
|
|
153
155
|
}
|
|
154
156
|
return await this.handleCompanyBasedLogin(user);
|
|
155
157
|
}
|
|
158
|
+
ensureUserActive(user) {
|
|
159
|
+
if (!user.isActive) {
|
|
160
|
+
throw new _common.ForbiddenException({
|
|
161
|
+
message: 'User account is deactivated',
|
|
162
|
+
messageKey: _config.AUTH_MESSAGES.LOGIN_ACCOUNT_DEACTIVATED
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
156
166
|
async handleCompanyBasedLogin(user) {
|
|
157
167
|
const permissionRepository = await this.getCompanyScopedRepository(_usercompanypermissionentity.UserCompanyPermission);
|
|
158
168
|
const branchRepository = await this.getCompanyScopedRepository(_companybranchentity.CompanyBranch);
|
|
@@ -200,8 +210,15 @@ let AuthenticationService = class AuthenticationService {
|
|
|
200
210
|
const existingUser = await userRepository.findOne({
|
|
201
211
|
where: {
|
|
202
212
|
email: dto.email
|
|
203
|
-
}
|
|
213
|
+
},
|
|
214
|
+
withDeleted: true
|
|
204
215
|
});
|
|
216
|
+
if (existingUser?.deletedAt) {
|
|
217
|
+
throw new _common.BadRequestException({
|
|
218
|
+
message: 'This account has been permanently deleted. Please contact your system administrator.',
|
|
219
|
+
messageKey: _config.AUTH_MESSAGES.REGISTER_ACCOUNT_DELETED
|
|
220
|
+
});
|
|
221
|
+
}
|
|
205
222
|
if (existingUser && this.isCompanyFeatureEnabled()) {
|
|
206
223
|
return await this.handleExistingUserCompanyRegistration(existingUser, dto);
|
|
207
224
|
}
|
|
@@ -211,6 +228,12 @@ let AuthenticationService = class AuthenticationService {
|
|
|
211
228
|
messageKey: _config.AUTH_MESSAGES.REGISTER_EMAIL_EXISTS
|
|
212
229
|
});
|
|
213
230
|
}
|
|
231
|
+
if (!this.isSignUpEnabled()) {
|
|
232
|
+
throw new _common.BadRequestException({
|
|
233
|
+
message: 'New account registration is currently disabled.',
|
|
234
|
+
messageKey: _config.AUTH_MESSAGES.REGISTER_SIGNUP_DISABLED
|
|
235
|
+
});
|
|
236
|
+
}
|
|
214
237
|
if (this.isEmailVerificationEnabled() && verifyUrlBase) {
|
|
215
238
|
const companyData = this.isCompanyFeatureEnabled() ? {
|
|
216
239
|
companySlug: dto.companySlug,
|
|
@@ -338,8 +361,15 @@ let AuthenticationService = class AuthenticationService {
|
|
|
338
361
|
const existingUser = await queryRunner.manager.findOne(_appuserentity.AppUser, {
|
|
339
362
|
where: {
|
|
340
363
|
email: pending.email
|
|
341
|
-
}
|
|
364
|
+
},
|
|
365
|
+
withDeleted: true
|
|
342
366
|
});
|
|
367
|
+
if (existingUser?.deletedAt) {
|
|
368
|
+
throw new _common.BadRequestException({
|
|
369
|
+
message: 'This account has been permanently deleted. Please contact your system administrator.',
|
|
370
|
+
messageKey: _config.AUTH_MESSAGES.REGISTER_ACCOUNT_DELETED
|
|
371
|
+
});
|
|
372
|
+
}
|
|
343
373
|
if (existingUser) {
|
|
344
374
|
throw new _common.BadRequestException({
|
|
345
375
|
message: 'This email has already been registered.',
|