Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#1591 add SSO via OIDC to Ditto UI #2032

Merged
merged 8 commits into from
Oct 4, 2024
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ private BinaryValidationResult tryToValidateWithJwtParser(final JsonWebToken jso
}

private BinaryValidationResult validateWithJwtParser(final JsonWebToken jsonWebToken, final JwtParser jwtParser) {
jwtParser.parseClaimsJws(jsonWebToken.getToken());
jwtParser.parse(jsonWebToken.getToken());

return BinaryValidationResult.valid();
}
Expand Down
9 changes: 9 additions & 0 deletions gateway/service/src/main/resources/gateway-dev.conf
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ ditto {
oauth {
# use unencrypted http for local oauth providers.
protocol = "http"

openid-connect-issuers {
fake = {
issuer = "localhost:9900/fake"
auth-subjects = [
"{{ jwt:sub }}"
]
}
}
}
}
}
Expand Down
8 changes: 7 additions & 1 deletion ui/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,18 @@ tbody {
display: inline;
}

.toast-header {
.toast-header-warn {
color: #842029;
background-color: #f8d7da;
border-color: #f5c2c7;
}

.toast-header-info {
color: #842029;
background-color: #ebd7f8;
border-color: #fbfbfb;
}

textarea {
white-space: pre;
overflow-wrap: normal;
Expand Down
22 changes: 11 additions & 11 deletions ui/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ import * as ConnectionsMonitor from './modules/connections/connectionsMonitor.js

import * as Authorization from './modules/environments/authorization.js';
import * as Environments from './modules/environments/environments.js';
import * as Operations from './modules/operations/servicesLogging.js';
import * as Piggyback from './modules/operations/piggyback.js';
import * as Operations from './modules/operations/servicesLogging.js';
import * as Templates from './modules/operations/templates.js';
import * as Policies from './modules/policies/policies.js';
import * as PoliciesJSON from './modules/policies/policiesJSON.js';
import * as PoliciesEntries from './modules/policies/policiesEntries.js';
import * as PoliciesImports from './modules/policies/policiesImports.js';
import * as PoliciesSubjects from './modules/policies/policiesSubjects';
import * as PoliciesJSON from './modules/policies/policiesJSON.js';
import * as PoliciesResources from './modules/policies/policiesResources';
import * as PoliciesSubjects from './modules/policies/policiesSubjects';
import * as Attributes from './modules/things/attributes.js';
import * as FeatureMessages from './modules/things/featureMessages.js';
import * as Features from './modules/things/features.js';
Expand All @@ -51,10 +51,10 @@ let mainNavbar;
document.addEventListener('DOMContentLoaded', async function() {
Utils.ready();
await Things.ready();
ThingsSearch.ready();
ThingsCRUD.ready();
await ThingsSearch.ready();
await ThingsCRUD.ready();
await ThingMessages.ready();
ThingsSSE.ready();
await ThingsSSE.ready();
MessagesIncoming.ready();
Attributes.ready();
await Fields.ready();
Expand All @@ -70,25 +70,25 @@ document.addEventListener('DOMContentLoaded', async function() {
Connections.ready();
ConnectionsCRUD.ready();
await ConnectionsMonitor.ready();
Operations.ready();
await Operations.ready();
Authorization.ready();
await Environments.ready();
Piggyback.ready();
Templates.ready();
await Piggyback.ready();
await Templates.ready();

const thingDescription = WoTDescription({
itemsId: 'tabItemsThing',
contentId: 'tabContentThing',
}, false);
Things.addChangeListener(thingDescription.onReferenceChanged);
thingDescription.ready();
await thingDescription.ready();

const featureDescription = WoTDescription({
itemsId: 'tabItemsFeatures',
contentId: 'tabContentFeatures',
}, true);
Features.addChangeListener(featureDescription.onReferenceChanged);
featureDescription.ready();
await featureDescription.ready();

// make dropdowns not cutting off
new Dropdown(document.querySelector('.dropdown-toggle'), {
Expand Down
211 changes: 130 additions & 81 deletions ui/modules/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@

import { EventSourcePolyfill } from 'event-source-polyfill';
import * as Environments from './environments/environments.js';
import { AuthMethod } from './environments/environments.js';
import * as Utils from './utils.js';
import { showError } from './utils.js';


const config = {
Expand Down Expand Up @@ -278,31 +280,69 @@ let authHeaderValue;
* Activates authorization header for api calls
* @param {boolean} forDevOps if true, the credentials for the dev ops api will be used.
*/
export function setAuthHeader(forDevOps) {
export function setAuthHeader(forDevOps: boolean) {
authHeaderValue = undefined;
let environment = Environments.current();
if (forDevOps) {
if (Environments.current().devopsAuth === 'basic') {
authHeaderKey = 'Authorization';
authHeaderValue = 'Basic ' + window.btoa(Environments.current().usernamePasswordDevOps);
} else if (Environments.current().devopsAuth === 'bearer') {
authHeaderKey = 'Authorization';
authHeaderValue ='Bearer ' + Environments.current().bearerDevOps;
let devopsAuthMethod = environment.authSettings?.devops?.method;
if (devopsAuthMethod === AuthMethod.basic) {
if (environment.authSettings.devops.basic.usernamePassword) {
authHeaderKey = 'Authorization';
authHeaderValue = 'Basic ' + window.btoa(environment.authSettings.devops.basic.usernamePassword);
} else {
showError('DevOps Username/password missing')
}
} else if (devopsAuthMethod === AuthMethod.bearer) {
if (environment.authSettings.devops.bearer.bearerToken) {
authHeaderKey = 'Authorization';
authHeaderValue = 'Bearer ' + environment.authSettings.devops.bearer.bearerToken;
} else {
showError('DevOps Bearer token missing')
}
} else if (devopsAuthMethod === AuthMethod.oidc) {
if (environment.authSettings.devops.oidc.bearerToken) {
authHeaderKey = 'Authorization';
authHeaderValue = 'Bearer ' + environment.authSettings.devops.oidc.bearerToken;
} else {
showError('DevOps SSO (Bearer) token missing')
}
} else {
authHeaderKey = 'Basic';
authHeaderValue = '';
authHeaderKey = 'Authorization';
authHeaderValue = 'Basic';
}
} else {
if (Environments.current().mainAuth === 'basic') {
authHeaderKey = 'Authorization';
authHeaderValue = 'Basic ' + window.btoa(Environments.current().usernamePassword);
} else if (Environments.current().mainAuth === 'pre') {
authHeaderKey = 'x-ditto-pre-authenticated';
authHeaderValue = Environments.current().dittoPreAuthenticatedUsername;
} else if (Environments.current().mainAuth === 'bearer') {
authHeaderKey = 'Authorization';
authHeaderValue ='Bearer ' + Environments.current().bearer;
let mainAuthMethod = environment.authSettings?.main?.method;
if (mainAuthMethod === AuthMethod.basic) {
if (environment.authSettings.main.basic.usernamePassword) {
authHeaderKey = 'Authorization';
authHeaderValue = 'Basic ' + window.btoa(environment.authSettings.main.basic.usernamePassword);
} else {
showError('Username/password missing')
}
} else if (mainAuthMethod === AuthMethod.pre) {
if (environment.authSettings.main.pre.dittoPreAuthenticatedUsername) {
authHeaderKey = 'x-ditto-pre-authenticated';
authHeaderValue = environment.authSettings.main.pre.dittoPreAuthenticatedUsername;
} else {
showError('Pre-Authenticated username missing')
}
} else if (mainAuthMethod === AuthMethod.bearer) {
if (environment.authSettings.main.bearer.bearerToken) {
authHeaderKey = 'Authorization';
authHeaderValue = 'Bearer ' + environment.authSettings.main.bearer.bearerToken;
} else {
showError('Bearer token missing')
}
} else if (mainAuthMethod === AuthMethod.oidc) {
if (environment.authSettings.main.oidc.bearerToken) {
authHeaderKey = 'Authorization';
authHeaderValue = 'Bearer ' + environment.authSettings.main.oidc.bearerToken;
} else {
showError('SSO (Bearer) token missing')
}
} else {
authHeaderKey = 'Basic';
authHeaderValue = '';
authHeaderKey = 'Authorization';
authHeaderValue = 'Basic';
}
}
}
Expand All @@ -325,78 +365,86 @@ function showDittoError(dittoErr, response) {

/**
* Calls the Ditto api
* @param {String} method 'POST', 'GET', 'DELETE', etc.
* @param {String} path of the Ditto call (e.g. '/things')
* @param {string} method 'POST', 'GET', 'DELETE', etc.
* @param {string} path of the Ditto call (e.g. '/things')
* @param {Object} body payload for the api call
* @param {Object} additionalHeaders object with additional header fields
* @param {boolean} returnHeaders request full response instead of json content
* @param {boolean} devOps default: false. Set true to avoid /api/2 path
* @param {boolean} returnErrorJson default: false. Set true to return the response of a failed HTTP call as JSON
* @return {Object} result as json object
*/
export async function callDittoREST(method,
path,
export async function callDittoREST(method: string,
path: string,
body = null,
additionalHeaders = null,
returnHeaders = false,
devOps = false,
returnErrorJson = false): Promise<any> {
let response;
const contentType = method === 'PATCH' ? 'application/merge-patch+json' : 'application/json';
try {
response = await fetch(Environments.current().api_uri + (devOps ? '' : '/api/2') + path, {
method: method,
headers: {
'Content-Type': contentType,
[authHeaderKey]: authHeaderValue,
...additionalHeaders,
},
...(method !== 'GET' && method !== 'DELETE' && body !== undefined) && {body: JSON.stringify(body)},
});
} catch (err) {
Utils.showError(err);
throw err;
}
if (!response.ok) {
if (returnErrorJson) {
if (authHeaderValue) {
let response;
const contentType = method === 'PATCH' ? 'application/merge-patch+json' : 'application/json';
try {
response = await fetch(Environments.current().api_uri + (devOps ? '' : '/api/2') + path, {
method: method,
headers: {
'Content-Type': contentType,
[authHeaderKey]: authHeaderValue,
...additionalHeaders,
},
...(method !== 'GET' && method !== 'DELETE' && body !== undefined) && {body: JSON.stringify(body)},
});
} catch (err) {
Utils.showError(err);
throw err;
}
if (!response.ok) {
if (returnErrorJson) {
if (returnHeaders) {
return response;
} else {
return response.json().then((dittoErr) => {
showDittoError(dittoErr, response);
return dittoErr;
});
}
} else {
response.json()
.then((dittoErr) => {
showDittoError(dittoErr, response);
})
.catch((err) => {
Utils.showError('No error details from Ditto', response.statusText, response.status);
});
throw new Error('An error occurred: ' + response.status);
}
}
if (response.status !== 204) {
if (returnHeaders) {
return response;
} else {
return response.json().then((dittoErr) => {
showDittoError(dittoErr, response);
return dittoErr;
});
return response.json();
}
} else {
response.json()
.then((dittoErr) => {
showDittoError(dittoErr, response);
})
.catch((err) => {
Utils.showError('No error details from Ditto', response.statusText, response.status);
});
throw new Error('An error occurred: ' + response.status);
}
}
if (response.status !== 204) {
if (returnHeaders) {
return response;
} else {
return response.json();
return null;
}
} else {
return null;
throw new Error("Authentication missing");
}
}

export function getEventSource(thingIds, urlParams) {
return new EventSourcePolyfill(
if (authHeaderValue) {
return new EventSourcePolyfill(
`${Environments.current().api_uri}/api/2/things?ids=${thingIds}${urlParams ? '&' + urlParams : ''}`, {
headers: {
[authHeaderKey]: authHeaderValue,
},
},
);
);
} else {
throw new Error("Authentication missing");
}
}

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

try {
response = await fetch(Environments.current().api_uri + params.path.replace('{{solutionId}}',
Environments.current().solutionId).replace('{{connectionId}}', connectionId), {
method: params.method,
headers: {
'Content-Type': operation === 'connectionCommand' ? 'text/plain' : 'application/json',
[authHeaderKey]: authHeaderValue,
},
...(body) && {body: body},
});
} catch (err) {
Utils.showError(err);
throw err;
if (authHeaderValue) {
try {
response = await fetch(Environments.current().api_uri + params.path
.replace('{{connectionId}}', connectionId), {
method: params.method,
headers: {
'Content-Type': operation === 'connectionCommand' ? 'text/plain' : 'application/json',
[authHeaderKey]: authHeaderValue,
},
...(body) && {body: body},
});
} catch (err) {
Utils.showError(err);
throw err;
}
} else {
throw new Error("Authentication missing");
}

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

export function env() {
if (Environments.current().api_uri.startsWith('https://things')) {
return 'things';
} else if (Environments.current().ditto_version === '2') {
if (Environments.current().ditto_version === 2) {
return 'ditto_2';
} else {
return 'ditto_3';
Expand Down
Loading
Loading