@@ -127,6 +127,79 @@ class CredentialsManagerSpec: QuickSpec {
127127
128128 }
129129
130+ describe ( " storage with scoped keys " ) {
131+
132+ afterEach {
133+ _ = credentialsManager. clear ( forAudience: Audience)
134+ _ = credentialsManager. clear ( forAudience: Audience, scope: Scope)
135+ _ = credentialsManager. clear ( forAudience: Audience, scope: NewScope)
136+ _ = credentialsManager. clear ( forAudience: Audience, scope: " read write " )
137+ _ = credentialsManager. clear ( forAudience: Audience, scope: " read " )
138+ }
139+
140+ it ( " should store api credentials without scope using audience as key " ) {
141+ let store = SimpleKeychain ( )
142+ credentialsManager = CredentialsManager ( authentication: authentication, storage: store)
143+
144+ expect ( credentialsManager. store ( apiCredentials: apiCredentials, forAudience: Audience) ) . to ( beTrue ( ) )
145+ expect ( fetchAPICredentials ( forAudience: Audience, from: store) ) . toNot ( beNil ( ) )
146+ }
147+
148+ it ( " should store api credentials with scope using compound key " ) {
149+ let store = SimpleKeychain ( )
150+ credentialsManager = CredentialsManager ( authentication: authentication, storage: store)
151+
152+ expect ( credentialsManager. store ( apiCredentials: apiCredentials, forAudience: Audience, forScope: " read write " ) ) . to ( beTrue ( ) )
153+
154+ expect ( fetchAPICredentials ( forAudience: Audience, forScope: " read write " , from: store) ) . toNot ( beNil ( ) )
155+ }
156+
157+ it ( " should store credentials for same audience with different scopes separately " ) {
158+ let store = SimpleKeychain ( )
159+ credentialsManager = CredentialsManager ( authentication: authentication, storage: store)
160+
161+ let apiCredentials1 = APICredentials ( accessToken: " token1 " , tokenType: TokenType, expiresIn: Date ( timeIntervalSinceNow: ExpiresIn) , scope: " read " )
162+ let apiCredentials2 = APICredentials ( accessToken: " token2 " , tokenType: TokenType, expiresIn: Date ( timeIntervalSinceNow: ExpiresIn) , scope: " write " )
163+
164+ expect ( credentialsManager. store ( apiCredentials: apiCredentials1, forAudience: Audience, forScope: " read " ) ) . to ( beTrue ( ) )
165+ expect ( credentialsManager. store ( apiCredentials: apiCredentials2, forAudience: Audience, forScope: " write " ) ) . to ( beTrue ( ) )
166+
167+ // Both should exist
168+ let retrieved1 = fetchAPICredentials ( forAudience: Audience, forScope: " read " , from: store)
169+ let retrieved2 = fetchAPICredentials ( forAudience: Audience, forScope: " write " , from: store)
170+ expect ( retrieved1? . accessToken) == " token1 "
171+ expect ( retrieved2? . accessToken) == " token2 "
172+ }
173+
174+ it ( " should clear api credentials only for specified scope " ) {
175+ let store = SimpleKeychain ( )
176+ credentialsManager = CredentialsManager ( authentication: authentication, storage: store)
177+
178+ _ = credentialsManager. store ( apiCredentials: apiCredentials, forAudience: Audience)
179+ _ = credentialsManager. store ( apiCredentials: apiCredentials, forAudience: Audience, forScope: " read " )
180+
181+ _ = credentialsManager. clear ( forAudience: Audience, scope: " read " )
182+
183+ expect ( fetchAPICredentials ( forAudience: Audience, from: store) ) . toNot ( beNil ( ) )
184+
185+ expect ( fetchAPICredentials ( forAudience: Audience, forScope: " read " , from: store) ) . to ( beNil ( ) )
186+ }
187+
188+ it ( " should not clear scoped credentials when clearing without scope " ) {
189+ let store = SimpleKeychain ( )
190+ credentialsManager = CredentialsManager ( authentication: authentication, storage: store)
191+
192+ _ = credentialsManager. store ( apiCredentials: apiCredentials, forAudience: Audience)
193+ _ = credentialsManager. store ( apiCredentials: apiCredentials, forAudience: Audience, forScope: " read " )
194+
195+ _ = credentialsManager. clear ( forAudience: Audience)
196+
197+ expect ( fetchAPICredentials ( forAudience: Audience, from: store) ) . to ( beNil ( ) )
198+ expect ( fetchAPICredentials ( forAudience: Audience, forScope: " read " , from: store) ) . toNot ( beNil ( ) )
199+ }
200+
201+ }
202+
130203 describe ( " custom storage " ) {
131204
132205 class CustomStore : CredentialsStorage {
@@ -1122,7 +1195,7 @@ class CredentialsManagerSpec: QuickSpec {
11221195 tokenType: TokenType,
11231196 expiresIn: Date ( timeIntervalSinceNow: ExpiresIn) ,
11241197 scope: " openid phone " )
1125- _ = credentialsManager. store ( apiCredentials: apiCredentials, forAudience: Audience)
1198+ _ = credentialsManager. store ( apiCredentials: apiCredentials, forAudience: Audience, forScope : " openid phone " )
11261199 waitUntil ( timeout: Timeout) { done in
11271200 credentialsManager. apiCredentials ( forAudience: Audience, scope: " openid phone " ) { result in
11281201 expect ( result) . to ( haveAPICredentials ( AccessToken) )
@@ -1140,7 +1213,7 @@ class CredentialsManagerSpec: QuickSpec {
11401213 tokenType: TokenType,
11411214 expiresIn: Date ( timeIntervalSinceNow: ExpiresIn) ,
11421215 scope: " openid phone " )
1143- _ = credentialsManager. store ( apiCredentials: apiCredentials, forAudience: Audience)
1216+ _ = credentialsManager. store ( apiCredentials: apiCredentials, forAudience: Audience, forScope : " openid phone " )
11441217 waitUntil ( timeout: Timeout) { done in
11451218 credentialsManager. apiCredentials ( forAudience: Audience, scope: " openid email " ) { result in
11461219 expect ( result) . to ( haveAPICredentials ( NewAccessToken) )
@@ -1185,6 +1258,64 @@ class CredentialsManagerSpec: QuickSpec {
11851258
11861259 }
11871260
1261+ context ( " retrieval of api credentials with scope " ) {
1262+
1263+ beforeEach {
1264+ _ = credentialsManager. store ( credentials: credentials)
1265+ }
1266+
1267+ afterEach {
1268+ _ = credentialsManager. clear ( )
1269+ _ = credentialsManager. clear ( forAudience: Audience)
1270+ _ = credentialsManager. clear ( forAudience: Audience, scope: Scope)
1271+ _ = credentialsManager. clear ( forAudience: Audience, scope: " openid phone " )
1272+ _ = credentialsManager. clear ( forAudience: Audience, scope: " different " )
1273+ _ = credentialsManager. clear ( forAudience: Audience, scope: " read write " )
1274+ }
1275+
1276+ it ( " should retrieve api credentials stored with matching scope " ) {
1277+ apiCredentials = APICredentials ( accessToken: AccessToken, tokenType: TokenType, expiresIn: Date ( timeIntervalSinceNow: ExpiresIn) , scope: Scope)
1278+ _ = credentialsManager. store ( apiCredentials: apiCredentials, forAudience: Audience, forScope: Scope)
1279+
1280+ waitUntil ( timeout: Timeout) { done in
1281+ credentialsManager. apiCredentials ( forAudience: Audience, scope: Scope) { result in
1282+ expect ( result) . to ( haveAPICredentials ( AccessToken) )
1283+ done ( )
1284+ }
1285+ }
1286+ }
1287+
1288+ it ( " should renew api credentials when scope does not match stored scope " ) {
1289+ NetworkStub . clearStubs ( )
1290+ NetworkStub . addStub ( condition: {
1291+ $0. isToken ( Domain) && $0. hasAtLeast ( [ " refresh_token " : RefreshToken, " audience " : Audience] )
1292+ } , response: authResponse ( accessToken: NewAccessToken, idToken: NewIdToken, expiresIn: ExpiresIn, scope: " different " ) )
1293+
1294+ apiCredentials = APICredentials ( accessToken: AccessToken, tokenType: TokenType, expiresIn: Date ( timeIntervalSinceNow: ExpiresIn) , scope: Scope)
1295+ _ = credentialsManager. store ( apiCredentials: apiCredentials, forAudience: Audience, forScope: Scope)
1296+
1297+ waitUntil ( timeout: Timeout) { done in
1298+ credentialsManager. apiCredentials ( forAudience: Audience, scope: " different " ) { result in
1299+ expect ( result) . to ( haveAPICredentials ( NewAccessToken) )
1300+ done ( )
1301+ }
1302+ }
1303+ }
1304+
1305+ it ( " should retrieve api credentials with scopes in different order " ) {
1306+ apiCredentials = APICredentials ( accessToken: AccessToken, tokenType: TokenType, expiresIn: Date ( timeIntervalSinceNow: ExpiresIn) , scope: " read write " )
1307+ _ = credentialsManager. store ( apiCredentials: apiCredentials, forAudience: Audience, forScope: " read write " )
1308+
1309+ waitUntil ( timeout: Timeout) { done in
1310+ credentialsManager. apiCredentials ( forAudience: Audience, scope: " write read " ) { result in
1311+ expect ( result) . to ( haveAPICredentials ( AccessToken) )
1312+ done ( )
1313+ }
1314+ }
1315+ }
1316+
1317+ }
1318+
11881319 context ( " serial exchange for api credentials from same thread " ) {
11891320
11901321 it ( " should yield the stored api credentials after the previous renewal operation succeeded " ) {
@@ -2442,7 +2573,14 @@ private func fetchCredentials(from store: CredentialsStorage) -> Credentials? {
24422573 return try ? NSKeyedUnarchiver . unarchivedObject ( ofClass: Credentials . self, from: data)
24432574}
24442575
2445- private func fetchAPICredentials( forAudience audience: String = Audience, from store: CredentialsStorage ) -> APICredentials ? {
2446- guard let data = store. getEntry ( forKey: audience) else { return nil }
2576+ private func fetchAPICredentials( forAudience audience: String = Audience, forScope scope: String ? = nil , from store: CredentialsStorage ) -> APICredentials ? {
2577+ let key : String
2578+ if let scope = scope {
2579+ let normalisedScopes = scope. split ( separator: " " ) . sorted ( ) . joined ( separator: " :: " )
2580+ key = " \( audience) :: \( normalisedScopes) "
2581+ } else {
2582+ key = audience
2583+ }
2584+ guard let data = store. getEntry ( forKey: key) else { return nil }
24472585 return try ? APICredentials ( from: data)
24482586}
0 commit comments