From 8956adf1d10b57f790c254b917d63c8e987c6902 Mon Sep 17 00:00:00 2001 From: Jim Kroon Date: Thu, 26 Sep 2024 01:35:23 +0200 Subject: [PATCH] Add authentication flow --- app/main/background.ts | 60 ++++++--- app/main/windows/auth.ts | 43 ++++-- app/main/worker.ts | 44 +++++- packages/authentication/src/index.ts | 10 +- .../src/controllers/authentication.ts | 16 +++ packages/platform/src/index.ts | 56 ++++++-- packages/platform/src/worker.ts | 127 ++++++++++-------- 7 files changed, 257 insertions(+), 99 deletions(-) diff --git a/app/main/background.ts b/app/main/background.ts index 0126cdd7..881d558a 100644 --- a/app/main/background.ts +++ b/app/main/background.ts @@ -5,11 +5,14 @@ import Platform from '@greenlight/platform' import Logger from '@greenlight/logger' import pkg from '../package.json' import MainWindow from './windows/main' +import AuthWindow from './windows/auth' +import ElectronStore from 'electron-store' export default class Application { public isProduction: boolean = process.env.NODE_ENV === 'production' public logger:Logger = new Logger('GreenlightApp') + public store = new ElectronStore() private _platform?:Platform private _mainWindow?:MainWindow @@ -22,19 +25,54 @@ export default class Application { } else { app.setPath('userData', `${app.getPath('userData')} (development)`) } + app.setName('Greenlight') + + // this.store.set('authentication.tokens', '{}') } async isReady() { await app.whenReady() } + getPlatform() { + return this._platform + } + loadPlatform(){ return new Promise((resolve, reject) => { this._platform = new Platform() + this._platform.onAuthLoad = () => { + this.logger.log('onAuthSave() Loading tokens from disk...') + const tokens = this.store.get('authentication.tokens', '{}') + + return { + action: 'auth_load', + tokens: JSON.stringify(tokens) + } + } + + this._platform.onAuthSave = (tokens) => { + this.logger.log('onAuthSave() Saving tokens to disk...') + this.store.set('authentication.tokens', JSON.parse(tokens)) + return true + } + + this._platform.onAuthClear = () => { + this.logger.log('onAuthSave() Clearing tokens from disk...') + return true + } this._platform.loadWorker(path.join(__dirname, 'worker.js')).then((authenticated:boolean) => { this.logger.log('loadPlatform() Platform loaded and authenticated') - dialog.showErrorBox('Worker success', 'Platform worker loaded!') + // dialog.showErrorBox('Worker success', 'Platform worker loaded!') + + this._platform?.api.Authentication.getGamertag() + + if(authenticated === false){ + // Not authenticated.. + new AuthWindow(this) + } + resolve(authenticated) }).catch((error:any) => { this.logger.error('loadPlatform() Platform failed to load. Critical error: '+error) @@ -45,26 +83,6 @@ export default class Application { } async spawnMainWindow() { - // const mainWindow = createWindow('main', { - // width: 1280, - // height: (this.isProduction) ? 800 : 1200, - // title: 'Greenlight', - // backgroundColor: 'rgb(26, 27, 30)', - // webPreferences: { - // preload: path.join(__dirname, 'preload.js'), - // }, - // }) - - // if (this.isProduction) { - // await mainWindow.loadURL('app://./boot') - // } else { - // const port = process.argv[2] - // await mainWindow.loadURL(`http://localhost:${port}/boot`) - // mainWindow.webContents.openDevTools({ - // mode: 'bottom' - // }) - // } - // this.logger.log('spawnMainWindow() Main application windows drawn') this._mainWindow = new MainWindow(this) } diff --git a/app/main/windows/auth.ts b/app/main/windows/auth.ts index 63d0127c..f63add4f 100644 --- a/app/main/windows/auth.ts +++ b/app/main/windows/auth.ts @@ -1,13 +1,19 @@ +import { session } from 'electron' +import Logger from '@greenlight/logger' import path from 'path' import Application from '../background' import { createWindow } from '../helpers' export default class AuthWindow { private _app:Application + public logger:Logger public window + private _authState + constructor(app:Application) { this._app = app + this.logger = this._app.logger.extend('AuthWindow') this.window = createWindow('main', { width: 400, @@ -18,15 +24,34 @@ export default class AuthWindow { preload: path.join(__dirname, 'preload.js'), }, }) + + this._app.getPlatform().api.Authentication.getAuthenticationUrl().then((authState) => { + this.logger.log('constructor()() Authentication URL received:', authState) + this._authState = authState + this.window.loadURL(authState.sisuAuth.MsaOauthRedirect) + }) + + session.defaultSession.webRequest.onHeadersReceived({ + urls: [ + 'https://login.live.com/oauth20_authorize.srf?*', + 'https://login.live.com/ppsecure/post.srf?*', + ], + }, (details, callback) => { + + const redirectUri = 'ms-xal-000000004c20a908://auth' + if(details.responseHeaders.Location !== undefined && details.responseHeaders.Location[0].includes(redirectUri)){ + this.logger.log('constructor() Got redirect URI from OAUTH:', details.responseHeaders.Location[0]) + this.window.close() + + this._app.getPlatform().api.Authentication.authenticateUser(this._authState, details.responseHeaders.Location[0]).then((result) => { + this.logger.log('constructor() Authenticated user:', result) + }) + + callback({ cancel: true }) + } else { + callback(details) + } + }) - // if (this._app.isProduction) { - // this.window.loadURL('app://./boot') - // } else { - // const port = process.argv[2] - // this.window.loadURL(`http://localhost:${port}/boot`) - // this.window.webContents.openDevTools({ - // mode: 'bottom' - // }) - // } } } \ No newline at end of file diff --git a/app/main/worker.ts b/app/main/worker.ts index de59f818..4349eec7 100644 --- a/app/main/worker.ts +++ b/app/main/worker.ts @@ -1,15 +1,57 @@ import worker from 'node:worker_threads' import GreenlightWorker from '@greenlight/platform/src/worker' import Logger from '@greenlight/logger' +import { AuthStore } from '@greenlight/authentication' + +export class AuthenticationStore extends AuthStore { + private _msgBus:worker.MessagePort + + constructor(port:worker.MessagePort) { + super() + console.log('constructor() AuthenticationStore created') + + this._msgBus = port + } + load() { + console.log('load() AuthenticationStore load tokens triggered', this._msgBus) + this._msgBus.postMessage({ + requestId: Math.floor(Math.random()*1000), + action: 'auth_load' + }) + return true + } + + save() { + console.log('save() AuthenticationStore save tokens triggered') + const tokens = JSON.stringify({ + userToken: this._userToken?.data, + sisuToken: this._sisuToken?.data, + jwtKeys: this._jwtKeys, + }) + this._msgBus.postMessage({ + requestId: Math.floor(Math.random()*1000), + action: 'auth_save', + params: [tokens] + }) + return true + } + + clear() { + console.log('clear() AuthenticationStore clear tokens triggered') + return true + } +} export default class Worker { private _platformWorker:GreenlightWorker public logger = new Logger('GreenlightWorker:main') + private _store constructor() { worker.parentPort?.once('message', (handler) => { try { - this._platformWorker = new GreenlightWorker(this.logger, handler.port) + this._store = new AuthenticationStore(handler.port) + this._platformWorker = new GreenlightWorker(this.logger, handler.port, this._store) this._platformWorker.once('ready', (result) => { if(result === true) worker.parentPort?.postMessage('ok'); diff --git a/packages/authentication/src/index.ts b/packages/authentication/src/index.ts index 791442d3..635d52e3 100644 --- a/packages/authentication/src/index.ts +++ b/packages/authentication/src/index.ts @@ -1,4 +1,4 @@ -import { Xal } from 'xal-node' +import { TokenStore, Xal } from 'xal-node' import StreamingToken from 'xal-node/dist/lib/tokens/streamingtoken' import AuthStore from './authstore' import Logger from '@greenlight/logger' @@ -23,14 +23,16 @@ export interface ISisuAuthenticationResponse { class Authentication { - private _Store = new AuthStore() - private _Xal:Xal = new Xal(this._Store) + private _Store:AuthStore + private _Xal:Xal public logger:Logger = new Logger('Authentication') private _xhomeToken: StreamingToken | undefined private _xcloudToken: StreamingToken | undefined - constructor() { + constructor(store:AuthStore = new AuthStore()) { + this._Store = store + this._Xal = new Xal(this._Store) this.logger.log('constructor() Creating new Authentication instance') this._Store.load() } diff --git a/packages/platform/src/controllers/authentication.ts b/packages/platform/src/controllers/authentication.ts index d24fb266..298d8b3e 100644 --- a/packages/platform/src/controllers/authentication.ts +++ b/packages/platform/src/controllers/authentication.ts @@ -1,4 +1,5 @@ import GreenlightPlatform from '..' +import { AuthenticationState } from '@greenlight/authentication' export default class Authentication { @@ -23,4 +24,19 @@ export default class Authentication { }) } + async getAuthenticationUrl():Promise { + return await this._platform.sendMessage({ + controller: 'Authentication', + action: 'getAuthenticationUrl' + }) + } + + async authenticateUser(authState:AuthenticationState, redirectUrl:string):Promise { + return await this._platform.sendMessage({ + controller: 'Authentication', + action: 'authenticateUser', + params: [authState, redirectUrl] + }) + } + } \ No newline at end of file diff --git a/packages/platform/src/index.ts b/packages/platform/src/index.ts index 1b0f6527..a579a577 100644 --- a/packages/platform/src/index.ts +++ b/packages/platform/src/index.ts @@ -51,7 +51,7 @@ export default class GreenlightPlatform { }) this._worker.postMessage({ port: this._mainChannel.port1 }, [this._mainChannel.port1]); - this._mainChannel.port2.on('message', (value) => this.message(value)); + this._mainChannel.port2.on('message', async (value) => await this.message(value)); }) } @@ -74,30 +74,64 @@ export default class GreenlightPlatform { }) } - message(value:WorkerMessageResponse) { + async message(value:WorkerMessageResponse|WorkerMessage) { this.logger.log('message() Received message:', JSON.stringify(value)); - if(value.requestId !== undefined){ - const promise = this._messagePromiseQueue.get(value.requestId) + // If message is WorkerMessageResponse + if('responseId' in value){ + const promise = this._messagePromiseQueue.get(value.responseId) if(promise){ - this._messagePromiseQueue.delete(value.requestId) + this._messagePromiseQueue.delete(value.responseId) promise.resolve(value.data) } else { - this.logger.error('message() Promise not found for requestId:', value.requestId) + this.logger.error('message() Promise not found for responseId:', value.responseId) + } + } else if('requestId' in value){ + if(value.action == 'auth_load'){ + const res = await this.onAuthLoad() + const message:WorkerMessageResponse = { + responseId: (value.requestId as number), + data: res + } + this.sendMessage(message) + } else if(value.action == 'auth_save'){ + await this.onAuthSave(value.params?.[0]) + } else if(value.action == 'auth_clear'){ + await this.onAuthClear() + } else { + this.logger.error('message() Unknown action:', value.action) } } } + onAuthLoad():any { + return false + } + + onAuthSave(tokens:string) { + return false + } + + onAuthClear() { + return false + } + _messagePromiseQueue = new Map() - sendMessage(value:WorkerMessage):Promise { + sendMessage(value:WorkerMessage|WorkerMessageResponse):Promise { return new Promise((resolve, reject) => { if(this._worker){ - value.requestId = Math.floor(Math.random() * 1000) - this.logger.log('sendMessage() Sending message to worker:', JSON.stringify(value)); - this._mainChannel.port2.postMessage(value) + if('responseId' in value) { + this.logger.log('sendMessage() Sending message to worker:', JSON.stringify(value)); + this._mainChannel.port2.postMessage(value) + resolve(true) + } else { + value.requestId = Math.floor(Math.random() * 1000) + this.logger.log('sendMessage() Sending message to worker:', JSON.stringify(value)); + this._mainChannel.port2.postMessage(value) - this._messagePromiseQueue.set(value.requestId, { resolve, reject }) + this._messagePromiseQueue.set(value.requestId, { resolve, reject }) + } } }) } diff --git a/packages/platform/src/worker.ts b/packages/platform/src/worker.ts index c078a054..a977f503 100644 --- a/packages/platform/src/worker.ts +++ b/packages/platform/src/worker.ts @@ -3,7 +3,7 @@ import EventEmitter from 'node:events' import GreenlightPlatform from './index' import Logger from '@greenlight/logger' -import Authentication from '@greenlight/authentication' +import Authentication, { AuthStore } from '@greenlight/authentication' import xCloudApi from '@greenlight/xcloudapi' import WebApi from '@greenlight/webapi' import StoreApi from '@greenlight/storeapi' @@ -16,7 +16,7 @@ export interface WorkerMessage { } export interface WorkerMessageResponse { - requestId: number + responseId: number data: any } @@ -30,28 +30,28 @@ interface Modules { export default class Worker extends EventEmitter { private _channel:MessagePort + private _tokenStoreHandler?:AuthStore public logger:Logger + public controllers:Modules - public controllers:Modules = { - Authentication: new Authentication() - } - - constructor(logger:Logger, port:MessagePort) { + constructor(logger:Logger, port:MessagePort, tokenStoreHandler?:any) { super() this._channel = port this.logger = logger.extend('thread-'+process.pid) + this._tokenStoreHandler = tokenStoreHandler + this.controllers = { + Authentication: new Authentication(tokenStoreHandler) + } this._channel.on('message', async (event) => await this.message(event)) - this.reloadModules() + // this.reloadModules() } reloadModules() { this.logger.log('reloadModules() Reloading modules') - this.controllers.Authentication = new Authentication() - this.logger.log('reloadModules() Authentication module loaded') if(this.controllers.Authentication.hasUserLogin() && this.controllers.Authentication.hasValidAuthentication()) { this.logger.log('reloadModules() User is logged in') @@ -119,60 +119,81 @@ export default class Worker extends EventEmitter { this.emit('ready', false) } - async message(value:Required) { + async message(value:Required) { this.logger.log('message() Worker received message:', JSON.stringify(value)) - if(value.controller === undefined && value.action == 'close') { - this.logger.log('message() Closing worker thread') - this.sendMessage({ - requestId: value.requestId, - data: true - }) - this.close() - return; - } + if('requestId' in value){ + if(value.controller === undefined && value.action == 'close') { + this.logger.log('message() Closing worker thread') + this.sendMessage({ + responseId: value.requestId, + data: true + }) + this.close() + return; + } + + if(value.controller === undefined && value.action == 'reloadModules') { + this.logger.log('message() Reloading modules...') + this.reloadModules() + this.sendMessage({ + responseId: value.requestId, + data: true + }) + return; + } - if(value.controller !== undefined){ - const controller = this.controllers[value.controller]; + if(value.controller !== undefined){ + const controller = this.controllers[value.controller]; - const action = value.action.split('.') + const action = value.action.split('.') - if(action.length > 1){ - if(action.length > 2) - throw new Error('action with more then one dot is not supported.') + if(action.length > 1){ + if(action.length > 2) + throw new Error('action with more then one dot is not supported.') - if (typeof (controller as any)[action[0]] !== 'undefined' && typeof (controller as any)[action[0]][action[1]] === 'function') { - this.sendMessage({ - requestId: value.requestId, - data: await ((controller as any)[action[0]][action[1]] as Function)(...value.params) - }) - return - } - } else { - if (typeof (controller as any)[value.action] === 'function') { - this.sendMessage({ - requestId: value.requestId, - data: await ((controller as any)[value.action] as Function)(...value.params) - }) - return + if (typeof (controller as any)[action[0]] !== 'undefined' && typeof (controller as any)[action[0]][action[1]] === 'function') { + this.sendMessage({ + responseId: value.requestId, + data: (value.params !== undefined) ? await ((controller as any)[action[0]][action[1]] as Function)(...value.params) : await ((controller as any)[action[0]][action[1]] as Function)() + }) + return + } + } else { + if (typeof (controller as any)[value.action] === 'function') { + this.sendMessage({ + responseId: value.requestId, + data: (value.params !== undefined) ? await ((controller as any)[value.action] as Function)(...value.params) : await ((controller as any)[value.action] as Function)() + }) + return + } } - } - this.logger.error('message() Function not found in controller:', value.controller, action, controller, ) - this.sendMessage({ - requestId: value.requestId, - data: null - }) + this.logger.error('message() Function not found in controller:', value.controller, action, controller, ) + this.sendMessage({ + responseId: value.requestId, + data: null + }) + + return - return + } + } + + if('responseId' in value) { + if(value.data.action === 'auth_load') { + this._tokenStoreHandler?.loadJson(value.data.tokens) + console.log('XAL STATE:', this._tokenStoreHandler, JSON.parse(value.data.tokens)) + this.reloadModules() + } } - this.logger.error('message() Controller function not found:', value.controller, value.action) - this.sendMessage({ - requestId: value.requestId, - data: null - }) - return + // this.logger.error('message() Controller function not found:', value.controller, value.action) + // this.sendMessage({ + // responseId: value.requestId, + // data: null + // }) + // return } sendMessage(value:WorkerMessageResponse) {