@@ -88,13 +88,16 @@ enum ConfigFileLoader {
8888 /// - missingSecretAccessKey: If the secret access key was not found
8989 public struct ConfigFileError : Error , Equatable {
9090 enum Internal : Equatable {
91+ case fileDoesNotExist
9192 case invalidINIFile
9293 case missingProfile
9394 case missingAccessKeyId
9495 case missingSecretAccessKey
9596 }
9697 let value : Internal
9798
99+ /// Credentials or profile ini file does not exist
100+ public static var fileDoesNotExist : Self { . init( value: . fileDoesNotExist) }
98101 /// Credentials or profile ini file failed to load
99102 public static var invalidINIFile : Self { . init( value: . invalidINIFile) }
100103 /// Cannot find profile in ini file
@@ -120,28 +123,39 @@ enum ConfigFileLoader {
120123 threadPool: NIOThreadPool = . singleton
121124 ) async throws -> SharedCredentials {
122125 let fileIO = NonBlockingFileIO ( threadPool: threadPool)
123- let credentialsINIParser : INIParser
126+ // Only treat "file does not exist" as a soft miss. Other failures (permission denied,
127+ // I/O errors, malformed INI) propagate so the caller sees why credential lookup failed
128+ // instead of silently falling through to the next provider.
129+ let credentialsINIParser : INIParser ?
124130 do {
125- // Load credentials file
126- credentialsINIParser = try await self . loadINIFile (
127- path: credentialsFilePath,
128- fileIO: fileIO
129- )
130- } catch {
131- // Throw `.noProvider` error if credential file cannot be loaded
132- throw CredentialProviderError . noProvider
131+ credentialsINIParser = try await self . loadINIFile ( path: credentialsFilePath, fileIO: fileIO)
132+ } catch let error as ConfigFileError where error == ConfigFileError . fileDoesNotExist {
133+ credentialsINIParser = nil
133134 }
134135 let configINIParser : INIParser ?
135136 do {
136- // Load profile config file
137- configINIParser = try await self . loadINIFile (
138- path: configFilePath,
139- fileIO: fileIO
140- )
141- } catch {
137+ configINIParser = try await self . loadINIFile ( path: configFilePath, fileIO: fileIO)
138+ } catch let error as ConfigFileError where error == ConfigFileError . fileDoesNotExist {
142139 configINIParser = nil
143140 }
144- return try self . parseSharedCredentials ( from: credentialsINIParser, configINIParser: configINIParser, for: profile)
141+
142+ // If neither file is available there's nothing to resolve — fall through to the next
143+ // provider in the chain, matching the historical contract for a missing credentials file.
144+ if credentialsINIParser == nil && configINIParser == nil {
145+ throw CredentialProviderError . noProvider
146+ }
147+
148+ do {
149+ return try self . parseSharedCredentials (
150+ from: credentialsINIParser,
151+ configINIParser: configINIParser,
152+ for: profile
153+ )
154+ } catch let error as ConfigFileError where error == . missingProfile && credentialsINIParser == nil {
155+ // The credentials file was unavailable and the config file didn't supply the profile
156+ // either — preserve the "fall through to the next provider" behavior.
157+ throw CredentialProviderError . noProvider
158+ }
145159 }
146160
147161 /// Load a file from disk without blocking the current thread
@@ -162,7 +176,13 @@ enum ConfigFileLoader {
162176 /// - fileIO: non-blocking file IO
163177 /// - Returns: INIParser
164178 static func loadINIFile( path: String , fileIO: NonBlockingFileIO ) async throws -> INIParser {
165- let buffer = try await loadFile ( path: path, fileIO: fileIO)
179+ let buffer : ByteBuffer
180+ do {
181+ buffer = try await loadFile ( path: path, fileIO: fileIO)
182+ } catch let error as IOError where error. errnoCode == ENOENT {
183+ throw ConfigFileError . fileDoesNotExist
184+ }
185+
166186 let content = String ( buffer: buffer)
167187 guard let parser = try ? INIParser ( content) else {
168188 throw ConfigFileError . invalidINIFile
@@ -184,34 +204,58 @@ enum ConfigFileLoader {
184204 /// - profile: named profile to load (optional)
185205 /// - Returns: Parsed SharedCredentials
186206 static func parseSharedCredentials(
187- from credentialsINIParser: INIParser ,
207+ from credentialsINIParser: INIParser ? ,
188208 configINIParser: INIParser ? ,
189209 for profile: String
190210 ) throws -> SharedCredentials {
191211 let config = try configINIParser. flatMap { try self . parseProfileConfig ( from: $0, for: profile) }
192- let credentials = try parseCredentials ( from: credentialsINIParser, for: profile, sourceProfile: config? . sourceProfile)
212+
213+ // The profile may live only in the config file (typical for assume-role chains whose
214+ // source is an SSO profile). Tolerate `.missingProfile` from `parseCredentials` when
215+ // the config file already supplies a `role_arn`, and skip the call entirely when
216+ // the credentials file is unavailable.
217+ let credentials : ProfileCredentials ?
218+ if let credentialsINIParser {
219+ do {
220+ credentials = try parseCredentials ( from: credentialsINIParser, for: profile, sourceProfile: config? . sourceProfile)
221+ } catch let error as ConfigFileError where error == . missingProfile && config? . roleArn != nil {
222+ credentials = nil
223+ }
224+ } else {
225+ credentials = nil
226+ }
193227
194228 // If `role_arn` is defined, check for source profile or credential source
195- if let roleArn = credentials. roleArn ?? config? . roleArn {
196- let sessionName = credentials. roleSessionName ?? config? . roleSessionName ?? UUID ( ) . uuidString
229+ if let roleArn = credentials? . roleArn ?? config? . roleArn {
230+ let sessionName = credentials? . roleSessionName ?? config? . roleSessionName ?? UUID ( ) . uuidString
197231 let region = config? . region ?? . useast1
198232 // If `source_profile` is defined, temporary credentials must be loaded via STS AssumeRole operation
199- if let _ = credentials. sourceProfile ?? config? . sourceProfile {
200- guard let accessKey = credentials. accessKey else {
233+ if let sourceProfileName = credentials? . sourceProfile ?? config? . sourceProfile {
234+ // If the source profile is an SSO profile (per the config file), use SSO to obtain
235+ // the source credentials instead of looking up static keys.
236+ if let configINIParser, Self . isSSOProfile ( in: configINIParser, profile: sourceProfileName) {
237+ return . assumeRole(
238+ roleArn: roleArn,
239+ sessionName: sessionName,
240+ region: region,
241+ sourceCredentialProvider: . sso( profileName: sourceProfileName)
242+ )
243+ }
244+ guard let accessKey = credentials? . accessKey else {
201245 throw ConfigFileError . missingAccessKeyId
202246 }
203- guard let secretAccessKey = credentials. secretAccessKey else {
247+ guard let secretAccessKey = credentials? . secretAccessKey else {
204248 throw ConfigFileError . missingSecretAccessKey
205249 }
206250 let provider : CredentialProviderFactory = . static(
207251 accessKeyId: accessKey,
208252 secretAccessKey: secretAccessKey,
209- sessionToken: credentials. sessionToken
253+ sessionToken: credentials? . sessionToken
210254 )
211255 return . assumeRole( roleArn: roleArn, sessionName: sessionName, region: region, sourceCredentialProvider: provider)
212256 }
213257 // If `credental_source` is defined, temporary credentials must be loaded from source
214- else if let credentialSource = credentials. credentialSource ?? config? . credentialSource {
258+ else if let credentialSource = credentials? . credentialSource ?? config? . credentialSource {
215259 let provider : CredentialProviderFactory
216260 switch credentialSource {
217261 case . environment:
@@ -228,6 +272,9 @@ enum ConfigFileLoader {
228272 }
229273
230274 // Return static credentials
275+ guard let credentials else {
276+ throw ConfigFileError . missingProfile
277+ }
231278 guard let accessKey = credentials. accessKey else {
232279 throw ConfigFileError . missingAccessKeyId
233280 }
@@ -238,6 +285,14 @@ enum ConfigFileLoader {
238285 return . staticCredential( credential: credential)
239286 }
240287
288+ /// Returns true if the given profile's section in the AWS config file declares SSO
289+ /// (either the modern `sso_session` reference or the legacy `sso_start_url` keys).
290+ static func isSSOProfile( in configINIParser: INIParser , profile: String ) -> Bool {
291+ let sectionKey = profile == Self . defaultProfile ? profile : " profile \( profile) "
292+ guard let section = configINIParser. sections [ sectionKey] else { return false }
293+ return section [ " sso_session " ] != nil || section [ " sso_start_url " ] != nil
294+ }
295+
241296 /// Parse profile configuraton from a file (passed in as byte-buffer), usually `~/.aws/config`
242297 ///
243298 /// - Parameters:
0 commit comments