@@ -12,6 +12,7 @@ import { hasProps, selectFrom } from '../../shared/utilities/tsUtils'
12
12
import { SsoToken , ClientRegistration } from './model'
13
13
import { SystemUtilities } from '../../shared/systemUtilities'
14
14
import { DevSettings } from '../../shared/settings'
15
+ import { statSync , Stats , readdirSync , unlinkSync } from 'fs'
15
16
16
17
interface RegistrationKey {
17
18
readonly region : string
@@ -33,32 +34,57 @@ export interface SsoCache {
33
34
const defaultCacheDir = path . join ( SystemUtilities . getHomeDirectory ( ) , '.aws' , 'sso' , 'cache' )
34
35
export const getCacheDir = ( ) => DevSettings . instance . get ( 'ssoCacheDirectory' , defaultCacheDir )
35
36
36
- export function getCache ( directory = getCacheDir ( ) ) : SsoCache {
37
+ export function getCache ( directory = getCacheDir ( ) , statFunc = getFileStats ) : SsoCache {
38
+ try {
39
+ deleteOldFiles ( directory , statFunc )
40
+ } catch ( e ) {
41
+ getLogger ( ) . warn ( 'auth: error deleting old files in sso cache: %s' , e )
42
+ }
43
+
37
44
return {
38
45
token : getTokenCache ( directory ) ,
39
46
registration : getRegistrationCache ( directory ) ,
40
47
}
41
48
}
42
49
43
- export function getRegistrationCache ( directory = getCacheDir ( ) ) : KeyedCache < ClientRegistration , RegistrationKey > {
44
- const hashScopes = ( scopes : string [ ] ) => {
45
- const shasum = crypto . createHash ( 'sha256' )
46
- scopes . forEach ( s => shasum . update ( s ) )
47
- return shasum . digest ( 'hex' )
50
+ function deleteOldFiles ( directory : string , statFunc : typeof getFileStats ) {
51
+ if ( ! isDirSafeToDeleteFrom ( directory ) ) {
52
+ getLogger ( ) . warn ( `Skipped deleting files in directory: ${ path . resolve ( directory ) } ` )
53
+ return
48
54
}
49
55
50
- const getTarget = ( key : RegistrationKey ) => {
51
- const suffix = `${ key . region } ${ key . scopes && key . scopes . length > 0 ? `-${ hashScopes ( key . scopes ) } ` : '' } `
52
- return path . join ( directory , `aws-toolkit-vscode-client-id-${ suffix } .json` )
53
- }
56
+ const fileNames = readdirSync ( directory )
57
+ fileNames . forEach ( fileName => {
58
+ const filePath = path . join ( directory , fileName )
59
+ if ( path . extname ( filePath ) === '.json' && isOldFile ( filePath , statFunc ) ) {
60
+ unlinkSync ( filePath )
61
+ getLogger ( ) . warn ( `auth: removed old cache file: ${ filePath } ` )
62
+ }
63
+ } )
64
+ }
65
+
66
+ export function isDirSafeToDeleteFrom ( dirPath : string ) : boolean {
67
+ const resolvedPath = path . resolve ( dirPath )
68
+ const isRoot = resolvedPath === path . resolve ( '/' )
69
+ const isCwd = resolvedPath === path . resolve ( '.' )
70
+ const isAbsolute = path . isAbsolute ( dirPath )
71
+ const pathDepth = resolvedPath . split ( path . sep ) . length
72
+
73
+ const isSafe = ! isRoot && ! isCwd && isAbsolute && pathDepth >= 5
74
+ return isSafe
75
+ }
54
76
77
+ export function getRegistrationCache ( directory = getCacheDir ( ) ) : KeyedCache < ClientRegistration , RegistrationKey > {
55
78
// Compatability for older Toolkit versions (format on disk is unchanged)
56
79
type StoredRegistration = Omit < ClientRegistration , 'expiresAt' > & { readonly expiresAt : string }
57
80
const read = ( data : StoredRegistration ) => ( { ...data , expiresAt : new Date ( data . expiresAt ) } )
58
81
const write = ( data : ClientRegistration ) => ( { ...data , expiresAt : data . expiresAt . toISOString ( ) } )
59
82
60
83
const logger = ( message : string ) => getLogger ( ) . debug ( `SSO registration cache: ${ message } ` )
61
- const cache : KeyedCache < StoredRegistration , RegistrationKey > = createDiskCache ( getTarget , logger )
84
+ const cache : KeyedCache < StoredRegistration , RegistrationKey > = createDiskCache (
85
+ ( registrationKey : RegistrationKey ) => getRegistrationCacheFile ( directory , registrationKey ) ,
86
+ logger
87
+ )
62
88
63
89
return mapCache ( cache , read , write )
64
90
}
@@ -112,24 +138,58 @@ export function getTokenCache(directory = getCacheDir()): KeyedCache<SsoAccess>
112
138
}
113
139
}
114
140
115
- const getTarget = ( ssoUrl : string ) => {
116
- const encoded = encodeURI ( ssoUrl )
117
- // Per the spec: 'SSO Login Token Flow' the access token must be
118
- // cached as the SHA1 hash of the bytes of the UTF-8 encoded
119
- // startUrl value with ".json" appended to the end.
141
+ const logger = ( message : string ) => getLogger ( ) . debug ( `SSO token cache: ${ message } ` )
142
+ const cache = createDiskCache < StoredToken , string > ( ( ssoUrl : string ) => getTokenCacheFile ( directory , ssoUrl ) , logger )
120
143
121
- const shasum = crypto . createHash ( 'sha1' )
122
- // Suppress warning because:
123
- // 1. SHA1 is prescribed by the AWS SSO spec
124
- // 2. the hashed startUrl value is not a secret
125
- shasum . update ( encoded ) // lgtm[js/weak-cryptographic-algorithm]
126
- const hashedUrl = shasum . digest ( 'hex' ) // lgtm[js/weak-cryptographic-algorithm]
144
+ return mapCache ( cache , read , write )
145
+ }
127
146
128
- return path . join ( directory , `${ hashedUrl } .json` )
147
+ function getFileStats ( file : string ) : Stats {
148
+ return statSync ( file )
149
+ }
150
+
151
+ const firstValidDate = new Date ( 2023 , 3 , 14 ) // April 14, 2023
152
+
153
+ /**
154
+ * @returns true if file is older than the first valid date
155
+ */
156
+ function isOldFile ( file : string , statFunc : typeof getFileStats ) : boolean {
157
+ try {
158
+ const statResult = statFunc ( file )
159
+ // Depending on the Windows filesystem, birthtime may be 0, so we fall back to ctime (last time metadata was changed)
160
+ // https://nodejs.org/api/fs.html#stat-time-values
161
+ return statResult . birthtimeMs !== 0
162
+ ? statResult . birthtimeMs < firstValidDate . getTime ( )
163
+ : statResult . ctime < firstValidDate
164
+ } catch ( err ) {
165
+ getLogger ( ) . debug ( `SSO cache file age not be verified: ${ file } : ${ err } ` )
166
+ return false // Assume it is no old since we cannot validate
129
167
}
168
+ }
130
169
131
- const logger = ( message : string ) => getLogger ( ) . debug ( `SSO token cache: ${ message } ` )
132
- const cache = createDiskCache < StoredToken , string > ( getTarget , logger )
170
+ export function getTokenCacheFile ( ssoCacheDir : string , ssoUrl : string ) : string {
171
+ const encoded = encodeURI ( ssoUrl )
172
+ // Per the spec: 'SSO Login Token Flow' the access token must be
173
+ // cached as the SHA1 hash of the bytes of the UTF-8 encoded
174
+ // startUrl value with ".json" appended to the end.
133
175
134
- return mapCache ( cache , read , write )
176
+ const shasum = crypto . createHash ( 'sha1' )
177
+ // Suppress warning because:
178
+ // 1. SHA1 is prescribed by the AWS SSO spec
179
+ // 2. the hashed startUrl value is not a secret
180
+ shasum . update ( encoded ) // lgtm[js/weak-cryptographic-algorithm]
181
+ const hashedUrl = shasum . digest ( 'hex' ) // lgtm[js/weak-cryptographic-algorithm]
182
+
183
+ return path . join ( ssoCacheDir , `${ hashedUrl } .json` )
184
+ }
185
+
186
+ export function getRegistrationCacheFile ( ssoCacheDir : string , key : RegistrationKey ) : string {
187
+ const hashScopes = ( scopes : string [ ] ) => {
188
+ const shasum = crypto . createHash ( 'sha256' )
189
+ scopes . forEach ( s => shasum . update ( s ) )
190
+ return shasum . digest ( 'hex' )
191
+ }
192
+
193
+ const suffix = `${ key . region } ${ key . scopes && key . scopes . length > 0 ? `-${ hashScopes ( key . scopes ) } ` : '' } `
194
+ return path . join ( ssoCacheDir , `aws-toolkit-vscode-client-id-${ suffix } .json` )
135
195
}
0 commit comments