Skip to content

Commit

Permalink
refactor: provides a library with classes for connecting with backend…
Browse files Browse the repository at this point in the history
…API and MicrosoftGraph based on a BaseAPI
  • Loading branch information
arnoldknott committed Dec 12, 2024
1 parent 115cda9 commit 572f7cb
Show file tree
Hide file tree
Showing 10 changed files with 132 additions and 79 deletions.
5 changes: 5 additions & 0 deletions frontend_svelte/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -69,5 +69,10 @@ export default [
parser: '@typescript-eslint/parser'
}
}
},
{
rules: {
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }]
}
}
];
39 changes: 24 additions & 15 deletions frontend_svelte/src/lib/server/apis.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import AppConfig from '$lib/server/config';
import { msalAuthProvider } from '$lib/server/oauth';
import { msalAuthProvider, type BaseOauthProvider } from '$lib/server/oauth';

const appConfig = await AppConfig.getInstance();

Expand All @@ -14,14 +14,11 @@ type RequestBody =
| ReadableStream<Uint8Array>;

class BaseAPI {
_getAccessToken: (sessionId: string, scopes: string[]) => Promise<string>;
oauthProvider: BaseOauthProvider;
apiBaseURL: string;

constructor(
getAccessToken: (sessionId: string, scopes: string[]) => Promise<string>,
apiBaseURL: string
) {
this._getAccessToken = getAccessToken;
constructor(oauthProvider: BaseOauthProvider, apiBaseURL: string) {
this.oauthProvider = oauthProvider;
this.apiBaseURL = apiBaseURL;
}

Expand All @@ -34,9 +31,9 @@ class BaseAPI {
return new Request(`${this.apiBaseURL}${path}`, {
...requestOptions,
headers: {
'Content-Type': 'application/json',
'content-type': 'application/json',
Authorization: `Bearer ${accessToken}`,
...requestOptions.headers,
...requestOptions.headers,
...headers
}
});
Expand All @@ -59,8 +56,17 @@ class BaseAPI {
headers: HeadersInit = {}
): Promise<Response> {
try {
const accessToken = await this._getAccessToken(session_id, scopes);
options.body = JSON.stringify(body);
// TBD: add a try catch block here!
const accessToken = await this.oauthProvider.getAccessToken(session_id, scopes);
// options.body = JSON.stringify(body);
if (body instanceof FormData) {
options.body = JSON.stringify(Object.fromEntries(body));
} else {
console.error('Invalid body type or not yet implemented: ' + typeof body);
throw new Error('Invalid body type');
}
// options.body = body;
options.method = 'POST';
const request = this.constructRequest(path, accessToken, options, headers);
return await fetch(request);
// const response = await fetch(`${this.apiBaseURL}${path}`, {
Expand All @@ -85,7 +91,9 @@ class BaseAPI {
headers: HeadersInit
): Promise<Response> {
try {
const accessToken = await this._getAccessToken(sessionId, scopes);
// TBD: add a try catch block here!
const accessToken = await this.oauthProvider.getAccessToken(sessionId, scopes);
options.method = 'GET';
const request = this.constructRequest(path, accessToken, options, headers);
return await fetch(request);
// const response = await fetch(`${this.apiBaseURL}${path}`, {
Expand All @@ -105,7 +113,7 @@ class BackendAPI extends BaseAPI {
static pathPrefix = '/api/v1';

constructor() {
super(msalAuthProvider.getAccessToken, `${appConfig.backend_origin}${BackendAPI.pathPrefix}`);
super(msalAuthProvider, `${appConfig.backend_origin}${BackendAPI.pathPrefix}`);
this.appConfig = appConfig;
}

Expand All @@ -117,6 +125,7 @@ class BackendAPI extends BaseAPI {
options: RequestInit = {},
headers: HeadersInit = {}
) {
console.log('=== src -lib - server - backendAPI - post - called ===');
return await super.post(session_id, path, body, scopes, options, headers);
}

Expand All @@ -137,7 +146,7 @@ class MicrosoftGraph extends BaseAPI {
appConfig: AppConfig;

constructor() {
super(msalAuthProvider.getAccessToken, appConfig.ms_graph_base_uri);
super(msalAuthProvider, appConfig.ms_graph_base_uri);
this.appConfig = appConfig;
}

Expand All @@ -163,4 +172,4 @@ class MicrosoftGraph extends BaseAPI {
}
}

export const microsoftGraph = new MicrosoftGraph();
export const microsoftGraph = new MicrosoftGraph();
3 changes: 2 additions & 1 deletion frontend_svelte/src/lib/server/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,8 @@ class RedisCache {

export const redisCache = new RedisCache();

process.on('exit', () => redisCache?.stopClient());
// TBD: consider getting this back in?
// process.on('exit', () => redisCache?.stopClient());

// OLD CODE:

Expand Down
13 changes: 12 additions & 1 deletion frontend_svelte/src/lib/server/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ const scopesBackend = [
const scopesMsGraph = ['User.Read', 'openid', 'profile', 'offline_access'];
const scoepsAzure = ['https://management.azure.com/user_impersonation']; // for onbehalfof workflow

class BaseOauthProvider {
constructor() {}

async getAccessToken(_sessionId: string, _scopes: string[]): Promise<string> {
throw new Error('Method not implemented.');
}
}

export type { BaseOauthProvider };

class RedisClientWrapper implements ICacheClient {
private redisClient: RedisClientType;

Expand Down Expand Up @@ -91,12 +101,13 @@ class RedisPartitionManager implements IPartitionManager {
}
}

class MicrosoftAuthenticationProvider {
class MicrosoftAuthenticationProvider extends BaseOauthProvider {
private msalCommonConfig;
private redisClientWrapper: RedisClientWrapper;
private cryptoProvider: CryptoProvider;

constructor(redisClient: RedisClientType) {
super();
// Common configuration for all users:
this.msalCommonConfig = {
auth: {
Expand Down
2 changes: 1 addition & 1 deletion frontend_svelte/src/lib/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export type Session = {
microsoftAccount?: AccountInfo; // TBD: change to MicrosoftAccount, containing Account, IdToken, AccessToken, RefreshToken, AppMetadata
microsoftProfile?: MicrosoftProfile;
userAgent?: string;
sessionId?: string;
sessionId: string;
};

export type ClientSession = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { Actions, PageServerLoad } from './$types';
import { msalAuthProvider } from '$lib/server/oauth';
import AppConfig from '$lib/server/config';
import { error } from '@sveltejs/kit';
// import { msalAuthProvider } from '$lib/server/oauth';
// import AppConfig from '$lib/server/config';
// import { error } from '@sveltejs/kit';
import { backendAPI } from '$lib/server/apis';

const appConfig = await AppConfig.getInstance();
// const appConfig = await AppConfig.getInstance();

// function removeEmpty( object: Object ): Object {
// console.log("=== object ===");
Expand All @@ -20,21 +21,25 @@ const appConfig = await AppConfig.getInstance();
// }, {});
// }

export const load: PageServerLoad = async ({ fetch, locals }) => {
export const load: PageServerLoad = async ({ locals }) => {
// either send a token or make the demo resource publically accessable by adding an access policy with flag public=True
// const sessionId = cookies.get('session_id');
const sessionId = locals.sessionData.sessionId;
if (!sessionId) {
throw error(401, 'No session id!');
}
const accessToken = await msalAuthProvider.getAccessToken(sessionId, [
`${appConfig.api_scope}/api.read`
]);
const response = await fetch(`${appConfig.backend_origin}/api/v1/demoresource/`, {
headers: {
Authorization: `Bearer ${accessToken}`
}
});

// before creating a class for backend access:
// if (!sessionId) {
// throw error(401, 'No session id!');
// }
// const accessToken = await msalAuthProvider.getAccessToken(sessionId, [
// `${appConfig.api_scope}/api.read`
// ]);
// const response = await fetch(`${appConfig.backend_origin}/api/v1/demoresource/`, {
// headers: {
// Authorization: `Bearer ${accessToken}`
// }
// });

const response = await backendAPI.get(sessionId, '/demoresource');
const demoResources = await response.json();
return { demoResources };
};
Expand All @@ -43,30 +48,37 @@ export const actions = {
default: async ({ locals, request }) => {
const data = await request.formData();

// console.log('=== data ===');
// console.log(data);
// const payload = JSON.stringify(data);
const payload = JSON.stringify(Object.fromEntries(data));
// console.log('=== payload ===');
// console.log(payload);
// before creating a class for backend access:
// // console.log('=== data ===');
// // console.log(data);
// // const payload = JSON.stringify(data);
// const payload = JSON.stringify(Object.fromEntries(data));
// // console.log('=== payload ===');
// // console.log(payload);

// // const sessionId = cookies.get('session_id');
// const sessionId = locals.sessionData.sessionId;
// if (!sessionId) {
// console.error('routes - demo-resource - page.server - no session id');
// throw error(401, 'No session id!');
// }
// const accessToken = await msalAuthProvider.getAccessToken(sessionId, [
// `${appConfig.api_scope}/api.write`
// ]);
// await fetch(`${appConfig.backend_origin}/api/v1/demoresource/`, {
// method: 'POST',
// headers: {
// Authorization: `Bearer ${accessToken}`,
// 'Content-Type': 'application/json'
// },
// body: payload
// });

// const payload = Object.fromEntries(data);

// const sessionId = cookies.get('session_id');
const sessionId = locals.sessionData.sessionId;
if (!sessionId) {
console.error('routes - demo-resource - page.server - no session id');
throw error(401, 'No session id!');
}
const accessToken = await msalAuthProvider.getAccessToken(sessionId, [
`${appConfig.api_scope}/api.write`
]);
await fetch(`${appConfig.backend_origin}/api/v1/demoresource/`, {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: payload
});
await backendAPI.post(sessionId, '/demoresource', data);

// console.log("=== data ===");
// console.log(data);
// const name = data.get('name');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,43 @@
import type { PageServerLoad } from './$types';
import AppConfig from '$lib/server/config';
import { msalAuthProvider } from '$lib/server/oauth';
import { error } from '@sveltejs/kit';
// import AppConfig from '$lib/server/config';
// import { msalAuthProvider } from '$lib/server/oauth';
// import { error } from '@sveltejs/kit';
import { microsoftGraph } from '$lib/server/apis';

const appConfig = await AppConfig.getInstance();
// const appConfig = await AppConfig.getInstance();

// TBD: add type PageServerLoad here?
export const load: PageServerLoad = async ({ locals }) => {
const sessionId = locals.sessionData.sessionId;
// const sessionId = cookies.get('session_id');
if (!sessionId) {
console.error('routes - playground - ms_graph_me - page.server - no session id');
throw error(401, 'No session id!');
}
const accessToken = await msalAuthProvider.getAccessToken(sessionId, ['User.Read']);
const response = await fetch(`${appConfig.ms_graph_base_uri}/me`, {
headers: {
Authorization: `Bearer ${accessToken}`
}
});

// if (!sessionId) {
// console.error('routes - playground - ms_graph_me - page.server - no session id');
// throw error(401, 'No session id!');
// }
// const accessToken = await msalAuthProvider.getAccessToken(sessionId, ['User.Read']);
// const response = await fetch(`${appConfig.ms_graph_base_uri}/me`, {
// headers: {
// Authorization: `Bearer ${accessToken}`
// }
// });

const response = await microsoftGraph.get(sessionId, '/me');

// a way of returning file content from server load function (untested):
// Read the file
// const filePath = 'path/to/your/file';
// const fileBuffer = await fs.readFile(filePath);
// const arrayBuffer = fileBuffer.buffer;
const pictureResponse = await fetch(`${appConfig.ms_graph_base_uri}/me/photo/$value`, {
headers: {
Authorization: `Bearer ${accessToken}`
}
});

// const pictureResponse = await fetch(`${appConfig.ms_graph_base_uri}/me/photo/$value`, {
// headers: {
// Authorization: `Bearer ${accessToken}`
// }
// });

const pictureResponse = await microsoftGraph.get(sessionId, '/me/photo/$value');

const userPictureBlob = await pictureResponse.blob();
const userPicture = await userPictureBlob.arrayBuffer();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
// TBD refactor using sessionData
const account = data.account;
const userProfile = data.userProfile;
// const userPicture = data.userPicture;
const userPicture = data.userPicture;
// This is the raw data fo the file - try demonstrating with a text file or md-file!
// console.log('ms_graph_me - userPicture');
// console.log(userPicture);
console.log('ms_graph_me - userPicture');
console.log(userPicture);
let userPictureURL: string | undefined = $state(undefined);
onMount(async () => {
Expand Down
3 changes: 2 additions & 1 deletion frontend_svelte/src/routes/(layout)/login/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ export const load: PageServerLoad = async ({ url, cookies, request }) => {
const sessionData: Session = {
status: 'authentication_pending',
loggedIn: false,
userAgent: userAgent || ''
userAgent: userAgent || '',
sessionId: sessionId
};

await redisCache.setSession(
Expand Down
6 changes: 6 additions & 0 deletions frontend_svelte/src/routes/(plain)/docs/security/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@
<li><em>OAuth2</em> for authentication and authorization</li>
<li><em>Access Control</em> for fine grained access permissions</li>
<li><em>Logging</em> for tracking access</li>
<li><em>CORS</em> configuration in backend for REST API, websockets and socket.io</li>
<li><em>CSRF</em> in redirect OAuth2 authentication process in frontend</li>
<li>
<em>Cookies</em> for session management in frontend secured with HttpOnly, Secure, SameSite,
and expiry settings
</li>
</ul>
</section>
</section>
Expand Down

0 comments on commit 572f7cb

Please sign in to comment.