Migrate to openid client (#5856)

* Create forksync.yml * update oidc to use openid-client * update oidc module requirements * working oidc+ includes all oauth2 clients automatically migrated. azure will need some kind of fix for the uid * update openid-client install checks * created overarching schema for OIDC * bug fixs for azure login * update schema prepare schema for unified oidc module * update 'oidc' to strategy variable * working azure+ groups groups from azure are in, you can use memberOf or transitiveMemberOf in config (Graphs API) * clean up old config import + working google oidc previous config map was recursive nonsense, changed to multiple IFs * added convertStrArray * de-expanded scope put all other auth strategies back to normal and fixed oidc strategy * swap back to using authlog debugger * Update meshcentral-config-schema.json * working google oidc + groups * working azure+groups (again) * init oidc docs very incomplete but basic config is present * add oidc * more work on docs * add scope and claim options plus fixed a few bugs and faults in my logic used logs correctly * further cleanup debug * more debug cleanup * continue documentation push fixed minor debug bugs also * more work on docs missing links, need to get azure preset docs, probably more. * done with docs its good enough for now * minor fix + presets get correct icon * fix google oidc not visible at login * fix bug with emailVerified property * fix logout bug + debug cleanup * fix strategy logout bug +cleanup * fixed preset login icon * fix alert + fix schema * terminate lines * Dutch language update 1.0.85 line up polish translation * Fixed guest web relay session revocation (#4667) * Updated French translation. * Add hook to allow adding custom api endpoints to Express routing * Updated German translation. * Update meshcentral-config-schema.json (change formatting) This way it is easier to edit and maintain * Fixed schema. * fix meshcentral-config-schema.json * add language selector to login (#5648) * add language selector to login * add showLanguageSelect to pick top or bottom boxe * remove additionalProperties: false in schema to allow comments #5697 Signed-off-by: si458 <simonsmith5521@gmail.com> * fix notes in docs * Fix web relay session handling and redirection due to bad merge * Added option to check HTTP origin. * add links and fix typo * move groups after strategy * Update version split in docs * Fix preset issuer URL in OIDC strategy * Update clientid and clientsecret to client_id and client_secret * Update meshcentral-config-schema.json and fix bad rebase * Update meshcentral-config-schema.json * fix bad rebase * fix bad rebase * Add 'connect-flash' to passport dependencies * Remove unnecessary passport dependencies - fix bad rebase * Fix auth strategy bug and remove console.log statement * Set groupType to the preset name if it exists, otherwise use the strategy name * remove finally block from * Refactor authentication logging in handleStrategyLogin to include strategy name --------- Signed-off-by: si458 <simonsmith5521@gmail.com> Co-authored-by: petervanv <58996467+petervanv@users.noreply.github.com> Co-authored-by: Ylian Saint-Hilaire <ysainthilaire@hotmail.com> Co-authored-by: Martin Mädler <martin.maedler@gmail.com> Co-authored-by: Fausto Gutierrez <28719096+faustogut@users.noreply.github.com> Co-authored-by: Simon Smith <simonsmith5521@gmail.com>

mstrhakr committed Mar 3, 2024 at 19:03 UTC 4be5b7273e3670b07f4b88331831e99e2e8b800f
11 files changed +2194 -846
common.js
+13
@@ -386,4 +386,17 @@ module.exports.moveOldFiles = function (filelist) {
386 for (var i in filelist) { if (fs.existsSync(filelist[i] + oldFileExt) == true) { extOk = false; } }
387 } while (extOk == false);
388 for (var i in filelist) { try { fs.renameSync(filelist[i], filelist[i] + oldFileExt); } catch (ex) { } }
389 +}
390 +
391 +// Convert strArray to Array, returns array if strArray or null if any other type
392 +module.exports.convertStrArray = function (object, split) {
393 + if (split && typeof object === 'string') {
394 + return object.split(split)
395 + } else if (typeof object === 'string') {
396 + return Array(object);
397 + } else if (Array.isArray(object)) {
398 + return object
399 + } else {
400 + return []
401 + }
402 }
\ No newline at end of file
docs/docs/meshcentral/index.md
+33 -1
@@ -1659,7 +1659,39 @@ Enabling SAML will require MeshCentral to install extra modules from NPM, so dep
1659
1660 !!!note
1661 MeshCentral only supports "POST". [For example Authentik's](https://github.com/Ylianst/MeshCentral/issues/4725) default setting is to use "Redirect" as a "Service Provider Binding".
1662 -
1662 +
1663 +### Generic OpenID Connect Setup
1664 +
1665 +Generally, if you are using an IdP that supports OpenID Connect (OIDC), you can use a very basic configuration to get started, and if needed, add more specific or advanced configurations later. Here is what your config file will look like with a basic, generic, configuration.
1666 +
1667 +``` json
1668 +{
1669 + "settings": {
1670 + "cert": "mesh.your.domain",
1671 + "port": 443,
1672 + "sqlite3": true
1673 + },
1674 + "domains": {
1675 + "": {
1676 + "title": "Mesh",
1677 + "title2": ".Your.Domain",
1678 + "authStrategies": {
1679 + "oidc": {
1680 + "issuer": "https://sso.your.domain",
1681 + "clientid": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
1682 + "clientsecret": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
1683 + "newAccounts": true
1684 + }
1685 + }
1686 + }
1687 + }
1688 +}
1689 +```
1690 +
1691 +As you can see, this is roughly the same as all the other OAuth2 based authentication strategies. These are the basics you need to get started using OpenID Connect because it's still authenticating with OAuth2. If you plan to take advantage of some of the more advanced features provided by this strategy you should consider reading the [additional strategy documentation](./openidConnectStrategy.md).
1692 +
1693 +> NOTE: MeshCentral will use `https://mesh.your.domain/auth-oidc-callback` as the default redirect uri.
1694 +
1695 ## Improvements to MeshCentral
1696
1697 In 2007, the first version of MeshCentral was built. We will refer to it as “MeshCentral1”. When MeshCentral1 was designed, HTML5 did not exist and web sockets where not implemented in any of the major browsers. Many design decisions were made at the time that are no longer optimal today. With the advent of the latest MeshCentral, MeshCentral1 is no longer supported and MeshCentral v2 has been significantly redesigned and mostly re-written based of previous version. Here is a list of improvements made in MeshCentral when compared with MeshCentral1:
docs/docs/meshcentral/openidConnectStrategy.md new
+654
@@ -0,0 +1,654 @@
1 +# Using the OpenID Connect Strategy on MeshCentral
2 +
3 +## Overview
4 +
5 +### Introduction
6 +
7 +There is a lot of information to go over, but first, why OpenID Connect?
8 +
9 +Esentially its because its both based on a industry standard authorization protocol, and is becoming an industry standard authentication protocol. Put simply it's reliable and reusable, and we use OpenID Connect for exactly those reasons, almost every everyone does, and we want to be able to integrate with almost anyone. This strategy allows us to expand the potential of MeshCentral through the potential of OpenID Connect.
10 +
11 +In this document, we will learn about the OpenID Connect specification at a high level, and then use that information to configure the OpenID Connect strategy for MeshCentral using a generic OpenID Connect compatible IdP. After that we will go over some advanced configurations and then continue by explaining how to use the new presets for popular IdPs, specifically Google or Azure. Then we will explore the configuration and usage of the groups feature.
12 +
13 +> ATTENTION: As of MeshCentral `v1.1.22` there are multiple config options being depreciated. Using any of the old configs will only generate a warning in the authlog and will not stop you from using this strategy at this time. If there is information found in both the new and old config locations the new config location will be used. We will go over the specifics later, now lets jump in.
14 +
15 +### Chart of Frequently Used Terms and Acronyms
16 +| Term | AKA | Descriptions |
17 +| --- | --- | --- |
18 +| OAuth 2.0 | OAuth2 | OAuth 2.0 is the industry-standard protocol for user *authorization*. |
19 +| OpenID Connect | OIDC | Identity layer built on top of OAuth2 for user *authentication*. |
20 +| Identity Provider | IdP | The *service used* to provide authentication and authorization. |
21 +| Preset Configs | Presets | Set of *pre-configured values* to allow some specific IdPs to connect correctly. |
22 +| OAuth2 Scope | Scope | A flag *requesting access* to a specific resource or endpoint |
23 +| OIDC Claim | Claim | A *returned property* in the user info provided by your IdP |
24 +| User Authentication | AuthN | Checks if you *are who you say you are*. Example: Username and password authentication |
25 +| User Authorization | AuthZ | Check if you have the *permissions* required to access a specific resource or endpoint |
26 +
27 +### OpenID Connect Technology Overview
28 +
29 +OpenID Connect is a simple identity layer built on top of the OAuth2 protocol. It allows Clients to verify the identity of the End-User based on the authentication performed by an “Authorization Server”, as well as to obtain basic profile information about the End-User in an interoperable and REST-like manner.
30 +
31 +OpenID Connect allows clients of all types, including Web-based, mobile, and JavaScript clients, to request and receive information about authenticated sessions and end-users. The specification suite is extensible, allowing participants to use optional features such as encryption of identity data, discovery of OpenID Providers, and logout, when it makes sense for them.
32 +
33 +That description was straight from [OpenID Connect Documentation](https://openid.net/connect/), but basically, OAuth2 is the foundation upon which OpenID Connect was built, allowing for wide ranging compatability and interconnection. OpenID Connect appends the secure user *authentication* OAuth2 is known for, with user *authorization* by allowing the request of additional *scopes* that provide additional *claims* or access to API's in an easily expandable way.
34 +
35 +## Basic Config
36 +
37 +### *Introduction*
38 +
39 +Generally, if you are using an IdP that supports OIDC, you can use a very basic configuration to get started, and if needed, add more specific or advanced configurations later. Here is what your config file will look like with a basic, generic, configuration.
40 +
41 +### *Basic Config File Example*
42 +
43 +``` json
44 +{
45 + "settings": {
46 + "cert": "mesh.your.domain",
47 + "port": 443,
48 + "sqlite3": true
49 + },
50 + "domains": {
51 + "": {
52 + "title": "MeshCentral",
53 + "title2": "Your sub-title",
54 + "authStrategies": {
55 + "oidc": {
56 + "issuer": "https://sso.your.domain",
57 + "clientid": "2d5685c5-0f32-4c1f-9f09-c60e0dbc948a",
58 + "clientsecret": "7PiGSLSLL4e7NGi67KM229tfK7Z7TqzQ",
59 + "newAccounts": true
60 + }
61 + }
62 + }
63 + }
64 +}
65 +```
66 +
67 +As you can see, this is roughly the same as all the other OAuth2 based authentication strategies. These are the basics you need to get started, however, if you plan to take advantage of some of the more advanced features provided by this strategy, you'll need to keep reading.
68 +
69 +In this most basic of setups, you only need the URL of the issuer, as well as a client ID and a client secret. Notice in this example that the callback URL (or client redirect uri) is not configured, thats because MeshCentral will use `https://mesh.your.domain/auth-oidc-callback` as the default. Once you've got your configuration saved, restart MeshCentral and you should see an OpenID Connect Single Sign-on button on the login screen.
70 +
71 +> WARNING: The redirect endpoint must EXACTLY match the value provided to your IdP or your will deny the connection.
72 +
73 +> ATTENTION: You are required to configure the cert property in the settings section for the default domain, and configure the dns property under each additional domain.
74 +
75 +## Advanced Options
76 +
77 +### Overview
78 +
79 +There are plenty of options at your disposal if you need them. In fact, you can configure any property that node-openid-client supports. The openid-client module supports far more customization than I know what to do with, if you want to know more check out [node-openid-client on GitHub](https://github.com/panva/node-openid-client) for expert level configuration details. There are plenty of things you can configure with this strategy and there is a lot of decumentation behind the tools used to make this all happen. I strongly recommend you explore the [config schema](https://github.com/Ylianst/MeshCentral/blob/master/meshcentral-config-schema.json), and if you have a complicated config maybe check out the [openid-client readme](https://github.com/panva/node-openid-client/blob/main/docs/README.md). Theres a list of resources at the end if you want more information on any specific topics. In the meantime, let’s take a look at an example of what your config file could look with a slightly more complicated configuration, including multiple manually defined endpoints.
80 +
81 +#### *Advanced Config File Example*
82 +
83 +``` json
84 +{
85 + "settings": {
86 + "cert": "mesh.your.domain",
87 + "port": 443,
88 + "redirPort": 80,
89 + "AgentPong": 300,
90 + "TLSOffload": "192.168.1.50",
91 + "SelfUpdate": false,
92 + "AllowFraming": false,
93 + "sqlite3": true,
94 + "WebRTC": true
95 + },
96 + "domains": {
97 + "": {
98 + "title": "Mesh",
99 + "title2": ".Your.Domain",
100 + "orphanAgentUser": "~oidc:e48f8ef3-a9cb-4c84-b6d1-fb7d294e963c",
101 + "authStrategies": {
102 + "oidc": {
103 + "issuer": {
104 + "issuer": "https://sso.your.domain",
105 + "authorization_endpoint": "https://auth.your.domain/auth-endpoint",
106 + "token_endpoint": "https://tokens.sso.your.domain/token-endpoint",
107 + "endsession_endpoint": "https://sso.your.domain/logout",
108 + "jwks_uri": "https://sso.your.domain/jwks-uri"
109 + },
110 + "client": {
111 + "client_id": "110d5612-0822-4449-a057-8a0dbe26eca5",
112 + "client_secret": "4TqST46K53o3Z2Q88p39YwR6YwJb7Cka",
113 + "redirect_uri": "https://mesh.your.domain/oauth2/oidc/redirect",
114 + "post_logout_redirect_uri": "https://mesh.your.domain/login",
115 + "token_endpoint_auth_method": "client_secret_post",
116 + "response_types": "authorization_code"
117 + },
118 + "custom": {
119 + "scope": [ "openid", "profile", "read.EmailAlias", "read.UserProfile" ],
120 + "preset": null
121 + },
122 + "groups": {
123 + "recursive": true,
124 + "required": ["Group1", "Group2"],
125 + "siteadmin": ["GroupA", "GroupB"],
126 + "revokeAdmin": true,
127 + "sync": {
128 + "filter": ["Group1", "GroupB", "OtherGroup"]
129 + },
130 + "claim": "GroupClaim",
131 + "scope": "read.GroupMemberships"
132 + },
133 + "logouturl": "https://sso.your.domain/logout?r=https://mesh.your.domain/login",
134 + "newAccounts": true
135 + },
136 + {...}
137 + }
138 + }
139 + }
140 +}
141 +```
142 +
143 +### "Issuer" Options
144 +
145 +#### *Introduction*
146 +
147 +In the advanced example config above, did you notice that the issuer property has changed from a *string* to an *object* compared to the basic example? This not only allows for much a much smaller config footprint when advanced issuer options are not required, it successfully fools you in to a false sense of confidence early on in this document. If you are manually configuring the issuer endpoints, keep in mind that MeshCentral will still attempt to discover **ALL** issuer information. Obviously if you manually configure an endpoint, it will be used even if the discovered information is different from your config.
148 +
149 +> NOTE: If you are using a preset, you dont need to define an issuer. If you do, the predefined information will be ignored.
150 +
151 +#### *Common Config Chart*
152 +
153 +| Name | Description | Default | Example | Required |
154 +| --- | --- | --- | --- | --- |
155 +| `issuer` | The primary URI that represents your Identity Providers authentication endpoints. | N/A | `"issuer": "https://sso.your.domain"`<br/>`"issuer": { "issuer": "https://sso.your.domain" }` | Unless using preset. |
156 +
157 +#### *Advanced Config Example*
158 +
159 +``` json
160 +"issuer": {
161 + "issuer": "https://sso.your.domain",
162 + "authorization_endpoint": "https://auth.your.domain/auth-endpoint",
163 + "token_endpoint": "https://tokens.sso.your.domain/token-endpoint",
164 + "endsession_endpoint": "https://sso.your.domain/logout",
165 + "jwks_uri": "https://sso.your.domain/jwks-uri"
166 +},
167 +```
168 +
169 +#### *Required and Commonly Used Configs*
170 +
171 +The `issuer` property in the `issuer` object is the only one required, and its only required if you aren't using a preset. Besides the issuer, these are mostly options related to the endpoints and their configuration. The schema below looks intimidating but it comes down to being able to support any IdP. Setting the issuer, and endsession_endpoint are the two main ones you want to setup.
172 +
173 +#### *Schema*
174 +
175 +``` json
176 +"issuer": {
177 + "type": ["string","object"],
178 + "format": "uri",
179 + "description": "Issuer options. Requires issuer URI (issuer.issuer) to discover missing information unless using preset",
180 + "properties": {
181 + "issuer": { "type": "string", "format": "uri", "description": "URI of the issuer." },
182 + "authorization_endpoint": { "type": "string", "format": "uri" },
183 + "token_endpoint": { "type": "string", "format": "uri" },
184 + "jwks_uri": { "type": "string", "format": "uri" },
185 + "userinfo_endpoint": { "type": "string", "format": "uri" },
186 + "revocation_endpoint": { "type": "string", "format": "uri" },
187 + "introspection_endpoint": { "type": "string", "format": "uri" },
188 + "end_session_endpoint": {
189 + "type": "string",
190 + "format": "uri",
191 + "description": "URI to direct users to when logging out of MeshCentral.",
192 + "default": "this.issuer/logout"
193 + },
194 + "registration_endpoint": { "type": "string", "format": "uri" },
195 + "token_endpoint_auth_methods_supported": { "type": "string" },
196 + "token_endpoint_auth_signing_alg_values_supported": { "type": "string" },
197 + "introspection_endpoint_auth_methods_supported": { "type": "string" },
198 + "introspection_endpoint_auth_signing_alg_values_supported": { "type": "string" },
199 + "revocation_endpoint_auth_methods_supported": { "type": "string" },
200 + "revocation_endpoint_auth_signing_alg_values_supported": { "type": "string" },
201 + "request_object_signing_alg_values_supported": { "type": "string" },
202 + "mtls_endpoint_aliases": {
203 + "type":"object",
204 + "properties": {
205 + "token_endpoint": { "type": "string", "format": "uri" },
206 + "userinfo_endpoint": { "type": "string", "format": "uri" },
207 + "revocation_endpoint": { "type": "string", "format": "uri" },
208 + "introspection_endpoint": { "type": "string", "format": "uri" }
209 + }
210 + }
211 + },
212 + "additionalProperties": false
213 +},
214 +```
215 +
216 +### "Client" Options
217 +
218 +#### *Introduction*
219 +
220 +There are just about as many option as possible here since openid-client also provides a Client class, because of this you are able to manually configure the client how ever you need. This includes setting your redirect URI to any available path, for example, if I was using the "google" preset and wanted to have Google redirect me back to "https://mesh.your.domain/oauth2/oidc/redirect/givemebackgooglemusicyoujerks", MeshCentral will now fully support you in that. One of the other options is the post logout redirect URI, and it is exactly what it sounds like. After MeshCentral logs out a user using the IdPs end session endpoint, it send the post logout redirect URI to your IdP to forward the user back to MeshCentral or to an valid URI such as a homepage.
221 +
222 +> NOTE: The client object is required, however an exception would be with using old configs, which will be discussed later.
223 +
224 +#### *Common Configs*
225 +
226 +| Name | Description | Default | Example | Required |
227 +| --- | --- | --- | --- | --- |
228 +| `client_id` | The client ID provided by your Identity Provider (IdP) | N/A | `bdd6aa4b-d2a2-4ceb-96d3-b3e23cd17678` | `true` |
229 +| `client_secret` | The client secret provided by your Identity Provider (IdP) | N/A | `vUg82LJ322rp2bvdzuVRh3dPn3oVo29m` | `true` |
230 +| `redirect_uri` | "URI your IdP sends you after successful authorization. | `https://mesh.your.domain/auth-oidc-callback` | `https://mesh.your.domain/oauth2/oidc/redirect` | `false` |
231 +| `post_logout_redirect_uri` | URI for your IdP to send you after logging out of IdP via MeshCentral. | `https://mesh.your.domain/login` | `https://site.your.other.domain/login` | `false` |
232 +
233 +#### *Advanced Config Example*
234 +
235 +``` json
236 +"client": {
237 + "client_id": "00b3875c-8d82-4238-a8ef-25303fa7f9f2",
238 + "client_secret": "7PP453H577xbFDCqG8nYEJg8M3u8GT8F",
239 + "redirect_uri": "https://mesh.your.domain/oauth2/oidc/redirect",
240 + "post_logout_redirect_uri": "https://mesh.your.domain/login",
241 + "token_endpoint_auth_method": "client_secret_post",
242 + "response_types": "authorization_code"
243 +},
244 +```
245 +
246 +#### *Required and Commonly Used Configs*
247 +
248 +There are many available options you can configure but most of them go unused. Although there are a few *commonly used* properties. The first two properties, `client_id` and `client_secret` are required. The next one `redirect_uri` is used to setup a custom URI for the redirect back to MeshCentral after being authenicated by your IdP. The `post_logout_redirect_uri` property is used to tell your IdP where to send you after being logged out. These work in conjunction with the issuers `end_session_url` to automatically fill in any blanks in the config.
249 +
250 +#### *Schema*
251 +``` json
252 +"client": {
253 + "type": "object",
254 + "description": "OIDC Client Options",
255 + "properties": {
256 + "client_id": {
257 + "type": "string",
258 + "description": "REQUIRED: The client ID provided by your Identity Provider (IdP)"
259 + },
260 + "client_secret": {
261 + "type": "string",
262 + "description": "REQUIRED: The client secret provided by your Identity Provider (IdP)"
263 + },
264 + "redirect_uri": {
265 + "type": "string",
266 + "format": "uri",
267 + "description": "URI your IdP sends you after successful authorization. This must match what is listed with your IdP. (Default is https://[currentHost][currentPath]/auth-oidc-callback)"
268 + },
269 + "post_logout_redirect_uri": {
270 + "type": "string",
271 + "format": "uri",
272 + "description": "URI for your IdP to send you after logging out of IdP via MeshCentral.",
273 + "default": "https:[currentHost][currentPath]/login"
274 + },
275 + "id_token_signed_response_alg": { "type": "string", "default": "RS256" },
276 + "id_token_encrypted_response_alg": { "type": "string" },
277 + "id_token_encrypted_response_enc": { "type": "string" },
278 + "userinfo_signed_response_alg": { "type": "string" },
279 + "userinfo_encrypted_response_alg": { "type": "string" },
280 + "userinfo_encrypted_response_enc": { "type": "string" },
281 + "response_types": { "type": ["string", "array"], "default": ["code"] },
282 + "default_max_age": { "type": "number" },
283 + "require_auth_time": { "type": "boolean", "default": false },
284 + "request_object_signing_alg": { "type": "string" },
285 + "request_object_encryption_alg": { "type": "string" },
286 + "request_object_encryption_enc": { "type": "string" },
287 + "token_endpoint_auth_method": {
288 + "type": "string",
289 + "default": "client_secret_basic",
290 + "enum": [ "none", "client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt" ]
291 + },
292 + "introspection_endpoint_auth_method": {
293 + "type": "string",
294 + "default": "client_secret_basic",
295 + "enum": [ "none", "client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt" ]
296 + },
297 + "revocation_endpoint_auth_method": {
298 + "type": "string",
299 + "default": "client_secret_basic",
300 + "enum": [ "none", "client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt" ]
301 + },
302 + "token_endpoint_auth_signing_alg": { "type": "string" },
303 + "introspection_endpoint_auth_signing_alg": { "type": "string" },
304 + "revocation_endpoint_auth_signing_alg": { "type": "string" },
305 + "tls_client_certificate_bound_access_tokens": { "type": "boolean" }
306 + },
307 + "required": [ "client_id", "client_secret" ],
308 + "additionalProperties": false
309 +},
310 +```
311 +
312 +### "Custom" Options
313 +
314 +#### *Introduction*
315 +
316 +These are all the options that dont fit with the issuer or client, including the presets. The presets define more than just the issuer URL used in discovery, they also define API endpoints, and specific ways to assemble your data. You are able to manually override most of the effects of the preset, but not all. You are able to manually configure the *scope* of the authorization request though, as well as choose which claims to use if your IdP uses something other than the defaults.
317 +
318 +> NOTE: The scope must be a string, an array of strings, or a space separated list of scopes as a single string.
319 +
320 +#### *Common Config Chart*
321 +
322 +| Name | Description | Default | Example | Required |
323 +| -------- | ------------------------------------------------ | --------------------------------------------------------- | ----------------------------------- | -------- |
324 +| `scope` | A list of scopes to request from the issuer. | `"openid profile email"` | `["openid", "profile"]` | `false` |
325 +| `claims` | A group of claims to use instead of the defaults | Defauts to name of property except that `uuid` used `sub` | `"claims": {"uuid": "unique_name"}` | `false` |
326 +
327 +#### *Advanced Config Example*
328 +
329 +``` json
330 +"custom": {
331 + "scope": [ "openid", "profile", "read.EmailAlias", "read.UserProfile" ],
332 + "preset": null,
333 + "claims": {
334 + "name": "nameOfUser",
335 + "email": "publicEmail"
336 + }
337 +},
338 +```
339 +
340 +> NOTE: You can `preset` to null if you want to explicitly disable presets.
341 +
342 +#### *Required and Commonly Used Configs*
343 +
344 +As should be apparent by the name alone, the custom property does not need to be configured and is used for optional or advanced configurations. With that said, lets look at few common options strategy will default to using the `openid`, `profile`, and `email` scopes to gather the required information about the user, if your IdP doesn't support or require all these, you can set up the scope manually. Combine that with the ability to set the group scope and you can end up with an entirely custom scope being sent to your IdP. Not to mention the claims property, which allows you to pick and choose what claims to use to gather your data in case you have issues with any of the default behaviors of OpenID Connect and your IdP. This is also where you would set the preset and any values required by the presets.
345 +
346 +#### *Schema*
347 +``` json
348 +"custom": {
349 + "type": "object",
350 + "properties": {
351 + "scope": {
352 + "type": ["string", "array"],
353 + "description": "A list of scopes to request from the issuer.",
354 + "default": "openid profile email",
355 + "examples": ["openid", ["openid", "profile"], "openid profile email", "openid profile email groups"]
356 + },
357 + "claims": {
358 + "type": "object",
359 + "properties": {
360 + "email": { "type": "string" },
361 + "name": { "type": "string" },
362 + "uuid": { "type": "string" }
363 + }
364 + },
365 + "preset": { "type": "string", "enum": ["azure", "google"]},
366 + "tenant_id": { "type": "string", "description": "REQUIRED FOR AZURE PRESET: Tenantid for Azure"},
367 + "customer_id": { "type": "string", "description": "REQUIRED FOR GOOGLE PRESET IF USING GROUPS: Customer ID from Google, should start with 'C'."}
368 + },
369 + "additionalProperties": false
370 +},
371 +```
372 +
373 +### "Groups" Options
374 +
375 +#### *Introduction*
376 +
377 +The groups option allows you to use the groups you already have with your IdP in MeshCentral in a few ways. First you can set a group that the authorized user must be in to sign in to MeshCentral. You can also allow users with the right memberships automatic admin privlidges, and there is even an option to revoke privlidges if the user is NOT in the admin group. Besides these filters, you can filter the sync property to mirror only certain groups as MeshCentral User Groups, dynamically created as the user logs in. You can of course simply enable sync and mirror all groups from your IdP as User Groups. Additionally you can define the scope and claim of the groups for a custom setup, again allowing for a wide range of IdPs to be used, even without a preset.
378 +
379 +#### *Common Config Chart*
380 +
381 +| Name | Description | Default | Example | Required |
382 +| --- | --- | --- | --- | --- |
383 +| `sync` | Allows you to mirror user groups from your IdP. | `false` | `"sync": { "filter": ["Group1", "Group2"] }`<br/>`"sync": true` | `false` |
384 +| `required` | Access is only granted to users who are a member<br/>of at least one of the listed required groups. | `undefined` | `"required": ["Group1", "Group2"]` | `false` |
385 +| `siteadmin` | Full site admin priviledges will be granted to users<br/>who are a member of at least one of the listed admin groups | `undefined` | `"siteadmin": ["Group1", "Group2"]` | `false` |
386 +| `revokeAdmin` | If true, admin privileges will be revoked from users<br/>who arent a member of at least one of the listed admin groups. | `true` | `"revokeAdmin": false` | `false` |
387 +
388 +#### *Advanced Config Example*
389 +
390 +``` json
391 +"groups": {
392 + "recursive": true,
393 + "required": ["Group1", "Group2"],
394 + "siteadmin": ["GroupA", "GroupB"],
395 + "revokeAdmin": false,
396 + "sync": {
397 + "filter": ["Group1", "GroupB", "OtherGroup"]
398 + },
399 + "claim": "GroupClaim",
400 + "scope": "read.GroupMemberships"
401 +},
402 +```
403 +
404 +#### *Required and Commonly Used Configs*
405 +
406 +As you can see in the schema below, there aren't any required properties in the groups object, however there are some commonly used ones. The first, and maybe most commonly used one, is the sync property. The sync property mirrors IdP provided groups into MeshCentral as user groups. You can then configure access as required to those groups, and as users log in, they will be added to the now existing groups if they are a member. You also have other options like using a custom *scope* or *claim* to get your IdP communicating with MeshCentral properly, without the use of preset configs. You also can set the required property if you need to limit authorization to users that are a member of at least one of the groups you set. or the siteadmin property to grant admin privilege, with the revokeAdmin property available to allow revoking admin rights also.
407 +
408 +#### *Schema*
409 +
410 +``` json
411 +"groups": {
412 + "type": "object",
413 + "properties": {
414 + "recursive": {
415 + "type": "boolean",
416 + "default": false,
417 + "description": "When true, the group memberships will be scanned recursively."
418 + },
419 + "required": {
420 + "type": [ "string", "array" ],
421 + "description": "Access is only granted to users who are a member of at least one of the listed required groups."
422 + },
423 + "siteadmin": {
424 + "type": [ "string", "array" ],
425 + "description": "Full site admin priviledges will be granted to users who are a member of at least one of the listed admin groups."
426 + },
427 + "revokeAdmin": {
428 + "type": "boolean",
429 + "default": false,
430 + "description": "If true, admin privileges will be revoked from users who are NOT a member of at least one of the listed admin groups."
431 + },
432 + "sync": {
433 + "type": [ "boolean", "object" ],
434 + "default": false,
435 + "description": "If true, all groups found during user login are mirrored into MeshCentral user groups.",
436 + "properties": {
437 + "filter": {
438 + "type": [ "string", "array" ],
439 + "description": "Only groups listed here are mirrored into MeshCentral user groups."
440 + }
441 + }
442 + },
443 + "scope": { "type": "string", "default": "groups", "description": "Custom scope to use." },
444 + "claim": { "type": "string", "default": "groups", "description": "Custom claim to use." }
445 + },
446 + "additionalProperties": false
447 +}
448 +```
449 +
450 +## Preset OpenID Connect Configurations
451 +
452 +### Overview
453 +
454 +#### *Introduction*
455 +
456 +Google is a blah and is used by tons of blahs as its so great. Lets move on.
457 +
458 +#### *Common Config Chart*
459 +
460 +> NOTE: All settings directly related to presets are in the custom section of the config.
461 +
462 +| Name | Description | Example | Required |
463 +| --- | --- | --- | --- |
464 +| `preset` | Manually enable the use of a preset. | `"preset": "google"`<br/>`"preset": "azure"` | `false` |
465 +| `customer_id` | Customer ID of the Google Workspaces instace you<br/>plan to use with the groups feature.| `"customer_id": ["Group1", "Group2"]` | If `google` preset is used with `groups` feature |
466 +| `tenant_id` | Tenant ID from Azure AD, this is required to use<br/>the `azure` preset as it is part of the issuer url. | `"siteadmin": ["Group1", "Group2"]` | `false` |
467 +
468 +### Google Preset
469 +
470 +#### *Prerequisites*
471 +
472 +> Check out this [documentation](https://developers.google.com/identity/protocols/oauth2/openid-connect) to get ready before we start.
473 +
474 +#### *Basic Config Example*
475 +
476 +``` json
477 +"oidc": {
478 + "client": {
479 + "client_id": "268438852161-r8xa7qxwf3rr0shp1xnpgmm70bnag21p.apps.googleusercontent.com",
480 + "client_secret": "ETFWBX-gFEaxfPXs1tWmAOkuWDFTgoL3nwh"
481 + }
482 +}
483 +```
484 +
485 +#### *Specifics*
486 +
487 +If you notice above I forgot to add any preset related configs, however because google tags the client ID we can detect that and automatically use the google preset. The above config is tested, the sentive data has been scrambled of course. That said, you would normally use this preset in more advaced setups, let take a look at an example.
488 +
489 +#### *Advanced Example with Groups*
490 +
491 +``` json
492 +"oidc": {
493 + "client": {
494 + "client_id": "424555768625-k7ub3ovqs0yp7mfo0usvyyx51nfii61c.apps.googleusercontent.com",
495 + "client_secret": "QLBCQY-nRYmjnFWv3nKyHGmwQEGLokP6ldk"
496 + },
497 + "custom": {
498 + "preset": "google",
499 + "customer_id": "C46kyhmps"
500 + },
501 + "groups": {
502 + "siteadmin": ["GroupA", "GroupB"],
503 + "revokeAdmin": true,
504 + "sync": true
505 + },
506 + "callbackURL": "https://mesh.your.domain/auth-oidc-google-callback"
507 +},
508 +```
509 +
510 +#### *Customer ID and Groups*
511 +
512 +As always, the client ID and secret are required, the customer ID on the other hand is only required if you plan to take advantage of the groups function *and* the google preset. This also requires you have a customer ID, if you have do, it is available in the Google Workspace Admin Console under Profile->View. Groups work the same as they would with any other IdP but they are pulled from the Workspace groups.
513 +
514 +#### *Schema*
515 +
516 +```json
517 +"custom": {
518 + "type": "object",
519 + "properties": {
520 + "preset": { "type": "string", "enum": ["azure", "google"]},
521 + "customer_id": { "type": "string", "description": "Customer ID from Google, should start with 'C'."}
522 + },
523 + "additionalProperties": false
524 +},
525 +```
526 +
527 +### Azure Preset
528 +
529 +#### *Prerequisites*
530 +
531 +To configure OIDC-based SSO, you need an Azure account with an active subscription. [Create an account](https://azure.microsoft.com/free/?WT.mc_id=A261C142F) for free. The account used for setup must be of the following roles: Global Administrator, Cloud Application Administrator, Application Administrator, or owner the service principal.
532 +
533 +> Check this [documentation](https://learn.microsoft.com/en-us/azure/active-directory/manage-apps/add-application-portal-setup-oidc-sso) for more information.
534 +
535 +#### *Basic Config Example*
536 +
537 +``` json
538 +"oidc": {
539 + "client": {
540 + "client_id": "a1gkl04i-40g8-2h74-6v41-2jm2o2x0x27r",
541 + "client_secret": "AxT6U5K4QtcyS6gF48gndL7Ys22BL15BWJImuq1O"
542 + },
543 + "custom": {
544 + "preset": "azure",
545 + "tenant_id": "46a6022g-4h33-1451-h1rc-08102ga3b5e4"
546 + }
547 +}
548 +```
549 +
550 +#### *Specifics*
551 +
552 +As with all other types of configuration for the OIDC strategy, the Azure preset requires a client ID and secret.The tenant ID is used as part of the issuer URI to make even the most basic AuthN requests so it is also required for the azure preset. besides that groups are available to the Azure preset as well as the recursive feature of groups. This allows you to search user groups recursively for groups they have membership in through other groups.
553 +
554 +> NOTE: The Azure AD preset uses the Tenant ID as part of the issuer URI:<br>`"https://login.microsoftonline.com/"` + `strategy`.custom.tenant_id + `"/v2.0"`
555 +
556 +#### *Advanced Example with Groups*
557 +
558 +``` json
559 +"oidc": {
560 + "client": {
561 + "client_id": "a1gkl04i-40g8-2h74-6v41-2jm2o2x0x27r",
562 + "client_secret": "AxT6U5K4QtcyS6gF48gndL7Ys22BL15BWJImuq1O"
563 + },
564 + "custom": {
565 + "preset": "azure",
566 + "tenant_id": "46a6022g-4h33-1451-h1rc-08102ga3b5e4"
567 + },
568 + "groups": {
569 + "recursive": true,
570 + "siteadmin": ["GroupA", "GroupB"],
571 + "revokeAdmin": true,
572 + "sync": true
573 + },
574 + "callbackURL": "https://mesh.your.domain/auth-oidc-azure-callback"
575 +},
576 +```
577 +
578 +#### *Schema*
579 +
580 +```json
581 +"custom": {
582 + "type": "object",
583 + "properties": {
584 + "preset": { "type": "string", "enum": ["azure", "google"]},
585 + "tenant_id": { "type": "string", "description": "Tenant ID from Azure AD."}
586 + },
587 + "additionalProperties": false
588 +},
589 +```
590 +
591 +## Depreciated Properties
592 +
593 +### Overview
594 +
595 +#### Introduction
596 +
597 +As of MeshCentral `v1.1.22` and the writing of this documentation, the node module that handles everything was changed from [passport-openid-connect](https://github.com/jaredhanson/passport-openidconnect) to [openid-client](https://github.com/panva/node-openid-client). As a result of this change, multiple properties in the config have been depcrecated; this means some options in the strategy arent being used anymore. These are often referred to as "old configs" by this documentation.
598 +
599 +#### *Migrating Old Configs*
600 +
601 +We upgraded but what about all the existing users, we couldn't just invalidate every config pre `v1.1.22`. So in an effort to allow greater flexibility to all users of MeshCentral, and what futures scholars will all agree was an obvious move, all the depreciated configs will continue working as expected. Using any of the old options will just generate a warning in the authlog and will not stop you from using this the OIDC strategy with outdated configs, however if both the equivalent new and old config are set the new config will be used.
602 +
603 +#### *Old Config Example*
604 +```json
605 +"oidc": {
606 + "newAccounts": true,
607 + "clientid": "421326444155-i1tt4bsmk3jm7dri6jldekl86rfpg07r.apps.googleusercontent.com",
608 + "clientsecret": "GNLXOL-kEDjufOCk6pIcTHtaHFOCgbT4hoi"
609 +}
610 +```
611 +
612 +This example was chosen because I wanted to highlight an advantage of supporting these old configs long term, even in a depreciated status. That is, the ability to copy your existing config from one of the related strategies without making any changes to your config by using the presets. This allows you to test out the oidc strategy without commiting to anything, since the user is always appended with the strategy used to login. In this example, the config was originally a google auth strategy config, changing the `"google"` to `"oidc"` is all that was done to the above config, besides obsfuscation of course.
613 +
614 +#### *Advcanced Old Config Example*
615 +
616 +``` json
617 +"oidc": {
618 + "authorizationURL": "https://sso.your.domain/api/oidc/authorization",
619 + "callbackURL": "https://mesh.your.domain/oauth2/oidc/callback",
620 + "clientid": "tZiPTMDNuSaQPapAQJtwDXVnYjjhQybc",
621 + "clientsecret": "vrQWspJxdVAxEFJdrxvxeQwWkooVcqdU",
622 + "issuer": "https://sso.your.domain",
623 + "tokenURL": "https://sso.your.domain/api/oidc/token",
624 + "userInfoURL": "https://sso.your.domain/api/oidc/userinfo",
625 + "logoutURL": "https://sso.your.domain/logout?rd=https://mesh.your.domain/login",
626 + "groups": {
627 + "recursive": true,
628 + "required": ["Group1", "Group2"],
629 + "siteadmin": ["GroupA", "GroupB"],
630 + "sync": {
631 + "filter": ["Group1", "GroupB", "OtherGroup"]
632 + }
633 + },
634 + "newAccounts": true
635 +},
636 +```
637 +
638 +#### *Upgrading to v1.1.22*
639 +
640 +If you were already using a meticulusly configured oidc strategy, all of your configs will still be used. You will simply see a warning in the logs if any depreciated properties were used. If you check the authLog there are additional details about the old config and provide the new place to put that information. In this advanced config, even the groups will continue to work just as they did before without any user intervention when upgrading from a version of MeshCentral pre v1.1.22. There are no step to take and no action is needed, moving the configs to the new locations is completely optional at the moment.
641 +
642 +# Links
643 +
644 +https://cloud.google.com/identity/docs/reference/rest/v1/groups/list
645 +
646 +https://www.onelogin.com/learn/authentication-vs-authorization
647 +
648 +https://auth0.com/docs/authenticate/protocols/openid-connect-protocol
649 +
650 +https://github.com/panva/node-openid-client
651 +
652 +https://openid.net/connect/
653 +
654 +> You just read `openidConnectStrategy.ms v1.0.1` by [@mstrhakr](https://github.com/mstrhakr)
\ No newline at end of file
meshcentral-config-schema.json
+393 -36
@@ -1135,7 +1135,10 @@
1135 }
1136 },
1137 "allowedOrigin": {
1138 - "type": [ "array", "boolean" ],
1138 + "type": [
1139 + "array",
1140 + "boolean"
1141 + ],
1142 "default": false,
1143 "uniqueItems": true,
1144 "description": "A list of allowed hostnames for HTTP request origin header. If false, a default list is created, if true, all hostnames are allowed.",
@@ -2451,7 +2454,10 @@
2454 }
2455 }
2456 }
2454 - }
2457 + },
2458 + "required": [
2459 + "certs"
2460 + ]
2461 },
2462 "amtAcmActivation": {
2463 "type": "object",
@@ -3020,93 +3026,444 @@
3026 },
3027 "oidc": {
3028 "type": "object",
3029 + "description": "Enables the use of OpenID Connect SSO",
3030 + "anyOf": [
3031 + {
3032 + "required": [
3033 + "client"
3034 + ]
3035 + },
3036 + {
3037 + "required": [
3038 + "client",
3039 + "custom"
3040 + ]
3041 + },
3042 + {
3043 + "required": [
3044 + "client",
3045 + "issuer"
3046 + ]
3047 + },
3048 + {
3049 + "required": [
3050 + "clientid",
3051 + "clientsecret",
3052 + "issuer"
3053 + ]
3054 + }
3055 + ],
3056 + "additionalProperties": false,
3057 "properties": {
3024 - "authorizationURL": {
3025 - "type": "string",
3026 - "format": "uri",
3027 - "description": "If set, this will be used as the authorization URL. (If set tokenURL and userInfoURL need set also)"
3058 + "newAccounts": {
3059 + "type": "boolean",
3060 + "description": "Enable the creation of new accounts based upon Idp Authorization",
3061 + "default": true
3062 },
3029 - "callbackURL": {
3030 - "type": "string",
3031 - "format": "uri",
3032 - "description": "Required, this is the URL that your SSO provider sends auth approval to."
3063 + "newAccountsUserGroups": {
3064 + "type": [
3065 + "string",
3066 + "array"
3067 + ],
3068 + "description": "Add all new users to these static MeshCentral user groups. Use this if the new groups section does not work with your preset.",
3069 + "uniqueItems": true,
3070 + "items": {
3071 + "type": "string"
3072 + }
3073 + },
3074 + "newAccountsRights": {
3075 + "type": [
3076 + "array",
3077 + "string"
3078 + ],
3079 + "uniqueItems": true,
3080 + "items": {
3081 + "type": "string"
3082 + }
3083 },
3084 "clientid": {
3035 - "type": "string"
3085 + "type": "string",
3086 + "depreciated": true,
3087 + "description": "REPLACED WITH 'client.client_id'"
3088 },
3089 "clientsecret": {
3038 - "type": "string"
3090 + "type": "string",
3091 + "description": "REPLACED WITH 'client.client_secret'"
3092 },
3040 - "issuer": {
3093 + "authorizationURL": {
3094 "type": "string",
3095 "format": "uri",
3043 - "description": "Full URL of SSO portal"
3096 + "depreciated": true,
3097 + "description": "REPLACED WITH 'issuer.authorization_endpoint'"
3098 },
3099 "tokenURL": {
3100 "type": "string",
3101 "format": "uri",
3048 - "description": "If set, this will be used as the token URL. (If set authorizationURL and userInfoURL need set also)"
3102 + "depreciated": true,
3103 + "description": "REPLACED WITH 'issuer.token_endpoint': If set, this will be used as the token URL."
3104 },
3105 "userInfoURL": {
3106 "type": "string",
3107 "format": "uri",
3053 - "description": "If set, this will be used as the user info URL. (If set authorizationURL and tokenURL need set also)"
3108 + "depreciated": true,
3109 + "description": "REPLACED WITH 'issuer.userinfo_endpoint': If set, this will be used as the user info URL."
3110 + },
3111 + "scope": {
3112 + "type": [
3113 + "string",
3114 + "array"
3115 + ],
3116 + "depreciated": true,
3117 + "description": "REPLACED WITH 'custom.scope': A list of scopes to request from the issuer."
3118 + },
3119 + "callbackURL": {
3120 + "type": "string",
3121 + "format": "uri",
3122 + "depreciated": true,
3123 + "description": "REPLACED WITH 'client.redirect_uri': The URI your IdP sends you back to after successful authorization. This must match what is listed with your IdP."
3124 },
3125 "logouturl": {
3126 "type": "string",
3127 "format": "uri",
3058 - "description": "Then set, the user will be redirected to this URL when hitting the logout link."
3128 + "description": "Overrides defaults ( [issuer.end_session_endpoint]?post_logout_redirect_uri=[post_logout_redirect_uri] OR [issuer.end_session_endpoint] )"
3129 },
3060 - "newAccounts": {
3061 - "type": "boolean",
3062 - "default": true
3130 + "client": {
3131 + "type": "object",
3132 + "description": "OIDC Client Options",
3133 + "properties": {
3134 + "client_id": {
3135 + "type": "string",
3136 + "description": "REQUIRED: The client ID provided by your Identity Provider (IdP)"
3137 + },
3138 + "client_secret": {
3139 + "type": "string",
3140 + "description": "REQUIRED: The client secret provided by your Identity Provider (IdP)"
3141 + },
3142 + "id_token_signed_response_alg": {
3143 + "type": "string",
3144 + "default": "RS256",
3145 + "description": "ADVANCED CONFIG: Check node-openid-client on GitHub for details"
3146 + },
3147 + "id_token_encrypted_response_alg": {
3148 + "type": "string",
3149 + "description": "ADVANCED CONFIG: Check node-openid-client on GitHub for details"
3150 + },
3151 + "id_token_encrypted_response_enc": {
3152 + "type": "string",
3153 + "description": "ADVANCED CONFIG: Check node-openid-client on GitHub for details"
3154 + },
3155 + "userinfo_signed_response_alg": {
3156 + "type": "string",
3157 + "description": "ADVANCED CONFIG: Check node-openid-client on GitHub for details"
3158 + },
3159 + "userinfo_encrypted_response_alg": {
3160 + "type": "string",
3161 + "description": "ADVANCED CONFIG: Check node-openid-client on GitHub for details"
3162 + },
3163 + "userinfo_encrypted_response_enc": {
3164 + "type": "string",
3165 + "description": "ADVANCED CONFIG: Check node-openid-client on GitHub for details"
3166 + },
3167 + "redirect_uri": {
3168 + "type": "string",
3169 + "format": "uri",
3170 + "description": "URI your IdP sends you after successful authorization. This must match what is listed with your IdP. (Default is https://[currentHost][currentPath]/auth-oidc-callback)"
3171 + },
3172 + "response_types": {
3173 + "type": [
3174 + "string",
3175 + "array"
3176 + ],
3177 + "description": "ADVANCED CONFIG: Check node-openid-client on GitHub for details",
3178 + "default": [
3179 + "code"
3180 + ]
3181 + },
3182 + "post_logout_redirect_uri": {
3183 + "type": "string",
3184 + "format": "uri",
3185 + "description": "URI for your IdP to send you after logging out of IdP via MeshCentral. (Default is https:[currentHost][currentPath]/login)"
3186 + },
3187 + "default_max_age": {
3188 + "type": "number",
3189 + "description": "ADVANCED CONFIG: Check node-openid-client on GitHub for details"
3190 + },
3191 + "require_auth_time": {
3192 + "type": "boolean",
3193 + "default": false,
3194 + "description": "ADVANCED CONFIG: Check node-openid-client on GitHub for details"
3195 + },
3196 + "request_object_signing_alg": {
3197 + "type": "string",
3198 + "description": "ADVANCED CONFIG: Check node-openid-client on GitHub for details"
3199 + },
3200 + "request_object_encryption_alg": {
3201 + "type": "string",
3202 + "description": "ADVANCED CONFIG: Check node-openid-client on GitHub for details"
3203 + },
3204 + "request_object_encryption_enc": {
3205 + "type": "string",
3206 + "description": "ADVANCED CONFIG: Check node-openid-client on GitHub for details"
3207 + },
3208 + "token_endpoint_auth_method": {
3209 + "type": "string",
3210 + "default": "client_secret_basic",
3211 + "enum": [
3212 + "none",
3213 + "client_secret_basic",
3214 + "client_secret_post",
3215 + "client_secret_jwt",
3216 + "private_key_jwt"
3217 + ],
3218 + "description": "ADVANCED CONFIG: Check node-openid-client on GitHub for details"
3219 + },
3220 + "introspection_endpoint_auth_method": {
3221 + "type": "string",
3222 + "default": "client_secret_basic",
3223 + "enum": [
3224 + "none",
3225 + "client_secret_basic",
3226 + "client_secret_post",
3227 + "client_secret_jwt",
3228 + "private_key_jwt"
3229 + ],
3230 + "description": "ADVANCED CONFIG: Check node-openid-client on GitHub for details"
3231 + },
3232 + "revocation_endpoint_auth_method": {
3233 + "type": "string",
3234 + "default": "client_secret_basic",
3235 + "enum": [
3236 + "none",
3237 + "client_secret_basic",
3238 + "client_secret_post",
3239 + "client_secret_jwt",
3240 + "private_key_jwt"
3241 + ],
3242 + "description": "ADVANCED CONFIG: Check node-openid-client on GitHub for details"
3243 + },
3244 + "token_endpoint_auth_signing_alg": {
3245 + "type": "string",
3246 + "description": "ADVANCED CONFIG: Check node-openid-client on GitHub for details"
3247 + },
3248 + "introspection_endpoint_auth_signing_alg": {
3249 + "type": "string",
3250 + "description": "ADVANCED CONFIG: Check node-openid-client on GitHub for details"
3251 + },
3252 + "revocation_endpoint_auth_signing_alg": {
3253 + "type": "string",
3254 + "description": "ADVANCED CONFIG: Check node-openid-client on GitHub for details"
3255 + },
3256 + "tls_client_certificate_bound_access_tokens": {
3257 + "type": "boolean",
3258 + "description": "ADVANCED CONFIG: Check node-openid-client on GitHub for details"
3259 + }
3260 + },
3261 + "required": [
3262 + "client_id",
3263 + "client_secret"
3264 + ],
3265 + "additionalProperties": false
3266 + },
3267 + "issuer": {
3268 + "type": [
3269 + "string",
3270 + "object"
3271 + ],
3272 + "format": "uri",
3273 + "description": "Issuer options. Requires issuer URI (issuer.issuer) to discover missing information unless using preset",
3274 + "properties": {
3275 + "issuer": {
3276 + "type": "string",
3277 + "format": "uri",
3278 + "description": "URI of the issuer."
3279 + },
3280 + "authorization_endpoint": {
3281 + "type": "string",
3282 + "format": "uri"
3283 + },
3284 + "token_endpoint": {
3285 + "type": "string",
3286 + "format": "uri"
3287 + },
3288 + "jwks_uri": {
3289 + "type": "string",
3290 + "format": "uri"
3291 + },
3292 + "userinfo_endpoint": {
3293 + "type": "string",
3294 + "format": "uri"
3295 + },
3296 + "revocation_endpoint": {
3297 + "type": "string",
3298 + "format": "uri"
3299 + },
3300 + "introspection_endpoint": {
3301 + "type": "string",
3302 + "format": "uri"
3303 + },
3304 + "end_session_endpoint": {
3305 + "type": "string",
3306 + "format": "uri",
3307 + "description": "URI to direct users to when logging out of MeshCentral. (Attempts to autodetect, defaults to '[issuer.issuer]/logout')"
3308 + },
3309 + "registration_endpoint": {
3310 + "type": "string",
3311 + "format": "uri"
3312 + },
3313 + "token_endpoint_auth_methods_supported": {
3314 + "type": "string"
3315 + },
3316 + "token_endpoint_auth_signing_alg_values_supported": {
3317 + "type": "string"
3318 + },
3319 + "introspection_endpoint_auth_methods_supported": {
3320 + "type": "string"
3321 + },
3322 + "introspection_endpoint_auth_signing_alg_values_supported": {
3323 + "type": "string"
3324 + },
3325 + "revocation_endpoint_auth_methods_supported": {
3326 + "type": "string"
3327 + },
3328 + "revocation_endpoint_auth_signing_alg_values_supported": {
3329 + "type": "string"
3330 + },
3331 + "request_object_signing_alg_values_supported": {
3332 + "type": "string"
3333 + },
3334 + "mtls_endpoint_aliases": {
3335 + "type": "object",
3336 + "properties": {
3337 + "token_endpoint": {
3338 + "type": "string",
3339 + "format": "uri"
3340 + },
3341 + "userinfo_endpoint": {
3342 + "type": "string",
3343 + "format": "uri"
3344 + },
3345 + "revocation_endpoint": {
3346 + "type": "string",
3347 + "format": "uri"
3348 + },
3349 + "introspection_endpoint": {
3350 + "type": "string",
3351 + "format": "uri"
3352 + }
3353 + }
3354 + }
3355 + },
3356 + "additionalProperties": false
3357 + },
3358 + "custom": {
3359 + "type": "object",
3360 + "properties": {
3361 + "scope": {
3362 + "type": [
3363 + "string",
3364 + "array"
3365 + ],
3366 + "description": "A list of scopes to request from the issuer.",
3367 + "default": "openid profile email",
3368 + "examples": [
3369 + "openid",
3370 + [
3371 + "openid",
3372 + "profile"
3373 + ],
3374 + "openid profile email",
3375 + "openid profile email groups"
3376 + ]
3377 + },
3378 + "claims": {
3379 + "type": "object",
3380 + "properties": {
3381 + "email": {
3382 + "type": "string"
3383 + },
3384 + "name": {
3385 + "type": "string"
3386 + },
3387 + "uuid": {
3388 + "type": "string"
3389 + }
3390 + }
3391 + },
3392 + "preset": {
3393 + "type": "string",
3394 + "enum": [
3395 + "azure",
3396 + "google"
3397 + ]
3398 + },
3399 + "tenant_id": {
3400 + "type": "string",
3401 + "description": "REQUIRED FOR AZURE PRESET: Tenantid for Azure"
3402 + },
3403 + "customer_id": {
3404 + "type": "string",
3405 + "description": "REQUIRED IF USING GROUPS: Customer ID from Google Workspace Admin Console (https://admin.google.com/ac/accountsettings/profile)"
3406 + }
3407 + },
3408 + "additionalProperties": false
3409 },
3410 "groups": {
3411 "type": "object",
3412 "properties": {
3413 + "recursive": {
3414 + "type": "boolean",
3415 + "default": false,
3416 + "description": "When true, the group memberships will be scanned recursively."
3417 + },
3418 "required": {
3419 "type": [
3420 "string",
3421 "array"
3422 ],
3072 - "description": "When set, the user must be part of one of the OIDC user groups to login to MeshCentral."
3423 + "description": "Access is only granted to users who are a member of at least one of the listed required groups."
3424 },
3425 "siteadmin": {
3426 "type": [
3427 "string",
3428 "array"
3429 ],
3079 - "description": "When set, users part of these groups will be promoted with site administrator in MeshCentral, users that are not part of these groups will be demoted."
3430 + "description": "Full site admin priviledges will be granted to users who are a member of at least one of the listed admin groups."
3431 + },
3432 + "revokeAdmin": {
3433 + "type": "boolean",
3434 + "description": "If true, admin privileges will be revoked from users who are NOT a member of at least one of the listed admin groups."
3435 },
3436 "sync": {
3437 "type": [
3438 "boolean",
3439 "object"
3440 ],
3086 - "description": "Allows some or all ODIC user groups to be mirrored within MeshCentral as user groups.",
3441 + "default": false,
3442 + "description": "If true, all groups found during user login are mirrored into MeshCentral user groups.",
3443 "properties": {
3088 - "enabled": {
3089 - "type": "boolean",
3090 - "default": false
3091 - },
3444 "filter": {
3445 "type": [
3446 "string",
3447 "array"
3448 ],
3097 - "description": "When set, limits what OIDC groups are mirrored into MeshCentral user groups."
3449 + "description": "Only groups listed here are mirrored into MeshCentral user groups."
3450 }
3451 }
3452 + },
3453 + "scope": {
3454 + "type": "string",
3455 + "default": "groups",
3456 + "description": "Custom scope to use."
3457 + },
3458 + "claim": {
3459 + "type": "string",
3460 + "default": "groups",
3461 + "description": "Custom claim to use."
3462 }
3101 - }
3463 + },
3464 + "additionalProperties": false
3465 }
3103 - },
3104 - "required": [
3105 - "issuer",
3106 - "clientid",
3107 - "clientsecret",
3108 - "callbackURL"
3109 - ]
3466 + }
3467 }
3468 }
3469 },
meshcentral.js
+14 -6
@@ -3758,9 +3758,9 @@ function CreateMeshCentralServer(config, args) {
3758 if (obj.authlogfile != null) { // Write authlog to file
3759 try {
3760 const d = new Date(), month = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][d.getMonth()];
3761 - msg = month + ' ' + d.getDate() + ' ' + obj.common.zeroPad(d.getHours(), 2) + ':' + obj.common.zeroPad(d.getMinutes(), 2) + ':' + d.getSeconds() + ' meshcentral ' + server + '[' + process.pid + ']: ' + msg + ((obj.platform == 'win32') ? '\r\n' : '\n');
3762 - obj.fs.write(obj.authlogfile, msg, function (err, written, string) { });
3763 - } catch (ex) { console.log(ex); }
3761 + str = month + ' ' + d.getDate() + ' ' + obj.common.zeroPad(d.getHours(), 2) + ':' + obj.common.zeroPad(d.getMinutes(), 2) + ':' + d.getSeconds() + ' meshcentral ' + server + '[' + process.pid + ']: ' + msg + ((obj.platform == 'win32') ? '\r\n' : '\n');
3762 + obj.fs.write(obj.authlogfile, str, function (err, written, string) { if (err) {console.error(err); } });
3763 + } catch (ex) { console.error(ex); }
3764 }
3765 }
3766
@@ -4001,14 +4001,22 @@ function mainStart() {
4001 if (mstsc == false) { config.domains[i].mstsc = false; }
4002 if (config.domains[i].ssh == true) { ssh = true; }
4003 if ((typeof config.domains[i].authstrategies == 'object')) {
4004 - if (passport == null) { passport = ['passport']; } // Passport v0.6.0 requires a patch, see https://github.com/jaredhanson/passport/issues/904
4004 + if (passport == null) { passport = ['passport@0.5.3']; } // Passport v0.6.0 is broken with cookie-session, see https://github.com/jaredhanson/passport/issues/904
4005 if ((typeof config.domains[i].authstrategies.twitter == 'object') && (typeof config.domains[i].authstrategies.twitter.clientid == 'string') && (typeof config.domains[i].authstrategies.twitter.clientsecret == 'string') && (passport.indexOf('passport-twitter') == -1)) { passport.push('passport-twitter'); }
4006 if ((typeof config.domains[i].authstrategies.google == 'object') && (typeof config.domains[i].authstrategies.google.clientid == 'string') && (typeof config.domains[i].authstrategies.google.clientsecret == 'string') && (passport.indexOf('passport-google-oauth20') == -1)) { passport.push('passport-google-oauth20'); }
4007 if ((typeof config.domains[i].authstrategies.github == 'object') && (typeof config.domains[i].authstrategies.github.clientid == 'string') && (typeof config.domains[i].authstrategies.github.clientsecret == 'string') && (passport.indexOf('passport-github2') == -1)) { passport.push('passport-github2'); }
4008 if ((typeof config.domains[i].authstrategies.reddit == 'object') && (typeof config.domains[i].authstrategies.reddit.clientid == 'string') && (typeof config.domains[i].authstrategies.reddit.clientsecret == 'string') && (passport.indexOf('passport-reddit') == -1)) { passport.push('passport-reddit'); }
4009 if ((typeof config.domains[i].authstrategies.azure == 'object') && (typeof config.domains[i].authstrategies.azure.clientid == 'string') && (typeof config.domains[i].authstrategies.azure.clientsecret == 'string') && (typeof config.domains[i].authstrategies.azure.tenantid == 'string') && (passport.indexOf('passport-azure-oauth2') == -1)) { passport.push('passport-azure-oauth2'); passport.push('jwt-simple'); }
4010 - if ((typeof config.domains[i].authstrategies.oidc == 'object') && (typeof config.domains[i].authstrategies.oidc.clientid == 'string') && (typeof config.domains[i].authstrategies.oidc.clientsecret == 'string') && (typeof config.domains[i].authstrategies.oidc.issuer == 'string') && (passport.indexOf('@mstrhakr/passport-openidconnect') == -1)) {
4011 - if ((nodeVersion >= 17) || ((Math.floor(nodeVersion) == 16) && (nodeVersion >= 16.13)) || ((Math.floor(nodeVersion) == 14) && (nodeVersion >= 14.15)) || ((Math.floor(nodeVersion) == 12) && (nodeVersion >= 12.19))) { passport.push('@mstrhakr/passport-openidconnect'); passport.push('openid-client'); passport.push('connect-flash'); } else { addServerWarning('This NodeJS version does not support OpenID.', 25); delete config.domains[i].authstrategies.oidc; }
4010 + if ((typeof config.domains[i].authstrategies.oidc == 'object') && (passport.indexOf('openid-client') == -1)) {
4011 + if ((nodeVersion >= 17)
4012 + || ((Math.floor(nodeVersion) == 16) && (nodeVersion >= 16.13))
4013 + || ((Math.floor(nodeVersion) == 14) && (nodeVersion >= 14.15))
4014 + || ((Math.floor(nodeVersion) == 12) && (nodeVersion >= 12.19))) {
4015 + passport.push('openid-client');
4016 + } else {
4017 + addServerWarning('This NodeJS version does not support OpenID Connect on MeshCentral.', 25);
4018 + delete config.domains[i].authstrategies.oidc;
4019 + }
4020 }
4021 if ((typeof config.domains[i].authstrategies.saml == 'object') || (typeof config.domains[i].authstrategies.jumpcloud == 'object')) { passport.push('passport-saml'); }
4022 }
sample-config-advanced.json
+10 -10
@@ -519,15 +519,14 @@
519 "cert": "saml.pem"
520 },
521 "oidc": {
522 - "authorizationURL": "https://sso.server.com/api/oidc/authorization",
523 - "callbackURL": "https://mesh.server.com/oidc-callback",
524 - "clientid": "00000000-0000-0000-0000-000000000000",
525 - "clientsecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
526 - "issuer": "https://sso.server.com",
527 - "tokenURL": "https://sso.server.com/api/oidc/token",
528 - "userInfoURL": "https://sso.server.com/api/oidc/userinfo",
529 - "logoutURL": "https://sso.server.com/logout",
530 - "newAccounts": true,
522 + "issuer": {
523 + "issuer": "https://sso.server.com",
524 + "end_session_endpoint": "https://sso.server.com/logout"
525 + },
526 + "client": {
527 + "client_id": "00000000-0000-0000-0000-000000000000",
528 + "client_secret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
529 + },
530 "groups": {
531 "required": [ "groupA", "groupB", "groupC" ],
532 "siteadmin": [ "groupA" ],
@@ -535,7 +534,8 @@
534 "enable": true,
535 "filter": [ "groupB", "groupC" ]
536 }
538 - }
537 + },
538 + "newAccounts": true
539 }
540 }
541 },
translate/translate.json
+1 -1
@@ -88493,4 +88493,4 @@
88493 ]
88494 }
88495 ]
88496 -}
\ No newline at end of file
88496 +}
views/login-mobile.handlebars
+5 -1
@@ -90,7 +90,9 @@
90 <a id="auth-github" href="auth-github" style="display:none"><img src="images/login/github32.png" loading="lazy" width="32" height="32" style="margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer" title="Sign-in using GitHub" /></a>
91 <a id="auth-reddit" href="auth-reddit" style="display:none"><img src="images/login/reddit32.png" loading="lazy" width="32" height="32" style="margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer" title="Sign-in using Reddit" /></a>
92 <a id="auth-azure" href="auth-azure" style="display:none"><img src="images/login/azure32.png" loading="lazy" width="32" height="32" style="margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer" title="Sign-in using Azure" /></a>
93 - <a id="auth-oidc" href="auth-oidc" style="display:none"><img src="images/login/oidc32.png" loading="lazy" width="32" height="32" style="margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer" title="Sign-in using OpenID Connect" /></a>
93 + <a id="auth-oidc" href="auth-oidc" style="display:none"><img src="images/login/oidc32.png" srcset="images/login/oidc64.png 2x" loading="lazy" width="32" height="32" style="margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer" title="Sign-in using OpenID Connect" /></a>
94 + <a id="auth-oidc-azure" href="auth-oidc" style="display:none"><img src="images/login/azure32.png" srcset="images/login/azure64.png 2x" loading="lazy" width="32" height="32" style="margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer" title="Sign-in with Azure using OpenID Connect" /></a>
95 + <a id="auth-oidc-google" href="auth-oidc" style="display:none"><img src="images/login/google32.png" srcset="images/login/google64.png 2x" loading="lazy" width="32" height="32" style="margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer" title="Sign-in with Google using OpenID Connect" /></a>
96 <a id="auth-jumpcloud" href="auth-jumpcloud" style="display:none"><img src="images/login/jumpcloud32.png" loading="lazy" width="32" height="32" style="margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer" title="Sign-in using JumpCloud" /></a>
97 <a id="auth-intel" href="auth-intel" style="display:none"><img src="images/login/intel32.png" loading="lazy" width="32" height="32" style="margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer" title="Sign-in using Intel" /></a>
98 <a id="auth-saml" href="auth-saml" style="display:none"><img src="images/login/generic32.png" loading="lazy" width="32" height="32" style="margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer" title="Single Sign-in" /></a>
@@ -400,6 +402,8 @@
402 if (authStrategies.indexOf('reddit') >= 0) { QV('auth-reddit', true); }
403 if (authStrategies.indexOf('azure') >= 0) { QV('auth-azure', true); }
404 if (authStrategies.indexOf('oidc') >= 0) { QV('auth-oidc', true); }
405 + if (authStrategies.indexOf('oidc-azure') >= 0) { QV('auth-oidc-azure', true); }
406 + if (authStrategies.indexOf('oidc-google') >= 0) { QV('auth-oidc-google', true); }
407 if (authStrategies.indexOf('jumpcloud') >= 0) { QV('auth-jumpcloud', true); }
408 if (authStrategies.indexOf('intel') >= 0) { QV('auth-intel', true); }
409 if (authStrategies.indexOf('saml') >= 0) { QV('auth-saml', true); }
views/login.handlebars
+4
@@ -84,6 +84,8 @@
84 <a id="auth-reddit" href="auth-reddit" style="display:none"><img src="images/login/reddit32.png" srcset="images/login/reddit64.png 2x" loading="lazy" width="32" height="32" style="margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer" title="Sign-in using Reddit" /></a>
85 <a id="auth-azure" href="auth-azure" style="display:none"><img src="images/login/azure32.png" srcset="images/login/azure64.png 2x" loading="lazy" width="32" height="32" style="margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer" title="Sign-in using Azure" /></a>
86 <a id="auth-oidc" href="auth-oidc" style="display:none"><img src="images/login/oidc32.png" srcset="images/login/oidc64.png 2x" loading="lazy" width="32" height="32" style="margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer" title="Sign-in using OpenID Connect" /></a>
87 + <a id="auth-oidc-azure" href="auth-oidc" style="display:none"><img src="images/login/azure32.png" srcset="images/login/azure64.png 2x" loading="lazy" width="32" height="32" style="margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer" title="Sign-in with Azure using OpenID Connect" /></a>
88 + <a id="auth-oidc-google" href="auth-oidc" style="display:none"><img src="images/login/google32.png" srcset="images/login/google64.png 2x" loading="lazy" width="32" height="32" style="margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer" title="Sign-in with Google using OpenID Connect" /></a>
89 <a id="auth-jumpcloud" href="auth-jumpcloud" style="display:none"><img src="images/login/jumpcloud32.png" srcset="images/login/jumpcloud64.png 2x" loading="lazy" width="32" height="32" style="margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer" title="Sign-in using JumpCloud" /></a>
90 <a id="auth-intel" href="auth-intel" style="display:none"><img src="images/login/intel32.png" srcset="images/login/intel64.png 2x" loading="lazy" width="32" height="32" style="margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer" title="Sign-in using Intel" /></a>
91 <a id="auth-saml" href="auth-saml" style="display:none"><img src="images/login/generic32.png" srcset="images/login/generic64.png 2x" loading="lazy" width="32" height="32" style="margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer" title="Single Sign-in" /></a>
@@ -425,6 +427,8 @@
427 if (authStrategies.indexOf('reddit') >= 0) { QV('auth-reddit', true); }
428 if (authStrategies.indexOf('azure') >= 0) { QV('auth-azure', true); }
429 if (authStrategies.indexOf('oidc') >= 0) { QV('auth-oidc', true); }
430 + if (authStrategies.indexOf('oidc-azure') >= 0) { QV('auth-oidc-azure', true); }
431 + if (authStrategies.indexOf('oidc-google') >= 0) { QV('auth-oidc-google', true); }
432 if (authStrategies.indexOf('jumpcloud') >= 0) { QV('auth-jumpcloud', true); }
433 if (authStrategies.indexOf('intel') >= 0) { QV('auth-intel', true); }
434 if (authStrategies.indexOf('saml') >= 0) { QV('auth-saml', true); }
views/login2.handlebars
+4
@@ -107,6 +107,8 @@
107 <a id="auth-reddit" href="auth-reddit" style="display:none"><img src="images/login/reddit32.png" srcset="images/login/reddit64.png 2x" loading="lazy" width="32" height="32" style="margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer" title="Sign-in using Reddit" /></a>
108 <a id="auth-azure" href="auth-azure" style="display:none"><img src="images/login/azure32.png" srcset="images/login/azure64.png 2x" loading="lazy" width="32" height="32" style="margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer" title="Sign-in using Azure" /></a>
109 <a id="auth-oidc" href="auth-oidc" style="display:none"><img src="images/login/oidc32.png" srcset="images/login/oidc64.png 2x" loading="lazy" width="32" height="32" style="margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer" title="Sign-in using OpenID Connect" /></a>
110 + <a id="auth-oidc-azure" href="auth-oidc" style="display:none"><img src="images/login/azure32.png" srcset="images/login/azure64.png 2x" loading="lazy" width="32" height="32" style="margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer" title="Sign-in with Azure using OpenID Connect" /></a>
111 + <a id="auth-oidc-google" href="auth-oidc" style="display:none"><img src="images/login/google32.png" srcset="images/login/google64.png 2x" loading="lazy" width="32" height="32" style="margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer" title="Sign-in with Google using OpenID Connect" /></a>
112 <a id="auth-jumpcloud" href="auth-jumpcloud" style="display:none"><img src="images/login/jumpcloud32.png" srcset="images/login/jumpcloud64.png 2x" loading="lazy" width="32" height="32" style="margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer" title="Sign-in using JumpCloud" /></a>
113 <a id="auth-intel" href="auth-intel" style="display:none"><img src="images/login/intel32.png" srcset="images/login/intel64.png 2x" loading="lazy" width="32" height="32" style="margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer" title="Sign-in using Intel" /></a>
114 <a id="auth-saml" href="auth-saml" style="display:none"><img src="images/login/generic32.png" srcset="images/login/generic64.png 2x" loading="lazy" width="32" height="32" style="margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer" title="Single Sign-in" /></a>
@@ -509,6 +511,8 @@
511 if (authStrategies.indexOf('reddit') >= 0) { QV('auth-reddit', true); }
512 if (authStrategies.indexOf('azure') >= 0) { QV('auth-azure', true); }
513 if (authStrategies.indexOf('oidc') >= 0) { QV('auth-oidc', true); }
514 + if (authStrategies.indexOf('oidc-azure') >= 0) { QV('auth-oidc-azure', true); }
515 + if (authStrategies.indexOf('oidc-google') >= 0) { QV('auth-oidc-google', true); }
516 if (authStrategies.indexOf('jumpcloud') >= 0) { QV('auth-jumpcloud', true); }
517 if (authStrategies.indexOf('intel') >= 0) { QV('auth-intel', true); }
518 if (authStrategies.indexOf('saml') >= 0) { QV('auth-saml', true); }
webserver.js
+1063 -791
@@ -457,7 +457,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
457
458 // Save this LDAP user to file if needed
459 if (typeof domain.ldapsaveusertofile == 'string') {
460 - obj.fs.appendFile(domain.ldapsaveusertofile, JSON.stringify(xxuser, null, 2) + '\r\n\r\n', function (err) { });
460 + obj.fs.appendFile(domain.ldapsaveusertofile, JSON.stringify(xxuser) + '\r\n\r\n', function (err) { });
461 }
462
463 // Work on getting the userid for this LDAP user
@@ -489,17 +489,17 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
489 if (Array.isArray(userMemberships) == false) { userMemberships = []; }
490
491 // See if the user is required to be part of an LDAP user group in order to log into this server.
492 - if (typeof domain.ldapuserrequiredgroupmembership == 'string') { domain.ldapuserrequiredgroupmembership = [ domain.ldapuserrequiredgroupmembership ]; }
492 + if (typeof domain.ldapuserrequiredgroupmembership == 'string') { domain.ldapuserrequiredgroupmembership = [domain.ldapuserrequiredgroupmembership]; }
493 if (Array.isArray(domain.ldapuserrequiredgroupmembership)) {
494 // Look for a matching LDAP user group
495 var userMembershipMatch = false;
496 for (var i in domain.ldapuserrequiredgroupmembership) { if (userMemberships.indexOf(domain.ldapuserrequiredgroupmembership[i]) >= 0) { userMembershipMatch = true; } }
497 - if (userMembershipMatch === false) { parent.debug('authlog', 'LDAP denying login to a user that is not a member of a LDAP required group.'); fn('denied'); return; } // If there is no match, deny the login
497 + if (userMembershipMatch === false) { parent.authLog('ldapHandler', 'LDAP denying login to a user that is not a member of a LDAP required group.'); fn('denied'); return; } // If there is no match, deny the login
498 }
499
500 // Check if user is in an site administrator group
501 var siteAdminGroup = null;
502 - if (typeof domain.ldapsiteadmingroups == 'string') { domain.ldapsiteadmingroups = [ domain.ldapsiteadmingroups ]; }
502 + if (typeof domain.ldapsiteadmingroups == 'string') { domain.ldapsiteadmingroups = [domain.ldapsiteadmingroups]; }
503 if (Array.isArray(domain.ldapsiteadmingroups)) {
504 siteAdminGroup = false;
505 for (var i in domain.ldapsiteadmingroups) {
@@ -559,7 +559,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
559 }
560
561 // Display user information extracted from LDAP data
562 - parent.debug('authlog', 'LDAP user login, id: ' + shortname + ', username: ' + username + ', email: ' + email + ', realname: ' + realname + ', phone: ' + phonenumber + ', image: ' + (userimage != null));
562 + parent.authLog('ldapHandler', 'LDAP user login, id: ' + shortname + ', username: ' + username + ', email: ' + email + ', realname: ' + realname + ', phone: ' + phonenumber + ', image: ' + (userimage != null));
563
564 // If there is a testing userid, use that
565 if (ldapHandlerFunc.ldapShortName) {
@@ -619,7 +619,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
619
620 // See if the user is a member of the site admin group.
621 if (typeof siteAdminGroup === 'string') {
622 - parent.debug('authlog', `LDAP: Granting site admin privilages to new user "${user.name}" found in admin group: ${siteAdminGroup}`);
622 + parent.authLog('ldapHandler', `LDAP: Granting site admin privilages to new user "${user.name}" found in admin group: ${siteAdminGroup}`);
623 user.siteadmin = 0xFFFFFFFF;
624 }
625
@@ -662,11 +662,11 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
662
663 // See if the user is a member of the site admin group.
664 if ((typeof siteAdminGroup === 'string') && (user.siteadmin !== 0xFFFFFFFF)) {
665 - parent.debug('authlog', `LDAP: Granting site admin privilages to user "${user.name}" found in administrator group: ${siteAdminGroup}`);
665 + parent.authLog('ldapHandler', `LDAP: Granting site admin privilages to user "${user.name}" found in administrator group: ${siteAdminGroup}`);
666 user.siteadmin = 0xFFFFFFFF;
667 userChanged = true;
668 } else if ((siteAdminGroup === false) && (user.siteadmin === 0xFFFFFFFF)) {
669 - parent.debug('authlog', `LDAP: Revoking site admin privilages from user "${user.name}" since they are not found in any administrator groups.`);
669 + parent.authLog('ldapHandler', `LDAP: Revoking site admin privilages from user "${user.name}" since they are not found in any administrator groups.`);
670 delete user.siteadmin;
671 userChanged = true;
672 }
@@ -836,17 +836,26 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
836 parent.debug('web', 'handleLogoutRequest: success.');
837
838 // If this user was logged in using an authentication strategy and there is a logout URL, use it.
839 - if ((userid != null) && (domain.authstrategies != null)) {
840 - const u = userid.split('/')[2];
841 - if (u.startsWith('~twitter:') && (domain.authstrategies.twitter != null) && (typeof domain.authstrategies.twitter.logouturl == 'string')) { res.redirect(domain.authstrategies.twitter.logouturl); return; }
842 - if (u.startsWith('~google:') && (domain.authstrategies.google != null) && (typeof domain.authstrategies.google.logouturl == 'string')) { res.redirect(domain.authstrategies.google.logouturl); return; }
843 - if (u.startsWith('~github:') && (domain.authstrategies.github != null) && (typeof domain.authstrategies.github.logouturl == 'string')) { res.redirect(domain.authstrategies.github.logouturl); return; }
844 - if (u.startsWith('~reddit:') && (domain.authstrategies.reddit != null) && (typeof domain.authstrategies.reddit.logouturl == 'string')) { res.redirect(domain.authstrategies.reddit.logouturl); return; }
845 - if (u.startsWith('~azure:') && (domain.authstrategies.azure != null) && (typeof domain.authstrategies.azure.logouturl == 'string')) { res.redirect(domain.authstrategies.azure.logouturl); return; }
846 - if (u.startsWith('~oidc:') && (domain.authstrategies.oidc != null) && (typeof domain.authstrategies.oidc.logouturl == 'string')) { res.redirect(domain.authstrategies.oidc.logouturl); return; }
847 - if (u.startsWith('~jumpcloud:') && (domain.authstrategies.jumpcloud != null) && (typeof domain.authstrategies.jumpcloud.logouturl == 'string')) { res.redirect(domain.authstrategies.jumpcloud.logouturl); return; }
848 - if (u.startsWith('~saml:') && (domain.authstrategies.saml != null) && (typeof domain.authstrategies.saml.logouturl == 'string')) { res.redirect(domain.authstrategies.saml.logouturl); return; }
849 - if (u.startsWith('~intel:') && (domain.authstrategies.intel != null) && (typeof domain.authstrategies.intel.logouturl == 'string')) { res.redirect(domain.authstrategies.intel.logouturl); return; }
839 + if ((userid != null) && (domain.authstrategies?.authStrategyFlags != null)) {
840 + let logouturl = null;
841 + let userStrategy = ((userid.split('/')[2]).split(':')[0]).substring(1);
842 + // Setup logout url for oidc
843 + if (userStrategy == 'oidc' && domain.authstrategies.oidc != null) {
844 + if (typeof domain.authstrategies.oidc.logouturl == 'string') {
845 + logouturl = domain.authstrategies.oidc.logouturl;
846 + } else if (typeof domain.authstrategies.oidc.issuer.end_session_endpoint == 'string' && typeof domain.authstrategies.oidc.client.post_logout_redirect_uri == 'string') {
847 + logouturl = domain.authstrategies.oidc.issuer.end_session_endpoint + '?post_logout_redirect_uri=' + domain.authstrategies.oidc.client.post_logout_redirect_uri;
848 + } else if (typeof domain.authstrategies.oidc.issuer.end_session_endpoint == 'string') {
849 + logouturl = domain.authstrategies.oidc.issuer.end_session_endpoint;
850 + }
851 + // Log out all other strategies
852 + } else if ((domain.authstrategies[userStrategy] != null) && (typeof domain.authstrategies[userStrategy].logouturl == 'string')) { logouturl = domain.authstrategies[userStrategy].logouturl; }
853 + // If custom logout was setup, use it
854 + if (logouturl != null) {
855 + parent.authLog('handleLogoutRequest', userStrategy.toUpperCase() + ': LOGOUT: ' + logouturl);
856 + res.redirect(logouturl);
857 + return;
858 + }
859 }
860
861 // This is the default logout redirect to the login page
@@ -1999,7 +2008,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
2008 // Send a notification
2009 obj.parent.DispatchEvent([user._id], obj, { action: 'notify', title: 'Email verified', value: user.email, nolog: 1, id: Math.random() });
2010
2002 - // Send to authlog
2011 + // Send to authLog
2012 obj.parent.authLog('https', 'Verified email address ' + user.email + ' for user ' + user.name, { useragent: req.headers['user-agent'] });
2013 }
2014 });
@@ -2035,7 +2044,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
2044 render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 1, msgid: 8, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27'), arg1: EscapeHtml(user.name), arg2: EscapeHtml(newpass) }, req, domain));
2045 parent.debug('web', 'handleCheckMailRequest: send temporary password.');
2046
2038 - // Send to authlog
2047 + // Send to authLog
2048 obj.parent.authLog('https', 'Performed account reset for user ' + user.name);
2049 }, 0);
2050 });
@@ -2575,61 +2584,69 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
2584 const domain = checkUserIpAddress(req, res);
2585 if (domain == null) { return; }
2586 if ((req.user != null) && (req.user.sid != null) && (req.user.strategy != null)) {
2578 - const authStrategy = req.user.strategy
2579 - parent.debug('authlog', `${authStrategy.toUpperCase()}: Verified user: ${JSON.stringify(req.user, null, 4)}` + JSON.stringify(req.user));
2580 -
2581 - // Check if any group related options exist
2582 - var userMemberships = [];
2583 - var siteAdminGroup = null;
2584 - if (typeof domain.authstrategies[authStrategy].groups === 'object') {
2585 - if (Array.isArray(req.user.groups)) { userMemberships = req.user.groups; }
2586 - else if (typeof req.user.groups == 'string') { userMemberships = [req.user.groups]; }
2587 - parent.debug('authlog', `${authStrategy.toUpperCase()}: Member Of: ${userMemberships.join(', ')}`);
2588 -
2589 - // See if the user is required to be part of a specific group in order to log into this server.
2590 - if (typeof domain.authstrategies[authStrategy].groups.required == 'string') { domain.authstrategies[authStrategy].groups.required = [domain.authstrategies[authStrategy].groups.required]; }
2591 - if (Array.isArray(domain.authstrategies[authStrategy].groups.required)) {
2592 - var userMembershipMatch = false;
2593 - for (var i in domain.authstrategies[authStrategy].groups.required) {
2594 - if (userMemberships.indexOf(domain.authstrategies[authStrategy].groups.required[i]) >= 0) {
2595 - userMembershipMatch = true;
2596 - parent.debug('authlog', `${authStrategy.toUpperCase()}: ${req.user.name} is member of required group: ${domain.authstrategies[authStrategy].groups.required[i]}`);
2587 + const strategy = domain.authstrategies[req.user.strategy];
2588 + const groups = { 'enabled': typeof strategy.groups == 'object' }
2589 + parent.authLog(req.user.strategy.toUpperCase(), `User Authorized: ${JSON.stringify(req.user)}`);
2590 + if (groups.enabled) { // Groups only available for OIDC strategy currently
2591 + groups.userMemberships = obj.common.convertStrArray(req.user.groups)
2592 + groups.syncEnabled = (strategy.groups.sync === true || strategy.groups.sync?.filter) ? true : false
2593 + groups.syncMemberships = []
2594 + groups.siteAdminEnabled = strategy.groups.siteadmin ? true : false
2595 + groups.grantAdmin = false
2596 + groups.revokeAdmin = strategy.groups.revokeAdmin ? strategy.groups.revokeAdmin : true
2597 + groups.requiredGroups = obj.common.convertStrArray(strategy.groups.required)
2598 + groups.siteAdmin = obj.common.convertStrArray(strategy.groups.siteadmin)
2599 + groups.syncFilter = obj.common.convertStrArray(strategy.groups.sync?.filter)
2600 +
2601 + // Fancy Logs
2602 + let groupMessage = ''
2603 + if (groups.userMemberships.length == 1) { groupMessage = ` Found membership: "${groups.userMemberships[0]}"` }
2604 + else { groupMessage = ` Found ${groups.userMemberships.length} memberships: ["${groups.userMemberships.join('", "')}"]` }
2605 + parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}"` + groupMessage);
2606 +
2607 + // Check user membership in required groups
2608 + if (groups.requiredGroups != null) {
2609 + let match = false
2610 + for (var i in groups.requiredGroups) {
2611 + if (groups.userMemberships.indexOf(groups.requiredGroups[i]) != -1) {
2612 + match = true;
2613 + parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" Membership to required group found: "${groups.requiredGroups[i]}"`);
2614 }
2615 }
2599 - if (userMembershipMatch === false) {
2600 - parent.debug('authlog', `${authStrategy}: User login denied. User not found in required group.`);
2616 + if (match === false) {
2617 + parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" Login denied. No memberhip to required group.`);
2618 req.session.loginmode = 1;
2602 - req.session.messageid = 100; // Unable to create account.
2619 + req.session.messageid = 111; // Access Denied.
2620 res.redirect(domain.url + getQueryPortion(req));
2621 return;
2622 }
2623 }
2624
2608 - // Check if user is in an administrator group
2609 - if (typeof domain.authstrategies[authStrategy].groups.siteadmin == 'string') { domain.authstrategies[authStrategy].groups.siteadmin = [ domain.authstrategies[authStrategy].groups.siteadmin ]; }
2610 - if (Array.isArray(domain.authstrategies[authStrategy].groups.siteadmin)) {
2611 - siteAdminGroup = false;
2612 - for (var i in domain.authstrategies[authStrategy].groups.siteadmin) {
2613 - if (userMemberships.indexOf(domain.authstrategies[authStrategy].groups.siteadmin[i]) >= 0) { siteAdminGroup = domain.authstrategies[authStrategy].groups.siteadmin[i]; }
2625 + // Check user membership in admin groups
2626 + if (groups.siteAdminEnabled === true) {
2627 + groups.grantAdmin = false;
2628 + for (var i in strategy.groups.siteadmin) {
2629 + if (groups.userMemberships.indexOf(strategy.groups.siteadmin[i]) >= 0) {
2630 + parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" User membership found in site admin group: "${strategy.groups.siteadmin[i]}"`);
2631 + groups.siteAdmin = strategy.groups.siteadmin[i];
2632 + groups.grantAdmin = true;
2633 + break;
2634 + }
2635 }
2636 }
2637
2617 - // See if we need to sync user-memberships (IdP) with user-groups (meshcentral)
2618 - if (domain.authstrategies[authStrategy].groups.sync === true) { domain.authstrategies[authStrategy].groups.sync = { enabled: true }; }
2619 - if (typeof domain.authstrategies[authStrategy].groups.sync.filter == 'string' || Array.isArray(domain.authstrategies[authStrategy].groups.sync.filter)) {
2620 - if (typeof domain.authstrategies[authStrategy].groups.sync.filter == 'string') { domain.authstrategies[authStrategy].groups.sync.filter = [ domain.authstrategies[authStrategy].groups.sync.filter ]; }
2621 - const filteredMemberships = [];
2622 - for (var i in userMemberships) {
2623 - for (var j in domain.authstrategies[authStrategy].groups.sync.filter) {
2624 - if (userMemberships[i].indexOf(domain.authstrategies[authStrategy].groups.sync.filter[j]) >= 0) { filteredMemberships.push(userMemberships[i]); }
2625 - }
2638 + // Check if we need to sync user-memberships (IdP) with user-groups (meshcentral)
2639 + if (groups.syncEnabled === true) {
2640 + for (var i in groups.syncFilter) {
2641 + if (groups.userMemberships.indexOf(groups.syncFilter[i]) >= 0) { groups.syncMemberships.push(groups.syncFilter[i]); }
2642 }
2627 - if (filteredMemberships.length > 0) {
2628 - parent.debug('authlog', `${authStrategy.toUpperCase()}: Filtered user memberships from config: ${filteredMemberships.join(', ')}`);
2629 - } else {
2630 - parent.debug('authlog', `${authStrategy.toUpperCase()}: No groups found with filter: ${domain.authstrategies[authStrategy].groups.sync.filter.join(', ')}`);
2643 + if (groups.syncMemberships.length > 0) {
2644 + parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" Filtered user memberships from config to sync: ${groups.syncMemberships.join(', ')}`);
2645 + } else {
2646 + groups.syncMemberships = null;
2647 + groups.syncEnabled = false
2648 + parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" No sync memberships found after filter: ${strategy.groups.sync.filter.join(', ')}`);
2649 }
2632 - userMemberships = filteredMemberships;
2650 }
2651 }
2652
@@ -2643,25 +2660,25 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
2660 if (domain.newaccounts === true) { newAccountAllowed = true; }
2661 if (obj.common.validateStrArray(domain.newaccountrealms)) { newAccountRealms = domain.newaccountrealms; }
2662
2646 - if ((domain.authstrategies != null) && (domain.authstrategies[authStrategy] != null)) {
2647 - if (domain.authstrategies[authStrategy].newaccounts === true) { newAccountAllowed = true; }
2648 - if (obj.common.validateStrArray(domain.authstrategies[authStrategy].newaccountrealms)) { newAccountRealms = domain.authstrategies[req.user.strategy].newaccountrealms; }
2663 + if (domain.authstrategies[req.user.strategy]) {
2664 + if (domain.authstrategies[req.user.strategy].newaccounts === true) { newAccountAllowed = true; }
2665 + if (obj.common.validateStrArray(domain.authstrategies[req.user.strategy].newaccountrealms)) { newAccountRealms = domain.authstrategies[req.user.strategy].newaccountrealms; }
2666 }
2667
2668 if (newAccountAllowed === true) {
2669 // Create the user
2653 - parent.debug('authlog', `${authStrategy.toUpperCase()}: Creating new login user: "${userid}"`);
2670 + parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: USER: "${req.user.sid}" Creating new login user: "${userid}"`);
2671 user = { type: 'user', _id: userid, name: req.user.name, email: req.user.email, creation: Math.floor(Date.now() / 1000), login: Math.floor(Date.now() / 1000), access: Math.floor(Date.now() / 1000), domain: domain.id };
2655 - if (req.user.email != null) { user.email = req.user.email; user.emailVerified = true; }
2672 + if (req.user.email != null) { user.email = req.user.email; user.emailVerified = req.user.email_verified ? req.user.email_verified : true; }
2673 if (domain.newaccountsrights) { user.siteadmin = domain.newaccountsrights; } // New accounts automatically assigned server rights.
2657 - if (domain.authstrategies[authStrategy].newaccountsrights) { user.siteadmin = obj.common.meshServerRightsArrayToNumber(domain.authstrategies[req.user.strategy].newaccountsrights); } // If there are specific SSO server rights, use these instead.
2674 + if (domain.authstrategies[req.user.strategy].newaccountsrights) { user.siteadmin = obj.common.meshServerRightsArrayToNumber(domain.authstrategies[req.user.strategy].newaccountsrights); } // If there are specific SSO server rights, use these instead.
2675 if (newAccountRealms) { user.groups = newAccountRealms; } // New accounts automatically part of some groups (Realms).
2676 obj.users[userid] = user;
2677
2678 // Auto-join any user groups
2679 var newaccountsusergroups = null;
2680 if (typeof domain.newaccountsusergroups == 'object') { newaccountsusergroups = domain.newaccountsusergroups; }
2664 - if (typeof domain.authstrategies[authStrategy].newaccountsusergroups == 'object') { newaccountsusergroups = domain.authstrategies[req.user.strategy].newaccountsusergroups; }
2681 + if (typeof domain.authstrategies[req.user.strategy].newaccountsusergroups == 'object') { newaccountsusergroups = domain.authstrategies[req.user.strategy].newaccountsusergroups; }
2682 if (newaccountsusergroups) {
2683 for (var i in newaccountsusergroups) {
2684 var ugrpid = newaccountsusergroups[i];
@@ -2684,13 +2701,16 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
2701 }
2702 }
2703
2687 - if (typeof domain.authstrategies[authStrategy].groups == 'object') {
2704 + if (groups.enabled === true) {
2705 // Sync the user groups if enabled
2689 - if ((typeof domain.authstrategies[authStrategy].groups.sync == 'object') && (domain.authstrategies[authStrategy].groups.sync.enabled === true)) { syncExternalUserGroups(domain, user, userMemberships, authStrategy) }
2690 -
2706 + if (groups.syncEnabled === true) {
2707 + // Set groupType to the preset name if it exists, otherwise use the strategy name
2708 + const groupType = domain.authstrategies[req.user.strategy].custom?.preset ? domain.authstrategies[req.user.strategy].custom.preset : req.user.strategy;
2709 + syncExternalUserGroups(domain, user, groups.syncMemberships, groupType);
2710 + }
2711 // See if the user is a member of the site admin group.
2692 - if (typeof siteAdminGroup === 'string') {
2693 - parent.debug('authlog', `${authStrategy.toUpperCase()}: Granting site admin privilages to new user "${user.name}" found in admin group: ${siteAdminGroup}`);
2712 + if (groups.grantAdmin === true) {
2713 + parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" Granting site admin privilages`);
2714 user.siteadmin = 0xFFFFFFFF;
2715 }
2716 }
@@ -2715,7 +2735,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
2735 obj.parent.DispatchEvent(targets, obj, loginEvent);
2736 } else {
2737 // New users not allowed
2718 - parent.debug('authlog', `${authStrategy.toUpperCase()}: Can\'t create new user, account creation is not allowed`);
2738 + parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: LOGIN FAILED: USER: "${req.user.sid}" New accounts are not allowed`);
2739 req.session.loginmode = 1;
2740 req.session.messageid = 100; // Unable to create account.
2741 res.redirect(domain.url + getQueryPortion(req));
@@ -2726,19 +2746,19 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
2746 var userChanged = false;
2747 if ((req.user.name != null) && (req.user.name != user.name)) { user.name = req.user.name; userChanged = true; }
2748 if ((req.user.email != null) && (req.user.email != user.email)) { user.email = req.user.email; user.emailVerified = true; userChanged = true; }
2729 -
2730 - if (typeof domain.authstrategies[authStrategy].groups == 'object') {
2731 - // Sync the user groups if enabled
2732 - if ((typeof domain.authstrategies[authStrategy].groups.sync == 'object') && (domain.authstrategies[authStrategy].groups.sync.enabled === true)) { syncExternalUserGroups(domain, user, userMemberships, authStrategy) }
2749
2750 + if (groups.enabled === true) {
2751 + // Sync the user groups if enabled
2752 + if (groups.syncEnabled === true) {
2753 + syncExternalUserGroups(domain, user, groups.syncMemberships, req.user.strategy)
2754 + }
2755 // See if the user is a member of the site admin group.
2735 - if ((typeof domain.authstrategies[authStrategy].groups.siteadmin !== 'undefined') && (domain.authstrategies[authStrategy].groups.siteadmin !== null)) {
2736 - if ((typeof siteAdminGroup === 'string') && (user.siteadmin !== 0xFFFFFFFF)) {
2737 - parent.debug('authlog', `${authStrategy.toUpperCase()}: Granting site admin privilages to user "${user.name}" found in administrator group: ${siteAdminGroup}`);
2738 - user.siteadmin = 0xFFFFFFFF;
2739 - userChanged = true;
2740 - } else if ((siteAdminGroup === false) && (user.siteadmin === 0xFFFFFFFF)) {
2741 - parent.debug('authlog', `${authStrategy.toUpperCase()}: Revoking site admin privilages from user "${user.name}" since they are not found in any administrator groups.`);
2756 + if (groups.siteAdminEnabled === true) {
2757 + if (groups.grantAdmin === true) {
2758 + parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" Granting site admin privilages`);
2759 + if (user.siteadmin !== 0xFFFFFFFF) { user.siteadmin = 0xFFFFFFFF; userChanged = true; }
2760 + } else if ((groups.revokeAdmin === true) && (user.siteadmin === 0xFFFFFFFF)) {
2761 + parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" Revoking site admin privilages.`);
2762 delete user.siteadmin;
2763 userChanged = true;
2764 }
@@ -2747,6 +2767,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
2767
2768 // Update db record for user if there are changes detected
2769 if (userChanged) {
2770 + parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: CHANGED: USER: "${req.user.sid}" Updating user database entry`);
2771 obj.db.SetUser(user);
2772
2773 // Event user change
@@ -2764,9 +2785,13 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
2785 const ua = obj.getUserAgentInfo(req);
2786 const loginEvent = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'login', msgid: 107, msgArgs: [req.clientIp, ua.browserStr, ua.osStr], msg: 'Account login', domain: domain.id, ip: req.clientIp, userAgent: req.headers['user-agent'], twoFactorType: 'sso' };
2787 obj.parent.DispatchEvent(targets, obj, loginEvent);
2767 - parent.debug('authlog', `${authStrategy.toUpperCase()}: User Logged In: Name: ${user.name} ID: ${user._id}`);
2788 + parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: LOGIN SUCCESS: USER: "${req.user.sid}"`);
2789 }
2769 - } else { parent.debug('warn', 'handleStrategyLogin: FAILED - No user'); }
2790 + } else {
2791 + parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: LOGIN FAILED: USER: "${req.user.sid}" REQUEST CONTAINS NO USER OR SID`);
2792 + }
2793 +
2794 + parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: User Authenticated: ${JSON.stringify(user)}`);
2795 //res.redirect(domain.url); // This does not handle cookie correctly.
2796 res.set('Content-Type', 'text/html');
2797 res.end('<html><head><meta http-equiv="refresh" content=0;url="' + domain.url + '"></head><body></body></html>');
@@ -3285,7 +3310,13 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
3310 if (typeof domain.authstrategies.github == 'object') { authStrategies.push('github'); }
3311 if (typeof domain.authstrategies.reddit == 'object') { authStrategies.push('reddit'); }
3312 if (typeof domain.authstrategies.azure == 'object') { authStrategies.push('azure'); }
3288 - if (typeof domain.authstrategies.oidc == 'object') { authStrategies.push('oidc'); }
3313 + if (typeof domain.authstrategies.oidc == 'object') {
3314 + if (obj.common.validateObject(domain.authstrategies.oidc.custom) && obj.common.validateString(domain.authstrategies.oidc.custom.preset)) {
3315 + authStrategies.push('oidc-' + domain.authstrategies.oidc.custom.preset);
3316 + } else {
3317 + authStrategies.push('oidc');
3318 + }
3319 + }
3320 if (typeof domain.authstrategies.intel == 'object') { authStrategies.push('intel'); }
3321 if (typeof domain.authstrategies.jumpcloud == 'object') { authStrategies.push('jumpcloud'); }
3322 if (typeof domain.authstrategies.saml == 'object') { authStrategies.push('saml'); }
@@ -5351,7 +5382,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
5382 var xdomain = (domain.dns == null) ? domain.id : '';
5383 if (xdomain != '') xdomain += '/';
5384 var meshsettings = '';
5354 - if (req.query.ac != '4'){ // If MeshCentral Assistant Monitor Mode, DONT INCLUDE SERVER DETAILS!
5385 + if (req.query.ac != '4') { // If MeshCentral Assistant Monitor Mode, DONT INCLUDE SERVER DETAILS!
5386 meshsettings += '\r\nMeshName=' + mesh.name + '\r\nMeshType=' + mesh.mtype + '\r\nMeshID=0x' + meshidhex + '\r\nServerID=' + serveridhex + '\r\n';
5387 if (obj.args.lanonly != true) { meshsettings += 'MeshServer=wss://' + serverName + ':' + httpsPort + '/' + xdomain + 'agent.ashx\r\n'; } else {
5388 meshsettings += 'MeshServer=local\r\n';
@@ -5430,7 +5461,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
5461
5462 // Send meshcmd for a specific platform back
5463 var agentid = parseInt(req.query.meshcmd);
5433 -
5464 +
5465 // If the agentid is 3 or 4, check if we have a signed MeshCmd.exe
5466 if ((agentid == 3) && (obj.parent.meshAgentBinaries[11000] != null)) { // Signed Windows MeshCmd.exe x86-32
5467 var stats = null, meshCmdPath = obj.parent.meshAgentBinaries[11000].path;
@@ -6138,7 +6169,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
6169 }
6170 if (obj.args.sessiontime != null) { sessionOptions.maxAge = (obj.args.sessiontime * 60000); } // sessiontime is minutes
6171 obj.app.use(require('cookie-session')(sessionOptions));
6141 - obj.app.use(function(request, response, next) { // Patch for passport 0.6.0 - https://github.com/jaredhanson/passport/issues/904
6172 + obj.app.use(function (request, response, next) { // Patch for passport 0.6.0 - https://github.com/jaredhanson/passport/issues/904
6173 if (request.session && !request.session.regenerate) {
6174 request.session.regenerate = function (cb) {
6175 cb()
@@ -6324,7 +6355,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
6355 var errlogpath = null;
6356 if (typeof parent.args.mesherrorlogpath == 'string') { errlogpath = parent.path.join(parent.args.mesherrorlogpath, 'mesherrors.txt'); } else { errlogpath = parent.getConfigFilePath('mesherrors.txt'); }
6357 parent.fs.appendFileSync(errlogpath, new Date().toLocaleString() + ': ' + `Error in res.send | ${err.code} | ${err.message} | ${res.stack}` + '\r\n');
6327 - } catch (ex) { console.log('ERROR: Unable to write to mesherrors.txt.'); }
6358 + } catch (ex) { parent.debug('error', 'Unable to write to mesherrors.txt.'); }
6359 }
6360 };
6361
@@ -6365,676 +6396,692 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
6396 });
6397 }
6398
6368 - // Setup all sharing domains
6399 + // Setup all sharing domains and check if auth strategies need setup
6400 + var setupSSO = false
6401 for (var i in parent.config.domains) {
6402 if ((parent.config.domains[i].dns == null) && (parent.config.domains[i].share != null)) { obj.app.use(parent.config.domains[i].url, obj.express.static(parent.config.domains[i].share)); }
6403 + if (typeof parent.config.domains[i].authstrategies == 'object') { setupSSO = true };
6404 + }
6405 +
6406 + if (setupSSO) {
6407 + setupAllDomainAuthStrategies().then(() => finalizeWebserver());
6408 + } else {
6409 + finalizeWebserver()
6410 }
6411
6412 // Setup all domain auth strategy passport.js
6374 - for (var i in parent.config.domains) {
6375 - if (typeof parent.config.domains[i].authstrategies == 'object') {
6376 - parent.config.domains[i].authstrategies.authStrategyFlags = 0;
6377 - const authStrategyFlags = setupDomainAuthStrategy(parent.config.domains[i]);
6378 - if (authStrategyFlags > 0) {
6379 - if (parent.config.domains[i].dns != null) {
6380 - if (typeof parent.config.domains[''].authstrategies != 'object') { parent.config.domains[''].authstrategies = { authStrategyFlags: 0 }; }
6381 - parent.config.domains[''].authstrategies.authStrategyFlags |= authStrategyFlags;
6382 - } else {
6383 - if (typeof parent.config.domains[i].authstrategies != 'object') { parent.config.domains[i].authstrategies = { authStrategyFlags: 0 }; }
6384 - parent.config.domains[i].authstrategies.authStrategyFlags |= authStrategyFlags;
6385 - }
6413 + async function setupAllDomainAuthStrategies() {
6414 + for (var i in parent.config.domains) {
6415 + if (parent.config.domains[i].dns != null) {
6416 + if (typeof parent.config.domains[''].authstrategies != 'object') { parent.config.domains[''].authstrategies = { 'authStrategyFlags': 0 }; }
6417 + parent.config.domains[''].authstrategies.authStrategyFlags |= await setupDomainAuthStrategy(parent.config.domains['']);
6418 + } else {
6419 + if (typeof parent.config.domains[i].authstrategies != 'object') { parent.config.domains[i].authstrategies = { 'authStrategyFlags': 0 }; }
6420 + parent.config.domains[i].authstrategies.authStrategyFlags |= await setupDomainAuthStrategy(parent.config.domains[i]);
6421 }
6422 }
6423 }
6389 -
6390 - // Setup all HTTP handlers
6391 - if (parent.pluginHandler != null) {
6392 - parent.pluginHandler.callHook('hook_setupHttpHandlers', obj, parent);
6393 - }
6394 - if (parent.multiServer != null) { obj.app.ws('/meshserver.ashx', function (ws, req) { parent.multiServer.CreatePeerInServer(parent.multiServer, ws, req, obj.args.tlsoffload == null); }); }
6395 - for (var i in parent.config.domains) {
6396 - if ((parent.config.domains[i].dns != null) || (parent.config.domains[i].share != null)) { continue; } // This is a subdomain with a DNS name, no added HTTP bindings needed.
6397 - var domain = parent.config.domains[i];
6398 - var url = domain.url;
6399 - if (typeof domain.rootredirect == 'string') {
6400 - // Root page redirects the user to a different URL
6401 - obj.app.get(url, handleRootRedirect);
6402 - } else {
6403 - // Present the login page as the root page
6404 - obj.app.get(url, handleRootRequest);
6405 - obj.app.post(url, obj.bodyParser.urlencoded({ extended: false }), handleRootPostRequest);
6406 - }
6407 - obj.app.get(url + 'refresh.ashx', function (req, res) { res.sendStatus(200); });
6408 - if ((domain.myserver !== false) && ((domain.myserver == null) || (domain.myserver.backup === true))) { obj.app.get(url + 'backup.zip', handleBackupRequest); }
6409 - if ((domain.myserver !== false) && ((domain.myserver == null) || (domain.myserver.restore === true))) { obj.app.post(url + 'restoreserver.ashx', obj.bodyParser.urlencoded({ extended: false }), handleRestoreRequest); }
6410 - obj.app.get(url + 'terms', handleTermsRequest);
6411 - obj.app.get(url + 'xterm', handleXTermRequest);
6412 - obj.app.get(url + 'login', handleRootRequest);
6413 - obj.app.post(url + 'login', obj.bodyParser.urlencoded({ extended: false }), handleRootPostRequest);
6414 - obj.app.post(url + 'tokenlogin', obj.bodyParser.urlencoded({ extended: false }), handleLoginRequest);
6415 - obj.app.get(url + 'logout', handleLogoutRequest);
6416 - obj.app.get(url + 'MeshServerRootCert.cer', handleRootCertRequest);
6417 - obj.app.post(url + 'changepassword', obj.bodyParser.urlencoded({ extended: false }), handlePasswordChangeRequest);
6418 - obj.app.post(url + 'deleteaccount', obj.bodyParser.urlencoded({ extended: false }), handleDeleteAccountRequest);
6419 - obj.app.post(url + 'createaccount', obj.bodyParser.urlencoded({ extended: false }), handleCreateAccountRequest);
6420 - obj.app.post(url + 'resetpassword', obj.bodyParser.urlencoded({ extended: false }), handleResetPasswordRequest);
6421 - obj.app.post(url + 'resetaccount', obj.bodyParser.urlencoded({ extended: false }), handleResetAccountRequest);
6422 - obj.app.get(url + 'checkmail', handleCheckMailRequest);
6423 - obj.app.get(url + 'agentinvite', handleAgentInviteRequest);
6424 - obj.app.get(url + 'userimage.ashx', handleUserImageRequest);
6425 - obj.app.post(url + 'amtevents.ashx', obj.bodyParser.urlencoded({ extended: false }), obj.handleAmtEventRequest);
6426 - obj.app.get(url + 'meshagents', obj.handleMeshAgentRequest);
6427 - obj.app.get(url + 'messenger', handleMessengerRequest);
6428 - obj.app.get(url + 'messenger.png', handleMessengerImageRequest);
6429 - obj.app.get(url + 'meshosxagent', obj.handleMeshOsxAgentRequest);
6430 - obj.app.get(url + 'meshsettings', obj.handleMeshSettingsRequest);
6431 - obj.app.get(url + 'devicepowerevents.ashx', obj.handleDevicePowerEvents);
6432 - obj.app.get(url + 'downloadfile.ashx', handleDownloadFile);
6433 - obj.app.get(url + 'commander.ashx', handleMeshCommander);
6434 - obj.app.post(url + 'uploadfile.ashx', obj.bodyParser.urlencoded({ extended: false }), handleUploadFile);
6435 - obj.app.post(url + 'uploadfilebatch.ashx', obj.bodyParser.urlencoded({ extended: false }), handleUploadFileBatch);
6436 - obj.app.post(url + 'uploadmeshcorefile.ashx', obj.bodyParser.urlencoded({ extended: false }), handleUploadMeshCoreFile);
6437 - obj.app.post(url + 'oneclickrecovery.ashx', obj.bodyParser.urlencoded({ extended: false }), handleOneClickRecoveryFile);
6438 - obj.app.get(url + 'userfiles/*', handleDownloadUserFiles);
6439 - obj.app.ws(url + 'echo.ashx', handleEchoWebSocket);
6440 - obj.app.ws(url + '2fahold.ashx', handle2faHoldWebSocket);
6441 - obj.app.ws(url + 'apf.ashx', function (ws, req) { obj.parent.mpsserver.onWebSocketConnection(ws, req); })
6442 - obj.app.get(url + 'webrelay.ashx', function (req, res) { res.send('Websocket connection expected'); });
6443 - obj.app.get(url + 'health.ashx', function (req, res) { res.send('ok'); }); // TODO: Perform more server checking.
6444 - obj.app.ws(url + 'webrelay.ashx', function (ws, req) { PerformWSSessionAuth(ws, req, false, handleRelayWebSocket); });
6445 - obj.app.ws(url + 'webider.ashx', function (ws, req) { PerformWSSessionAuth(ws, req, false, function (ws1, req1, domain, user, cookie, authData) { obj.meshIderHandler.CreateAmtIderSession(obj, obj.db, ws1, req1, obj.args, domain, user); }); });
6446 - obj.app.ws(url + 'control.ashx', function (ws, req) {
6447 - getWebsocketArgs(ws, req, function (ws, req) {
6448 - const domain = getDomain(req);
6449 - if (obj.CheckWebServerOriginName(domain, req) == false) {
6450 - try { ws.send(JSON.stringify({ action: 'close', cause: 'invalidorigin', msg: 'invalidorigin' })); } catch (ex) { }
6451 - try { ws.close(); } catch (ex) { }
6452 - return;
6453 - }
6454 - if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { ws.close(); return; } // Check 3FA URL key
6455 - PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie, authData) {
6456 - if (user == null) { // User is not authenticated, perform inner server authentication
6457 - if (req.headers['x-meshauth'] === '*') {
6458 - PerformWSSessionInnerAuth(ws, req, domain, function (ws1, req1, domain, user) { obj.meshUserHandler.CreateMeshUser(obj, obj.db, ws1, req1, obj.args, domain, user, authData); }); // User is authenticated
6424 + function setupHTTPHandlers() {
6425 + // Setup all HTTP handlers
6426 + if (parent.pluginHandler != null) {
6427 + parent.pluginHandler.callHook('hook_setupHttpHandlers', obj, parent);
6428 + }
6429 + if (parent.multiServer != null) { obj.app.ws('/meshserver.ashx', function (ws, req) { parent.multiServer.CreatePeerInServer(parent.multiServer, ws, req, obj.args.tlsoffload == null); }); }
6430 + for (var i in parent.config.domains) {
6431 + if ((parent.config.domains[i].dns != null) || (parent.config.domains[i].share != null)) { continue; } // This is a subdomain with a DNS name, no added HTTP bindings needed.
6432 + var domain = parent.config.domains[i];
6433 + var url = domain.url;
6434 + if (typeof domain.rootredirect == 'string') {
6435 + // Root page redirects the user to a different URL
6436 + obj.app.get(url, handleRootRedirect);
6437 + } else {
6438 + // Present the login page as the root page
6439 + obj.app.get(url, handleRootRequest);
6440 + obj.app.post(url, obj.bodyParser.urlencoded({ extended: false }), handleRootPostRequest);
6441 + }
6442 + obj.app.get(url + 'refresh.ashx', function (req, res) { res.sendStatus(200); });
6443 + if ((domain.myserver !== false) && ((domain.myserver == null) || (domain.myserver.backup === true))) { obj.app.get(url + 'backup.zip', handleBackupRequest); }
6444 + if ((domain.myserver !== false) && ((domain.myserver == null) || (domain.myserver.restore === true))) { obj.app.post(url + 'restoreserver.ashx', obj.bodyParser.urlencoded({ extended: false }), handleRestoreRequest); }
6445 + obj.app.get(url + 'terms', handleTermsRequest);
6446 + obj.app.get(url + 'xterm', handleXTermRequest);
6447 + obj.app.get(url + 'login', handleRootRequest);
6448 + obj.app.post(url + 'login', obj.bodyParser.urlencoded({ extended: false }), handleRootPostRequest);
6449 + obj.app.post(url + 'tokenlogin', obj.bodyParser.urlencoded({ extended: false }), handleLoginRequest);
6450 + obj.app.get(url + 'logout', handleLogoutRequest);
6451 + obj.app.get(url + 'MeshServerRootCert.cer', handleRootCertRequest);
6452 + obj.app.post(url + 'changepassword', obj.bodyParser.urlencoded({ extended: false }), handlePasswordChangeRequest);
6453 + obj.app.post(url + 'deleteaccount', obj.bodyParser.urlencoded({ extended: false }), handleDeleteAccountRequest);
6454 + obj.app.post(url + 'createaccount', obj.bodyParser.urlencoded({ extended: false }), handleCreateAccountRequest);
6455 + obj.app.post(url + 'resetpassword', obj.bodyParser.urlencoded({ extended: false }), handleResetPasswordRequest);
6456 + obj.app.post(url + 'resetaccount', obj.bodyParser.urlencoded({ extended: false }), handleResetAccountRequest);
6457 + obj.app.get(url + 'checkmail', handleCheckMailRequest);
6458 + obj.app.get(url + 'agentinvite', handleAgentInviteRequest);
6459 + obj.app.get(url + 'userimage.ashx', handleUserImageRequest);
6460 + obj.app.post(url + 'amtevents.ashx', obj.bodyParser.urlencoded({ extended: false }), obj.handleAmtEventRequest);
6461 + obj.app.get(url + 'meshagents', obj.handleMeshAgentRequest);
6462 + obj.app.get(url + 'messenger', handleMessengerRequest);
6463 + obj.app.get(url + 'messenger.png', handleMessengerImageRequest);
6464 + obj.app.get(url + 'meshosxagent', obj.handleMeshOsxAgentRequest);
6465 + obj.app.get(url + 'meshsettings', obj.handleMeshSettingsRequest);
6466 + obj.app.get(url + 'devicepowerevents.ashx', obj.handleDevicePowerEvents);
6467 + obj.app.get(url + 'downloadfile.ashx', handleDownloadFile);
6468 + obj.app.get(url + 'commander.ashx', handleMeshCommander);
6469 + obj.app.post(url + 'uploadfile.ashx', obj.bodyParser.urlencoded({ extended: false }), handleUploadFile);
6470 + obj.app.post(url + 'uploadfilebatch.ashx', obj.bodyParser.urlencoded({ extended: false }), handleUploadFileBatch);
6471 + obj.app.post(url + 'uploadmeshcorefile.ashx', obj.bodyParser.urlencoded({ extended: false }), handleUploadMeshCoreFile);
6472 + obj.app.post(url + 'oneclickrecovery.ashx', obj.bodyParser.urlencoded({ extended: false }), handleOneClickRecoveryFile);
6473 + obj.app.get(url + 'userfiles/*', handleDownloadUserFiles);
6474 + obj.app.ws(url + 'echo.ashx', handleEchoWebSocket);
6475 + obj.app.ws(url + '2fahold.ashx', handle2faHoldWebSocket);
6476 + obj.app.ws(url + 'apf.ashx', function (ws, req) { obj.parent.mpsserver.onWebSocketConnection(ws, req); })
6477 + obj.app.get(url + 'webrelay.ashx', function (req, res) { res.send('Websocket connection expected'); });
6478 + obj.app.get(url + 'health.ashx', function (req, res) { res.send('ok'); }); // TODO: Perform more server checking.
6479 + obj.app.ws(url + 'webrelay.ashx', function (ws, req) { PerformWSSessionAuth(ws, req, false, handleRelayWebSocket); });
6480 + obj.app.ws(url + 'webider.ashx', function (ws, req) { PerformWSSessionAuth(ws, req, false, function (ws1, req1, domain, user, cookie, authData) { obj.meshIderHandler.CreateAmtIderSession(obj, obj.db, ws1, req1, obj.args, domain, user); }); });
6481 + obj.app.ws(url + 'control.ashx', function (ws, req) {
6482 + getWebsocketArgs(ws, req, function (ws, req) {
6483 + const domain = getDomain(req);
6484 + if (obj.CheckWebServerOriginName(domain, req) == false) {
6485 + try { ws.send(JSON.stringify({ action: 'close', cause: 'invalidorigin', msg: 'invalidorigin' })); } catch (ex) { }
6486 + try { ws.close(); } catch (ex) { }
6487 + return;
6488 + }
6489 + if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { ws.close(); return; } // Check 3FA URL key
6490 + PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie, authData) {
6491 + if (user == null) { // User is not authenticated, perform inner server authentication
6492 + if (req.headers['x-meshauth'] === '*') {
6493 + PerformWSSessionInnerAuth(ws, req, domain, function (ws1, req1, domain, user) { obj.meshUserHandler.CreateMeshUser(obj, obj.db, ws1, req1, obj.args, domain, user, authData); }); // User is authenticated
6494 + } else {
6495 + try { ws.close(); } catch (ex) { } // user is not authenticated and inner authentication was not requested, disconnect now.
6496 + }
6497 } else {
6460 - try { ws.close(); } catch (ex) { } // user is not authenticated and inner authentication was not requested, disconnect now.
6498 + obj.meshUserHandler.CreateMeshUser(obj, obj.db, ws1, req1, obj.args, domain, user, authData); // User is authenticated
6499 }
6462 - } else {
6463 - obj.meshUserHandler.CreateMeshUser(obj, obj.db, ws1, req1, obj.args, domain, user, authData); // User is authenticated
6464 - }
6500 + });
6501 });
6502 });
6467 - });
6468 - obj.app.ws(url + 'devicefile.ashx', function (ws, req) { obj.meshDeviceFileHandler.CreateMeshDeviceFile(obj, ws, null, req, domain); });
6469 - obj.app.get(url + 'devicefile.ashx', handleDeviceFile);
6470 - obj.app.get(url + 'agentdownload.ashx', handleAgentDownloadFile);
6471 - obj.app.get(url + 'logo.png', handleLogoRequest);
6472 - obj.app.get(url + 'loginlogo.png', handleLoginLogoRequest);
6473 - obj.app.post(url + 'translations', obj.bodyParser.urlencoded({ extended: false }), handleTranslationsRequest);
6474 - obj.app.get(url + 'welcome.jpg', handleWelcomeImageRequest);
6475 - obj.app.get(url + 'welcome.png', handleWelcomeImageRequest);
6476 - obj.app.get(url + 'recordings.ashx', handleGetRecordings);
6477 - obj.app.ws(url + 'recordings.ashx', handleGetRecordingsWebSocket);
6478 - obj.app.get(url + 'player.htm', handlePlayerRequest);
6479 - obj.app.get(url + 'player', handlePlayerRequest);
6480 - obj.app.get(url + 'sharing', handleSharingRequest);
6481 - obj.app.ws(url + 'agenttransfer.ashx', handleAgentFileTransfer); // Setup agent to/from server file transfer handler
6482 - obj.app.ws(url + 'meshrelay.ashx', function (ws, req) {
6483 - PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie, authData) {
6484 - if (((parent.config.settings.desktopmultiplex === true) || (domain.desktopmultiplex === true)) && (req.query.p == 2)) {
6485 - obj.meshDesktopMultiplexHandler.CreateMeshRelay(obj, ws1, req1, domain, user, cookie); // Desktop multiplexor 1-to-n
6486 - } else {
6487 - obj.meshRelayHandler.CreateMeshRelay(obj, ws1, req1, domain, user, cookie); // Normal relay 1-to-1
6488 - }
6489 - });
6490 - });
6491 - if (obj.args.wanonly != true) { // If the server is not in WAN mode, allow server relayed connections.
6492 - obj.app.ws(url + 'localrelay.ashx', function (ws, req) {
6503 + obj.app.ws(url + 'devicefile.ashx', function (ws, req) { obj.meshDeviceFileHandler.CreateMeshDeviceFile(obj, ws, null, req, domain); });
6504 + obj.app.get(url + 'devicefile.ashx', handleDeviceFile);
6505 + obj.app.get(url + 'agentdownload.ashx', handleAgentDownloadFile);
6506 + obj.app.get(url + 'logo.png', handleLogoRequest);
6507 + obj.app.get(url + 'loginlogo.png', handleLoginLogoRequest);
6508 + obj.app.post(url + 'translations', obj.bodyParser.urlencoded({ extended: false }), handleTranslationsRequest);
6509 + obj.app.get(url + 'welcome.jpg', handleWelcomeImageRequest);
6510 + obj.app.get(url + 'welcome.png', handleWelcomeImageRequest);
6511 + obj.app.get(url + 'recordings.ashx', handleGetRecordings);
6512 + obj.app.ws(url + 'recordings.ashx', handleGetRecordingsWebSocket);
6513 + obj.app.get(url + 'player.htm', handlePlayerRequest);
6514 + obj.app.get(url + 'player', handlePlayerRequest);
6515 + obj.app.get(url + 'sharing', handleSharingRequest);
6516 + obj.app.ws(url + 'agenttransfer.ashx', handleAgentFileTransfer); // Setup agent to/from server file transfer handler
6517 + obj.app.ws(url + 'meshrelay.ashx', function (ws, req) {
6518 PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie, authData) {
6494 - if ((user == null) || (cookie == null)) {
6495 - try { ws1.close(); } catch (ex) { }
6519 + if (((parent.config.settings.desktopmultiplex === true) || (domain.desktopmultiplex === true)) && (req.query.p == 2)) {
6520 + obj.meshDesktopMultiplexHandler.CreateMeshRelay(obj, ws1, req1, domain, user, cookie); // Desktop multiplexor 1-to-n
6521 } else {
6497 - obj.meshRelayHandler.CreateLocalRelay(obj, ws1, req1, domain, user, cookie); // Local relay
6522 + obj.meshRelayHandler.CreateMeshRelay(obj, ws1, req1, domain, user, cookie); // Normal relay 1-to-1
6523 }
6524 });
6525 });
6501 - }
6502 - if (domain.agentinvitecodes == true) {
6503 - obj.app.get(url + 'invite', handleInviteRequest);
6504 - obj.app.post(url + 'invite', obj.bodyParser.urlencoded({ extended: false }), handleInviteRequest);
6505 - }
6506 - if (parent.pluginHandler != null) {
6507 - obj.app.get(url + 'pluginadmin.ashx', obj.handlePluginAdminReq);
6508 - obj.app.post(url + 'pluginadmin.ashx', obj.bodyParser.urlencoded({ extended: false }), obj.handlePluginAdminPostReq);
6509 - obj.app.get(url + 'pluginHandler.js', obj.handlePluginJS);
6510 - }
6511 -
6512 - // New account CAPTCHA request
6513 - if ((domain.newaccountscaptcha != null) && (domain.newaccountscaptcha !== false)) {
6514 - obj.app.get(url + 'newAccountCaptcha.ashx', handleNewAccountCaptchaRequest);
6515 - }
6516 -
6517 - // Check CrowdSec Bounser if configured
6518 - if (parent.crowdSecBounser != null) {
6519 - obj.app.get(url + 'captcha.ashx', handleCaptchaGetRequest);
6520 - obj.app.post(url + 'captcha.ashx', obj.bodyParser.urlencoded({ extended: false }), handleCaptchaPostRequest);
6521 - }
6522 -
6523 - // Setup IP-KVM relay if supported
6524 - if (domain.ipkvm) {
6525 - obj.app.ws(url + 'ipkvm.ashx/*', function (ws, req) {
6526 - const domain = getDomain(req);
6527 - if (domain == null) { parent.debug('web', 'ipkvm: failed domain checks.'); try { ws.close(); } catch (ex) { } return; }
6528 - parent.ipKvmManager.handleIpKvmWebSocket(domain, ws, req);
6529 - });
6530 - obj.app.get(url + 'ipkvm.ashx/*', function (req, res, next) {
6531 - const domain = getDomain(req);
6532 - if (domain == null) return;
6533 - parent.ipKvmManager.handleIpKvmGet(domain, req, res, next);
6534 - });
6535 - }
6536 -
6537 - // Setup RDP unless indicated as disabled
6538 - if (domain.mstsc !== false) {
6539 - obj.app.get(url + 'mstsc.html', function (req, res) { handleMSTSCRequest(req, res, 'mstsc'); });
6540 - obj.app.ws(url + 'mstscrelay.ashx', function (ws, req) {
6541 - const domain = getDomain(req);
6542 - if (domain == null) { parent.debug('web', 'mstsc: failed checks.'); try { ws.close(); } catch (e) { } return; }
6543 - // If no user is logged in and we have a default user, set it now.
6544 - if ((req.session.userid == null) && (typeof obj.args.user == 'string') && (obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()])) { req.session.userid = 'user/' + domain.id + '/' + obj.args.user.toLowerCase(); }
6545 - try { require('./apprelays.js').CreateMstscRelay(obj, obj.db, ws, req, obj.args, domain); } catch (ex) { console.log(ex); }
6546 - });
6547 - }
6548 -
6549 - // Setup SSH if needed
6550 - if (domain.ssh === true) {
6551 - obj.app.get(url + 'ssh.html', function (req, res) { handleMSTSCRequest(req, res, 'ssh'); });
6552 - obj.app.ws(url + 'sshrelay.ashx', function (ws, req) {
6553 - const domain = getDomain(req);
6554 - if (domain == null) { parent.debug('web', 'ssh: failed checks.'); try { ws.close(); } catch (e) { } return; }
6555 - // If no user is logged in and we have a default user, set it now.
6556 - if ((req.session.userid == null) && (typeof obj.args.user == 'string') && (obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()])) { req.session.userid = 'user/' + domain.id + '/' + obj.args.user.toLowerCase(); }
6557 - try { require('./apprelays.js').CreateSshRelay(obj, obj.db, ws, req, obj.args, domain); } catch (ex) { console.log(ex); }
6558 - });
6559 - obj.app.ws(url + 'sshterminalrelay.ashx', function (ws, req) {
6560 - PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie, authData) {
6561 - require('./apprelays.js').CreateSshTerminalRelay(obj, obj.db, ws1, req1, domain, user, cookie, obj.args);
6526 + if (obj.args.wanonly != true) { // If the server is not in WAN mode, allow server relayed connections.
6527 + obj.app.ws(url + 'localrelay.ashx', function (ws, req) {
6528 + PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie, authData) {
6529 + if ((user == null) || (cookie == null)) {
6530 + try { ws1.close(); } catch (ex) { }
6531 + } else {
6532 + obj.meshRelayHandler.CreateLocalRelay(obj, ws1, req1, domain, user, cookie); // Local relay
6533 + }
6534 + });
6535 });
6563 - });
6564 - obj.app.ws(url + 'sshfilesrelay.ashx', function (ws, req) {
6565 - PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie, authData) {
6566 - require('./apprelays.js').CreateSshFilesRelay(obj, obj.db, ws1, req1, domain, user, cookie, obj.args);
6536 + }
6537 + if (domain.agentinvitecodes == true) {
6538 + obj.app.get(url + 'invite', handleInviteRequest);
6539 + obj.app.post(url + 'invite', obj.bodyParser.urlencoded({ extended: false }), handleInviteRequest);
6540 + }
6541 + if (parent.pluginHandler != null) {
6542 + obj.app.get(url + 'pluginadmin.ashx', obj.handlePluginAdminReq);
6543 + obj.app.post(url + 'pluginadmin.ashx', obj.bodyParser.urlencoded({ extended: false }), obj.handlePluginAdminPostReq);
6544 + obj.app.get(url + 'pluginHandler.js', obj.handlePluginJS);
6545 + }
6546 +
6547 + // New account CAPTCHA request
6548 + if ((domain.newaccountscaptcha != null) && (domain.newaccountscaptcha !== false)) {
6549 + obj.app.get(url + 'newAccountCaptcha.ashx', handleNewAccountCaptchaRequest);
6550 + }
6551 +
6552 + // Check CrowdSec Bounser if configured
6553 + if (parent.crowdSecBounser != null) {
6554 + obj.app.get(url + 'captcha.ashx', handleCaptchaGetRequest);
6555 + obj.app.post(url + 'captcha.ashx', obj.bodyParser.urlencoded({ extended: false }), handleCaptchaPostRequest);
6556 + }
6557 +
6558 + // Setup IP-KVM relay if supported
6559 + if (domain.ipkvm) {
6560 + obj.app.ws(url + 'ipkvm.ashx/*', function (ws, req) {
6561 + const domain = getDomain(req);
6562 + if (domain == null) { parent.debug('web', 'ipkvm: failed domain checks.'); try { ws.close(); } catch (ex) { } return; }
6563 + parent.ipKvmManager.handleIpKvmWebSocket(domain, ws, req);
6564 });
6568 - });
6569 - }
6570 -
6571 - // Setup firebase push only server
6572 - if ((obj.parent.firebase != null) && (obj.parent.config.firebase)) {
6573 - if (obj.parent.config.firebase.pushrelayserver) { parent.debug('email', 'Firebase-pushrelay-handler'); obj.app.post(url + 'firebaserelay.aspx', obj.bodyParser.urlencoded({ extended: false }), handleFirebasePushOnlyRelayRequest); }
6574 - if (obj.parent.config.firebase.relayserver) { parent.debug('email', 'Firebase-relay-handler'); obj.app.ws(url + 'firebaserelay.aspx', handleFirebaseRelayRequest); }
6575 - }
6576 -
6577 - // Setup auth strategies using passport if needed
6578 - if (typeof domain.authstrategies == 'object') {
6579 - // Twitter
6580 - if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.twitter) != 0) {
6581 - obj.app.get(url + 'auth-twitter', function (req, res, next) {
6582 - var domain = getDomain(req);
6583 - if (domain.passport == null) { next(); return; }
6584 - domain.passport.authenticate('twitter-' + domain.id)(req, res, function (err) { console.log('c1', err, req.session); next(); });
6565 + obj.app.get(url + 'ipkvm.ashx/*', function (req, res, next) {
6566 + const domain = getDomain(req);
6567 + if (domain == null) return;
6568 + parent.ipKvmManager.handleIpKvmGet(domain, req, res, next);
6569 });
6586 - obj.app.get(url + 'auth-twitter-callback', function (req, res, next) {
6587 - var domain = getDomain(req);
6588 - if (domain.passport == null) { next(); return; }
6589 - if ((Object.keys(req.session).length == 0) && (req.query.nmr == null)) {
6590 - // This is an empty session likely due to the 302 redirection, redirect again (this is a bit of a hack).
6591 - var url = req.url;
6592 - if (url.indexOf('?') >= 0) { url += '&nmr=1'; } else { url += '?nmr=1'; } // Add this to the URL to prevent redirect loop.
6593 - res.set('Content-Type', 'text/html');
6594 - res.end('<html><head><meta http-equiv="refresh" content=0;url="' + url + '"></head><body></body></html>');
6595 - } else {
6596 - domain.passport.authenticate('twitter-' + domain.id, { failureRedirect: '/' })(req, res, function (err) { if (err != null) { console.log(err); } next(); });
6597 - }
6598 - }, handleStrategyLogin);
6570 }
6600 -
6601 - // Google
6602 - if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.google) != 0) {
6603 - obj.app.get(url + 'auth-google', function (req, res, next) {
6604 - var domain = getDomain(req);
6605 - if (domain.passport == null) { next(); return; }
6606 - domain.passport.authenticate('google-' + domain.id, { scope: ['profile', 'email'] })(req, res, next);
6571 +
6572 + // Setup RDP unless indicated as disabled
6573 + if (domain.mstsc !== false) {
6574 + obj.app.get(url + 'mstsc.html', function (req, res) { handleMSTSCRequest(req, res, 'mstsc'); });
6575 + obj.app.ws(url + 'mstscrelay.ashx', function (ws, req) {
6576 + const domain = getDomain(req);
6577 + if (domain == null) { parent.debug('web', 'mstsc: failed checks.'); try { ws.close(); } catch (e) { } return; }
6578 + // If no user is logged in and we have a default user, set it now.
6579 + if ((req.session.userid == null) && (typeof obj.args.user == 'string') && (obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()])) { req.session.userid = 'user/' + domain.id + '/' + obj.args.user.toLowerCase(); }
6580 + try { require('./apprelays.js').CreateMstscRelay(obj, obj.db, ws, req, obj.args, domain); } catch (ex) { console.log(ex); }
6581 });
6608 - obj.app.get(url + 'auth-google-callback', function (req, res, next) {
6609 - var domain = getDomain(req);
6610 - if (domain.passport == null) { next(); return; }
6611 - domain.passport.authenticate('google-' + domain.id, { failureRedirect: '/' })(req, res, function (err) { if (err != null) { console.log(err); } next(); });
6612 - }, handleStrategyLogin);
6613 - }
6614 -
6615 - // GitHub
6616 - if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.github) != 0) {
6617 - obj.app.get(url + 'auth-github', function (req, res, next) {
6618 - var domain = getDomain(req);
6619 - if (domain.passport == null) { next(); return; }
6620 - domain.passport.authenticate('github-' + domain.id, { scope: ['user:email'] })(req, res, next);
6582 + }
6583 +
6584 + // Setup SSH if needed
6585 + if (domain.ssh === true) {
6586 + obj.app.get(url + 'ssh.html', function (req, res) { handleMSTSCRequest(req, res, 'ssh'); });
6587 + obj.app.ws(url + 'sshrelay.ashx', function (ws, req) {
6588 + const domain = getDomain(req);
6589 + if (domain == null) { parent.debug('web', 'ssh: failed checks.'); try { ws.close(); } catch (e) { } return; }
6590 + // If no user is logged in and we have a default user, set it now.
6591 + if ((req.session.userid == null) && (typeof obj.args.user == 'string') && (obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()])) { req.session.userid = 'user/' + domain.id + '/' + obj.args.user.toLowerCase(); }
6592 + try { require('./apprelays.js').CreateSshRelay(obj, obj.db, ws, req, obj.args, domain); } catch (ex) { console.log(ex); }
6593 });
6622 - obj.app.get(url + 'auth-github-callback', function (req, res, next) {
6623 - var domain = getDomain(req);
6624 - if (domain.passport == null) { next(); return; }
6625 - domain.passport.authenticate('github-' + domain.id, { failureRedirect: '/' })(req, res, next);
6626 - }, handleStrategyLogin);
6627 - }
6628 -
6629 - // Reddit
6630 - if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.reddit) != 0) {
6631 - obj.app.get(url + 'auth-reddit', function (req, res, next) {
6632 - var domain = getDomain(req);
6633 - if (domain.passport == null) { next(); return; }
6634 - domain.passport.authenticate('reddit-' + domain.id, { state: obj.parent.encodeCookie({ 'p': 'reddit' }, obj.parent.loginCookieEncryptionKey), duration: 'permanent' })(req, res, next);
6594 + obj.app.ws(url + 'sshterminalrelay.ashx', function (ws, req) {
6595 + PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie, authData) {
6596 + require('./apprelays.js').CreateSshTerminalRelay(obj, obj.db, ws1, req1, domain, user, cookie, obj.args);
6597 + });
6598 + });
6599 + obj.app.ws(url + 'sshfilesrelay.ashx', function (ws, req) {
6600 + PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie, authData) {
6601 + require('./apprelays.js').CreateSshFilesRelay(obj, obj.db, ws1, req1, domain, user, cookie, obj.args);
6602 + });
6603 });
6636 - obj.app.get(url + 'auth-reddit-callback', function (req, res, next) {
6637 - var domain = getDomain(req);
6638 - if (domain.passport == null) { next(); return; }
6639 - if ((Object.keys(req.session).length == 0) && (req.query.nmr == null)) {
6640 - // This is an empty session likely due to the 302 redirection, redirect again (this is a bit of a hack).
6641 - var url = req.url;
6642 - if (url.indexOf('?') >= 0) { url += '&nmr=1'; } else { url += '?nmr=1'; } // Add this to the URL to prevent redirect loop.
6643 - res.set('Content-Type', 'text/html');
6644 - res.end('<html><head><meta http-equiv="refresh" content=0;url="' + url + '"></head><body></body></html>');
6645 - } else {
6646 - if (req.query.state != null) {
6647 - var c = obj.parent.decodeCookie(req.query.state, obj.parent.loginCookieEncryptionKey, 10); // 10 minute timeout
6648 - if ((c != null) && (c.p == 'reddit')) { domain.passport.authenticate('reddit-' + domain.id, { failureRedirect: '/' })(req, res, next); return; }
6649 - }
6650 - next();
6651 - }
6652 - }, handleStrategyLogin);
6604 }
6605 +
6606 + // Setup firebase push only server
6607 + if ((obj.parent.firebase != null) && (obj.parent.config.firebase)) {
6608 + if (obj.parent.config.firebase.pushrelayserver) { parent.debug('email', 'Firebase-pushrelay-handler'); obj.app.post(url + 'firebaserelay.aspx', obj.bodyParser.urlencoded({ extended: false }), handleFirebasePushOnlyRelayRequest); }
6609 + if (obj.parent.config.firebase.relayserver) { parent.debug('email', 'Firebase-relay-handler'); obj.app.ws(url + 'firebaserelay.aspx', handleFirebaseRelayRequest); }
6610 + }
6611 +
6612 + // Setup auth strategies using passport if needed
6613 + if (typeof domain.authstrategies == 'object') {
6614 + // Twitter
6615 + if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.twitter) != 0) {
6616 + obj.app.get(url + 'auth-twitter', function (req, res, next) {
6617 + var domain = getDomain(req);
6618 + if (domain.passport == null) { next(); return; }
6619 + domain.passport.authenticate('twitter-' + domain.id)(req, res, function (err) { console.log('c1', err, req.session); next(); });
6620 + });
6621 + obj.app.get(url + 'auth-twitter-callback', function (req, res, next) {
6622 + var domain = getDomain(req);
6623 + if (domain.passport == null) { next(); return; }
6624 + if ((Object.keys(req.session).length == 0) && (req.query.nmr == null)) {
6625 + // This is an empty session likely due to the 302 redirection, redirect again (this is a bit of a hack).
6626 + var url = req.url;
6627 + if (url.indexOf('?') >= 0) { url += '&nmr=1'; } else { url += '?nmr=1'; } // Add this to the URL to prevent redirect loop.
6628 + res.set('Content-Type', 'text/html');
6629 + res.end('<html><head><meta http-equiv="refresh" content=0;url="' + url + '"></head><body></body></html>');
6630 + } else {
6631 + domain.passport.authenticate('twitter-' + domain.id, { failureRedirect: '/' })(req, res, function (err) { if (err != null) { console.log(err); } next(); });
6632 + }
6633 + }, handleStrategyLogin);
6634 + }
6635
6655 - // Azure
6656 - if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.azure) != 0) {
6657 - obj.app.get(url + 'auth-azure', function (req, res, next) {
6658 - var domain = getDomain(req);
6659 - if (domain.passport == null) { next(); return; }
6660 - domain.passport.authenticate('azure-' + domain.id, { state: obj.parent.encodeCookie({ 'p': 'azure' }, obj.parent.loginCookieEncryptionKey) })(req, res, next);
6661 - });
6662 - obj.app.get(url + 'auth-azure-callback', function (req, res, next) {
6663 - var domain = getDomain(req);
6664 - if (domain.passport == null) { next(); return; }
6665 - if ((Object.keys(req.session).length == 0) && (req.query.nmr == null)) {
6666 - // This is an empty session likely due to the 302 redirection, redirect again (this is a bit of a hack).
6667 - var url = req.url;
6668 - if (url.indexOf('?') >= 0) { url += '&nmr=1'; } else { url += '?nmr=1'; } // Add this to the URL to prevent redirect loop.
6669 - res.set('Content-Type', 'text/html');
6670 - res.end('<html><head><meta http-equiv="refresh" content=0;url="' + url + '"></head><body></body></html>');
6671 - } else {
6672 - if (req.query.state != null) {
6673 - var c = obj.parent.decodeCookie(req.query.state, obj.parent.loginCookieEncryptionKey, 10); // 10 minute timeout
6674 - if ((c != null) && (c.p == 'azure')) { domain.passport.authenticate('azure-' + domain.id, { failureRedirect: '/' })(req, res, next); return; }
6636 + // Google
6637 + if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.google) != 0) {
6638 + obj.app.get(url + 'auth-google', function (req, res, next) {
6639 + var domain = getDomain(req);
6640 + if (domain.passport == null) { next(); return; }
6641 + domain.passport.authenticate('google-' + domain.id, { scope: ['profile', 'email'] })(req, res, next);
6642 + });
6643 + obj.app.get(url + 'auth-google-callback', function (req, res, next) {
6644 + var domain = getDomain(req);
6645 + if (domain.passport == null) { next(); return; }
6646 + domain.passport.authenticate('google-' + domain.id, { failureRedirect: '/' })(req, res, function (err) { if (err != null) { console.log(err); } next(); });
6647 + }, handleStrategyLogin);
6648 + }
6649 +
6650 + // GitHub
6651 + if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.github) != 0) {
6652 + obj.app.get(url + 'auth-github', function (req, res, next) {
6653 + var domain = getDomain(req);
6654 + if (domain.passport == null) { next(); return; }
6655 + domain.passport.authenticate('github-' + domain.id, { scope: ['user:email'] })(req, res, next);
6656 + });
6657 + obj.app.get(url + 'auth-github-callback', function (req, res, next) {
6658 + var domain = getDomain(req);
6659 + if (domain.passport == null) { next(); return; }
6660 + domain.passport.authenticate('github-' + domain.id, { failureRedirect: '/' })(req, res, next);
6661 + }, handleStrategyLogin);
6662 + }
6663 +
6664 + // Reddit
6665 + if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.reddit) != 0) {
6666 + obj.app.get(url + 'auth-reddit', function (req, res, next) {
6667 + var domain = getDomain(req);
6668 + if (domain.passport == null) { next(); return; }
6669 + domain.passport.authenticate('reddit-' + domain.id, { state: obj.parent.encodeCookie({ 'p': 'reddit' }, obj.parent.loginCookieEncryptionKey), duration: 'permanent' })(req, res, next);
6670 + });
6671 + obj.app.get(url + 'auth-reddit-callback', function (req, res, next) {
6672 + var domain = getDomain(req);
6673 + if (domain.passport == null) { next(); return; }
6674 + if ((Object.keys(req.session).length == 0) && (req.query.nmr == null)) {
6675 + // This is an empty session likely due to the 302 redirection, redirect again (this is a bit of a hack).
6676 + var url = req.url;
6677 + if (url.indexOf('?') >= 0) { url += '&nmr=1'; } else { url += '?nmr=1'; } // Add this to the URL to prevent redirect loop.
6678 + res.set('Content-Type', 'text/html');
6679 + res.end('<html><head><meta http-equiv="refresh" content=0;url="' + url + '"></head><body></body></html>');
6680 + } else {
6681 + if (req.query.state != null) {
6682 + var c = obj.parent.decodeCookie(req.query.state, obj.parent.loginCookieEncryptionKey, 10); // 10 minute timeout
6683 + if ((c != null) && (c.p == 'reddit')) { domain.passport.authenticate('reddit-' + domain.id, { failureRedirect: '/' })(req, res, next); return; }
6684 + }
6685 + next();
6686 + }
6687 + }, handleStrategyLogin);
6688 + }
6689 +
6690 + // Azure
6691 + if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.azure) != 0) {
6692 + obj.app.get(url + 'auth-azure', function (req, res, next) {
6693 + var domain = getDomain(req);
6694 + if (domain.passport == null) { next(); return; }
6695 + domain.passport.authenticate('azure-' + domain.id, { state: obj.parent.encodeCookie({ 'p': 'azure' }, obj.parent.loginCookieEncryptionKey) })(req, res, next);
6696 + });
6697 + obj.app.get(url + 'auth-azure-callback', function (req, res, next) {
6698 + var domain = getDomain(req);
6699 + if (domain.passport == null) { next(); return; }
6700 + if ((Object.keys(req.session).length == 0) && (req.query.nmr == null)) {
6701 + // This is an empty session likely due to the 302 redirection, redirect again (this is a bit of a hack).
6702 + var url = req.url;
6703 + if (url.indexOf('?') >= 0) { url += '&nmr=1'; } else { url += '?nmr=1'; } // Add this to the URL to prevent redirect loop.
6704 + res.set('Content-Type', 'text/html');
6705 + res.end('<html><head><meta http-equiv="refresh" content=0;url="' + url + '"></head><body></body></html>');
6706 + } else {
6707 + if (req.query.state != null) {
6708 + var c = obj.parent.decodeCookie(req.query.state, obj.parent.loginCookieEncryptionKey, 10); // 10 minute timeout
6709 + if ((c != null) && (c.p == 'azure')) { domain.passport.authenticate('azure-' + domain.id, { failureRedirect: '/' })(req, res, next); return; }
6710 + }
6711 + next();
6712 }
6676 - next();
6713 + }, handleStrategyLogin);
6714 + }
6715 +
6716 + // Setup OpenID Connect URLs
6717 + if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.oidc) != 0) {
6718 +
6719 + obj.app.get(url + 'auth-oidc', function (req, res, next) {
6720 + var domain = getDomain(req);
6721 + if (domain.passport == null) { next(); return; }
6722 + domain.passport.authenticate(`oidc-${domain.id}`, { failureRedirect: '/', failureFlash: true })(req, res, next);
6723 + });
6724 + let redirectPath
6725 + if (typeof domain.authstrategies.oidc.client.redirect_uri == 'string') {
6726 + redirectPath = (new URL(domain.authstrategies.oidc.client.redirect_uri)).pathname
6727 + } else if (Array.isArray(domain.authstrategies.oidc.client.redirect_uris)) {
6728 + redirectPath = (new URL(domain.authstrategies.oidc.client.redirect_uris[0])).pathname
6729 + } else {
6730 + redirectPath = url + 'auth-oidc-callback'
6731 }
6678 - }, handleStrategyLogin);
6679 - }
6732 + obj.app.get(redirectPath, obj.bodyParser.urlencoded({ extended: false }), function (req, res, next) {
6733 + var domain = getDomain(req);
6734 + if (domain.passport == null) { next(); return; }
6735 + domain.passport.authenticate(`oidc-${domain.id}`, { failureRedirect: '/', failureFlash: true })(req, res, next);
6736 + }, handleStrategyLogin);
6737 + }
6738
6681 - // Generic OpenID Connect
6682 - if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.oidc) != 0) {
6683 - var flash = require('connect-flash');
6684 - obj.app.use(flash());
6685 - obj.app.get(url + 'auth-oidc', function (req, res, next) {
6686 - var domain = getDomain(req);
6687 - if (domain.passport == null) { next(); return; }
6688 - domain.passport.authenticate('oidc-' + domain.id, { failureRedirect: '/', failureFlash: true })(req, res, next);
6689 - });
6690 - obj.app.get(url + 'oidc-callback', function (req, res, next) {
6691 - var domain = getDomain(req);
6692 - if (domain.passport == null) { next(); return; }
6693 - domain.passport.authenticate('oidc-' + domain.id, { failureRedirect: '/', failureFlash: true })(req, res, next);
6694 - }, handleStrategyLogin);
6695 - }
6696 -
6697 - // Generic SAML
6698 - if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.saml) != 0) {
6699 - obj.app.get(url + 'auth-saml', function (req, res, next) {
6700 - var domain = getDomain(req);
6701 - if (domain.passport == null) { next(); return; }
6702 - domain.passport.authenticate('saml-' + domain.id, { failureRedirect: '/', failureFlash: true })(req, res, next);
6703 - });
6704 - obj.app.post(url + 'auth-saml-callback', obj.bodyParser.urlencoded({ extended: false }), function (req, res, next) {
6705 - var domain = getDomain(req);
6706 - if (domain.passport == null) { next(); return; }
6707 - domain.passport.authenticate('saml-' + domain.id, { failureRedirect: '/', failureFlash: true })(req, res, next);
6708 - }, handleStrategyLogin);
6709 - }
6710 -
6711 - // Intel SAML
6712 - if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.intelSaml) != 0) {
6713 - obj.app.get(url + 'auth-intel', function (req, res, next) {
6714 - var domain = getDomain(req);
6715 - if (domain.passport == null) { next(); return; }
6716 - domain.passport.authenticate('isaml-' + domain.id, { failureRedirect: '/', failureFlash: true })(req, res, next);
6717 - });
6718 - obj.app.post(url + 'auth-intel-callback', obj.bodyParser.urlencoded({ extended: false }), function (req, res, next) {
6719 - var domain = getDomain(req);
6720 - if (domain.passport == null) { next(); return; }
6721 - domain.passport.authenticate('isaml-' + domain.id, { failureRedirect: '/', failureFlash: true })(req, res, next);
6722 - }, handleStrategyLogin);
6723 - }
6724 -
6725 - // JumpCloud SAML
6726 - if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.jumpCloudSaml) != 0) {
6727 - obj.app.get(url + 'auth-jumpcloud', function (req, res, next) {
6728 - var domain = getDomain(req);
6729 - if (domain.passport == null) { next(); return; }
6730 - domain.passport.authenticate('jumpcloud-' + domain.id, { failureRedirect: '/', failureFlash: true })(req, res, next);
6731 - });
6732 - obj.app.post(url + 'auth-jumpcloud-callback', obj.bodyParser.urlencoded({ extended: false }), function (req, res, next) {
6733 - var domain = getDomain(req);
6734 - if (domain.passport == null) { next(); return; }
6735 - domain.passport.authenticate('jumpcloud-' + domain.id, { failureRedirect: '/', failureFlash: true })(req, res, next);
6736 - }, handleStrategyLogin);
6737 - }
6738 - }
6739 + // Generic SAML
6740 + if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.saml) != 0) {
6741 + obj.app.get(url + 'auth-saml', function (req, res, next) {
6742 + var domain = getDomain(req);
6743 + if (domain.passport == null) { next(); return; }
6744 + domain.passport.authenticate('saml-' + domain.id, { failureRedirect: '/', failureFlash: true })(req, res, next);
6745 + });
6746 + obj.app.post(url + 'auth-saml-callback', obj.bodyParser.urlencoded({ extended: false }), function (req, res, next) {
6747 + var domain = getDomain(req);
6748 + if (domain.passport == null) { next(); return; }
6749 + domain.passport.authenticate('saml-' + domain.id, { failureRedirect: '/', failureFlash: true })(req, res, next);
6750 + }, handleStrategyLogin);
6751 + }
6752
6740 - // Server redirects
6741 - if (parent.config.domains[i].redirects) { for (var j in parent.config.domains[i].redirects) { if (j[0] != '_') { obj.app.get(url + j, obj.handleDomainRedirect); } } }
6753 + // Intel SAML
6754 + if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.intelSaml) != 0) {
6755 + obj.app.get(url + 'auth-intel', function (req, res, next) {
6756 + var domain = getDomain(req);
6757 + if (domain.passport == null) { next(); return; }
6758 + domain.passport.authenticate('isaml-' + domain.id, { failureRedirect: '/', failureFlash: true })(req, res, next);
6759 + });
6760 + obj.app.post(url + 'auth-intel-callback', obj.bodyParser.urlencoded({ extended: false }), function (req, res, next) {
6761 + var domain = getDomain(req);
6762 + if (domain.passport == null) { next(); return; }
6763 + domain.passport.authenticate('isaml-' + domain.id, { failureRedirect: '/', failureFlash: true })(req, res, next);
6764 + }, handleStrategyLogin);
6765 + }
6766
6743 - // Server picture
6744 - obj.app.get(url + 'serverpic.ashx', function (req, res) {
6745 - // Check if we have "server.jpg" in the data folder, if so, use that.
6746 - if ((parent.configurationFiles != null) && (parent.configurationFiles['server.png'] != null)) {
6747 - res.set({ 'Content-Type': 'image/png' });
6748 - res.send(parent.configurationFiles['server.png']);
6749 - } else {
6767 + // JumpCloud SAML
6768 + if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.jumpCloudSaml) != 0) {
6769 + obj.app.get(url + 'auth-jumpcloud', function (req, res, next) {
6770 + var domain = getDomain(req);
6771 + if (domain.passport == null) { next(); return; }
6772 + domain.passport.authenticate('jumpcloud-' + domain.id, { failureRedirect: '/', failureFlash: true })(req, res, next);
6773 + });
6774 + obj.app.post(url + 'auth-jumpcloud-callback', obj.bodyParser.urlencoded({ extended: false }), function (req, res, next) {
6775 + var domain = getDomain(req);
6776 + if (domain.passport == null) { next(); return; }
6777 + domain.passport.authenticate('jumpcloud-' + domain.id, { failureRedirect: '/', failureFlash: true })(req, res, next);
6778 + }, handleStrategyLogin);
6779 + }
6780 + }
6781 +
6782 + // Server redirects
6783 + if (parent.config.domains[i].redirects) { for (var j in parent.config.domains[i].redirects) { if (j[0] != '_') { obj.app.get(url + j, obj.handleDomainRedirect); } } }
6784 +
6785 + // Server picture
6786 + obj.app.get(url + 'serverpic.ashx', function (req, res) {
6787 // Check if we have "server.jpg" in the data folder, if so, use that.
6751 - var p = obj.path.join(obj.parent.datapath, 'server.png');
6752 - if (obj.fs.existsSync(p)) {
6753 - // Use the data folder server picture
6754 - try { res.sendFile(p); } catch (ex) { res.sendStatus(404); }
6788 + if ((parent.configurationFiles != null) && (parent.configurationFiles['server.png'] != null)) {
6789 + res.set({ 'Content-Type': 'image/png' });
6790 + res.send(parent.configurationFiles['server.png']);
6791 } else {
6756 - var domain = getDomain(req);
6757 - if ((domain != null) && (domain.webpublicpath != null) && (obj.fs.existsSync(obj.path.join(domain.webpublicpath, 'images/server-256.png')))) {
6758 - // Use the domain server picture
6759 - try { res.sendFile(obj.path.join(domain.webpublicpath, 'images/server-256.png')); } catch (ex) { res.sendStatus(404); }
6760 - } else if (parent.webPublicOverridePath && obj.fs.existsSync(obj.path.join(obj.parent.webPublicOverridePath, 'images/server-256.png'))) {
6761 - // Use the override server picture
6762 - try { res.sendFile(obj.path.join(obj.parent.webPublicOverridePath, 'images/server-256.png')); } catch (ex) { res.sendStatus(404); }
6792 + // Check if we have "server.jpg" in the data folder, if so, use that.
6793 + var p = obj.path.join(obj.parent.datapath, 'server.png');
6794 + if (obj.fs.existsSync(p)) {
6795 + // Use the data folder server picture
6796 + try { res.sendFile(p); } catch (ex) { res.sendStatus(404); }
6797 } else {
6764 - // Use the default server picture
6765 - try { res.sendFile(obj.path.join(obj.parent.webPublicPath, 'images/server-256.png')); } catch (ex) { res.sendStatus(404); }
6798 + var domain = getDomain(req);
6799 + if ((domain != null) && (domain.webpublicpath != null) && (obj.fs.existsSync(obj.path.join(domain.webpublicpath, 'images/server-256.png')))) {
6800 + // Use the domain server picture
6801 + try { res.sendFile(obj.path.join(domain.webpublicpath, 'images/server-256.png')); } catch (ex) { res.sendStatus(404); }
6802 + } else if (parent.webPublicOverridePath && obj.fs.existsSync(obj.path.join(obj.parent.webPublicOverridePath, 'images/server-256.png'))) {
6803 + // Use the override server picture
6804 + try { res.sendFile(obj.path.join(obj.parent.webPublicOverridePath, 'images/server-256.png')); } catch (ex) { res.sendStatus(404); }
6805 + } else {
6806 + // Use the default server picture
6807 + try { res.sendFile(obj.path.join(obj.parent.webPublicPath, 'images/server-256.png')); } catch (ex) { res.sendStatus(404); }
6808 + }
6809 }
6810 }
6768 - }
6769 - });
6770 -
6771 - // Receive mesh agent connections
6772 - obj.app.ws(url + 'agent.ashx', function (ws, req) {
6773 - var domain = checkAgentIpAddress(ws, req);
6774 - if (domain == null) { parent.debug('web', 'Got agent connection with bad domain or blocked IP address ' + req.clientIp + ', holding.'); return; }
6775 - if (domain.agentkey && ((req.query.key == null) || (domain.agentkey.indexOf(req.query.key) == -1))) { return; } // If agent key is required and not provided or not valid, just hold the websocket and do nothing.
6776 - //console.log('Agent connect: ' + req.clientIp);
6777 - try { obj.meshAgentHandler.CreateMeshAgent(obj, obj.db, ws, req, obj.args, domain); } catch (e) { console.log(e); }
6778 - });
6779 -
6780 - // Setup MQTT broker over websocket
6781 - if (obj.parent.mqttbroker != null) {
6782 - obj.app.ws(url + 'mqtt.ashx', function (ws, req) {
6783 - var domain = checkAgentIpAddress(ws, req);
6784 - if (domain == null) { parent.debug('web', 'Got agent connection with bad domain or blocked IP address ' + req.clientIp + ', holding.'); return; }
6785 - var serialtunnel = SerialTunnel();
6786 - serialtunnel.xtransport = 'ws';
6787 - serialtunnel.xdomain = domain;
6788 - serialtunnel.xip = req.clientIp;
6789 - ws.on('message', function (b) { serialtunnel.updateBuffer(Buffer.from(b, 'binary')) });
6790 - serialtunnel.forwardwrite = function (b) { ws.send(b, 'binary') }
6791 - ws.on('close', function () { serialtunnel.emit('end'); });
6792 - obj.parent.mqttbroker.handle(serialtunnel); // Pass socket wrapper to MQTT broker
6811 });
6794 - }
6795 -
6796 - // Setup any .well-known folders
6797 - var p = obj.parent.path.join(obj.parent.datapath, '.well-known' + ((parent.config.domains[i].id == '') ? '' : ('-' + parent.config.domains[i].id)));
6798 - if (obj.parent.fs.existsSync(p)) { obj.app.use(url + '.well-known', obj.express.static(p)); }
6799 -
6800 - // Setup the alternative agent-only port
6801 - if (obj.agentapp) {
6802 - // Receive mesh agent connections on alternate port
6803 - obj.agentapp.ws(url + 'agent.ashx', function (ws, req) {
6812 +
6813 + // Receive mesh agent connections
6814 + obj.app.ws(url + 'agent.ashx', function (ws, req) {
6815 var domain = checkAgentIpAddress(ws, req);
6816 if (domain == null) { parent.debug('web', 'Got agent connection with bad domain or blocked IP address ' + req.clientIp + ', holding.'); return; }
6817 if (domain.agentkey && ((req.query.key == null) || (domain.agentkey.indexOf(req.query.key) == -1))) { return; } // If agent key is required and not provided or not valid, just hold the websocket and do nothing.
6818 + //console.log('Agent connect: ' + req.clientIp);
6819 try { obj.meshAgentHandler.CreateMeshAgent(obj, obj.db, ws, req, obj.args, domain); } catch (e) { console.log(e); }
6820 });
6809 -
6810 - // Setup mesh relay on alternative agent-only port
6811 - obj.agentapp.ws(url + 'meshrelay.ashx', function (ws, req) {
6812 - PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie, authData) {
6813 - if (((parent.config.settings.desktopmultiplex === true) || (domain.desktopmultiplex === true)) && (req.query.p == 2)) {
6814 - obj.meshDesktopMultiplexHandler.CreateMeshRelay(obj, ws1, req1, domain, user, cookie); // Desktop multiplexor 1-to-n
6815 - } else {
6816 - obj.meshRelayHandler.CreateMeshRelay(obj, ws1, req1, domain, user, cookie); // Normal relay 1-to-1
6817 - }
6821 +
6822 + // Setup MQTT broker over websocket
6823 + if (obj.parent.mqttbroker != null) {
6824 + obj.app.ws(url + 'mqtt.ashx', function (ws, req) {
6825 + var domain = checkAgentIpAddress(ws, req);
6826 + if (domain == null) { parent.debug('web', 'Got agent connection with bad domain or blocked IP address ' + req.clientIp + ', holding.'); return; }
6827 + var serialtunnel = SerialTunnel();
6828 + serialtunnel.xtransport = 'ws';
6829 + serialtunnel.xdomain = domain;
6830 + serialtunnel.xip = req.clientIp;
6831 + ws.on('message', function (b) { serialtunnel.updateBuffer(Buffer.from(b, 'binary')) });
6832 + serialtunnel.forwardwrite = function (b) { ws.send(b, 'binary') }
6833 + ws.on('close', function () { serialtunnel.emit('end'); });
6834 + obj.parent.mqttbroker.handle(serialtunnel); // Pass socket wrapper to MQTT broker
6835 });
6819 - });
6820 -
6821 - // Allows agents to transfer files
6822 - obj.agentapp.ws(url + 'devicefile.ashx', function (ws, req) { obj.meshDeviceFileHandler.CreateMeshDeviceFile(obj, ws, null, req, domain); });
6823 -
6824 - // Setup agent to/from server file transfer handler
6825 - obj.agentapp.ws(url + 'agenttransfer.ashx', handleAgentFileTransfer); // Setup agent to/from server file transfer handler
6826 -
6827 - // Setup agent downloads for meshcore updates
6828 - obj.agentapp.get(url + 'meshagents', obj.handleMeshAgentRequest);
6829 - }
6830 -
6831 - // Setup web relay on this web server if needed
6832 - // We set this up when a DNS name is used as a web relay instead of a port
6833 - if (obj.args.relaydns != null) {
6834 - obj.webRelayRouter = require('express').Router();
6835 -
6836 - // This is the magic URL that will setup the relay session
6837 - obj.webRelayRouter.get('/control-redirect.ashx', function (req, res, next) {
6838 - if (obj.args.relaydns.indexOf(req.hostname) == -1) { res.sendStatus(404); return; }
6839 - if ((req.session.userid == null) && obj.args.user && obj.users['user//' + obj.args.user.toLowerCase()]) { req.session.userid = 'user//' + obj.args.user.toLowerCase(); } // Use a default user if needed
6840 - res.set({ 'Cache-Control': 'no-store' });
6841 - parent.debug('web', 'webRelaySetup');
6842 -
6843 - // Decode the relay cookie
6844 - if (req.query.c == null) { res.sendStatus(404); return; }
6845 -
6846 - // Decode and check if this relay cookie is valid
6847 - var userid, domainid, domain, nodeid, addr, port, appid, webSessionId, expire, publicid;
6848 - const urlCookie = obj.parent.decodeCookie(req.query.c, parent.loginCookieEncryptionKey, 32); // Allow cookies up to 32 minutes old. The web page will renew this cookie every 30 minutes.
6849 - if (urlCookie == null) { res.sendStatus(404); return; }
6850 -
6851 - // Decode the incoming cookie
6852 - if ((urlCookie.ruserid != null) && (urlCookie.x != null)) {
6853 - if (parent.webserver.destroyedSessions[urlCookie.ruserid + '/' + urlCookie.x] != null) { res.sendStatus(404); return; }
6854 -
6855 - // This is a standard user, figure out what our web relay will be.
6856 - if (req.session.x != urlCookie.x) { req.session.x = urlCookie.x; } // Set the sessionid if missing
6857 - if (req.session.userid != urlCookie.ruserid) { req.session.userid = urlCookie.ruserid; } // Set the session userid if missing
6858 - if (req.session.z) { delete req.session.z; } // Clear the web relay guest session
6859 - userid = req.session.userid;
6860 - domainid = userid.split('/')[1];
6861 - domain = parent.config.domains[domainid];
6862 - nodeid = ((req.query.relayid != null) ? req.query.relayid : req.query.n);
6863 - addr = (req.query.addr != null) ? req.query.addr : '127.0.0.1';
6864 - port = parseInt(req.query.p);
6865 - appid = parseInt(req.query.appid);
6866 - webSessionId = req.session.userid + '/' + req.session.x;
6867 -
6868 - // Check that all the required arguments are present
6869 - if ((req.session.userid == null) || (req.session.x == null) || (req.query.n == null) || (req.query.p == null) || (parent.webserver.destroyedSessions[webSessionId] != null) || ((req.query.appid != 1) && (req.query.appid != 2))) { res.redirect('/'); return; }
6870 - } else if (urlCookie.r == 8) {
6871 - // This is a guest user, figure out what our web relay will be.
6872 - userid = urlCookie.userid;
6873 - domainid = userid.split('/')[1];
6874 - domain = parent.config.domains[domainid];
6875 - nodeid = urlCookie.nid;
6876 - addr = (urlCookie.addr != null) ? urlCookie.addr : '127.0.0.1';
6877 - port = urlCookie.port;
6878 - appid = (urlCookie.p == 16) ? 2 : 1; // appid: 1 = HTTP, 2 = HTTPS
6879 - webSessionId = userid + '/' + urlCookie.pid;
6880 - publicid = urlCookie.pid;
6881 - if (req.session.x) { delete req.session.x; } // Clear the web relay sessionid
6882 - if (req.session.userid) { delete req.session.userid; } // Clear the web relay userid
6883 - if (req.session.z != webSessionId) { req.session.z = webSessionId; } // Set the web relay guest session
6884 - expire = urlCookie.expire;
6885 - if ((expire != null) && (expire <= Date.now())) { parent.debug('webrelay', 'expired link'); res.sendStatus(404); return; }
6886 - }
6887 -
6888 - // No session identifier was setup, exit now
6889 - if (webSessionId == null) { res.sendStatus(404); return; }
6890 -
6891 - // Check that we have an exact session on any of the relay DNS names
6892 - var xrelaySessionId, xrelaySession, freeRelayHost, oldestRelayTime, oldestRelayHost;
6893 - for (var hostIndex in obj.args.relaydns) {
6894 - const host = obj.args.relaydns[hostIndex];
6895 - xrelaySessionId = webSessionId + '/' + host;
6896 - xrelaySession = webRelaySessions[xrelaySessionId];
6897 - if (xrelaySession == null) {
6898 - // We found an unused hostname, save this as it could be useful.
6899 - if (freeRelayHost == null) { freeRelayHost = host; }
6900 - } else {
6901 - // Check if we already have a relay session that matches exactly what we want
6902 - if ((xrelaySession.domain.id == domain.id) && (xrelaySession.userid == userid) && (xrelaySession.nodeid == nodeid) && (xrelaySession.addr == addr) && (xrelaySession.port == port) && (xrelaySession.appid == appid)) {
6903 - // We found an exact match, we are all setup already, redirect to root of that DNS name
6904 - if (host == req.hostname) {
6905 - // Request was made on the same host, redirect to root.
6906 - res.redirect('/');
6907 - } else {
6908 - // Request was made to a different host
6909 - const httpport = ((args.aliasport != null) ? args.aliasport : args.port);
6910 - res.redirect('https://' + host + ((httpport != 443) ? (':' + httpport) : '') + '/');
6911 - }
6912 - return;
6836 + }
6837 +
6838 + // Setup any .well-known folders
6839 + var p = obj.parent.path.join(obj.parent.datapath, '.well-known' + ((parent.config.domains[i].id == '') ? '' : ('-' + parent.config.domains[i].id)));
6840 + if (obj.parent.fs.existsSync(p)) { obj.app.use(url + '.well-known', obj.express.static(p)); }
6841 +
6842 + // Setup the alternative agent-only port
6843 + if (obj.agentapp) {
6844 + // Receive mesh agent connections on alternate port
6845 + obj.agentapp.ws(url + 'agent.ashx', function (ws, req) {
6846 + var domain = checkAgentIpAddress(ws, req);
6847 + if (domain == null) { parent.debug('web', 'Got agent connection with bad domain or blocked IP address ' + req.clientIp + ', holding.'); return; }
6848 + if (domain.agentkey && ((req.query.key == null) || (domain.agentkey.indexOf(req.query.key) == -1))) { return; } // If agent key is required and not provided or not valid, just hold the websocket and do nothing.
6849 + try { obj.meshAgentHandler.CreateMeshAgent(obj, obj.db, ws, req, obj.args, domain); } catch (e) { console.log(e); }
6850 + });
6851 +
6852 + // Setup mesh relay on alternative agent-only port
6853 + obj.agentapp.ws(url + 'meshrelay.ashx', function (ws, req) {
6854 + PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie, authData) {
6855 + if (((parent.config.settings.desktopmultiplex === true) || (domain.desktopmultiplex === true)) && (req.query.p == 2)) {
6856 + obj.meshDesktopMultiplexHandler.CreateMeshRelay(obj, ws1, req1, domain, user, cookie); // Desktop multiplexor 1-to-n
6857 + } else {
6858 + obj.meshRelayHandler.CreateMeshRelay(obj, ws1, req1, domain, user, cookie); // Normal relay 1-to-1
6859 }
6914 -
6915 - // Keep a record of the oldest web relay session, this could be useful.
6916 - if (oldestRelayHost == null) {
6917 - // Oldest host not set yet, set it
6918 - oldestRelayHost = host;
6919 - oldestRelayTime = xrelaySession.lastOperation;
6860 + });
6861 + });
6862 +
6863 + // Allows agents to transfer files
6864 + obj.agentapp.ws(url + 'devicefile.ashx', function (ws, req) { obj.meshDeviceFileHandler.CreateMeshDeviceFile(obj, ws, null, req, domain); });
6865 +
6866 + // Setup agent to/from server file transfer handler
6867 + obj.agentapp.ws(url + 'agenttransfer.ashx', handleAgentFileTransfer); // Setup agent to/from server file transfer handler
6868 +
6869 + // Setup agent downloads for meshcore updates
6870 + obj.agentapp.get(url + 'meshagents', obj.handleMeshAgentRequest);
6871 + }
6872 +
6873 + // Setup web relay on this web server if needed
6874 + // We set this up when a DNS name is used as a web relay instead of a port
6875 + if (obj.args.relaydns != null) {
6876 + obj.webRelayRouter = require('express').Router();
6877 +
6878 + // This is the magic URL that will setup the relay session
6879 + obj.webRelayRouter.get('/control-redirect.ashx', function (req, res, next) {
6880 + if (obj.args.relaydns.indexOf(req.hostname) == -1) { res.sendStatus(404); return; }
6881 + if ((req.session.userid == null) && obj.args.user && obj.users['user//' + obj.args.user.toLowerCase()]) { req.session.userid = 'user//' + obj.args.user.toLowerCase(); } // Use a default user if needed
6882 + res.set({ 'Cache-Control': 'no-store' });
6883 + parent.debug('web', 'webRelaySetup');
6884 +
6885 + // Decode the relay cookie
6886 + if (req.query.c == null) { res.sendStatus(404); return; }
6887 +
6888 + // Decode and check if this relay cookie is valid
6889 + var userid, domainid, domain, nodeid, addr, port, appid, webSessionId, expire, publicid;
6890 + const urlCookie = obj.parent.decodeCookie(req.query.c, parent.loginCookieEncryptionKey, 32); // Allow cookies up to 32 minutes old. The web page will renew this cookie every 30 minutes.
6891 + if (urlCookie == null) { res.sendStatus(404); return; }
6892 +
6893 + // Decode the incoming cookie
6894 + if ((urlCookie.ruserid != null) && (urlCookie.x != null)) {
6895 + if (parent.webserver.destroyedSessions[urlCookie.ruserid + '/' + urlCookie.x] != null) { res.sendStatus(404); return; }
6896 +
6897 + // This is a standard user, figure out what our web relay will be.
6898 + if (req.session.x != urlCookie.x) { req.session.x = urlCookie.x; } // Set the sessionid if missing
6899 + if (req.session.userid != urlCookie.ruserid) { req.session.userid = urlCookie.ruserid; } // Set the session userid if missing
6900 + if (req.session.z) { delete req.session.z; } // Clear the web relay guest session
6901 + userid = req.session.userid;
6902 + domainid = userid.split('/')[1];
6903 + domain = parent.config.domains[domainid];
6904 + nodeid = ((req.query.relayid != null) ? req.query.relayid : req.query.n);
6905 + addr = (req.query.addr != null) ? req.query.addr : '127.0.0.1';
6906 + port = parseInt(req.query.p);
6907 + appid = parseInt(req.query.appid);
6908 + webSessionId = req.session.userid + '/' + req.session.x;
6909 +
6910 + // Check that all the required arguments are present
6911 + if ((req.session.userid == null) || (req.session.x == null) || (req.query.n == null) || (req.query.p == null) || (parent.webserver.destroyedSessions[webSessionId] != null) || ((req.query.appid != 1) && (req.query.appid != 2))) { res.redirect('/'); return; }
6912 + } else if (urlCookie.r == 8) {
6913 + // This is a guest user, figure out what our web relay will be.
6914 + userid = urlCookie.userid;
6915 + domainid = userid.split('/')[1];
6916 + domain = parent.config.domains[domainid];
6917 + nodeid = urlCookie.nid;
6918 + addr = (urlCookie.addr != null) ? urlCookie.addr : '127.0.0.1';
6919 + port = urlCookie.port;
6920 + appid = (urlCookie.p == 16) ? 2 : 1; // appid: 1 = HTTP, 2 = HTTPS
6921 + webSessionId = userid + '/' + urlCookie.pid;
6922 + publicid = urlCookie.pid;
6923 + if (req.session.x) { delete req.session.x; } // Clear the web relay sessionid
6924 + if (req.session.userid) { delete req.session.userid; } // Clear the web relay userid
6925 + if (req.session.z != webSessionId) { req.session.z = webSessionId; } // Set the web relay guest session
6926 + expire = urlCookie.expire;
6927 + if ((expire != null) && (expire <= Date.now())) { parent.debug('webrelay', 'expired link'); res.sendStatus(404); return; }
6928 + }
6929 +
6930 + // No session identifier was setup, exit now
6931 + if (webSessionId == null) { res.sendStatus(404); return; }
6932 +
6933 + // Check that we have an exact session on any of the relay DNS names
6934 + var xrelaySessionId, xrelaySession, freeRelayHost, oldestRelayTime, oldestRelayHost;
6935 + for (var hostIndex in obj.args.relaydns) {
6936 + const host = obj.args.relaydns[hostIndex];
6937 + xrelaySessionId = webSessionId + '/' + host;
6938 + xrelaySession = webRelaySessions[xrelaySessionId];
6939 + if (xrelaySession == null) {
6940 + // We found an unused hostname, save this as it could be useful.
6941 + if (freeRelayHost == null) { freeRelayHost = host; }
6942 } else {
6921 - // Check if this host is older then oldest so far
6922 - if (oldestRelayTime > xrelaySession.lastOperation) {
6943 + // Check if we already have a relay session that matches exactly what we want
6944 + if ((xrelaySession.domain.id == domain.id) && (xrelaySession.userid == userid) && (xrelaySession.nodeid == nodeid) && (xrelaySession.addr == addr) && (xrelaySession.port == port) && (xrelaySession.appid == appid)) {
6945 + // We found an exact match, we are all setup already, redirect to root of that DNS name
6946 + if (host == req.hostname) {
6947 + // Request was made on the same host, redirect to root.
6948 + res.redirect('/');
6949 + } else {
6950 + // Request was made to a different host
6951 + const httpport = ((args.aliasport != null) ? args.aliasport : args.port);
6952 + res.redirect('https://' + host + ((httpport != 443) ? (':' + httpport) : '') + '/');
6953 + }
6954 + return;
6955 + }
6956 +
6957 + // Keep a record of the oldest web relay session, this could be useful.
6958 + if (oldestRelayHost == null) {
6959 + // Oldest host not set yet, set it
6960 oldestRelayHost = host;
6961 oldestRelayTime = xrelaySession.lastOperation;
6962 + } else {
6963 + // Check if this host is older then oldest so far
6964 + if (oldestRelayTime > xrelaySession.lastOperation) {
6965 + oldestRelayHost = host;
6966 + oldestRelayTime = xrelaySession.lastOperation;
6967 + }
6968 }
6969 }
6970 }
6928 - }
6929 -
6930 - // Check that the user has rights to access this device
6931 - parent.webserver.GetNodeWithRights(domain, userid, nodeid, function (node, rights, visible) {
6932 - // If there is no remote control or relay rights, reject this web relay
6933 - if ((rights & 0x00200008) == 0) { res.sendStatus(404); return; } // MESHRIGHT_REMOTECONTROL or MESHRIGHT_RELAY
6934 -
6935 - // Check if there is a free relay DNS name we can use
6936 - var selectedHost = null;
6937 - if (freeRelayHost != null) {
6938 - // There is a free one, use it.
6939 - selectedHost = freeRelayHost;
6940 - } else {
6941 - // No free ones, close the oldest one
6942 - selectedHost = oldestRelayHost;
6943 - }
6944 - xrelaySessionId = webSessionId + '/' + selectedHost;
6945 -
6946 - if (selectedHost == req.hostname) {
6947 - // If this web relay session id is not free, close it now
6948 - xrelaySession = webRelaySessions[xrelaySessionId];
6949 - if (xrelaySession != null) { xrelaySession.close(); delete webRelaySessions[xrelaySessionId]; }
6950 -
6951 - // Create a web relay session
6952 - const relaySession = require('./apprelays.js').CreateWebRelaySession(obj, db, req, args, domain, userid, nodeid, addr, port, appid, xrelaySessionId, expire, node.mtype);
6953 - relaySession.xpublicid = publicid;
6954 - relaySession.onclose = function (sessionId) {
6955 - // Remove the relay session
6956 - delete webRelaySessions[sessionId];
6957 - // If there are not more relay sessions, clear the cleanup timer
6958 - if ((Object.keys(webRelaySessions).length == 0) && (obj.cleanupTimer != null)) { clearInterval(webRelayCleanupTimer); obj.cleanupTimer = null; }
6971 +
6972 + // Check that the user has rights to access this device
6973 + parent.webserver.GetNodeWithRights(domain, userid, nodeid, function (node, rights, visible) {
6974 + // If there is no remote control or relay rights, reject this web relay
6975 + if ((rights & 0x00200008) == 0) { res.sendStatus(404); return; } // MESHRIGHT_REMOTECONTROL or MESHRIGHT_RELAY
6976 +
6977 + // Check if there is a free relay DNS name we can use
6978 + var selectedHost = null;
6979 + if (freeRelayHost != null) {
6980 + // There is a free one, use it.
6981 + selectedHost = freeRelayHost;
6982 + } else {
6983 + // No free ones, close the oldest one
6984 + selectedHost = oldestRelayHost;
6985 }
6960 -
6961 - // Set the multi-tunnel session
6962 - webRelaySessions[xrelaySessionId] = relaySession;
6963 -
6964 - // Setup the cleanup timer if needed
6965 - if (obj.cleanupTimer == null) { webRelayCleanupTimer = setInterval(checkWebRelaySessionsTimeout, 10000); }
6966 -
6967 - // Redirect to root.
6968 - res.redirect('/');
6969 - } else {
6970 - if (req.query.noredirect != null) {
6971 - // No redirects allowed, fail here. This is important to make sure there is no redirect cascades
6972 - res.sendStatus(404);
6986 + xrelaySessionId = webSessionId + '/' + selectedHost;
6987 +
6988 + if (selectedHost == req.hostname) {
6989 + // If this web relay session id is not free, close it now
6990 + xrelaySession = webRelaySessions[xrelaySessionId];
6991 + if (xrelaySession != null) { xrelaySession.close(); delete webRelaySessions[xrelaySessionId]; }
6992 +
6993 + // Create a web relay session
6994 + const relaySession = require('./apprelays.js').CreateWebRelaySession(obj, db, req, args, domain, userid, nodeid, addr, port, appid, xrelaySessionId, expire, node.mtype);
6995 + relaySession.xpublicid = publicid;
6996 + relaySession.onclose = function (sessionId) {
6997 + // Remove the relay session
6998 + delete webRelaySessions[sessionId];
6999 + // If there are not more relay sessions, clear the cleanup timer
7000 + if ((Object.keys(webRelaySessions).length == 0) && (obj.cleanupTimer != null)) { clearInterval(webRelayCleanupTimer); obj.cleanupTimer = null; }
7001 + }
7002 +
7003 + // Set the multi-tunnel session
7004 + webRelaySessions[xrelaySessionId] = relaySession;
7005 +
7006 + // Setup the cleanup timer if needed
7007 + if (obj.cleanupTimer == null) { webRelayCleanupTimer = setInterval(checkWebRelaySessionsTimeout, 10000); }
7008 +
7009 + // Redirect to root.
7010 + res.redirect('/');
7011 } else {
6974 - // Request was made to a different host, redirect using the full URL so an HTTP cookie can be created on the other DNS name.
6975 - const httpport = ((args.aliasport != null) ? args.aliasport : args.port);
6976 - res.redirect('https://' + selectedHost + ((httpport != 443) ? (':' + httpport) : '') + req.url + '&noredirect=1');
7012 + if (req.query.noredirect != null) {
7013 + // No redirects allowed, fail here. This is important to make sure there is no redirect cascades
7014 + res.sendStatus(404);
7015 + } else {
7016 + // Request was made to a different host, redirect using the full URL so an HTTP cookie can be created on the other DNS name.
7017 + const httpport = ((args.aliasport != null) ? args.aliasport : args.port);
7018 + res.redirect('https://' + selectedHost + ((httpport != 443) ? (':' + httpport) : '') + req.url + '&noredirect=1');
7019 + }
7020 }
6978 - }
7021 + });
7022 });
7023 +
7024 + // Handle all incoming requests as web relays
7025 + obj.webRelayRouter.get('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
7026 +
7027 + // Handle all incoming requests as web relays
7028 + obj.webRelayRouter.post('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
7029 +
7030 + // Handle all incoming requests as web relays
7031 + obj.webRelayRouter.put('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
7032 +
7033 + // Handle all incoming requests as web relays
7034 + obj.webRelayRouter.delete('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
7035 +
7036 + // Handle all incoming requests as web relays
7037 + obj.webRelayRouter.options('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
7038 +
7039 + // Handle all incoming requests as web relays
7040 + obj.webRelayRouter.head('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
7041 + }
7042 +
7043 + // Indicates to ExpressJS that the override public folder should be used to serve static files.
7044 + if (parent.config.domains[i].webpublicpath != null) {
7045 + // Use domain public path
7046 + obj.app.use(url, obj.express.static(parent.config.domains[i].webpublicpath));
7047 + } else if (obj.parent.webPublicOverridePath != null) {
7048 + // Use override path
7049 + obj.app.use(url, obj.express.static(obj.parent.webPublicOverridePath));
7050 + }
7051 +
7052 + // Indicates to ExpressJS that the default public folder should be used to serve static files.
7053 + obj.app.use(url, obj.express.static(obj.parent.webPublicPath));
7054 +
7055 + // Start regular disconnection list flush every 2 minutes.
7056 + obj.wsagentsDisconnectionsTimer = setInterval(function () { obj.wsagentsDisconnections = {}; }, 120000);
7057 + }
7058 + }
7059 + function finalizeWebserver() {
7060 + // Setup all HTTP handlers
7061 + setupHTTPHandlers()
7062 +
7063 + // Handle 404 error
7064 + if (obj.args.nice404 !== false) {
7065 + obj.app.use(function (req, res, next) {
7066 + parent.debug('web', '404 Error ' + req.url);
7067 + var domain = getDomain(req);
7068 + if ((domain == null) || (domain.auth == 'sspi')) { res.sendStatus(404); return; }
7069 + if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL
7070 + const cspNonce = obj.crypto.randomBytes(15).toString('base64');
7071 + res.set({ 'Content-Security-Policy': "default-src 'none'; script-src 'self' 'nonce-" + cspNonce + "'; img-src 'self'; style-src 'self' 'nonce-" + cspNonce + "';" }); // This page supports very tight CSP policy
7072 + res.status(404).render(getRenderPage((domain.sitestyle == 2) ? 'error4042' : 'error404', req, domain), getRenderArgs({ cspNonce: cspNonce }, req, domain));
7073 });
6981 -
6982 - // Handle all incoming requests as web relays
6983 - obj.webRelayRouter.get('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
6984 -
6985 - // Handle all incoming requests as web relays
6986 - obj.webRelayRouter.post('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
6987 -
6988 - // Handle all incoming requests as web relays
6989 - obj.webRelayRouter.put('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
6990 -
6991 - // Handle all incoming requests as web relays
6992 - obj.webRelayRouter.delete('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
6993 -
6994 - // Handle all incoming requests as web relays
6995 - obj.webRelayRouter.options('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
6996 -
6997 - // Handle all incoming requests as web relays
6998 - obj.webRelayRouter.head('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
6999 - }
7000 -
7001 - // Indicates to ExpressJS that the override public folder should be used to serve static files.
7002 - if (parent.config.domains[i].webpublicpath != null) {
7003 - // Use domain public path
7004 - obj.app.use(url, obj.express.static(parent.config.domains[i].webpublicpath));
7005 - } else if (obj.parent.webPublicOverridePath != null) {
7006 - // Use override path
7007 - obj.app.use(url, obj.express.static(obj.parent.webPublicOverridePath));
7074 }
7075
7010 - // Indicates to ExpressJS that the default public folder should be used to serve static files.
7011 - obj.app.use(url, obj.express.static(obj.parent.webPublicPath));
7076 + // Start server on a free port.
7077 + CheckListenPort(obj.args.port, obj.args.portbind, StartWebServer);
7078
7013 - // Start regular disconnection list flush every 2 minutes.
7014 - obj.wsagentsDisconnectionsTimer = setInterval(function () { obj.wsagentsDisconnections = {}; }, 120000);
7015 - }
7079 + // Start on a second agent-only alternative port if needed.
7080 + if (obj.args.agentport) { CheckListenPort(obj.args.agentport, obj.args.agentportbind, StartAltWebServer); }
7081
7017 - // Handle 404 error
7018 - if (obj.args.nice404 !== false) {
7019 - obj.app.use(function (req, res, next) {
7020 - parent.debug('web', '404 Error ' + req.url);
7021 - var domain = getDomain(req);
7022 - if ((domain == null) || (domain.auth == 'sspi')) { res.sendStatus(404); return; }
7023 - if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL
7024 - const cspNonce = obj.crypto.randomBytes(15).toString('base64');
7025 - res.set({ 'Content-Security-Policy': "default-src 'none'; script-src 'self' 'nonce-" + cspNonce + "'; img-src 'self'; style-src 'self' 'nonce-" + cspNonce + "';" }); // This page supports very tight CSP policy
7026 - res.status(404).render(getRenderPage((domain.sitestyle == 2) ? 'error4042' : 'error404', req, domain), getRenderArgs({ cspNonce: cspNonce }, req, domain));
7027 - });
7082 + // We are done starting the web server.
7083 + if (doneFunc) doneFunc();
7084 }
7029 -
7030 - // Start server on a free port.
7031 - CheckListenPort(obj.args.port, obj.args.portbind, StartWebServer);
7032 -
7033 - // Start on a second agent-only alternative port if needed.
7034 - if (obj.args.agentport) { CheckListenPort(obj.args.agentport, obj.args.agentportbind, StartAltWebServer); }
7035 -
7036 - // We are done starting the web server.
7037 - if (doneFunc) doneFunc();
7085 }
7086
7087 // Auth strategy flags
@@ -7051,14 +7098,14 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
7098 }
7099
7100 // Setup auth strategies for a domain
7054 - function setupDomainAuthStrategy(domain) {
7055 - // Return the auth strategies that have been setup
7056 - var authStrategyFlags = 0;
7101 + async function setupDomainAuthStrategy(domain) {
7102 + // Return binary flags representing all auth strategies that have been setup
7103 + let authStrategyFlags = 0;
7104
7105 // Setup auth strategies using passport if needed
7106 if (typeof domain.authstrategies != 'object') return authStrategyFlags;
7107
7061 - const url = domain.url;
7108 + const url = domain.url
7109 const passport = domain.passport = require('passport');
7110 passport.serializeUser(function (user, done) { done(null, user.sid); });
7111 passport.deserializeUser(function (sid, done) { done(null, { sid: sid }); });
@@ -7067,12 +7114,12 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
7114 // Twitter
7115 if ((typeof domain.authstrategies.twitter == 'object') && (typeof domain.authstrategies.twitter.clientid == 'string') && (typeof domain.authstrategies.twitter.clientsecret == 'string')) {
7116 const TwitterStrategy = require('passport-twitter');
7070 - var options = { consumerKey: domain.authstrategies.twitter.clientid, consumerSecret: domain.authstrategies.twitter.clientsecret };
7117 + let options = { consumerKey: domain.authstrategies.twitter.clientid, consumerSecret: domain.authstrategies.twitter.clientsecret };
7118 if (typeof domain.authstrategies.twitter.callbackurl == 'string') { options.callbackURL = domain.authstrategies.twitter.callbackurl; } else { options.callbackURL = url + 'auth-twitter-callback'; }
7072 - parent.debug('authlog', 'Adding Twitter SSO with options: ' + JSON.stringify(options));
7119 + parent.authLog('setupDomainAuthStrategy', 'Adding Twitter SSO with options: ' + JSON.stringify(options));
7120 passport.use('twitter-' + domain.id, new TwitterStrategy(options,
7121 function (token, tokenSecret, profile, cb) {
7075 - parent.debug('authlog', 'Twitter profile: ' + JSON.stringify(profile));
7122 + parent.authLog('setupDomainAuthStrategy', 'Twitter profile: ' + JSON.stringify(profile));
7123 var user = { sid: '~twitter:' + profile.id, name: profile.displayName, strategy: 'twitter' };
7124 if ((typeof profile.emails == 'object') && (profile.emails[0] != null) && (typeof profile.emails[0].value == 'string')) { user.email = profile.emails[0].value; }
7125 return cb(null, user);
@@ -7084,12 +7131,12 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
7131 // Google
7132 if ((typeof domain.authstrategies.google == 'object') && (typeof domain.authstrategies.google.clientid == 'string') && (typeof domain.authstrategies.google.clientsecret == 'string')) {
7133 const GoogleStrategy = require('passport-google-oauth20');
7087 - var options = { clientID: domain.authstrategies.google.clientid, clientSecret: domain.authstrategies.google.clientsecret };
7134 + let options = { clientID: domain.authstrategies.google.clientid, clientSecret: domain.authstrategies.google.clientsecret };
7135 if (typeof domain.authstrategies.google.callbackurl == 'string') { options.callbackURL = domain.authstrategies.google.callbackurl; } else { options.callbackURL = url + 'auth-google-callback'; }
7089 - parent.debug('authlog', 'Adding Google SSO with options: ' + JSON.stringify(options));
7136 + parent.authLog('setupDomainAuthStrategy', 'Adding Google SSO with options: ' + JSON.stringify(options));
7137 passport.use('google-' + domain.id, new GoogleStrategy(options,
7138 function (token, tokenSecret, profile, cb) {
7092 - parent.debug('authlog', 'Google profile: ' + JSON.stringify(profile));
7139 + parent.authLog('setupDomainAuthStrategy', 'Google profile: ' + JSON.stringify(profile));
7140 var user = { sid: '~google:' + profile.id, name: profile.displayName, strategy: 'google' };
7141 if ((typeof profile.emails == 'object') && (profile.emails[0] != null) && (typeof profile.emails[0].value == 'string') && (profile.emails[0].verified == true)) { user.email = profile.emails[0].value; }
7142 return cb(null, user);
@@ -7101,12 +7148,12 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
7148 // Github
7149 if ((typeof domain.authstrategies.github == 'object') && (typeof domain.authstrategies.github.clientid == 'string') && (typeof domain.authstrategies.github.clientsecret == 'string')) {
7150 const GitHubStrategy = require('passport-github2');
7104 - var options = { clientID: domain.authstrategies.github.clientid, clientSecret: domain.authstrategies.github.clientsecret };
7151 + let options = { clientID: domain.authstrategies.github.clientid, clientSecret: domain.authstrategies.github.clientsecret };
7152 if (typeof domain.authstrategies.github.callbackurl == 'string') { options.callbackURL = domain.authstrategies.github.callbackurl; } else { options.callbackURL = url + 'auth-github-callback'; }
7106 - parent.debug('authlog', 'Adding Github SSO with options: ' + JSON.stringify(options));
7153 + parent.authLog('setupDomainAuthStrategy', 'Adding Github SSO with options: ' + JSON.stringify(options));
7154 passport.use('github-' + domain.id, new GitHubStrategy(options,
7155 function (token, tokenSecret, profile, cb) {
7109 - parent.debug('authlog', 'Github profile: ' + JSON.stringify(profile));
7156 + parent.authLog('setupDomainAuthStrategy', 'Github profile: ' + JSON.stringify(profile));
7157 var user = { sid: '~github:' + profile.id, name: profile.displayName, strategy: 'github' };
7158 if ((typeof profile.emails == 'object') && (profile.emails[0] != null) && (typeof profile.emails[0].value == 'string')) { user.email = profile.emails[0].value; }
7159 return cb(null, user);
@@ -7118,12 +7165,12 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
7165 // Reddit
7166 if ((typeof domain.authstrategies.reddit == 'object') && (typeof domain.authstrategies.reddit.clientid == 'string') && (typeof domain.authstrategies.reddit.clientsecret == 'string')) {
7167 const RedditStrategy = require('passport-reddit');
7121 - var options = { clientID: domain.authstrategies.reddit.clientid, clientSecret: domain.authstrategies.reddit.clientsecret };
7168 + let options = { clientID: domain.authstrategies.reddit.clientid, clientSecret: domain.authstrategies.reddit.clientsecret };
7169 if (typeof domain.authstrategies.reddit.callbackurl == 'string') { options.callbackURL = domain.authstrategies.reddit.callbackurl; } else { options.callbackURL = url + 'auth-reddit-callback'; }
7123 - parent.debug('authlog', 'Adding Reddit SSO with options: ' + JSON.stringify(options));
7170 + parent.authLog('setupDomainAuthStrategy', 'Adding Reddit SSO with options: ' + JSON.stringify(options));
7171 passport.use('reddit-' + domain.id, new RedditStrategy.Strategy(options,
7172 function (token, tokenSecret, profile, cb) {
7126 - parent.debug('authlog', 'Reddit profile: ' + JSON.stringify(profile));
7173 + parent.authLog('setupDomainAuthStrategy', 'Reddit profile: ' + JSON.stringify(profile));
7174 var user = { sid: '~reddit:' + profile.id, name: profile.name, strategy: 'reddit' };
7175 if ((typeof profile.emails == 'object') && (profile.emails[0] != null) && (typeof profile.emails[0].value == 'string')) { user.email = profile.emails[0].value; }
7176 return cb(null, user);
@@ -7135,14 +7182,14 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
7182 // Azure
7183 if ((typeof domain.authstrategies.azure == 'object') && (typeof domain.authstrategies.azure.clientid == 'string') && (typeof domain.authstrategies.azure.clientsecret == 'string')) {
7184 const AzureOAuth2Strategy = require('passport-azure-oauth2');
7138 - var options = { clientID: domain.authstrategies.azure.clientid, clientSecret: domain.authstrategies.azure.clientsecret, tenant: domain.authstrategies.azure.tenantid };
7185 + let options = { clientID: domain.authstrategies.azure.clientid, clientSecret: domain.authstrategies.azure.clientsecret, tenant: domain.authstrategies.azure.tenantid };
7186 if (typeof domain.authstrategies.azure.callbackurl == 'string') { options.callbackURL = domain.authstrategies.azure.callbackurl; } else { options.callbackURL = url + 'auth-azure-callback'; }
7140 - parent.debug('authlog', 'Adding Azure SSO with options: ' + JSON.stringify(options));
7187 + parent.authLog('setupDomainAuthStrategy', 'Adding Azure SSO with options: ' + JSON.stringify(options));
7188 passport.use('azure-' + domain.id, new AzureOAuth2Strategy(options,
7189 function (accessToken, refreshtoken, params, profile, done) {
7190 var userex = null;
7191 try { userex = require('jwt-simple').decode(params.id_token, '', true); } catch (ex) { }
7145 - parent.debug('authlog', 'Azure profile: ' + JSON.stringify(userex));
7192 + parent.authLog('setupDomainAuthStrategy', 'Azure profile: ' + JSON.stringify(userex));
7193 var user = null;
7194 if (userex != null) {
7195 var user = { sid: '~azure:' + userex.unique_name, name: userex.name, strategy: 'azure' };
@@ -7154,69 +7201,26 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
7201 authStrategyFlags |= domainAuthStrategyConsts.azure;
7202 }
7203
7157 - // Generic OpenID Connect
7158 - if ((typeof domain.authstrategies.oidc == 'object') && (typeof domain.authstrategies.oidc.clientid == 'string') && (typeof domain.authstrategies.oidc.clientsecret == 'string') && (typeof domain.authstrategies.oidc.issuer == 'string')) {
7159 - const OIDCStrategy = require('@mstrhakr/passport-openidconnect');
7160 - const options = {
7161 - issuer: domain.authstrategies.oidc.issuer,
7162 - clientID: domain.authstrategies.oidc.clientid,
7163 - clientSecret: domain.authstrategies.oidc.clientsecret,
7164 - scope: ['profile', 'email'],
7165 - };
7166 - if (typeof domain.authstrategies.oidc.authorizationurl == 'string') {options.authorizationURL = domain.authstrategies.oidc.authorizationurl; }
7167 - if (typeof domain.authstrategies.oidc.tokenurl == 'string') { options.tokenURL = domain.authstrategies.oidc.tokenurl; }
7168 - if (typeof domain.authstrategies.oidc.userinfourl == 'string') { options.userInfoURL = domain.authstrategies.oidc.userinfourl; }
7169 - if (typeof domain.authstrategies.oidc.callbackurl == 'string') { options.callbackURL = domain.authstrategies.oidc.callbackurl; }
7170 -
7171 - const discoverOptions = async function(options){
7172 - if ((typeof domain.authstrategies.oidc.authorizationurl != 'string') || (typeof domain.authstrategies.oidc.tokenurl != 'string') || (typeof domain.authstrategies.oidc.userinfourl != 'string')) {
7173 - const Issuer = require('openid-client').Issuer;
7174 - parent.debug('authlog', `OIDC: Attempting to discover well known endpoints for ${options.issuer}`);
7175 - var issuer = await Issuer.discover(options.issuer)
7176 - if (typeof domain.authstrategies.oidc.authorizationurl == 'string') { options.authorizationURL = domain.authstrategies.oidc.authorizationurl; } else { options.authorizationURL = issuer.metadata.authorization_endpoint; }
7177 - if (typeof domain.authstrategies.oidc.tokenurl == 'string') { options.tokenURL = domain.authstrategies.oidc.tokenurl; } else { options.tokenURL = issuer.metadata.token_endpoint; }
7178 - if (typeof domain.authstrategies.oidc.userinfourl == 'string') { options.userInfoURL = domain.authstrategies.oidc.userinfourl; } else { options.userInfoURL = issuer.metadata.userinfo_endpoint; }
7179 - if (typeof domain.authstrategies.oidc.callbackurl == 'string') { options.callbackURL = domain.authstrategies.oidc.callbackurl; } else { options.callbackURL = url + 'oidc-callback'; }
7180 - parent.debug('authlog', 'OIDC: Discovered: ' + JSON.stringify(options, null, 4));
7181 - }
7182 - return options;
7183 - }
7184 - if (typeof domain.authstrategies.oidc.groups == 'object') { options.scope.push('groups') }
7185 - discoverOptions(options).then(function(options) {
7186 - passport.use('oidc-' + domain.id, new OIDCStrategy.Strategy(options,
7187 - function verify(issuer, profile, verified) {
7188 - parent.debug('authlog', `OIDC: Connecting to ${issuer} with the following options ` + JSON.stringify(options, null, 4));
7189 - var user = { sid: '~oidc:' + profile.id, name: profile.displayName, strategy: 'oidc' };
7190 - if (typeof profile.emails == 'object') { if (typeof profile.emails[0].value == 'string') { user.email = profile.emails[0].value; } else { user.email = profile.emails[0].value[0]; } } else if (typeof profile.emails == 'string') { user.email = profile.emails; }
7191 - if (options.scope.indexOf('groups') >= 0) { if ( Array.isArray(profile.groups[0].value) ) { user.groups = profile.groups[0].value; } else { user.groups = [profile.groups[0].value]; } }
7192 - parent.debug('authlog', `oidc: Configured:\nUser: ${JSON.stringify(user, null, 4)}\nFROM\nProfile: ${JSON.stringify(profile, null, 4)}`);
7193 - return verified(null, user);
7194 - }
7195 - ))
7196 - });
7197 - authStrategyFlags |= domainAuthStrategyConsts.oidc;
7198 - }
7199 -
7204 // Generic SAML
7205 if (typeof domain.authstrategies.saml == 'object') {
7206 if ((typeof domain.authstrategies.saml.cert != 'string') || (typeof domain.authstrategies.saml.idpurl != 'string')) {
7203 - console.log('ERROR: Missing SAML configuration.');
7207 + parent.debug('error', 'Missing SAML configuration.');
7208 } else {
7209 const certPath = obj.common.joinPath(obj.parent.datapath, domain.authstrategies.saml.cert);
7210 var cert = obj.fs.readFileSync(certPath);
7211 if (cert == null) {
7208 - console.log('ERROR: Unable to read SAML IdP certificate: ' + domain.authstrategies.saml.cert);
7212 + parent.debug('error', 'Unable to read SAML IdP certificate: ' + domain.authstrategies.saml.cert);
7213 } else {
7214 var options = { entryPoint: domain.authstrategies.saml.idpurl, issuer: 'meshcentral' };
7215 if (typeof domain.authstrategies.saml.callbackurl == 'string') { options.callbackUrl = domain.authstrategies.saml.callbackurl; } else { options.callbackUrl = url + 'auth-saml-callback'; }
7216 if (domain.authstrategies.saml.disablerequestedauthncontext != null) { options.disableRequestedAuthnContext = domain.authstrategies.saml.disablerequestedauthncontext; }
7217 if (typeof domain.authstrategies.saml.entityid == 'string') { options.issuer = domain.authstrategies.saml.entityid; }
7214 - parent.debug('authlog', 'Adding SAML SSO with options: ' + JSON.stringify(options, null, 4));
7218 + parent.authLog('setupDomainAuthStrategy', 'Adding SAML SSO with options: ' + JSON.stringify(options));
7219 options.cert = cert.toString().split('-----BEGIN CERTIFICATE-----').join('').split('-----END CERTIFICATE-----').join('');
7220 const SamlStrategy = require('passport-saml').Strategy;
7221 passport.use('saml-' + domain.id, new SamlStrategy(options,
7222 function (profile, done) {
7219 - parent.debug('authlog', 'SAML profile: ' + JSON.stringify(profile, null, 4));
7223 + parent.authLog('setupDomainAuthStrategy', 'SAML profile: ' + JSON.stringify(profile));
7224 if (typeof profile.nameID != 'string') { return done(); }
7225 var user = { sid: '~saml:' + profile.nameID, name: profile.nameID, strategy: 'saml' };
7226 if (typeof profile.displayname == 'string') {
@@ -7228,7 +7232,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
7232 return done(null, user);
7233 }
7234 ));
7231 - authStrategyFlags |= domainAuthStrategyConsts.saml;
7235 + authStrategyFlags |= domainAuthStrategyConsts.saml
7236 }
7237 }
7238 }
@@ -7236,22 +7240,22 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
7240 // Intel SAML
7241 if (typeof domain.authstrategies.intel == 'object') {
7242 if ((typeof domain.authstrategies.intel.cert != 'string') || (typeof domain.authstrategies.intel.idpurl != 'string')) {
7239 - console.log('ERROR: Missing Intel SAML configuration.');
7243 + parent.debug('error', 'Missing Intel SAML configuration.');
7244 } else {
7245 var cert = obj.fs.readFileSync(obj.common.joinPath(obj.parent.datapath, domain.authstrategies.intel.cert));
7246 if (cert == null) {
7243 - console.log('ERROR: Unable to read Intel SAML IdP certificate: ' + domain.authstrategies.intel.cert);
7247 + parent.debug('error', 'Unable to read Intel SAML IdP certificate: ' + domain.authstrategies.intel.cert);
7248 } else {
7249 var options = { entryPoint: domain.authstrategies.intel.idpurl, issuer: 'meshcentral' };
7250 if (typeof domain.authstrategies.intel.callbackurl == 'string') { options.callbackUrl = domain.authstrategies.intel.callbackurl; } else { options.callbackUrl = url + 'auth-intel-callback'; }
7251 if (domain.authstrategies.intel.disablerequestedauthncontext != null) { options.disableRequestedAuthnContext = domain.authstrategies.intel.disablerequestedauthncontext; }
7252 if (typeof domain.authstrategies.intel.entityid == 'string') { options.issuer = domain.authstrategies.intel.entityid; }
7249 - parent.debug('authlog', 'Adding Intel SSO with options: ' + JSON.stringify(options, null, 4));
7253 + parent.authLog('setupDomainAuthStrategy', 'Adding Intel SSO with options: ' + JSON.stringify(options));
7254 options.cert = cert.toString().split('-----BEGIN CERTIFICATE-----').join('').split('-----END CERTIFICATE-----').join('');
7255 const SamlStrategy = require('passport-saml').Strategy;
7256 passport.use('isaml-' + domain.id, new SamlStrategy(options,
7257 function (profile, done) {
7254 - parent.debug('authlog', 'Intel profile: ' + JSON.stringify(profile, null, 4));
7258 + parent.authLog('setupDomainAuthStrategy', 'Intel profile: ' + JSON.stringify(profile));
7259 if (typeof profile.nameID != 'string') { return done(); }
7260 var user = { sid: '~intel:' + profile.nameID, name: profile.nameID, strategy: 'intel' };
7261 if ((typeof profile.firstname == 'string') && (typeof profile.lastname == 'string')) { user.name = profile.firstname + ' ' + profile.lastname; }
@@ -7261,7 +7265,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
7265 return done(null, user);
7266 }
7267 ));
7264 - authStrategyFlags |= domainAuthStrategyConsts.intelSaml;
7268 + authStrategyFlags |= domainAuthStrategyConsts.intelSaml
7269 }
7270 }
7271 }
@@ -7269,21 +7273,21 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
7273 // JumpCloud SAML
7274 if (typeof domain.authstrategies.jumpcloud == 'object') {
7275 if ((typeof domain.authstrategies.jumpcloud.cert != 'string') || (typeof domain.authstrategies.jumpcloud.idpurl != 'string')) {
7272 - console.log('ERROR: Missing JumpCloud SAML configuration.');
7276 + parent.debug('error', 'Missing JumpCloud SAML configuration.');
7277 } else {
7278 var cert = obj.fs.readFileSync(obj.common.joinPath(obj.parent.datapath, domain.authstrategies.jumpcloud.cert));
7279 if (cert == null) {
7276 - console.log('ERROR: Unable to read JumpCloud IdP certificate: ' + domain.authstrategies.jumpcloud.cert);
7280 + parent.debug('error', 'Unable to read JumpCloud IdP certificate: ' + domain.authstrategies.jumpcloud.cert);
7281 } else {
7282 var options = { entryPoint: domain.authstrategies.jumpcloud.idpurl, issuer: 'meshcentral' };
7283 if (typeof domain.authstrategies.jumpcloud.callbackurl == 'string') { options.callbackUrl = domain.authstrategies.jumpcloud.callbackurl; } else { options.callbackUrl = url + 'auth-jumpcloud-callback'; }
7284 if (typeof domain.authstrategies.jumpcloud.entityid == 'string') { options.issuer = domain.authstrategies.jumpcloud.entityid; }
7281 - parent.debug('authlog', 'Adding JumpCloud SSO with options: ' + JSON.stringify(options, null, 4));
7285 + parent.authLog('setupDomainAuthStrategy', 'Adding JumpCloud SSO with options: ' + JSON.stringify(options));
7286 options.cert = cert.toString().split('-----BEGIN CERTIFICATE-----').join('').split('-----END CERTIFICATE-----').join('');
7287 const SamlStrategy = require('passport-saml').Strategy;
7288 passport.use('jumpcloud-' + domain.id, new SamlStrategy(options,
7289 function (profile, done) {
7286 - parent.debug('authlog', 'JumpCloud profile: ' + JSON.stringify(profile, null, 4));
7290 + parent.authLog('setupDomainAuthStrategy', 'JumpCloud profile: ' + JSON.stringify(profile));
7291 if (typeof profile.nameID != 'string') { return done(); }
7292 var user = { sid: '~jumpcloud:' + profile.nameID, name: profile.nameID, strategy: 'jumpcloud' };
7293 if ((typeof profile.firstname == 'string') && (typeof profile.lastname == 'string')) { user.name = profile.firstname + ' ' + profile.lastname; }
@@ -7291,11 +7295,281 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
7295 return done(null, user);
7296 }
7297 ));
7294 - authStrategyFlags |= domainAuthStrategyConsts.jumpCloudSaml;
7298 + authStrategyFlags |= domainAuthStrategyConsts.jumpCloudSaml
7299 }
7300 }
7301 }
7302
7303 + // Setup OpenID Connect Authentication Strategy
7304 + if (obj.common.validateObject(domain.authstrategies.oidc)) {
7305 + parent.authLog('setupDomainAuthStrategy', `OIDC: Setting up strategy for domain: ${domain.id}`);
7306 + // Ensure required objects exist
7307 + let initStrategy = domain.authstrategies.oidc
7308 + if (typeof initStrategy.issuer == 'string') { initStrategy.issuer = { 'issuer': initStrategy.issuer } }
7309 + let strategy = migrateOldConfigs(Object.assign({ 'client': {}, 'issuer': {}, 'options': {}, 'custom': {}, 'obj': { 'openidClient': require('openid-client') } }, initStrategy))
7310 + let preset = obj.common.validateString(strategy.custom.preset) ? strategy.custom.preset : null
7311 + if (!preset) {
7312 + if (typeof strategy.custom.tenant_id == 'string') { strategy.custom.preset = preset = 'azure' }
7313 + if (strategy.custom.customer_id || strategy.custom.identitysource || strategy.client.client_id.split('.')[2] == 'googleusercontent') { strategy.custom.preset = preset = 'google' }
7314 + }
7315 +
7316 + // Check issuer url
7317 + let presetIssuer
7318 + if (preset == 'azure') { presetIssuer = 'https://login.microsoftonline.com/' + strategy.custom.tenant_id + '/v2.0'; }
7319 + if (preset == 'google') { presetIssuer = 'https://accounts.google.com'; }
7320 + if (!obj.common.validateString(strategy.issuer.issuer)) {
7321 + if (!preset) {
7322 + let error = new Error('OIDC: Missing issuer URI.');
7323 + parent.authLog('error', `${error.message} STRATEGY: ${JSON.stringify(strategy)}`);
7324 + throw error;
7325 + } else {
7326 + strategy.issuer.issuer = presetIssuer
7327 + parent.authLog('setupDomainAuthStrategy', `OIDC: PRESET: ${preset.toUpperCase()}: Using preset issuer: ${presetIssuer}`);
7328 + }
7329 + } else if ((typeof strategy.issuer.issuer == 'string') && (typeof strategy.custom.preset == 'string')) {
7330 + let error = new Error(`OIDC: PRESET: ${strategy.custom.preset.toUpperCase()}: PRESET OVERRIDDEN: CONFIG ISSUER: ${strategy.issuer.issuer} PRESET ISSUER: ${presetIssuer}`);
7331 + parent.authLog('setupDomainAuthStrategy', error.message);
7332 + console.warn(error)
7333 + }
7334 +
7335 + // Setup Strategy Options
7336 + strategy.custom.scope = obj.common.convertStrArray(strategy.custom.scope, ' ')
7337 + if (strategy.custom.scope.length > 1) {
7338 + strategy.options = Object.assign(strategy.options, { 'params': { 'scope': strategy.custom.scope } })
7339 + } else {
7340 + strategy.options = Object.assign(strategy.options, { 'params': { 'scope': ['openid', 'profile', 'email'] } })
7341 + }
7342 + if (typeof strategy.groups == 'object') {
7343 + let groupScope = strategy.groups.scope || null
7344 + if (groupScope == null) {
7345 + if (preset == 'azure') { groupScope = 'Group.Read.All' }
7346 + if (preset == 'google') { groupScope = 'https://www.googleapis.com/auth/cloud-identity.groups.readonly' }
7347 + if (typeof preset != 'string') { groupScope = 'groups' }
7348 + }
7349 + strategy.options.params.scope.push(groupScope)
7350 + }
7351 + strategy.options.params.scope = strategy.options.params.scope.join(' ')
7352 +
7353 + // Discover additional information if available, use endpoints from config if present
7354 + let issuer
7355 + try {
7356 + parent.authLog('setupDomainAuthStrategy', `OIDC: Discovering Issuer Endpoints: ${strategy.issuer.issuer}`);
7357 + issuer = await strategy.obj.openidClient.Issuer.discover(strategy.issuer.issuer);
7358 + } catch (err) {
7359 + let error = new Error('OIDC: Discovery failed.', { cause: err });
7360 + parent.authLog('setupDomainAuthStrategy', `ERROR: ${JSON.stringify(error)} ISSUER_URI: ${strategy.issuer.issuer}`);
7361 + throw error
7362 + }
7363 + if (Object.keys(strategy.issuer).length > 1) {
7364 + parent.authLog('setupDomainAuthStrategy', `OIDC: Adding Issuer Metadata: ${JSON.stringify(strategy.issuer)}`);
7365 + issuer = new strategy.obj.openidClient.Issuer(Object.assign(issuer?.metadata, strategy.issuer));
7366 + }
7367 + strategy.issuer = issuer?.metadata
7368 + strategy.obj.issuer = issuer
7369 +
7370 + // Make sure redirect_uri and post_logout_redirect_uri exist before continuing
7371 + if (!strategy.client.redirect_uri) {
7372 + strategy.client.redirect_uri = 'https://' + parent.config.settings.cert + url + 'auth-oidc-callback';
7373 + }
7374 + if (!strategy.client.post_logout_redirect_uri) {
7375 + strategy.client.post_logout_redirect_uri = 'https://' + parent.config.settings.cert + url + 'login';
7376 + }
7377 +
7378 + // Create client and overwrite in options
7379 + let client = new issuer.Client(strategy.client)
7380 + strategy.options = Object.assign(strategy.options, { 'client': client });
7381 + strategy.client = client.metadata
7382 + strategy.obj.client = client
7383 +
7384 + // Setup strategy and save configs for later
7385 + passport.use('oidc-' + domain.id, new strategy.obj.openidClient.Strategy(strategy.options, oidcCallback));
7386 + if (domain.dns == null) {
7387 + parent.config.domains[''].authstrategies.oidc = strategy;
7388 + } else if (typeof parent.config.domains[domain.id].authstrategies.oidc == 'object') {
7389 + parent.config.domains[domain.id].authstrategies.oidc = strategy;
7390 + }
7391 + parent.debug('verbose', 'OIDC: Saved Configuration: ' + JSON.stringify(strategy));
7392 + if (preset) { parent.authLog('setupDomainAuthStrategy', 'OIDC: ' + preset.toUpperCase() + ': Setup Complete'); }
7393 + else { parent.authLog('setupDomainAuthStrategy', 'OIDC: Setup Complete'); }
7394 +
7395 + authStrategyFlags |= domainAuthStrategyConsts.oidc
7396 +
7397 + function migrateOldConfigs(strategy) {
7398 + let oldConfigs = {
7399 + 'client': {
7400 + 'clientid': 'client_id',
7401 + 'clientsecret': 'client_secret',
7402 + 'callbackurl': 'redirect_uri'
7403 + },
7404 + 'issuer': {
7405 + 'authorizationurl': 'authorization_endpoint',
7406 + 'tokenurl': 'token_endpoint',
7407 + 'userinfourl': 'userinfo_endpoint'
7408 + },
7409 + 'custom': {
7410 + 'tenantid': 'tenant_id',
7411 + 'customerid': 'customer_id'
7412 + }
7413 + }
7414 + for (var type in oldConfigs) {
7415 + for (const [key, value] of Object.entries(oldConfigs[type])) {
7416 + if (Object.hasOwn(strategy, key)) {
7417 + if (strategy[type][value] && obj.common.validateString(strategy[type][value])) {
7418 + let error = new Error('OIDC: OLD CONFIG: Config conflict, new config overrides old config');
7419 + parent.authLog('migrateOldConfigs', `${JSON.stringify(error)} OLD CONFIG: ${key}: ${strategy[key]} NEW CONFIG: ${value}:${strategy[type][value]}`);
7420 + } else {
7421 + parent.authLog('migrateOldConfigs', `OIDC: OLD CONFIG: Moving old config to new location. strategy.${key} => strategy.${type}.${value}`);
7422 + strategy[type][value] = strategy[key];
7423 + }
7424 + delete strategy[key]
7425 + }
7426 + }
7427 + }
7428 + if (typeof strategy.scope == 'string') {
7429 + if (!strategy.custom.scope) {
7430 + strategy.custom.scope = strategy.scope;
7431 + strategy.options.params = { 'scope': strategy.scope };
7432 + parent.authLog('migrateOldConfigs', `OIDC: OLD CONFIG: Moving old config to new location. strategy.scope => strategy.custom.scope`);
7433 + } else {
7434 + let error = new Error('OIDC: OLD CONFIG: Config conflict, using new config values.');
7435 + parent.authLog('migrateOldConfigs', `${error.message} OLD CONFIG: strategy.scope: ${strategy.scope} NEW CONFIG: strategy.custom.scope:${strategy.custom.scope}`);
7436 + parent.debug('warning', error.message)
7437 + }
7438 + delete strategy.scope
7439 + }
7440 + return strategy
7441 + }
7442 +
7443 + // Callback function must be able to grab info from API's using the access token, would prefer to use the token here.
7444 + function oidcCallback(tokenset, profile, verified) {
7445 + // Initialize user object
7446 + let user = { 'strategy': 'oidc' }
7447 + let claims = obj.common.validateObject(strategy.custom.claims) ? strategy.custom.claims : null
7448 + user.sid = obj.common.validateString(profile.sub) ? '~oidc:' + profile.sub : null
7449 + user.name = obj.common.validateString(profile.name) ? profile.name : null
7450 + user.email = obj.common.validateString(profile.email) ? profile.email : null
7451 + if (claims != null) {
7452 + user.sid = obj.common.validateString(profile[claims.uuid]) ? '~oidc:' + profile[claims.uuid] : user.sid
7453 + user.name = obj.common.validateString(profile[claims.name]) ? profile[claims.name] : user.name
7454 + user.email = obj.common.validateString(profile[claims.email]) ? profile[claims.email] : user.email
7455 + }
7456 + user.emailVerified = profile.email_verified ? profile.email_verified : obj.common.validateEmail(user.email),
7457 + user.groups = obj.common.validateStrArray(profile.groups, 1) ? profile.groups : null
7458 + user.preset = obj.common.validateString(strategy.custom.preset) ? strategy.custom.preset : null
7459 + if (obj.common.validateString(strategy.groups.claim)) {
7460 + user.groups = obj.common.validateStrArray(profile[strategy.groups.claim], 1) ? profile[strategy.groups.claim] : null
7461 + }
7462 +
7463 + // Setup end session enpoint if not already configured this requires an auth token
7464 + try {
7465 + if (!strategy.issuer.end_session_endpoint) {
7466 + strategy.issuer.end_session_endpoint = strategy.obj.client.endSessionUrl({ 'id_token_hint': tokenset })
7467 + }
7468 + } catch (err) {
7469 + let error = new Error('OIDC: Discovering end_session_endpoint failed. Using Default.', { cause: err });
7470 + strategy.issuer.end_session_endpoint = strategy.issuer.issuer + '/logout';
7471 + parent.debug('error', `${error.message} end_session_endpoint: ${strategy.issuer.end_session_endpoint} post_logout_redirect_uri: ${strategy.client.post_logout_redirect_uri} TOKENSET: ${JSON.stringify(tokenset)}`);
7472 + parent.authLog('oidcCallback', error.message);
7473 + }
7474 +
7475 + // Setup presets and groups, get groups from API if needed then return
7476 + if (strategy.groups && typeof user.preset == 'string') {
7477 + getGroups(user.preset, tokenset).then((groups) => {
7478 + user = Object.assign(user, { 'groups': groups });
7479 + return verified(null, user);
7480 + }).catch((err) => {
7481 + let error = new Error('OIDC: GROUPS: No groups found due to error:', { cause: err });
7482 + parent.debug('error', `${JSON.stringify(error)}`);
7483 + parent.authLog('oidcCallback', error.message);
7484 + user.groups = [];
7485 + return verified(null, user);
7486 + });
7487 + } else {
7488 + return verified(null, user);
7489 + }
7490 +
7491 + async function getGroups(preset, tokenset) {
7492 + let url = '';
7493 + if (preset == 'azure') { url = strategy.groups.recursive == true ? 'https://graph.microsoft.com/v1.0/me/transitiveMemberOf' : 'https://graph.microsoft.com/v1.0/me/memberOf'; }
7494 + if (preset == 'google') { url = strategy.custom.customer_id ? 'https://cloudidentity.googleapis.com/v1/groups?parent=customers/' + strategy.custom.customer_id : strategy.custom.identitysource ? 'https://cloudidentity.googleapis.com/v1/groups?parent=identitysources/' + strategy.custom.identitysource : null; }
7495 + return new Promise((resolve, reject) => {
7496 + const options = {
7497 + 'headers': { authorization: 'Bearer ' + tokenset.access_token }
7498 + }
7499 + const req = require('https').get(url, options, (res) => {
7500 + let data = []
7501 + res.on('data', (chunk) => {
7502 + data.push(chunk);
7503 + });
7504 + res.on('end', () => {
7505 + if (res.statusCode < 200 || res.statusCode >= 300) {
7506 + let error = new Error('OIDC: GROUPS: Bad response code from API, statusCode: ' + res.statusCode);
7507 + parent.authLog('getGroups', `ERROR: ${error.message} URL: ${url} OPTIONS: ${JSON.stringify(options)}`);
7508 + console.error(error);
7509 + reject(error);
7510 + }
7511 + if (data.length == 0) {
7512 + let error = new Error('OIDC: GROUPS: Getting groups from API failed, request returned no data in response.');
7513 + parent.authLog('getGroups', `ERROR: ${error.message} URL: ${url} OPTIONS: ${JSON.stringify(options)}`);
7514 + console.error(error);
7515 + reject(error);
7516 + }
7517 + try {
7518 + if (Buffer.isBuffer(data[0])) {
7519 + data = Buffer.concat(data);
7520 + data = data.toString();
7521 + } else { // else if (typeof data[0] == 'string')
7522 + data = data.join();
7523 + }
7524 + } catch (err) {
7525 + let error = new Error('OIDC: GROUPS: Getting groups from API failed. Error joining response data.', { cause: err });
7526 + parent.authLog('getGroups', `ERROR: ${error.message} URL: ${url} OPTIONS: ${JSON.stringify(options)}`);
7527 + console.error(error);
7528 + reject(error);
7529 + }
7530 + if (preset == 'azure') {
7531 + data = JSON.parse(data);
7532 + if (data.error) {
7533 + let error = new Error('OIDC: GROUPS: Getting groups from API failed. Error joining response data.', { cause: data.error });
7534 + parent.authLog('getGroups', `ERROR: ${error.message} URL: ${url} OPTIONS: ${JSON.stringify(options)}`);
7535 + console.error(error);
7536 + reject(error);
7537 + }
7538 + data = data.value;
7539 + }
7540 + if (preset == 'google') {
7541 + data = data.split('\n');
7542 + data = data.join('');
7543 + data = JSON.parse(data);
7544 + data = data.groups;
7545 + }
7546 + let groups = []
7547 + for (var i in data) {
7548 + if (typeof data[i].displayName == 'string') {
7549 + groups.push(data[i].displayName);
7550 + }
7551 + }
7552 + if (groups.length == 0) {
7553 + let warn = new Error('OIDC: GROUPS: No groups returned from API.');
7554 + parent.authLog('getGroups', `WARN: ${warn.message} DATA: ${data}`);
7555 + console.warn(warn);
7556 + resolve(groups);
7557 + } else {
7558 + resolve(groups);
7559 + }
7560 + });
7561 + });
7562 + req.on('error', (err) => {
7563 + let error = new Error('OIDC: GROUPS: Request error.', { cause: err });
7564 + parent.authLog('getGroups', `ERROR: ${error.message} URL: ${url} OPTIONS: ${JSON.stringify(options)}`);
7565 + console.error(error);
7566 + reject(error);
7567 + });
7568 + req.end();
7569 + });
7570 + }
7571 + }
7572 + }
7573 return authStrategyFlags;
7574 }
7575
@@ -7823,7 +8097,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
8097 });
8098 obj.parent.updateServerState('servername', certificates.CommonName);
8099 }
7826 - obj.parent.authLog('https', 'Server listening on ' + ((addr != null) ? addr : '0.0.0.0') + ' port ' + port + '.');
8100 + obj.parent.debug('https', 'Server listening on ' + ((addr != null) ? addr : '0.0.0.0') + ' port ' + port + '.');
8101 obj.parent.updateServerState('https-port', port);
8102 if (args.aliasport != null) { obj.parent.updateServerState('https-aliasport', args.aliasport); }
8103 } else {
@@ -7862,7 +8136,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
8136 } else {
8137 obj.tcpAltServer = obj.tlsAltServer.listen(port, addr, function () { console.log('MeshCentral HTTPS agent-only server running on ' + ((agentAliasDns != null) ? agentAliasDns : certificates.CommonName) + ':' + port + ((agentAliasPort != null) ? (', alias port ' + agentAliasPort) : '') + '.'); });
8138 }
7865 - obj.parent.authLog('https', 'Server listening on 0.0.0.0 port ' + port + '.');
8139 + obj.parent.debug('https', 'Server listening on 0.0.0.0 port ' + port + '.');
8140 obj.parent.updateServerState('https-agent-port', port);
8141 } else {
8142 obj.tcpAltServer = obj.agentapp.listen(port, addr, function () { console.log('MeshCentral HTTP agent-only server running on port ' + port + ((agentAliasPort != null) ? (', alias port ' + agentAliasPort) : '') + '.'); });
@@ -8017,9 +8291,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
8291 }
8292 };
8293
8020 - //
8021 - // Access Control Functions
8022 - //
8294 + /* Access Control Functions */
8295
8296 // Remove user rights
8297 function removeUserRights(rights, user) {
@@ -8793,7 +9065,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
9065
9066 }
9067
8796 - // Insure exclusivity of a push messaging token for Android device
9068 + // Ensure exclusivity of a push messaging token for Android device
9069 obj.removePmtFromAllOtherNodes = function (node) {
9070 if (typeof node.pmt != 'string') return;
9071 db.Get('pmt_' + node.pmt, function (err, docs) {
@@ -8829,7 +9101,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
9101 }
9102
9103 // Return decoded user agent information
8832 - obj.getUserAgentInfo = function(req) {
9104 + obj.getUserAgentInfo = function (req) {
9105 var browser = 'Unknown', os = 'Unknown';
9106 try {
9107 const ua = obj.uaparser((typeof req == 'string') ? req : req.headers['user-agent']);
@@ -9154,7 +9426,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
9426 parent.DispatchEvent(['*', ugrpid, user._id], obj, event); // Even if DB change stream is active, this event must be acted upon.
9427
9428 // Log in the auth log
9157 - parent.authLog('https', 'Created ' + userMembershipType + ' user group ' + ugrp.name);
9429 + parent.authLog('https', userMembershipType.toUpperCase() + ': Created user group ' + ugrp.name);
9430 }
9431
9432 if (existingUserMemberships[ugrpid] == null) {
@@ -9181,7 +9453,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
9453 parent.DispatchEvent(['*', ugrp._id, user._id], obj, event);
9454
9455 // Log in the auth log
9184 - parent.authLog('https', 'Adding ' + user.name + ' to ' + userMembershipType + ' user group ' + userMemberships[i] + '.');
9456 + parent.authLog('https', userMembershipType.toUpperCase() + ': Adding ' + user.name + ' to user group ' + userMemberships[i] + '.');
9457 } else {
9458 // User is already part of this user group
9459 delete existingUserMemberships[ugrpid];
@@ -9191,7 +9463,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
9463 // Remove the user from any memberships they don't belong to anymore
9464 for (var ugrpid in existingUserMemberships) {
9465 var ugrp = obj.userGroups[ugrpid];
9194 - parent.authLog('https', 'Removing ' + user.name + ' from ' + userMembershipType + ' user group ' + ugrp.name + '.');
9466 + parent.authLog('https', userMembershipType.toUpperCase() + ': Removing ' + user.name + ' from user group ' + ugrp.name + '.');
9467 if ((user.links != null) && (user.links[ugrpid] != null)) {
9468 delete user.links[ugrpid];
9469