import * as vscode from 'vscode'; import * as https from 'https'; import * as logging from '../logging/LoggerEventHub'; import axios, { AxiosResponse } from 'axios'; const agent = new https.Agent({ rejectUnauthorized: false }); export enum OidcGrantTypes { password = "password", code = "code", token = "token" } export class OidcProviderConfiguration { constructor( public readonly name: string, public readonly idpBaseUrl: vscode.Uri, public readonly redirectUrl: vscode.Uri, public readonly clientId: string, public readonly clientSecret: string | undefined, public readonly scopes: string, public readonly grantType: OidcGrantTypes = OidcGrantTypes.password, ){} } export class OidcCredentials { public readonly readableIdToken : any | undefined; public readonly readableAccessToken : any; public get authTokensExpired() : boolean { return (new Date().getTime() - this.authTokensExpiration) > 0; } public get refreshTokenExpired() : boolean { return (new Date().getTime() - this.refreshTokenExpiration) > 0; } constructor( public readonly providerName: string, public readonly idToken: string | undefined, public readonly accessToken: string, public readonly refreshToken: string, public readonly authTokensExpiration: number, public readonly refreshTokenExpiration: number, ){ if(idToken) this.readableIdToken = JSON.parse(Buffer.from(idToken.split(".")[1],'base64').toString()); this.readableAccessToken = JSON.parse(Buffer.from(accessToken.split(".")[1],'base64').toString()); this.authTokensExpiration = (new Date()).getTime() + this.authTokensExpiration*1000; this.refreshTokenExpiration = (new Date()).getTime() + this.refreshTokenExpiration*1000; } public serialize(): any { return { provider_name: this.providerName, id_token: this.idToken, access_token: this.accessToken, refresh_token: this.refreshToken, auth_tokens_expiration: this.authTokensExpiration, refresh_token_expiration: this.refreshTokenExpiration, } } public static deserialize(descriptor: any): OidcCredentials { return new OidcCredentials( descriptor.provider_name, descriptor.id_token, descriptor.access_token, descriptor.refresh_token, descriptor.auth_tokens_expiration, descriptor.refresh_token_expiration ); } } export class AuthUserDescriptor implements vscode.AuthenticationSession, vscode.AuthenticationSessionAccountInformation { public readonly username: string; public readonly id: string; public readonly accessToken: string; public readonly scopes: Array; public readonly label: string; public readonly account: vscode.AuthenticationSessionAccountInformation; public get expired(): boolean { return this.oidcCredentials.authTokensExpired; } constructor(public readonly oidcCredentials: OidcCredentials){ this.username = this.oidcCredentials.readableAccessToken.preferred_username; this.id = this.oidcCredentials.readableAccessToken.sub; this.accessToken = this.oidcCredentials.accessToken; this.scopes = this.oidcCredentials.readableAccessToken.scope.split(" "); this.label = this.username; this.account = { id: this.oidcCredentials.providerName + ";" + this.id, label: this.label }; } public serialize(): any { return { oidc_credentials: this.oidcCredentials.serialize() } } public static deserialize(descriptor: any): AuthUserDescriptor { return new AuthUserDescriptor(OidcCredentials.deserialize(descriptor.oidc_credentials)); } } /** * @class * @implements {vscode.AuthenticationProvider} * @description Authorizer allows user authentication OIDC protocol based on vscode platform. Singleton instance can be used across vscode extensions. No multiple user sessions are supported */ export class Authorizer implements vscode.AuthenticationProvider { private logger : logging.LoggerEventHub; private currentUsers : Array = []; private _onDidChangeSessions: vscode.EventEmitter = new vscode.EventEmitter(); private constructor(public readonly namspaceApp: string, public readonly oidcProviderConfs: Array) { this.onDidChangeSessions = this._onDidChangeSessions.event; this.logger = new logging.LoggerEventHub(namspaceApp); } public onDidChangeSessions: vscode.Event; public async getSessions(scopes?: string[]): Promise { return this.currentUsers.filter(cu => !cu.expired); } public async createSession(scopes: string[]): Promise { const session : vscode.AuthenticationSession = await this.login(); this._onDidChangeSessions.fire({ added: [ session ] }); return session; } public async removeSession(sessionId: string): Promise { const session : vscode.AuthenticationSession = await this.logout(sessionId); this._onDidChangeSessions.fire({ removed: [ session ] }); } /** * @static @method createInstance * * @description Register a singleton Authorizer instance for specified namespace if it doesn't exist * @param namespaceApp Application namespace for the authorizer * @param oidcProviderConf OIDC provider parameters */ public static createInstance(namespaceApp: string, oidcProviderConfs: Array) : Authorizer { try { const authorizer = new Authorizer(namespaceApp, oidcProviderConfs); vscode.authentication.registerAuthenticationProvider(namespaceApp,namespaceApp, authorizer); return authorizer; } catch (error) { const message = "Authentication provider already exist for " + namespaceApp; throw new Error(message); } } private async login(): Promise { try { const providerName: string | undefined = await vscode.window.showQuickPick( this.oidcProviderConfs.map(prov => prov.name) , {placeHolder: "Backend environemnt"}); const provider: OidcProviderConfiguration | undefined = this.oidcProviderConfs.find( prov => prov.name == providerName); if(!provider) throw new Error("No valid provider available"); const user = this.currentUsers.find(cu => cu.oidcCredentials.providerName == providerName) if(user && !user.expired) return user; this.currentUsers = this.currentUsers.filter(cu => cu.oidcCredentials.providerName == providerName) switch(provider.grantType){ case OidcGrantTypes.password: const username: string | undefined = await vscode.window.showInputBox({placeHolder: "Insert username"}); const password: string | undefined = await vscode.window.showInputBox({placeHolder: "Insert password", password: true}); const url : vscode.Uri = vscode.Uri.joinPath(provider.idpBaseUrl , "/openid-connect/token"); const body : string = "client_id=" + provider.clientId + "&client_secret=" + provider.clientSecret + "&scopes=" + provider.scopes + "&grant_type=" + provider.grantType + "&username=" + encodeURI(username || "")+ "&password=" + encodeURI(password || "") const response : AxiosResponse = await axios.post(url.toString(), body, { httpsAgent: agent, headers: { "Content-Type": "application/x-www-form-urlencoded" }}); const idToken : string | undefined = response.data.id_token; const accessToken : string = response.data.access_token; const refreshToken : string = response.data.refresh_token; const authTokensExpiration: number = response.data.expires_in; const refreshTokenExpiration : number = response.data.refresh_expires_in; const currentUserDescriptor : AuthUserDescriptor = new AuthUserDescriptor(new OidcCredentials(provider.name, idToken, accessToken, refreshToken, authTokensExpiration, refreshTokenExpiration)); this.currentUsers.push(currentUserDescriptor); break case OidcGrantTypes.code: case OidcGrantTypes.token: break; default: throw new Error("OIDC grant type not supported"); } const currentUser = this.currentUsers.find(cu => cu.oidcCredentials.providerName == providerName) if(!currentUser) throw new Error("Login failed"); return currentUser; } catch (error) { this.logger.broadcastEvent(new logging.LoggerEventHubMessage((error as Error).message, logging.LoggerEventHubMessageSeverity.error, true)); throw error; } } private async refresh() { throw new Error("Method not implemented"); } private async logout(sessionId: string) : Promise { try{ const user = this.currentUsers.find(cu => cu.id == sessionId) if(!user || user?.expired) throw new Error("No current user session available"); const provider = this.oidcProviderConfs.find(prov => prov.name == user.oidcCredentials.providerName); if(!provider) throw new Error("Unexcepted error: provider not found for current valid user"); this.currentUsers = this.currentUsers.filter(cu => cu.id != sessionId); return user; } catch (error) { this.logger.broadcastEvent(new logging.LoggerEventHubMessage((error as Error).message, logging.LoggerEventHubMessageSeverity.error, true)); throw error; } } }