cloudAuth.ts 6.42 KB
Newer Older
1 2
import { Request, Server } from '@hapi/hapi';
import axios from 'axios';
3 4
import chalk from 'chalk';
import { cli } from 'cli-ux';
5 6 7 8 9 10 11 12
import Conf = require('conf');
import { createHash } from 'crypto';
import open = require('open');
import { stringify } from 'querystring';
import { cpu, mem, osInfo, system } from 'systeminformation';
import { v4 as uuidv4 } from 'uuid';

const cloudUrl = 'https://cloud-beta.rocket.chat';
13 14 15 16 17 18 19 20 21 22
const clientId = '5d8e59c5d48080ef5497e522';
const scope = 'offline_access marketplace';

export interface ICloudToken {
    access_token: string;
    expires_in: number;
    scope: string;
    refresh_token: string;
    token_type: string;
}
23

24 25 26
export interface ICloudAuthStorage {
    token: ICloudToken;
    expiresAt: Date;
27 28 29 30 31
}

export class CloudAuth {
    private config: Conf;
    private codeVerifier: string;
32
    private port = 3005;
33 34 35 36
    private server: Server;
    private redirectUri: string;

    constructor() {
37
        this.redirectUri = `http://localhost:${ this.port }/callback`;
38 39 40
        this.codeVerifier = uuidv4() + uuidv4();
    }

41
    public async executeAuthFlow(): Promise<string> {
42 43 44 45
        await this.initialize();

        return new Promise((resolve, reject) => {
            try {
46
                this.server = new Server({ host: 'localhost', port: this.port });
47 48 49 50 51 52 53 54
                this.server.route({
                    method: 'GET',
                    path: '/callback',
                    handler: async (request: Request) => {
                        try {
                            const code = request.query.code;
                            const token = await this.fetchToken(code);

55
                            resolve(token.access_token);
56 57 58 59 60 61 62 63 64 65 66
                            return 'Thank you. You can close this tab.';
                        } catch (err) {
                            reject(err);
                        } finally {
                            this.server.stop();
                        }
                    },
                });

                const codeChallenge = createHash('sha256').update(this.codeVerifier).digest('base64');
                const authorizeUrl = this.buildAuthorizeUrl(codeChallenge);
67 68 69
                cli.log(chalk.green('*') + ' ' + chalk.white('...if your browser does not open, open this:')
                    + ' ' + chalk.underline(chalk.blue(authorizeUrl)));

70 71 72 73 74 75 76 77 78 79 80 81 82
                open(authorizeUrl);

                this.server.start();
            } catch (e) {
                // tslint:disable-next-line:no-console
                console.log('Error inside of the execute:', e);
            }
        });
    }

    public async hasToken(): Promise<boolean> {
        await this.initialize();

83
        return this.config.has('rcc.token.access_token');
84 85 86 87 88
    }

    public async getToken(): Promise<string> {
        await this.initialize();

89 90 91 92 93 94 95 96
        const item: ICloudAuthStorage = this.config.get('rcc');
        if (new Date() < new Date(item.expiresAt)) {
            return item.token.access_token;
        }

        await this.refreshToken();

        return this.config.get('rcc.token.access_token', '') as string;
97 98
    }

99
    private async fetchToken(code: string | Array<string>): Promise<ICloudToken> {
100 101 102 103
        try {
            const request = {
                grant_type: 'authorization_code',
                redirect_uri: this.redirectUri,
104
                client_id: clientId,
105 106 107 108
                code,
                code_verifier: this.codeVerifier,
            };

109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
            const res = await axios.post(`${ cloudUrl }/api/oauth/token`, stringify(request));
            const tokenInfo: ICloudToken = res.data;

            const expiresAt = new Date();
            expiresAt.setSeconds(expiresAt.getSeconds() + tokenInfo.expires_in);

            const storageItem: ICloudAuthStorage = {
                token: tokenInfo,
                expiresAt,
            };

            this.config.set('rcc', storageItem);

            return tokenInfo;
        } catch (err) {
            // tslint:disable-next-line:no-console
            console.log(`[${ err.res.statusCode }] error getting token: ${ err.data.error } (${ err.data.requestId })`);

            throw err;
        }
    }

    private async refreshToken(): Promise<void> {
        const refreshToken = this.config.get('rcc.token.refresh_token', '');

        const request = {
            client_id: clientId,
            refresh_token: refreshToken,
            scope,
            grant_type: 'refresh_token',
            redirect_uri: this.redirectUri,
        };

        try {
            const res = await axios.post(`${ cloudUrl }/api/oauth/token`, stringify(request));
            const tokenInfo: ICloudToken = res.data;
145

146 147
            const expiresAt = new Date();
            expiresAt.setSeconds(expiresAt.getSeconds() + tokenInfo.expires_in);
148

149 150 151 152 153
            this.config.set('rcc.token.access_token', tokenInfo.access_token);
            this.config.set('rcc.token.expires_in', tokenInfo.expires_in);
            this.config.set('rcc.token.scope', tokenInfo.scope);
            this.config.set('rcc.token.token_type', tokenInfo.token_type);
            this.config.set('rcc.expiresAt', expiresAt);
154
        } catch (err) {
155
            const d = err.data;
156
            // tslint:disable-next-line:no-console
157
            console.log(`[${ err.res.statusCode }] error getting token refreshed: ${ d.error } (${ d.requestId })`);
158 159 160 161 162 163 164

            throw err;
        }
    }

    private buildAuthorizeUrl(codeChallenge: string) {
        const data = {
165
            client_id: clientId,
166
            response_type: 'code',
167
            scope,
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184
            redirect_uri: this.redirectUri,
            state: uuidv4(),
            code_challenge_method: 'S256',
            code_challenge: codeChallenge,
        };

        const params = stringify(data);
        const authorizeUrl = `${ cloudUrl }/authorize?${params}`;
        return authorizeUrl;
    }

    private async initialize(): Promise<void> {
        if (typeof this.config !== 'undefined') {
            return;
        }

        this.config = new Conf({
185
            projectName: 'chat.rocket.apps-cli',
186 187 188 189 190 191 192 193 194 195 196 197 198 199
            encryptionKey: await this.getEncryptionKey(),
        });
    }

    private async getEncryptionKey(): Promise<string> {
        const s = await system();
        const c = await cpu();
        const m = await mem();
        const o = await osInfo();

        return s.manufacturer + ';' + s.uuid + ';' + String(c.processors) + ';'
                + c.vendor + ';' + m.total + ';' + o.platform + ';' + o.release;
    }
}