Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions source/core/oauth/OAuthClient.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
class OAuthClient {
constructor(config) {
if (!config) {
throw new Error('OAuthClient requires a config object')
}

this.config = config
this.token = null
this.expirationTime = 0
this._refreshPromise = null
}

getToken() {
return this.token
}

isExpired() {
return Date.now() > this.expirationTime
}

async refreshAccessToken() {
// Dedupe concurrent refreshes so parallel requests share one token call.
if (this._refreshPromise) {
return this._refreshPromise
}

const data = new URLSearchParams({
grant_type: 'client_credentials',
client_id: this.config.clientId,
client_secret: this.config.clientSecret,
})

this._refreshPromise = (async () => {
try {
const response = await fetch(this.config.tokenEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: data.toString(),
})

if (!response.ok) {
console.error(`Failed to retrieve access token: ${response.status}`)
return
}

const json = await response.json()
this.token = json.access_token
this.expirationTime = Date.now() + json.expires_in * 1000
} catch (error) {
console.error(`Failed to retrieve access token due to exception: ${error.message}`)
} finally {
this._refreshPromise = null
}
})()

return this._refreshPromise
}

async getValidToken() {
if (this.isExpired()) {
await this.refreshAccessToken()
}
return this.token
}
}

/*
* Singleton instance holder.
* The host application is responsible for calling configureOAuthClient() at
* startup with credentials sourced from wherever it sees fit (env vars,
* runtime config, user input, etc.). The library itself does not read any
* build-time env vars, so it stays bundler-agnostic.
*/

let oAuthClientInstance = null

export function configureOAuthClient(config) {
if (!config || !config.clientId || !config.clientSecret || !config.tokenEndpoint) {
console.warn('configureOAuthClient called without complete config; OAuth disabled.')
return null
}
oAuthClientInstance = new OAuthClient(config)
return oAuthClientInstance
}

export function getOAuthClient() {
return oAuthClientInstance
}

export function resetOAuthClient() {
oAuthClientInstance = null
}
12 changes: 11 additions & 1 deletion source/core/sweapi/Collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
******************************* END LICENSE BLOCK ***************************/

import SweCollectionDataParser from "../parsers/sweapi/collection/SweCollectionDataParser";
import {getOAuthClient} from "osh-js/source/core/oauth/OAuthClient";

class Collection {
/**
Expand Down Expand Up @@ -45,10 +46,19 @@ class Collection {
const queryString = `${this.filter.toQueryString()}&offset=${offset}&limit=${this.pageSize}`;
const fullUrl = this.url + '?' + queryString;

const headers = {};
const oAuthClient = getOAuthClient();
if (oAuthClient !== null) {
const token = await oAuthClient.getValidToken();
if (token) {
headers['Authorization'] = 'Bearer ' + token;
}
}

const jsonResponse = await fetch(fullUrl, {
method: 'GET',
credentials: 'include',
headers: {}
headers: headers
}).then((response) => {
if (!response.ok) {
const err = new Error(`Got ${response.status} response from ${fullUrl}`);
Expand Down
74 changes: 39 additions & 35 deletions source/core/sweapi/SensorWebApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import WebSocketConnector from "../connector/WebSocketConnector";
import {assertDefined, isDefined} from "../utils/Utils";
import MqttTopicConnector from "../connector/MqttTopicConnector";
import MqttConnector from "../connector/MqttConnector";
import {getOAuthClient} from "osh-js/source/core/oauth/OAuthClient";

class SensorWebApi {

Expand Down Expand Up @@ -102,63 +103,66 @@ class SensorWebApi {
this._network.stream.connector.connect();
}

getHeaders() {
const headers = {
};
async getHeaders() {
const headers = {};

if('connectorOpts' in this.networkProperties){
if('username' in this.networkProperties.connectorOpts && 'password' in this.networkProperties.connectorOpts) {
headers['Authorization'] = 'Basic ' +
btoa(this.networkProperties.connectorOpts.username + ":" + this.networkProperties.connectorOpts.password);
const oAuthClient = getOAuthClient();
if (oAuthClient !== null) {
const token = await oAuthClient.getValidToken();
if (token) {
headers['Authorization'] = 'Bearer ' + token;
}
} else if ('connectorOpts' in this.networkProperties) {
const opts = this.networkProperties.connectorOpts;
if ('username' in opts && 'password' in opts) {
headers['Authorization'] = 'Basic ' + btoa(opts.username + ':' + opts.password);
} else {
for(let key in this.networkProperties.connectorOpts) {
headers[key] = this.networkProperties.connectorOpts[key];
for (const key in opts) {
headers[key] = opts[key];
}
}
}
return headers;
}

fetchAsJson(apiUrl, queryString) {
const fullUrl = this.baseUrl() + apiUrl + '?' +queryString;
async fetchAsJson(apiUrl, queryString) {
const fullUrl = this.baseUrl() + apiUrl + '?' + queryString;

const headers = this.getHeaders();
const headers = await this.getHeaders();

return fetch(fullUrl, {
method: 'GET',
credentials: 'include',
headers: headers
}
).then(function (response) {
if (!response.ok) {
const err = new Error(`Got ${response.status} response from ${this.baseUrl()}`);
err.response = response;
throw err;
}
return response.json();
const response = await fetch(fullUrl, {
method: 'GET',
credentials: 'include',
headers: headers
});
}

postAsJson(apiUrl, jsonPayload) {
const fullUrl = this.baseUrl() + apiUrl;
if (!response.ok) {
const err = new Error(`Got ${response.status} response from ${this.baseUrl()}`);
err.response = response;
throw err;
}
return response.json();
}

const headers = this.getHeaders();
async postAsJson(apiUrl, jsonPayload) {
const fullUrl = this.baseUrl() + apiUrl;

const headers = await this.getHeaders();
headers['Accept'] = 'application/json';
headers['Content-Type'] = 'application/json';

fetch(fullUrl, {
const response = await fetch(fullUrl, {
method: 'POST',
headers: headers,
credentials: 'include',
body: jsonPayload
}).then(function (response) {
if (!response.ok) {
const err = new Error(`Got ${response.status} response from ${fullUrl}`);
err.response = response;
throw err;
}
});

if (!response.ok) {
const err = new Error(`Got ${response.status} response from ${fullUrl}`);
err.response = response;
throw err;
}
}
}
export default SensorWebApi;