From e32ecba5521f6ef0ac73f3028846b0be88bd8842 Mon Sep 17 00:00:00 2001 From: Mats Johansen Date: Tue, 30 Apr 2024 11:03:43 +0200 Subject: [PATCH] feat(authentication wip): get refresh token --- packages/lib/src/stores/auth.ts | 137 +++++------ packages/lib/src/stores/negotiate.ts | 324 ++++++++++++++++----------- 2 files changed, 260 insertions(+), 201 deletions(-) diff --git a/packages/lib/src/stores/auth.ts b/packages/lib/src/stores/auth.ts index 9e20274..eabdc8e 100644 --- a/packages/lib/src/stores/auth.ts +++ b/packages/lib/src/stores/auth.ts @@ -4,96 +4,103 @@ */ import { writable } from "svelte/store"; -export const authStore = writable(""); +export const authStore = writable(""); fetchAccessToken(); -export async function fetchAccessToken() { +/** + * Fetches the access token from the backend service + */ +export async function fetchAccessToken(): Promise { const response = await fetch(`/oauth2/auth`, { method: "GET", credentials: "include", - - }) - + }); + const temporaryToken = response.headers.get("Authorization"); if (!temporaryToken) { console.error("No temporary token found in response headers"); return; } - console.log('temporaryToken', temporaryToken); - exchangeCodeForToken(temporaryToken) - + console.log("temporaryToken", temporaryToken); + exchangeCodeForToken(temporaryToken); } // Placeholder values, replace these with your actual configurations -const clientId = 'bridgehead-test-private'; -const clientSecret = 'mmDjwfaoLeTzdRUeGZRDEIaYXgY3zL6r'; -const redirectUri = window.location.origin -const tokenEndpoint = 'https://login.verbis.dkfz.de/realms/test-realm-01/protocol/openid-connect/auth'; +const clientId = "bridgehead-test-private"; +const clientSecret = "mmDjwfaoLeTzdRUeGZRDEIaYXgY3zL6r"; +// const redirectUri = window.location.origin +const tokenEndpoint = + "https://login.verbis.dkfz.de/realms/test-realm-01/protocol/openid-connect/auth"; const refreshTokenTimeInSeconds = 300; // 5 minutes -let accessToken = ''; -let refreshToken = ''; +let accessToken = ""; +let refreshToken = ""; -// Function to perform the OAuth2 token exchange -async function exchangeCodeForToken(code) { - const requestBody = new URLSearchParams(); - requestBody.append('grant_type', 'authorization_code'); - requestBody.append('client_id', clientId); - // requestBody.append('redirect_uri', redirectUri); - requestBody.append('code', code); +/** + * exchanges the temporary code received from the OAuth2 provider for an access token + * @param code the temporary code received from the OAuth2 provider + */ +async function exchangeCodeForToken(code: string): Promise { + const requestBody = new URLSearchParams(); + requestBody.append("grant_type", "refresh_token"); + requestBody.append("client_id", clientId); + // requestBody.append('redirect_uri', redirectUri); + requestBody.append("code", code); - try { - const response = await fetch(tokenEndpoint, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: requestBody, - }); + try { + const response = await fetch(tokenEndpoint, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: requestBody, + }); - const responseData = await response.json(); - accessToken = responseData.access_token; - refreshToken = responseData.refresh_token; + const responseData = await response.json(); + accessToken = responseData.access_token; + refreshToken = responseData.refresh_token; - console.log('refreshToken', refreshToken); - console.log('accessToken', accessToken); - startTokenRefreshTimer(); - return true; - } catch (error) { - console.error('Token exchange failed:', error); - return false; - } + console.log("refreshToken", refreshToken); + console.log("accessToken", accessToken); + startTokenRefreshTimer(); + } catch (error) { + console.error("Token exchange failed:", error); + } } -// Function to refresh the access token using the refresh token -async function refreshAccessToken() { - const requestBody = new URLSearchParams(); - requestBody.append('grant_type', 'refresh_token'); - requestBody.append('client_id', clientId); - requestBody.append('client_secret', clientSecret); - requestBody.append('refresh_token', refreshToken); +/** + * Function to refresh the access token using the refresh token + */ +async function refreshAccessToken(): Promise { + const requestBody = new URLSearchParams(); + requestBody.append("grant_type", "refresh_token"); + requestBody.append("client_id", clientId); + requestBody.append("client_secret", clientSecret); + requestBody.append("refresh_token", refreshToken); - try { - const response = await fetch(tokenEndpoint, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: requestBody, - }); + try { + const response = await fetch(tokenEndpoint, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: requestBody, + }); - const responseData = await response.json(); - accessToken = responseData.access_token; - startTokenRefreshTimer(); - console.log('Token refreshed'); - } catch (error) { - console.error('Token refresh failed:', error); - } + const responseData = await response.json(); + accessToken = responseData.access_token; + startTokenRefreshTimer(); + console.log("Token refreshed"); + } catch (error) { + console.error("Token refresh failed:", error); + } } -// Function to start the token refresh timer -function startTokenRefreshTimer() { - setInterval(refreshAccessToken, refreshTokenTimeInSeconds * 1000); +/** + * Function to start the token refresh timer + */ +function startTokenRefreshTimer(): void { + setInterval(refreshAccessToken, refreshTokenTimeInSeconds * 1000); } diff --git a/packages/lib/src/stores/negotiate.ts b/packages/lib/src/stores/negotiate.ts index 61763bf..5cdc3a0 100644 --- a/packages/lib/src/stores/negotiate.ts +++ b/packages/lib/src/stores/negotiate.ts @@ -4,7 +4,7 @@ import type { QueryItem } from "../types/queryData"; import { buildAstFromQuery } from "../helpers/ast-transformer"; import type { AstElement, AstTopLayer } from "../types/ast"; import { lensOptions } from "./options"; -import { v4 as uuidv4 } from 'uuid'; +import { v4 as uuidv4 } from "uuid"; import type { Collection } from "../types/collection"; import type { SendableQuery } from "../types/queryData"; import { authStore } from "./auth"; @@ -12,6 +12,7 @@ import { translateAstToCql } from "../cql-translator-service/ast-to-cql-translat import { buildLibrary, buildMeasure } from "../helpers/cql-measure"; import type { Measure } from "../types/backend"; import { measureStore } from "./measures"; +import type { LensOptions } from "../types/options"; export const negotiateStore = writable([]); @@ -20,140 +21,150 @@ let currentQuery: QueryItem[][] = [[]]; // NOTE: // This is currently hard coded, as this configuration can not be replicated for this class easily (compare with variable in searchbutton) // With the merge of the multiple backends branch, an option for that will be already introduced. -let backendMeasures: string = "DKTK_STRAT_DEF_IN_INITIAL_POPULATION"; +const backendMeasures: string = "DKTK_STRAT_DEF_IN_INITIAL_POPULATION"; let currentMeasures: Measure[] = []; authStore.subscribe((value) => { - authHeader=value; -}) - -queryStore.subscribe(query =>{ - currentQuery=query; + authHeader = value; }); -measureStore.subscribe(measures => { - currentMeasures=measures; -}) +queryStore.subscribe((query) => { + currentQuery = query; +}); +measureStore.subscribe((measures) => { + currentMeasures = measures; +}); /** * Recursively builds a human readable query string from the AST - * @param queryLayer the current layer of the query + * @param queryLayer the current layer of the query * @param humanReadableQuery string to append to * @returns a human readable query string */ -const buildHumanReadableRecursively = (queryLayer: AstElement, humanReadableQuery: string): string => { +export const buildHumanReadableRecursively = ( + queryLayer: AstElement, + humanReadableQuery: string, +): string => { if ( queryLayer === null || - !('children' in queryLayer) || - 'children' in queryLayer && - ( - queryLayer.children === null || - queryLayer.children.length === 0 || - queryLayer.children[0] === null - ) + !("children" in queryLayer) || + ("children" in queryLayer && + (queryLayer.children === null || + queryLayer.children.length === 0 || + queryLayer.children[0] === null)) ) { - return humanReadableQuery + return humanReadableQuery; } - if (queryLayer.children.length > 1) { - humanReadableQuery += '(' + humanReadableQuery += "("; } queryLayer.children.forEach((child: AstElement, index: number): void => { - - if ('type' in child && 'value' in child && 'key' in child) { - if (typeof child.value === 'string') { - humanReadableQuery += `(${child.key} ${child.type} ${child.value})` + if ("type" in child && "value" in child && "key" in child) { + if (typeof child.value === "string") { + humanReadableQuery += `(${child.key} ${child.type} ${child.value})`; } - if (typeof child.value === 'object' && 'min' in child.value && 'max' in child.value) { - humanReadableQuery += `(${child.key} ${child.type} ${child.value.min} and ${child.value.max})` + if ( + typeof child.value === "object" && + "min" in child.value && + "max" in child.value + ) { + humanReadableQuery += `(${child.key} ${child.type} ${child.value.min} and ${child.value.max})`; } } - humanReadableQuery = buildHumanReadableRecursively(child, humanReadableQuery) + humanReadableQuery = buildHumanReadableRecursively( + child, + humanReadableQuery, + ); if (index === queryLayer.children.length - 1) { } if (index < queryLayer.children.length - 1) { - humanReadableQuery += ` ${queryLayer.operand} ` + humanReadableQuery += ` ${queryLayer.operand} `; } - - }) + }); if (queryLayer.children.length > 1) { - humanReadableQuery += ')' + humanReadableQuery += ")"; } - return humanReadableQuery -} - + return humanReadableQuery; +}; /** * @returns a human readable query string built from the current query -*/ + */ export const getHumanReadableQuery = (): string => { - let humanReadableQuery: string = ""; - let query: AstTopLayer queryStore.subscribe((value: QueryItem[][]) => { - query = buildAstFromQuery(value) - }) - - humanReadableQuery = buildHumanReadableRecursively(query, humanReadableQuery) - - return humanReadableQuery -} + const query: AstTopLayer = buildAstFromQuery(value); + humanReadableQuery = buildHumanReadableRecursively( + query, + humanReadableQuery, + ); + }); + return humanReadableQuery; +}; /** * sets all options needed for the negotiator -*/ - -let negotiateOptions: any = {} -const siteCollectionMap: Map = new Map() - + */ +type NegotiateOptions = { + siteMapping: { site: string; collection: string }[]; + negotiatorURL: string; + newProjectUrl: string; + editProjectUrl: string; +}; + +let negotiateOptions: NegotiateOptions; +const siteCollectionMap: Map = new Map(); const urlParams: URLSearchParams = new URLSearchParams(window.location.search); const collectionParams: string | null = urlParams.get("collections"); -lensOptions.subscribe((options: any) => { - if (!options) return - - negotiateOptions = options.negotiateOptions - options.negotiateOptions.siteMapping.forEach(({ site, collection }) => { - siteCollectionMap.set(site, collection) - if (collectionParams !== null && collectionParams.split(',').includes(collection)) { - negotiateStore.update((value) => { - return [...value, site] - }) - } - }) -}) - +lensOptions.subscribe((options: LensOptions) => { + if (!options) return; + + negotiateOptions = options.negotiateOptions as NegotiateOptions; + negotiateOptions?.siteMapping?.forEach( + ({ site, collection }: { site: string; collection: string }) => { + siteCollectionMap.set(site, collection); + if ( + collectionParams !== null && + collectionParams.split(",").includes(collection) + ) { + negotiateStore.update((value) => { + return [...value, site]; + }); + } + }, + ); +}); /** * @param sitesToNegotiate the sites to negotiate with * @returns an array of Collection objects -*/ + */ export const getCollections = (sitesToNegotiate: string[]): Collection[] => { - - let siteCollections: Collection[] = [] + const siteCollections: Collection[] = []; sitesToNegotiate.forEach((site: string) => { - let collectionId: string = "" + let collectionId: string = ""; // TODO: Why is site id mapped to Uppercase? - if (siteCollectionMap.has(site) && siteCollectionMap.get(site) !== '') { - collectionId = siteCollectionMap.get(site) || "" + if (siteCollectionMap.has(site) && siteCollectionMap.get(site) !== "") { + collectionId = siteCollectionMap.get(site) || ""; } - const siteId: string = site.split(':collection:')[0] + const siteId: string = site.split(":collection:")[0]; /** - * Create Collection object with stratifier coming from response store - * + * Create Collection object with stratifier coming from response store + * * seems to need only stratifier 'custodian', why? */ @@ -173,7 +184,7 @@ export const getCollections = (sitesToNegotiate: string[]): Collection[] => { // }) // ) - if(collectionId !== "") { + if (collectionId !== "") { siteCollections.push({ siteId, site, @@ -181,84 +192,103 @@ export const getCollections = (sitesToNegotiate: string[]): Collection[] => { /** * TODO: add the local redirect uri here */ - localRedirectUri: 'some uri' - }) + localRedirectUri: "some uri", + }); } - }) - - console.log('getCollections -> return siteCollections', siteCollections); - return siteCollections -} + }); + console.log("getCollections -> return siteCollections", siteCollections); + return siteCollections; +}; /** * builds a sendable query object from the current query * sends query to negotiator * redirects to negotiator * @param sitesToNegotiate the sites to negotiate with -*/ -export const negotiate = async (sitesToNegotiate: string[]) => { - + */ +export const negotiate = async (sitesToNegotiate: string[]): void => { //TODO: get auth token here - let sendableQuery!: SendableQuery + let sendableQuery!: SendableQuery; queryStore.subscribe((value: QueryItem[][]) => { - const uuid = uuidv4() + const uuid = uuidv4(); sendableQuery = { query: value, - id: `${uuid}__search__${uuid}` - } - }) + id: `${uuid}__search__${uuid}`, + }; + }); - const queryBase64String: string = btoa(JSON.stringify(sendableQuery.query)) + const queryBase64String: string = btoa(JSON.stringify(sendableQuery.query)); const humanReadable: string = getHumanReadableQuery(); - const collections: Collection[] = getCollections(sitesToNegotiate) + const collections: Collection[] = getCollections(sitesToNegotiate); // TODO: Implement proper configuration option for the switch between negotiator and project manager - let negotiatorResponse = (false) - ? await sendRequestToNegotiator(sendableQuery, humanReadable, collections, queryBase64String) - : await sendRequestToProjectManager(sendableQuery, humanReadable, collections, queryBase64String) - - const indexOfQuestionMark = negotiatorResponse.redirect_uri.toString().indexOf('?') - const subpage = "/project-view" - const negotiationURI = negotiatorResponse.redirect_uri.toString().slice(0, indexOfQuestionMark) + `${subpage}` + negotiatorResponse.redirect_uri.toString().slice(indexOfQuestionMark) - - window.location.href = negotiationURI -} - + const negotiatorResponse = false + ? await sendRequestToNegotiator( + sendableQuery, + humanReadable, + collections, + queryBase64String, + ) + : await sendRequestToProjectManager( + sendableQuery, + humanReadable, + collections, + queryBase64String, + ); + + const indexOfQuestionMark = negotiatorResponse.redirect_uri + .toString() + .indexOf("?"); + const subpage = "/project-view"; + const negotiationURI = + negotiatorResponse.redirect_uri + .toString() + .slice(0, indexOfQuestionMark) + + `${subpage}` + + negotiatorResponse.redirect_uri.toString().slice(indexOfQuestionMark); + + window.location.href = negotiationURI; +}; /** - * + * * @param sendableQuery the query to be sent to the negotiator * @param humanReadable a human readable query string to view in the negotiator project * @param collections the collections to negotiate with + * @param queryBase64String the query in base64 string format * @returns the redirect uri from the negotiator */ -async function sendRequestToNegotiator(sendableQuery: SendableQuery, humanReadable: string, collections: Collection[], queryBase64String: string): Promise { - - let base64Query: string = btoa(JSON.stringify(sendableQuery.query)) +async function sendRequestToNegotiator( + sendableQuery: SendableQuery, + humanReadable: string, + collections: Collection[], + queryBase64String: string, +): Promise { + const base64Query: string = btoa(JSON.stringify(sendableQuery.query)); const returnURL: string = `${window.location.protocol}//${window.location.host}/?nToken=${sendableQuery.id}&query=${base64Query}`; const response: Response = await fetch( - `${negotiateOptions.negotiatorURL}?nToken=${sendableQuery.id}`, + `${negotiateOptions.negotiatorURL}?nToken=${sendableQuery.id}`, { method: "POST", headers: { - 'Accept': 'application/json; charset=utf-8', + Accept: "application/json; charset=utf-8", "Content-Type": "application/json", - 'Authorization': 'Basic YmJtcmktZGlyZWN0b3J5Omw5RFJLVUROcTBTbDAySXhaUGQ2' - + Authorization: + "Basic YmJtcmktZGlyZWN0b3J5Omw5RFJLVUROcTBTbDAySXhaUGQ2", }, body: JSON.stringify({ humanReadable: humanReadable, URL: returnURL, collections: collections, nToken: sendableQuery.id, - query: queryBase64String + query: queryBase64String, }), - } + }, ); - return response.json(); + return response; } /** @@ -266,52 +296,74 @@ async function sendRequestToNegotiator(sendableQuery: SendableQuery, humanReadab * @param sendableQuery the query to be sent to the negotiator * @param humanReadable a human readable query string to view in the negotiator project * @param collections the collections to negotiate with + * @param queryBase64String the query in base64 string format * @returns the redirect uri from the negotiator - * */ -async function sendRequestToProjectManager(sendableQuery: SendableQuery, humanReadable: string, collections: Collection[], queryBase64String: string): Promise { - - const queryParam: string = (queryBase64String != "") ? - `&query=${queryBase64String}` : "" - - const negotiationPartners = collections.map(collection => collection.collectionId.toLocaleLowerCase()).join(',') + */ +async function sendRequestToProjectManager( + sendableQuery: SendableQuery, + humanReadable: string, + collections: Collection[], + queryBase64String: string, +): Promise { + const queryParam: string = + queryBase64String != "" ? `&query=${queryBase64String}` : ""; + + const negotiationPartners = collections + .map((collection) => collection.collectionId.toLocaleLowerCase()) + .join(","); const returnURL: string = `${window.location.protocol}//${window.location.host}/?collections=${negotiationPartners}${queryParam}`; - const urlParams: URLSearchParams = new URLSearchParams(window.location.search); + const urlParams: URLSearchParams = new URLSearchParams( + window.location.search, + ); const projectCode: string | null = urlParams.get("project-code"); - const projectCodeParam: string = projectCode ? `&project-code=${projectCode}` : ""; - const negotiateUrl = projectCode ? negotiateOptions.editProjectUrl : negotiateOptions.newProjectUrl; - - const response: Response = await fetch( + const projectCodeParam: string = projectCode + ? `&project-code=${projectCode}` + : ""; + const negotiateUrl = projectCode + ? negotiateOptions.editProjectUrl + : negotiateOptions.newProjectUrl; + + const response: Response & { redirect_uri: string } = await fetch( `${negotiateUrl}?explorer-ids=${negotiationPartners}&query-format=CQL_DATA&human-readable=${humanReadable}&explorer-url=${encodeURIComponent(returnURL)}${projectCodeParam}`, { method: "POST", headers: { - 'returnAccept': 'application/json; charset=utf-8', - 'Content-Type': 'application/json', - 'Authorization': authHeader + returnAccept: "application/json; charset=utf-8", + "Content-Type": "application/json", + Authorization: authHeader, }, - body: getCql() - } - ).then(response => response.json()); + body: getCql(), + }, + ).then((response) => response.json()); /** * replace query-code with project-code * TODO: remove when backend bug is fixed */ if (response.redirect_uri) { - response.redirect_uri = response.redirect_uri.replace('query-code', 'project-code'); + response.redirect_uri = response.redirect_uri.replace( + "query-code", + "project-code", + ); } return response; } +/** + * @returns a base64 encoded CQL query + */ function getCql(): string { // NOTE: $ only works within svelte components const ast = buildAstFromQuery(currentQuery); const cql = translateAstToCql(ast, false, backendMeasures); - const library = buildLibrary(`${cql}`) - const measure = buildMeasure(library.url, currentMeasures.map(measureItem => measureItem.measure)) - const query = {lang: "cql", lib: library, measure: measure}; + const library = buildLibrary(`${cql}`); + const measure = buildMeasure( + library.url, + currentMeasures.map((measureItem) => measureItem.measure), + ); + const query = { lang: "cql", lib: library, measure: measure }; return btoa(decodeURI(JSON.stringify(query))); }