Skip to content

Commit 5ebe68d

Browse files
authored
Merge pull request #2032 from beyonnex-io/feature/ditto-ui-sso
#1591 add SSO via OIDC to Ditto UI
2 parents f80002e + ce0f1e4 commit 5ebe68d

27 files changed

+1440
-340
lines changed

gateway/service/src/main/java/org/eclipse/ditto/gateway/service/security/authentication/jwt/DefaultJwtValidator.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ private BinaryValidationResult tryToValidateWithJwtParser(final JsonWebToken jso
8080
}
8181

8282
private BinaryValidationResult validateWithJwtParser(final JsonWebToken jsonWebToken, final JwtParser jwtParser) {
83-
jwtParser.parseClaimsJws(jsonWebToken.getToken());
83+
jwtParser.parse(jsonWebToken.getToken());
8484

8585
return BinaryValidationResult.valid();
8686
}

gateway/service/src/main/resources/gateway-dev.conf

+9
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ ditto {
2626
oauth {
2727
# use unencrypted http for local oauth providers.
2828
protocol = "http"
29+
30+
openid-connect-issuers {
31+
fake = {
32+
issuer = "localhost:9900/fake"
33+
auth-subjects = [
34+
"{{ jwt:sub }}"
35+
]
36+
}
37+
}
2938
}
3039
}
3140
}

ui/main.scss

+7-1
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,18 @@ tbody {
5959
display: inline;
6060
}
6161

62-
.toast-header {
62+
.toast-header-warn {
6363
color: #842029;
6464
background-color: #f8d7da;
6565
border-color: #f5c2c7;
6666
}
6767

68+
.toast-header-info {
69+
color: #842029;
70+
background-color: #ebd7f8;
71+
border-color: #fbfbfb;
72+
}
73+
6874
textarea {
6975
white-space: pre;
7076
overflow-wrap: normal;

ui/main.ts

+11-11
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,15 @@ import * as ConnectionsMonitor from './modules/connections/connectionsMonitor.js
2020

2121
import * as Authorization from './modules/environments/authorization.js';
2222
import * as Environments from './modules/environments/environments.js';
23-
import * as Operations from './modules/operations/servicesLogging.js';
2423
import * as Piggyback from './modules/operations/piggyback.js';
24+
import * as Operations from './modules/operations/servicesLogging.js';
2525
import * as Templates from './modules/operations/templates.js';
2626
import * as Policies from './modules/policies/policies.js';
27-
import * as PoliciesJSON from './modules/policies/policiesJSON.js';
2827
import * as PoliciesEntries from './modules/policies/policiesEntries.js';
2928
import * as PoliciesImports from './modules/policies/policiesImports.js';
30-
import * as PoliciesSubjects from './modules/policies/policiesSubjects';
29+
import * as PoliciesJSON from './modules/policies/policiesJSON.js';
3130
import * as PoliciesResources from './modules/policies/policiesResources';
31+
import * as PoliciesSubjects from './modules/policies/policiesSubjects';
3232
import * as Attributes from './modules/things/attributes.js';
3333
import * as FeatureMessages from './modules/things/featureMessages.js';
3434
import * as Features from './modules/things/features.js';
@@ -51,10 +51,10 @@ let mainNavbar;
5151
document.addEventListener('DOMContentLoaded', async function() {
5252
Utils.ready();
5353
await Things.ready();
54-
ThingsSearch.ready();
55-
ThingsCRUD.ready();
54+
await ThingsSearch.ready();
55+
await ThingsCRUD.ready();
5656
await ThingMessages.ready();
57-
ThingsSSE.ready();
57+
await ThingsSSE.ready();
5858
MessagesIncoming.ready();
5959
Attributes.ready();
6060
await Fields.ready();
@@ -70,25 +70,25 @@ document.addEventListener('DOMContentLoaded', async function() {
7070
Connections.ready();
7171
ConnectionsCRUD.ready();
7272
await ConnectionsMonitor.ready();
73-
Operations.ready();
73+
await Operations.ready();
7474
Authorization.ready();
7575
await Environments.ready();
76-
Piggyback.ready();
77-
Templates.ready();
76+
await Piggyback.ready();
77+
await Templates.ready();
7878

7979
const thingDescription = WoTDescription({
8080
itemsId: 'tabItemsThing',
8181
contentId: 'tabContentThing',
8282
}, false);
8383
Things.addChangeListener(thingDescription.onReferenceChanged);
84-
thingDescription.ready();
84+
await thingDescription.ready();
8585

8686
const featureDescription = WoTDescription({
8787
itemsId: 'tabItemsFeatures',
8888
contentId: 'tabContentFeatures',
8989
}, true);
9090
Features.addChangeListener(featureDescription.onReferenceChanged);
91-
featureDescription.ready();
91+
await featureDescription.ready();
9292

9393
// make dropdowns not cutting off
9494
new Dropdown(document.querySelector('.dropdown-toggle'), {

ui/modules/api.ts

+130-81
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414

1515
import { EventSourcePolyfill } from 'event-source-polyfill';
1616
import * as Environments from './environments/environments.js';
17+
import { AuthMethod } from './environments/environments.js';
1718
import * as Utils from './utils.js';
19+
import { showError } from './utils.js';
1820

1921

2022
const config = {
@@ -278,31 +280,69 @@ let authHeaderValue;
278280
* Activates authorization header for api calls
279281
* @param {boolean} forDevOps if true, the credentials for the dev ops api will be used.
280282
*/
281-
export function setAuthHeader(forDevOps) {
283+
export function setAuthHeader(forDevOps: boolean) {
284+
authHeaderValue = undefined;
285+
let environment = Environments.current();
282286
if (forDevOps) {
283-
if (Environments.current().devopsAuth === 'basic') {
284-
authHeaderKey = 'Authorization';
285-
authHeaderValue = 'Basic ' + window.btoa(Environments.current().usernamePasswordDevOps);
286-
} else if (Environments.current().devopsAuth === 'bearer') {
287-
authHeaderKey = 'Authorization';
288-
authHeaderValue ='Bearer ' + Environments.current().bearerDevOps;
287+
let devopsAuthMethod = environment.authSettings?.devops?.method;
288+
if (devopsAuthMethod === AuthMethod.basic) {
289+
if (environment.authSettings.devops.basic.usernamePassword) {
290+
authHeaderKey = 'Authorization';
291+
authHeaderValue = 'Basic ' + window.btoa(environment.authSettings.devops.basic.usernamePassword);
292+
} else {
293+
showError('DevOps Username/password missing')
294+
}
295+
} else if (devopsAuthMethod === AuthMethod.bearer) {
296+
if (environment.authSettings.devops.bearer.bearerToken) {
297+
authHeaderKey = 'Authorization';
298+
authHeaderValue = 'Bearer ' + environment.authSettings.devops.bearer.bearerToken;
299+
} else {
300+
showError('DevOps Bearer token missing')
301+
}
302+
} else if (devopsAuthMethod === AuthMethod.oidc) {
303+
if (environment.authSettings.devops.oidc.bearerToken) {
304+
authHeaderKey = 'Authorization';
305+
authHeaderValue = 'Bearer ' + environment.authSettings.devops.oidc.bearerToken;
306+
} else {
307+
showError('DevOps SSO (Bearer) token missing')
308+
}
289309
} else {
290-
authHeaderKey = 'Basic';
291-
authHeaderValue = '';
310+
authHeaderKey = 'Authorization';
311+
authHeaderValue = 'Basic';
292312
}
293313
} else {
294-
if (Environments.current().mainAuth === 'basic') {
295-
authHeaderKey = 'Authorization';
296-
authHeaderValue = 'Basic ' + window.btoa(Environments.current().usernamePassword);
297-
} else if (Environments.current().mainAuth === 'pre') {
298-
authHeaderKey = 'x-ditto-pre-authenticated';
299-
authHeaderValue = Environments.current().dittoPreAuthenticatedUsername;
300-
} else if (Environments.current().mainAuth === 'bearer') {
301-
authHeaderKey = 'Authorization';
302-
authHeaderValue ='Bearer ' + Environments.current().bearer;
314+
let mainAuthMethod = environment.authSettings?.main?.method;
315+
if (mainAuthMethod === AuthMethod.basic) {
316+
if (environment.authSettings.main.basic.usernamePassword) {
317+
authHeaderKey = 'Authorization';
318+
authHeaderValue = 'Basic ' + window.btoa(environment.authSettings.main.basic.usernamePassword);
319+
} else {
320+
showError('Username/password missing')
321+
}
322+
} else if (mainAuthMethod === AuthMethod.pre) {
323+
if (environment.authSettings.main.pre.dittoPreAuthenticatedUsername) {
324+
authHeaderKey = 'x-ditto-pre-authenticated';
325+
authHeaderValue = environment.authSettings.main.pre.dittoPreAuthenticatedUsername;
326+
} else {
327+
showError('Pre-Authenticated username missing')
328+
}
329+
} else if (mainAuthMethod === AuthMethod.bearer) {
330+
if (environment.authSettings.main.bearer.bearerToken) {
331+
authHeaderKey = 'Authorization';
332+
authHeaderValue = 'Bearer ' + environment.authSettings.main.bearer.bearerToken;
333+
} else {
334+
showError('Bearer token missing')
335+
}
336+
} else if (mainAuthMethod === AuthMethod.oidc) {
337+
if (environment.authSettings.main.oidc.bearerToken) {
338+
authHeaderKey = 'Authorization';
339+
authHeaderValue = 'Bearer ' + environment.authSettings.main.oidc.bearerToken;
340+
} else {
341+
showError('SSO (Bearer) token missing')
342+
}
303343
} else {
304-
authHeaderKey = 'Basic';
305-
authHeaderValue = '';
344+
authHeaderKey = 'Authorization';
345+
authHeaderValue = 'Basic';
306346
}
307347
}
308348
}
@@ -325,78 +365,86 @@ function showDittoError(dittoErr, response) {
325365

326366
/**
327367
* Calls the Ditto api
328-
* @param {String} method 'POST', 'GET', 'DELETE', etc.
329-
* @param {String} path of the Ditto call (e.g. '/things')
368+
* @param {string} method 'POST', 'GET', 'DELETE', etc.
369+
* @param {string} path of the Ditto call (e.g. '/things')
330370
* @param {Object} body payload for the api call
331371
* @param {Object} additionalHeaders object with additional header fields
332372
* @param {boolean} returnHeaders request full response instead of json content
333373
* @param {boolean} devOps default: false. Set true to avoid /api/2 path
334374
* @param {boolean} returnErrorJson default: false. Set true to return the response of a failed HTTP call as JSON
335375
* @return {Object} result as json object
336376
*/
337-
export async function callDittoREST(method,
338-
path,
377+
export async function callDittoREST(method: string,
378+
path: string,
339379
body = null,
340380
additionalHeaders = null,
341381
returnHeaders = false,
342382
devOps = false,
343383
returnErrorJson = false): Promise<any> {
344-
let response;
345-
const contentType = method === 'PATCH' ? 'application/merge-patch+json' : 'application/json';
346-
try {
347-
response = await fetch(Environments.current().api_uri + (devOps ? '' : '/api/2') + path, {
348-
method: method,
349-
headers: {
350-
'Content-Type': contentType,
351-
[authHeaderKey]: authHeaderValue,
352-
...additionalHeaders,
353-
},
354-
...(method !== 'GET' && method !== 'DELETE' && body !== undefined) && {body: JSON.stringify(body)},
355-
});
356-
} catch (err) {
357-
Utils.showError(err);
358-
throw err;
359-
}
360-
if (!response.ok) {
361-
if (returnErrorJson) {
384+
if (authHeaderValue) {
385+
let response;
386+
const contentType = method === 'PATCH' ? 'application/merge-patch+json' : 'application/json';
387+
try {
388+
response = await fetch(Environments.current().api_uri + (devOps ? '' : '/api/2') + path, {
389+
method: method,
390+
headers: {
391+
'Content-Type': contentType,
392+
[authHeaderKey]: authHeaderValue,
393+
...additionalHeaders,
394+
},
395+
...(method !== 'GET' && method !== 'DELETE' && body !== undefined) && {body: JSON.stringify(body)},
396+
});
397+
} catch (err) {
398+
Utils.showError(err);
399+
throw err;
400+
}
401+
if (!response.ok) {
402+
if (returnErrorJson) {
403+
if (returnHeaders) {
404+
return response;
405+
} else {
406+
return response.json().then((dittoErr) => {
407+
showDittoError(dittoErr, response);
408+
return dittoErr;
409+
});
410+
}
411+
} else {
412+
response.json()
413+
.then((dittoErr) => {
414+
showDittoError(dittoErr, response);
415+
})
416+
.catch((err) => {
417+
Utils.showError('No error details from Ditto', response.statusText, response.status);
418+
});
419+
throw new Error('An error occurred: ' + response.status);
420+
}
421+
}
422+
if (response.status !== 204) {
362423
if (returnHeaders) {
363424
return response;
364425
} else {
365-
return response.json().then((dittoErr) => {
366-
showDittoError(dittoErr, response);
367-
return dittoErr;
368-
});
426+
return response.json();
369427
}
370428
} else {
371-
response.json()
372-
.then((dittoErr) => {
373-
showDittoError(dittoErr, response);
374-
})
375-
.catch((err) => {
376-
Utils.showError('No error details from Ditto', response.statusText, response.status);
377-
});
378-
throw new Error('An error occurred: ' + response.status);
379-
}
380-
}
381-
if (response.status !== 204) {
382-
if (returnHeaders) {
383-
return response;
384-
} else {
385-
return response.json();
429+
return null;
386430
}
387431
} else {
388-
return null;
432+
throw new Error("Authentication missing");
389433
}
390434
}
391435

392436
export function getEventSource(thingIds, urlParams) {
393-
return new EventSourcePolyfill(
437+
if (authHeaderValue) {
438+
return new EventSourcePolyfill(
394439
`${Environments.current().api_uri}/api/2/things?ids=${thingIds}${urlParams ? '&' + urlParams : ''}`, {
395440
headers: {
396441
[authHeaderKey]: authHeaderValue,
397442
},
398443
},
399-
);
444+
);
445+
} else {
446+
throw new Error("Authentication missing");
447+
}
400448
}
401449

402450
/**
@@ -409,7 +457,6 @@ export function getEventSource(thingIds, urlParams) {
409457
* @return {*} promise to the result
410458
*/
411459
export async function callConnectionsAPI(operation, successCallback, connectionId = '', connectionJson = null, command = null) {
412-
Utils.assert((env() !== 'things' || Environments.current().solutionId), 'No solutionId configured in environment');
413460
const params = config[env()][operation];
414461
let response;
415462
let body;
@@ -424,19 +471,23 @@ export async function callConnectionsAPI(operation, successCallback, connectionI
424471
body = command;
425472
}
426473

427-
try {
428-
response = await fetch(Environments.current().api_uri + params.path.replace('{{solutionId}}',
429-
Environments.current().solutionId).replace('{{connectionId}}', connectionId), {
430-
method: params.method,
431-
headers: {
432-
'Content-Type': operation === 'connectionCommand' ? 'text/plain' : 'application/json',
433-
[authHeaderKey]: authHeaderValue,
434-
},
435-
...(body) && {body: body},
436-
});
437-
} catch (err) {
438-
Utils.showError(err);
439-
throw err;
474+
if (authHeaderValue) {
475+
try {
476+
response = await fetch(Environments.current().api_uri + params.path
477+
.replace('{{connectionId}}', connectionId), {
478+
method: params.method,
479+
headers: {
480+
'Content-Type': operation === 'connectionCommand' ? 'text/plain' : 'application/json',
481+
[authHeaderKey]: authHeaderValue,
482+
},
483+
...(body) && {body: body},
484+
});
485+
} catch (err) {
486+
Utils.showError(err);
487+
throw err;
488+
}
489+
} else {
490+
throw new Error("Authentication missing");
440491
}
441492

442493
if (!response.ok) {
@@ -479,9 +530,7 @@ export async function callConnectionsAPI(operation, successCallback, connectionI
479530
}
480531

481532
export function env() {
482-
if (Environments.current().api_uri.startsWith('https://things')) {
483-
return 'things';
484-
} else if (Environments.current().ditto_version === '2') {
533+
if (Environments.current().ditto_version === 2) {
485534
return 'ditto_2';
486535
} else {
487536
return 'ditto_3';

0 commit comments

Comments
 (0)