diff --git a/EXAMPLES.md b/EXAMPLES.md index ff6341cd..62a11e86 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -492,6 +492,9 @@ function MyComponent() { const clearFirstApiCache = async () => { // Clear cached credentials for a specific API await clearApiCredentials('https://first-api.example.com'); + + // Or clear with specific scope + await clearApiCredentials('https://first-api.example.com', 'read:data'); }; return ( @@ -531,6 +534,12 @@ console.log('Scope:', apiCredentials.scope); await auth0.credentialsManager.clearApiCredentials( 'https://first-api.example.com' ); + +// Clear with specific scope +await auth0.credentialsManager.clearApiCredentials( + 'https://first-api.example.com', + 'read:data write:data' +); ``` ### Web Platform Configuration diff --git a/android/src/main/java/com/auth0/react/A0Auth0Module.kt b/android/src/main/java/com/auth0/react/A0Auth0Module.kt index a2c7f352..9cd830fa 100644 --- a/android/src/main/java/com/auth0/react/A0Auth0Module.kt +++ b/android/src/main/java/com/auth0/react/A0Auth0Module.kt @@ -328,8 +328,8 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0 } @ReactMethod - override fun clearApiCredentials(audience: String, promise: Promise) { - secureCredentialsManager.clearApiCredentials(audience) + override fun clearApiCredentials(audience: String, scope: String?, promise: Promise) { + secureCredentialsManager.clearApiCredentials(audience, scope) promise.resolve(true) } diff --git a/android/src/main/oldarch/com/auth0/react/A0Auth0Spec.kt b/android/src/main/oldarch/com/auth0/react/A0Auth0Spec.kt index 5d6da063..ec052015 100644 --- a/android/src/main/oldarch/com/auth0/react/A0Auth0Spec.kt +++ b/android/src/main/oldarch/com/auth0/react/A0Auth0Spec.kt @@ -63,7 +63,7 @@ abstract class A0Auth0Spec(context: ReactApplicationContext) : ReactContextBaseJ @ReactMethod @DoNotStrip - abstract fun clearApiCredentials(audience: String, promise: Promise) + abstract fun clearApiCredentials(audience: String, scope: String?, promise: Promise) @ReactMethod @DoNotStrip diff --git a/ios/A0Auth0.mm b/ios/A0Auth0.mm index 1ab6952c..e107e7c0 100644 --- a/ios/A0Auth0.mm +++ b/ios/A0Auth0.mm @@ -87,9 +87,10 @@ - (dispatch_queue_t)methodQueue } RCT_EXPORT_METHOD(clearApiCredentials: (NSString *)audience + scope:(NSString * _Nullable)scope resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { - [self.nativeBridge clearApiCredentialsWithAudience:audience resolve:resolve reject:reject]; + [self.nativeBridge clearApiCredentialsWithAudience:audience scope:scope resolve:resolve reject:reject]; } diff --git a/ios/NativeBridge.swift b/ios/NativeBridge.swift index e0d67eaf..7e6c8f0f 100644 --- a/ios/NativeBridge.swift +++ b/ios/NativeBridge.swift @@ -372,10 +372,10 @@ public class NativeBridge: NSObject { } - @objc public func clearApiCredentials(audience: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { - // The clear(forAudience:) method returns a boolean indicating success. + @objc public func clearApiCredentials(audience: String, scope: String?, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { + // The clear(forAudience:scope:) method returns a boolean indicating success. // We can resolve the promise with this boolean value. - resolve(credentialsManager.clear(forAudience: audience)) + resolve(credentialsManager.clear(forAudience: audience, scope: scope)) } @objc public func getClientId() -> String { diff --git a/src/core/interfaces/ICredentialsManager.ts b/src/core/interfaces/ICredentialsManager.ts index 7f17f880..936f1872 100644 --- a/src/core/interfaces/ICredentialsManager.ts +++ b/src/core/interfaces/ICredentialsManager.ts @@ -136,18 +136,24 @@ export interface ICredentialsManager { /** * Removes cached credentials for a specific audience. + * Optionally filter by scope to clear only specific scope-based credentials. * * This clears the stored API credentials for the given audience, forcing the next * `getApiCredentials` call for this audience to perform a fresh token exchange. * * @param audience The identifier of the API for which to clear credentials. + * @param scope Optional scope to clear. If credentials were fetched with a scope, it is recommended to pass the same scope when clearing them. * @returns A promise that resolves when the credentials are cleared. * @throws {CredentialsManagerError} If the operation fails. * * @example * ```typescript + * // Clear all credentials for an audience * await credentialsManager.clearApiCredentials('https://api.example.com'); + * + * // Clear credentials for specific scope (recommended) + * await credentialsManager.clearApiCredentials('https://api.example.com', 'read:data'); * ``` */ - clearApiCredentials(audience: string): Promise; + clearApiCredentials(audience: string, scope?: string): Promise; } diff --git a/src/platforms/native/adapters/NativeCredentialsManager.ts b/src/platforms/native/adapters/NativeCredentialsManager.ts index 60feecc0..dd5f1d61 100644 --- a/src/platforms/native/adapters/NativeCredentialsManager.ts +++ b/src/platforms/native/adapters/NativeCredentialsManager.ts @@ -40,21 +40,12 @@ export class NativeCredentialsManager implements ICredentialsManager { ); } - hasValidCredentials(minTtl?: number): Promise { - return this.handleError(this.bridge.hasValidCredentials(minTtl)); + async clearCredentials(): Promise { + return this.handleError(this.bridge.clearCredentials()); } - async clearCredentials(): Promise { - await this.handleError(this.bridge.clearCredentials()); - // Also clear the DPoP key when clearing credentials - // Ignore errors from DPoP key clearing - this matches iOS behavior - // where we log the error but don't fail the operation - try { - await this.bridge.clearDPoPKey(); - } catch { - // Silently ignore DPoP key clearing errors - // The main credentials are already cleared at this point - } + async hasValidCredentials(minTtl?: number): Promise { + return this.handleError(this.bridge.hasValidCredentials(minTtl)); } async getApiCredentials( @@ -70,8 +61,8 @@ export class NativeCredentialsManager implements ICredentialsManager { return new ApiCredentials(nativeCredentials as IApiCredentials); } - clearApiCredentials(audience: string): Promise { - return this.handleError(this.bridge.clearApiCredentials(audience)); + async clearApiCredentials(audience: string, scope?: string): Promise { + return this.handleError(this.bridge.clearApiCredentials(audience, scope)); } getSSOCredentials( diff --git a/src/platforms/native/adapters/__tests__/NativeCredentialsManager.spec.ts b/src/platforms/native/adapters/__tests__/NativeCredentialsManager.spec.ts index 53d04145..81340845 100644 --- a/src/platforms/native/adapters/__tests__/NativeCredentialsManager.spec.ts +++ b/src/platforms/native/adapters/__tests__/NativeCredentialsManager.spec.ts @@ -357,7 +357,7 @@ describe('NativeCredentialsManager', () => { ).rejects.toThrow(CredentialsManagerError); }); - it('should clear credentials on success', async () => { + it('should clear credentials for audience without scope', async () => { mockBridge.clearApiCredentials.mockResolvedValue(undefined); await expect( @@ -365,7 +365,46 @@ describe('NativeCredentialsManager', () => { ).resolves.toBeUndefined(); expect(mockBridge.clearApiCredentials).toHaveBeenCalledWith( - 'https://api.example.com' + 'https://api.example.com', + undefined + ); + }); + + it('should clear credentials for audience with scope', async () => { + mockBridge.clearApiCredentials.mockResolvedValue(undefined); + + await expect( + manager.clearApiCredentials( + 'https://api.example.com', + 'read:data write:data' + ) + ).resolves.toBeUndefined(); + + expect(mockBridge.clearApiCredentials).toHaveBeenCalledWith( + 'https://api.example.com', + 'read:data write:data' + ); + }); + + it('should handle multiple different audiences', async () => { + mockBridge.clearApiCredentials.mockResolvedValue(undefined); + + await manager.clearApiCredentials('https://api1.example.com'); + await manager.clearApiCredentials( + 'https://api2.example.com', + 'admin:write' + ); + + expect(mockBridge.clearApiCredentials).toHaveBeenCalledTimes(2); + expect(mockBridge.clearApiCredentials).toHaveBeenNthCalledWith( + 1, + 'https://api1.example.com', + undefined + ); + expect(mockBridge.clearApiCredentials).toHaveBeenNthCalledWith( + 2, + 'https://api2.example.com', + 'admin:write' ); }); }); diff --git a/src/platforms/native/bridge/INativeBridge.ts b/src/platforms/native/bridge/INativeBridge.ts index b7cc038c..7a62ccdf 100644 --- a/src/platforms/native/bridge/INativeBridge.ts +++ b/src/platforms/native/bridge/INativeBridge.ts @@ -123,7 +123,15 @@ export interface INativeBridge { parameters?: object ): Promise; - clearApiCredentials(audience: string): Promise; + /** + * Clears API credentials for a specific audience from secure storage. + * Optionally filter by scope to clear only specific scope-based credentials. + * + * @param audience The audience of the API. + * @param scope Optional scope to clear. If credentials were fetched with a scope, it is recommended to pass the same scope when clearing them. + */ + clearApiCredentials(audience: string, scope?: string): Promise; + /** * Clears credentials from secure storage. */ diff --git a/src/platforms/native/bridge/NativeBridgeManager.ts b/src/platforms/native/bridge/NativeBridgeManager.ts index 5a200df4..a1e36643 100644 --- a/src/platforms/native/bridge/NativeBridgeManager.ts +++ b/src/platforms/native/bridge/NativeBridgeManager.ts @@ -165,10 +165,11 @@ export class NativeBridgeManager implements INativeBridge { ); } - clearApiCredentials(audience: string): Promise { + clearApiCredentials(audience: string, scope?: string): Promise { return this.a0_call( Auth0NativeModule.clearApiCredentials.bind(Auth0NativeModule), - audience + audience, + scope ); } diff --git a/src/platforms/web/adapters/WebCredentialsManager.ts b/src/platforms/web/adapters/WebCredentialsManager.ts index 6d55e63f..770edcec 100644 --- a/src/platforms/web/adapters/WebCredentialsManager.ts +++ b/src/platforms/web/adapters/WebCredentialsManager.ts @@ -122,9 +122,10 @@ export class WebCredentialsManager implements ICredentialsManager { throw new CredentialsManagerError(authError); } - async clearApiCredentials(audience: string): Promise { + async clearApiCredentials(audience: string, scope?: string): Promise { + const scopeInfo = scope ? ` and scope ${scope}` : ''; console.warn( - `'clearApiCredentials' for audience ${audience} is a no-op on the web. @auth0/auth0-spa-js handles credential storage automatically.` + `'clearApiCredentials' for audience ${audience}${scopeInfo} is a no-op on the web. @auth0/auth0-spa-js handles credential storage automatically.` ); return Promise.resolve(); } diff --git a/src/platforms/web/adapters/__tests__/WebCredentialsManager.spec.ts b/src/platforms/web/adapters/__tests__/WebCredentialsManager.spec.ts index a2bef6f2..a540c9d0 100644 --- a/src/platforms/web/adapters/__tests__/WebCredentialsManager.spec.ts +++ b/src/platforms/web/adapters/__tests__/WebCredentialsManager.spec.ts @@ -341,12 +341,34 @@ describe('WebCredentialsManager', () => { }); describe('clearApiCredentials', () => { - it('should log a warning and resolve without doing anything', async () => { + it('should log a warning without scope and resolve without doing anything', async () => { await credentialsManager.clearApiCredentials('https://api.example.com'); expect(consoleWarnSpy).toHaveBeenCalledWith( "'clearApiCredentials' for audience https://api.example.com is a no-op on the web. @auth0/auth0-spa-js handles credential storage automatically." ); }); + + it('should log a warning with scope and resolve without doing anything', async () => { + await credentialsManager.clearApiCredentials( + 'https://api.example.com', + 'read:data write:data' + ); + + expect(consoleWarnSpy).toHaveBeenCalledWith( + "'clearApiCredentials' for audience https://api.example.com and scope read:data write:data is a no-op on the web. @auth0/auth0-spa-js handles credential storage automatically." + ); + }); + + it('should handle empty scope string', async () => { + await credentialsManager.clearApiCredentials( + 'https://api.example.com', + '' + ); + + expect(consoleWarnSpy).toHaveBeenCalledWith( + "'clearApiCredentials' for audience https://api.example.com is a no-op on the web. @auth0/auth0-spa-js handles credential storage automatically." + ); + }); }); }); diff --git a/src/specs/NativeA0Auth0.ts b/src/specs/NativeA0Auth0.ts index a0da007c..575d3bcc 100644 --- a/src/specs/NativeA0Auth0.ts +++ b/src/specs/NativeA0Auth0.ts @@ -65,9 +65,14 @@ export interface Spec extends TurboModule { ): Promise; /** - * Clear API credentials for a specific audience + * Clear API credentials for a specific audience. Optionally filter by scope. + * @param audience The audience to clear credentials for. + * @param scope The scope to clear credentials for. If not provided, clears all credentials for the audience. */ - clearApiCredentials(audience: string): Promise; + clearApiCredentials( + audience: string, + scope: string | undefined + ): Promise; /** * Start web authentication