11import { homedir } from 'node:os' ;
2- import * as path from 'node:path' ;
2+ import path from 'node:path' ;
33import { mkdirSync , existsSync , writeFileSync , readFileSync , rmSync } from 'node:fs' ;
4- import * as crypto from 'node:crypto' ;
4+ import crypto from 'node:crypto' ;
55import { Buffer } from 'node:buffer' ;
66import { logger } from '@redocly/openapi-core' ;
77import { type AuthToken , RedoclyOAuthDeviceFlow } from './device-flow.js' ;
@@ -10,35 +10,32 @@ const SALT = '4618dbc9-8aed-4e27-aaf0-225f4603e5a4';
1010const CRYPTO_ALGORITHM = 'aes-256-cbc' ;
1111
1212export class RedoclyOAuthClient {
13- private dir : string ;
14- private cipher : crypto . Cipher ;
15- private decipher : crypto . Decipher ;
16-
17- constructor ( private clientName : string , private version : string ) {
18- this . dir = path . join ( homedir ( ) , '.redocly' ) ;
19- if ( ! existsSync ( this . dir ) ) {
20- mkdirSync ( this . dir ) ;
21- }
13+ private static readonly TOKEN_FILE = 'credentials' ;
14+ private static readonly LEGACY_TOKEN_FILE = 'auth.json' ;
15+
16+ private readonly dir : string ;
17+ private readonly key : Buffer ;
18+ private readonly iv : Buffer ;
19+
20+ constructor ( ) {
21+ const homeDirPath = homedir ( ) ;
22+
23+ this . dir = path . join ( homeDirPath , '.redocly' ) ;
24+ mkdirSync ( this . dir , { recursive : true } ) ;
25+
26+ this . key = crypto . createHash ( 'sha256' ) . update ( `${ homeDirPath } ${ SALT } ` ) . digest ( ) ; // 32-byte key
27+ this . iv = crypto . createHash ( 'md5' ) . update ( homeDirPath ) . digest ( ) ; // 16-byte IV
28+
29+ // TODO: Remove this after few months
30+ const legacyTokenPath = path . join ( this . dir , RedoclyOAuthClient . LEGACY_TOKEN_FILE ) ;
2231
23- const homeDirPath = process . env . HOME as string ;
24- const hash = crypto . createHash ( 'sha256' ) ;
25- hash . update ( `${ homeDirPath } ${ SALT } ` ) ;
26- const hashHex = hash . digest ( 'hex' ) ;
27-
28- const key = Buffer . alloc (
29- 32 ,
30- Buffer . from ( hashHex ) . toString ( 'base64' )
31- ) . toString ( ) as crypto . CipherKey ;
32- const iv = Buffer . alloc (
33- 16 ,
34- Buffer . from ( process . env . HOME as string ) . toString ( 'base64' )
35- ) . toString ( ) as crypto . BinaryLike ;
36- this . cipher = crypto . createCipheriv ( CRYPTO_ALGORITHM , key , iv ) ;
37- this . decipher = crypto . createDecipheriv ( CRYPTO_ALGORITHM , key , iv ) ;
32+ if ( existsSync ( legacyTokenPath ) ) {
33+ rmSync ( legacyTokenPath ) ;
34+ }
3835 }
3936
40- async login ( baseUrl : string ) {
41- const deviceFlow = new RedoclyOAuthDeviceFlow ( baseUrl , this . clientName , this . version ) ;
37+ public async login ( baseUrl : string ) {
38+ const deviceFlow = new RedoclyOAuthDeviceFlow ( baseUrl ) ;
4239
4340 const token = await deviceFlow . run ( ) ;
4441 if ( ! token ) {
@@ -47,66 +44,95 @@ export class RedoclyOAuthClient {
4744 this . saveToken ( token ) ;
4845 }
4946
50- async logout ( ) {
47+ public async logout ( ) {
5148 try {
5249 this . removeToken ( ) ;
5350 } catch ( err ) {
5451 // do nothing
5552 }
5653 }
5754
58- async isAuthorized ( baseUrl : string , apiKey ?: string ) {
59- const deviceFlow = new RedoclyOAuthDeviceFlow ( baseUrl , this . clientName , this . version ) ;
60-
55+ public async isAuthorized ( reuniteUrl : string , apiKey ?: string ) : Promise < boolean > {
6156 if ( apiKey ) {
62- return await deviceFlow . verifyApiKey ( apiKey ) ;
57+ const deviceFlow = new RedoclyOAuthDeviceFlow ( reuniteUrl ) ;
58+ return deviceFlow . verifyApiKey ( apiKey ) ;
6359 }
6460
61+ const token = await this . getToken ( reuniteUrl ) ;
62+
63+ return Boolean ( token ) ;
64+ }
65+
66+ public getToken = async ( reuniteUrl : string ) : Promise < AuthToken | null > => {
67+ const deviceFlow = new RedoclyOAuthDeviceFlow ( reuniteUrl ) ;
6568 const token = await this . readToken ( ) ;
69+
6670 if ( ! token ) {
67- return false ;
71+ return null ;
6872 }
6973
70- const isValidAccessToken = await deviceFlow . verifyToken ( token . access_token ) ;
74+ const isValid = await deviceFlow . verifyToken ( token . access_token ) ;
7175
72- if ( isValidAccessToken ) {
73- return true ;
76+ if ( isValid ) {
77+ return token ;
7478 }
7579
7680 try {
7781 const newToken = await deviceFlow . refreshToken ( token . refresh_token ) ;
7882 await this . saveToken ( newToken ) ;
83+ return newToken ;
7984 } catch {
80- return false ;
85+ await this . removeToken ( ) ;
86+ return null ;
8187 }
88+ } ;
8289
83- return true ;
90+ private get tokenPath ( ) {
91+ return path . join ( this . dir , RedoclyOAuthClient . TOKEN_FILE ) ;
8492 }
8593
86- private async saveToken ( token : AuthToken ) {
94+ private async saveToken ( token : AuthToken ) : Promise < void > {
8795 try {
88- const encrypted =
89- this . cipher . update ( JSON . stringify ( token ) , 'utf8' , 'hex' ) + this . cipher . final ( 'hex' ) ;
90- writeFileSync ( path . join ( this . dir , 'auth.json' ) , encrypted ) ;
96+ const encrypted = this . encryptToken ( token ) ;
97+ writeFileSync ( this . tokenPath , encrypted , 'utf8' ) ;
9198 } catch ( error ) {
9299 logger . error ( `Error saving tokens: ${ error } ` ) ;
93100 }
94101 }
95102
96- private async readToken ( ) {
103+ private async readToken ( ) : Promise < AuthToken | null > {
104+ if ( ! existsSync ( this . tokenPath ) ) {
105+ return null ;
106+ }
107+
97108 try {
98- const token = readFileSync ( path . join ( this . dir , 'auth.json' ) , 'utf8' ) ;
99- const decrypted = this . decipher . update ( token , 'hex' , 'utf8' ) + this . decipher . final ( 'utf8' ) ;
100- return decrypted ? JSON . parse ( decrypted ) : null ;
109+ const encrypted = readFileSync ( this . tokenPath , 'utf8' ) ;
110+ return this . decryptToken ( encrypted ) ;
101111 } catch {
102112 return null ;
103113 }
104114 }
105115
106- private async removeToken ( ) {
107- const tokenPath = path . join ( this . dir , 'auth.json' ) ;
108- if ( existsSync ( tokenPath ) ) {
109- rmSync ( tokenPath ) ;
116+ private async removeToken ( ) : Promise < void > {
117+ if ( existsSync ( this . tokenPath ) ) {
118+ rmSync ( this . tokenPath ) ;
110119 }
111120 }
121+
122+ private encryptToken ( token : AuthToken ) : string {
123+ const cipher = crypto . createCipheriv ( CRYPTO_ALGORITHM , this . key , this . iv ) ;
124+ const encrypted = Buffer . concat ( [ cipher . update ( JSON . stringify ( token ) , 'utf8' ) , cipher . final ( ) ] ) ;
125+
126+ return encrypted . toString ( 'hex' ) ;
127+ }
128+
129+ private decryptToken ( encryptedToken : string ) : AuthToken {
130+ const decipher = crypto . createDecipheriv ( CRYPTO_ALGORITHM , this . key , this . iv ) ;
131+ const decrypted = Buffer . concat ( [
132+ decipher . update ( Buffer . from ( encryptedToken , 'hex' ) ) ,
133+ decipher . final ( ) ,
134+ ] ) ;
135+
136+ return JSON . parse ( decrypted . toString ( 'utf8' ) ) ;
137+ }
112138}
0 commit comments