From af3027930a3d66e663ca6712fb5eb494d068cd4b Mon Sep 17 00:00:00 2001 From: Subhankar Maiti Date: Wed, 17 Dec 2025 10:35:13 +0530 Subject: [PATCH 1/5] feat: enhance credential management with audience and scope filtering --- EXAMPLES.md | 20 ++++++++++++ .../java/com/auth0/react/A0Auth0Module.kt | 30 ++++++++++------- .../oldarch/com/auth0/react/A0Auth0Spec.kt | 4 +-- ios/NativeBridge.swift | 32 ++++++++++++------- src/core/interfaces/ICredentialsManager.ts | 13 ++++++-- .../adapters/NativeCredentialsManager.ts | 21 ++++-------- src/platforms/native/bridge/INativeBridge.ts | 16 ++++++++-- .../native/bridge/NativeBridgeManager.ts | 11 ++++--- .../web/adapters/WebCredentialsManager.ts | 13 ++++++-- src/specs/NativeA0Auth0.ts | 18 ++++++++--- 10 files changed, 122 insertions(+), 56 deletions(-) diff --git a/EXAMPLES.md b/EXAMPLES.md index ff6341cd..380aa60f 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -492,6 +492,14 @@ 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'); + }; + + const clearSpecificCredentials = async () => { + // You can also use clearCredentials with audience/scope + await clearCredentials('https://first-api.example.com', 'read:data'); }; return ( @@ -531,6 +539,18 @@ 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' +); + +// Or use clearCredentials with audience/scope +await auth0.credentialsManager.clearCredentials( + 'https://first-api.example.com', + 'read: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..32bd57e1 100644 --- a/android/src/main/java/com/auth0/react/A0Auth0Module.kt +++ b/android/src/main/java/com/auth0/react/A0Auth0Module.kt @@ -271,16 +271,22 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0 } @ReactMethod - override fun clearCredentials(promise: Promise) { - secureCredentialsManager.clearCredentials() - - // Also clear DPoP key if DPoP is enabled - if (useDPoP) { - try { - DPoP.clearKeyPair() - } catch (e: Exception) { - // Log error but don't fail the operation - android.util.Log.w(NAME, "Failed to clear DPoP key", e) + override fun clearCredentials(audience: String?, scope: String?, promise: Promise) { + if (audience != null) { + // Clear API credentials for specific audience and scope + secureCredentialsManager.clearApiCredentials(audience, scope) + } else { + // Clear all credentials + secureCredentialsManager.clearCredentials() + + // Also clear DPoP key if DPoP is enabled + if (useDPoP) { + try { + DPoP.clearKeyPair() + } catch (e: Exception) { + // Log error but don't fail the operation + android.util.Log.w(NAME, "Failed to clear DPoP key", e) + } } } @@ -328,8 +334,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..649db97c 100644 --- a/android/src/main/oldarch/com/auth0/react/A0Auth0Spec.kt +++ b/android/src/main/oldarch/com/auth0/react/A0Auth0Spec.kt @@ -49,7 +49,7 @@ abstract class A0Auth0Spec(context: ReactApplicationContext) : ReactContextBaseJ @ReactMethod @DoNotStrip - abstract fun clearCredentials(promise: Promise) + abstract fun clearCredentials(audience: String?, scope: String?, promise: Promise) @ReactMethod @DoNotStrip @@ -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/NativeBridge.swift b/ios/NativeBridge.swift index e0d67eaf..1971aa67 100644 --- a/ios/NativeBridge.swift +++ b/ios/NativeBridge.swift @@ -235,16 +235,24 @@ public class NativeBridge: NSObject { resolve(credentialsManager.canRenew() || credentialsManager.hasValid(minTTL: minTTL)) } - @objc public func clearCredentials(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { - let removed = credentialsManager.clear() + @objc public func clearCredentials(audience: String?, scope: String?, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { + let removed: Bool - // Also clear DPoP key if DPoP is enabled - if self.useDPoP { - do { - try DPoP.clearKeypair() - } catch { - // Log error but don't fail the operation - print("Warning: Failed to clear DPoP key: \(error.localizedDescription)") + if let audience = audience { + // Clear API credentials for specific audience and scope + removed = credentialsManager.clear(forAudience: audience, scope: scope) + } else { + // Clear all credentials + removed = credentialsManager.clear() + + // Also clear DPoP key if DPoP is enabled + if self.useDPoP { + do { + try DPoP.clearKeypair() + } catch { + // Log error but don't fail the operation + print("Warning: Failed to clear DPoP key: \(error.localizedDescription)") + } } } @@ -372,10 +380,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..7b3ea8f4 100644 --- a/src/core/interfaces/ICredentialsManager.ts +++ b/src/core/interfaces/ICredentialsManager.ts @@ -45,10 +45,13 @@ export interface ICredentialsManager { /** * Removes all credentials from the device's storage. + * Optionally filter by audience and scope to clear specific credentials. * + * @param audience Optional audience to clear credentials for. If not provided, clears all credentials. + * @param scope Optional scope to clear. Only applicable when audience is provided. * @returns A promise that resolves when the credentials have been cleared. */ - clearCredentials(): Promise; + clearCredentials(audience?: string, scope?: string): Promise; /** * Obtains session transfer credentials for performing Native to Web SSO. @@ -136,18 +139,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 not provided, clears all credentials for the audience. * @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 + * 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..96158d82 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(audience?: string, scope?: string): Promise { + return this.handleError(this.bridge.clearCredentials(audience, scope)); } - 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/bridge/INativeBridge.ts b/src/platforms/native/bridge/INativeBridge.ts index b7cc038c..87da3ec0 100644 --- a/src/platforms/native/bridge/INativeBridge.ts +++ b/src/platforms/native/bridge/INativeBridge.ts @@ -123,11 +123,23 @@ 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 not provided, clears all credentials for the audience. + */ + clearApiCredentials(audience: string, scope?: string): Promise; + /** * Clears credentials from secure storage. + * Optionally filter by audience and scope to clear specific credentials. + * + * @param audience Optional audience to clear credentials for. If not provided, clears all credentials. + * @param scope Optional scope to clear. Only applicable when audience is provided. */ - clearCredentials(): Promise; + clearCredentials(audience?: string, scope?: string): Promise; /** * Resumes the web authentication flow with the provided URL. diff --git a/src/platforms/native/bridge/NativeBridgeManager.ts b/src/platforms/native/bridge/NativeBridgeManager.ts index 5a200df4..3ec660e9 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 ); } @@ -179,9 +180,11 @@ export class NativeBridgeManager implements INativeBridge { ); } - async clearCredentials(): Promise { + async clearCredentials(audience?: string, scope?: string): Promise { return this.a0_call( - Auth0NativeModule.clearCredentials.bind(Auth0NativeModule) + Auth0NativeModule.clearCredentials.bind(Auth0NativeModule), + audience, + scope ); } diff --git a/src/platforms/web/adapters/WebCredentialsManager.ts b/src/platforms/web/adapters/WebCredentialsManager.ts index 6d55e63f..5892f7ac 100644 --- a/src/platforms/web/adapters/WebCredentialsManager.ts +++ b/src/platforms/web/adapters/WebCredentialsManager.ts @@ -97,8 +97,14 @@ export class WebCredentialsManager implements ICredentialsManager { return this.client.isAuthenticated(); } - async clearCredentials(): Promise { + async clearCredentials(audience?: string, scope?: string): Promise { try { + // For web, if audience is provided, delegate to clearApiCredentials + if (audience) { + return this.clearApiCredentials(audience, scope); + } + + // Otherwise, clear all credentials await this.client.logout({ openUrl: false }); } catch (e: any) { const code = e.error ?? 'ClearCredentialsFailed'; @@ -122,9 +128,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/specs/NativeA0Auth0.ts b/src/specs/NativeA0Auth0.ts index a0da007c..828ea828 100644 --- a/src/specs/NativeA0Auth0.ts +++ b/src/specs/NativeA0Auth0.ts @@ -50,9 +50,14 @@ export interface Spec extends TurboModule { hasValidCredentials(minTTL: Int32): Promise; /** - * Clear credentials + * Clear credentials. Optionally filter by audience and scope. + * @param audience The audience to clear credentials for. If not provided, clears all credentials. + * @param scope The scope to clear credentials for. Only applicable when audience is provided. */ - clearCredentials(): Promise; + clearCredentials( + audience: string | undefined, + scope: string | undefined + ): Promise; /** * Get API credentials for a specific audience @@ -65,9 +70,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 From 409a0c0ba8d6c561ee0b3a9c108a388363d6ebd8 Mon Sep 17 00:00:00 2001 From: Subhankar Maiti Date: Wed, 17 Dec 2025 11:01:52 +0530 Subject: [PATCH 2/5] feat: enhance clearCredentials and clearApiCredentials methods to support audience and scope parameters --- .../java/com/auth0/react/A0Auth0Module.kt | 19 +++++++++---------- ios/A0Auth0.mm | 9 ++++++--- ios/NativeBridge.swift | 16 ++++++++-------- 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/android/src/main/java/com/auth0/react/A0Auth0Module.kt b/android/src/main/java/com/auth0/react/A0Auth0Module.kt index 32bd57e1..bde1d4ad 100644 --- a/android/src/main/java/com/auth0/react/A0Auth0Module.kt +++ b/android/src/main/java/com/auth0/react/A0Auth0Module.kt @@ -278,18 +278,17 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0 } else { // Clear all credentials secureCredentialsManager.clearCredentials() - - // Also clear DPoP key if DPoP is enabled - if (useDPoP) { - try { - DPoP.clearKeyPair() - } catch (e: Exception) { - // Log error but don't fail the operation - android.util.Log.w(NAME, "Failed to clear DPoP key", e) - } + } + // Also clear DPoP key if DPoP is enabled + if (useDPoP) { + try { + DPoP.clearKeyPair() + } catch (e: Exception) { + // Log error but don't fail the operation + android.util.Log.w(NAME, "Failed to clear DPoP key", e) } } - + promise.resolve(true) } diff --git a/ios/A0Auth0.mm b/ios/A0Auth0.mm index 1ab6952c..6979165a 100644 --- a/ios/A0Auth0.mm +++ b/ios/A0Auth0.mm @@ -46,9 +46,11 @@ - (dispatch_queue_t)methodQueue } -RCT_EXPORT_METHOD(clearCredentials:(RCTPromiseResolveBlock)resolve +RCT_EXPORT_METHOD(clearCredentials:(NSString * _Nullable)audience + scope:(NSString * _Nullable)scope + resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { - [self.nativeBridge clearCredentialsWithResolve:resolve reject:reject]; + [self.nativeBridge clearCredentialsWithAudience:audience scope:scope resolve:resolve reject:reject]; } @@ -87,9 +89,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 1971aa67..40c8610e 100644 --- a/ios/NativeBridge.swift +++ b/ios/NativeBridge.swift @@ -244,15 +244,15 @@ public class NativeBridge: NSObject { } else { // Clear all credentials removed = credentialsManager.clear() + } - // Also clear DPoP key if DPoP is enabled - if self.useDPoP { - do { - try DPoP.clearKeypair() - } catch { - // Log error but don't fail the operation - print("Warning: Failed to clear DPoP key: \(error.localizedDescription)") - } + // Also clear DPoP key if DPoP is enabled + if self.useDPoP { + do { + try DPoP.clearKeypair() + } catch { + // Log error but don't fail the operation + print("Warning: Failed to clear DPoP key: \(error.localizedDescription)") } } From 8d655ae80f869bbc2033ea22a430182f419468bf Mon Sep 17 00:00:00 2001 From: Subhankar Maiti Date: Wed, 17 Dec 2025 11:07:41 +0530 Subject: [PATCH 3/5] feat: enhance clearCredentials and clearApiCredentials methods to handle audience and scope parameters --- .../NativeCredentialsManager.spec.ts | 79 ++++++++++++++++++- .../__tests__/WebCredentialsManager.spec.ts | 50 +++++++++++- 2 files changed, 123 insertions(+), 6 deletions(-) diff --git a/src/platforms/native/adapters/__tests__/NativeCredentialsManager.spec.ts b/src/platforms/native/adapters/__tests__/NativeCredentialsManager.spec.ts index 53d04145..347aaff4 100644 --- a/src/platforms/native/adapters/__tests__/NativeCredentialsManager.spec.ts +++ b/src/platforms/native/adapters/__tests__/NativeCredentialsManager.spec.ts @@ -129,10 +129,44 @@ describe('NativeCredentialsManager', () => { }); describe('clearCredentials', () => { - it('should call the bridge to clear credentials', async () => { + it('should call the bridge to clear all credentials when no parameters provided', async () => { mockBridge.clearCredentials.mockResolvedValueOnce(); await manager.clearCredentials(); expect(mockBridge.clearCredentials).toHaveBeenCalledTimes(1); + expect(mockBridge.clearCredentials).toHaveBeenCalledWith( + undefined, + undefined + ); + }); + + it('should call the bridge to clear credentials for specific audience', async () => { + mockBridge.clearCredentials.mockResolvedValueOnce(); + await manager.clearCredentials('https://api.example.com'); + expect(mockBridge.clearCredentials).toHaveBeenCalledTimes(1); + expect(mockBridge.clearCredentials).toHaveBeenCalledWith( + 'https://api.example.com', + undefined + ); + }); + + it('should call the bridge to clear credentials for specific audience and scope', async () => { + mockBridge.clearCredentials.mockResolvedValueOnce(); + await manager.clearCredentials('https://api.example.com', 'read:data'); + expect(mockBridge.clearCredentials).toHaveBeenCalledTimes(1); + expect(mockBridge.clearCredentials).toHaveBeenCalledWith( + 'https://api.example.com', + 'read:data' + ); + }); + + it('should propagate errors from the bridge', async () => { + const clearError = new AuthError('CLEAR_FAILED', 'Failed to clear', { + code: 'CLEAR_FAILED', + }); + mockBridge.clearCredentials.mockRejectedValueOnce(clearError); + await expect(manager.clearCredentials()).rejects.toThrow( + CredentialsManagerError + ); }); }); @@ -357,7 +391,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 +399,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/web/adapters/__tests__/WebCredentialsManager.spec.ts b/src/platforms/web/adapters/__tests__/WebCredentialsManager.spec.ts index a2bef6f2..9d8c0db0 100644 --- a/src/platforms/web/adapters/__tests__/WebCredentialsManager.spec.ts +++ b/src/platforms/web/adapters/__tests__/WebCredentialsManager.spec.ts @@ -203,13 +203,35 @@ describe('WebCredentialsManager', () => { }); describe('clearCredentials', () => { - it('should call logout with openUrl false', async () => { + it('should call logout with openUrl false when no parameters provided', async () => { await credentialsManager.clearCredentials(); expect(mockSpaClient.logout).toHaveBeenCalledWith({ openUrl: false }); + expect(consoleWarnSpy).not.toHaveBeenCalled(); }); - it('should handle logout errors', async () => { + it('should delegate to clearApiCredentials when audience is provided', async () => { + await credentialsManager.clearCredentials('https://api.example.com'); + + expect(mockSpaClient.logout).not.toHaveBeenCalled(); + 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 delegate to clearApiCredentials with scope when both audience and scope provided', async () => { + await credentialsManager.clearCredentials( + 'https://api.example.com', + 'read:data' + ); + + expect(mockSpaClient.logout).not.toHaveBeenCalled(); + expect(consoleWarnSpy).toHaveBeenCalledWith( + "'clearApiCredentials' for audience https://api.example.com and scope read:data is a no-op on the web. @auth0/auth0-spa-js handles credential storage automatically." + ); + }); + + it('should handle logout errors when clearing all credentials', async () => { const logoutError = new Error('Logout failed'); mockSpaClient.logout.mockRejectedValue(logoutError); @@ -341,12 +363,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." + ); + }); }); }); From 4dccea8ade77cbc987438e080048a2a3a0164a8c Mon Sep 17 00:00:00 2001 From: Subhankar Maiti Date: Wed, 17 Dec 2025 12:43:57 +0530 Subject: [PATCH 4/5] feat: refactor clearCredentials method to remove audience and scope parameters --- EXAMPLES.md | 11 ------ .../java/com/auth0/react/A0Auth0Module.kt | 13 +++---- .../oldarch/com/auth0/react/A0Auth0Spec.kt | 2 +- ios/A0Auth0.mm | 6 ++-- ios/NativeBridge.swift | 12 ++----- src/core/interfaces/ICredentialsManager.ts | 5 +-- .../adapters/NativeCredentialsManager.ts | 4 +-- .../NativeCredentialsManager.spec.ts | 36 +------------------ src/platforms/native/bridge/INativeBridge.ts | 6 +--- .../native/bridge/NativeBridgeManager.ts | 6 ++-- .../web/adapters/WebCredentialsManager.ts | 8 +---- .../__tests__/WebCredentialsManager.spec.ts | 26 ++------------ src/specs/NativeA0Auth0.ts | 9 ++--- 13 files changed, 21 insertions(+), 123 deletions(-) diff --git a/EXAMPLES.md b/EXAMPLES.md index 380aa60f..62a11e86 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -497,11 +497,6 @@ function MyComponent() { await clearApiCredentials('https://first-api.example.com', 'read:data'); }; - const clearSpecificCredentials = async () => { - // You can also use clearCredentials with audience/scope - await clearCredentials('https://first-api.example.com', 'read:data'); - }; - return ( // Your UI components ); @@ -545,12 +540,6 @@ await auth0.credentialsManager.clearApiCredentials( 'https://first-api.example.com', 'read:data write:data' ); - -// Or use clearCredentials with audience/scope -await auth0.credentialsManager.clearCredentials( - 'https://first-api.example.com', - 'read: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 bde1d4ad..9cd830fa 100644 --- a/android/src/main/java/com/auth0/react/A0Auth0Module.kt +++ b/android/src/main/java/com/auth0/react/A0Auth0Module.kt @@ -271,14 +271,9 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0 } @ReactMethod - override fun clearCredentials(audience: String?, scope: String?, promise: Promise) { - if (audience != null) { - // Clear API credentials for specific audience and scope - secureCredentialsManager.clearApiCredentials(audience, scope) - } else { - // Clear all credentials - secureCredentialsManager.clearCredentials() - } + override fun clearCredentials(promise: Promise) { + secureCredentialsManager.clearCredentials() + // Also clear DPoP key if DPoP is enabled if (useDPoP) { try { @@ -288,7 +283,7 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0 android.util.Log.w(NAME, "Failed to clear DPoP key", e) } } - + 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 649db97c..ec052015 100644 --- a/android/src/main/oldarch/com/auth0/react/A0Auth0Spec.kt +++ b/android/src/main/oldarch/com/auth0/react/A0Auth0Spec.kt @@ -49,7 +49,7 @@ abstract class A0Auth0Spec(context: ReactApplicationContext) : ReactContextBaseJ @ReactMethod @DoNotStrip - abstract fun clearCredentials(audience: String?, scope: String?, promise: Promise) + abstract fun clearCredentials(promise: Promise) @ReactMethod @DoNotStrip diff --git a/ios/A0Auth0.mm b/ios/A0Auth0.mm index 6979165a..e107e7c0 100644 --- a/ios/A0Auth0.mm +++ b/ios/A0Auth0.mm @@ -46,11 +46,9 @@ - (dispatch_queue_t)methodQueue } -RCT_EXPORT_METHOD(clearCredentials:(NSString * _Nullable)audience - scope:(NSString * _Nullable)scope - resolve:(RCTPromiseResolveBlock)resolve +RCT_EXPORT_METHOD(clearCredentials:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { - [self.nativeBridge clearCredentialsWithAudience:audience scope:scope resolve:resolve reject:reject]; + [self.nativeBridge clearCredentialsWithResolve:resolve reject:reject]; } diff --git a/ios/NativeBridge.swift b/ios/NativeBridge.swift index 40c8610e..7e6c8f0f 100644 --- a/ios/NativeBridge.swift +++ b/ios/NativeBridge.swift @@ -235,17 +235,9 @@ public class NativeBridge: NSObject { resolve(credentialsManager.canRenew() || credentialsManager.hasValid(minTTL: minTTL)) } - @objc public func clearCredentials(audience: String?, scope: String?, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { - let removed: Bool + @objc public func clearCredentials(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { + let removed = credentialsManager.clear() - if let audience = audience { - // Clear API credentials for specific audience and scope - removed = credentialsManager.clear(forAudience: audience, scope: scope) - } else { - // Clear all credentials - removed = credentialsManager.clear() - } - // Also clear DPoP key if DPoP is enabled if self.useDPoP { do { diff --git a/src/core/interfaces/ICredentialsManager.ts b/src/core/interfaces/ICredentialsManager.ts index 7b3ea8f4..1d7eb138 100644 --- a/src/core/interfaces/ICredentialsManager.ts +++ b/src/core/interfaces/ICredentialsManager.ts @@ -45,13 +45,10 @@ export interface ICredentialsManager { /** * Removes all credentials from the device's storage. - * Optionally filter by audience and scope to clear specific credentials. * - * @param audience Optional audience to clear credentials for. If not provided, clears all credentials. - * @param scope Optional scope to clear. Only applicable when audience is provided. * @returns A promise that resolves when the credentials have been cleared. */ - clearCredentials(audience?: string, scope?: string): Promise; + clearCredentials(): Promise; /** * Obtains session transfer credentials for performing Native to Web SSO. diff --git a/src/platforms/native/adapters/NativeCredentialsManager.ts b/src/platforms/native/adapters/NativeCredentialsManager.ts index 96158d82..dd5f1d61 100644 --- a/src/platforms/native/adapters/NativeCredentialsManager.ts +++ b/src/platforms/native/adapters/NativeCredentialsManager.ts @@ -40,8 +40,8 @@ export class NativeCredentialsManager implements ICredentialsManager { ); } - async clearCredentials(audience?: string, scope?: string): Promise { - return this.handleError(this.bridge.clearCredentials(audience, scope)); + async clearCredentials(): Promise { + return this.handleError(this.bridge.clearCredentials()); } async hasValidCredentials(minTtl?: number): Promise { diff --git a/src/platforms/native/adapters/__tests__/NativeCredentialsManager.spec.ts b/src/platforms/native/adapters/__tests__/NativeCredentialsManager.spec.ts index 347aaff4..81340845 100644 --- a/src/platforms/native/adapters/__tests__/NativeCredentialsManager.spec.ts +++ b/src/platforms/native/adapters/__tests__/NativeCredentialsManager.spec.ts @@ -129,44 +129,10 @@ describe('NativeCredentialsManager', () => { }); describe('clearCredentials', () => { - it('should call the bridge to clear all credentials when no parameters provided', async () => { + it('should call the bridge to clear credentials', async () => { mockBridge.clearCredentials.mockResolvedValueOnce(); await manager.clearCredentials(); expect(mockBridge.clearCredentials).toHaveBeenCalledTimes(1); - expect(mockBridge.clearCredentials).toHaveBeenCalledWith( - undefined, - undefined - ); - }); - - it('should call the bridge to clear credentials for specific audience', async () => { - mockBridge.clearCredentials.mockResolvedValueOnce(); - await manager.clearCredentials('https://api.example.com'); - expect(mockBridge.clearCredentials).toHaveBeenCalledTimes(1); - expect(mockBridge.clearCredentials).toHaveBeenCalledWith( - 'https://api.example.com', - undefined - ); - }); - - it('should call the bridge to clear credentials for specific audience and scope', async () => { - mockBridge.clearCredentials.mockResolvedValueOnce(); - await manager.clearCredentials('https://api.example.com', 'read:data'); - expect(mockBridge.clearCredentials).toHaveBeenCalledTimes(1); - expect(mockBridge.clearCredentials).toHaveBeenCalledWith( - 'https://api.example.com', - 'read:data' - ); - }); - - it('should propagate errors from the bridge', async () => { - const clearError = new AuthError('CLEAR_FAILED', 'Failed to clear', { - code: 'CLEAR_FAILED', - }); - mockBridge.clearCredentials.mockRejectedValueOnce(clearError); - await expect(manager.clearCredentials()).rejects.toThrow( - CredentialsManagerError - ); }); }); diff --git a/src/platforms/native/bridge/INativeBridge.ts b/src/platforms/native/bridge/INativeBridge.ts index 87da3ec0..3a857a87 100644 --- a/src/platforms/native/bridge/INativeBridge.ts +++ b/src/platforms/native/bridge/INativeBridge.ts @@ -134,12 +134,8 @@ export interface INativeBridge { /** * Clears credentials from secure storage. - * Optionally filter by audience and scope to clear specific credentials. - * - * @param audience Optional audience to clear credentials for. If not provided, clears all credentials. - * @param scope Optional scope to clear. Only applicable when audience is provided. */ - clearCredentials(audience?: string, scope?: string): Promise; + clearCredentials(): Promise; /** * Resumes the web authentication flow with the provided URL. diff --git a/src/platforms/native/bridge/NativeBridgeManager.ts b/src/platforms/native/bridge/NativeBridgeManager.ts index 3ec660e9..a1e36643 100644 --- a/src/platforms/native/bridge/NativeBridgeManager.ts +++ b/src/platforms/native/bridge/NativeBridgeManager.ts @@ -180,11 +180,9 @@ export class NativeBridgeManager implements INativeBridge { ); } - async clearCredentials(audience?: string, scope?: string): Promise { + async clearCredentials(): Promise { return this.a0_call( - Auth0NativeModule.clearCredentials.bind(Auth0NativeModule), - audience, - scope + Auth0NativeModule.clearCredentials.bind(Auth0NativeModule) ); } diff --git a/src/platforms/web/adapters/WebCredentialsManager.ts b/src/platforms/web/adapters/WebCredentialsManager.ts index 5892f7ac..770edcec 100644 --- a/src/platforms/web/adapters/WebCredentialsManager.ts +++ b/src/platforms/web/adapters/WebCredentialsManager.ts @@ -97,14 +97,8 @@ export class WebCredentialsManager implements ICredentialsManager { return this.client.isAuthenticated(); } - async clearCredentials(audience?: string, scope?: string): Promise { + async clearCredentials(): Promise { try { - // For web, if audience is provided, delegate to clearApiCredentials - if (audience) { - return this.clearApiCredentials(audience, scope); - } - - // Otherwise, clear all credentials await this.client.logout({ openUrl: false }); } catch (e: any) { const code = e.error ?? 'ClearCredentialsFailed'; diff --git a/src/platforms/web/adapters/__tests__/WebCredentialsManager.spec.ts b/src/platforms/web/adapters/__tests__/WebCredentialsManager.spec.ts index 9d8c0db0..a540c9d0 100644 --- a/src/platforms/web/adapters/__tests__/WebCredentialsManager.spec.ts +++ b/src/platforms/web/adapters/__tests__/WebCredentialsManager.spec.ts @@ -203,35 +203,13 @@ describe('WebCredentialsManager', () => { }); describe('clearCredentials', () => { - it('should call logout with openUrl false when no parameters provided', async () => { + it('should call logout with openUrl false', async () => { await credentialsManager.clearCredentials(); expect(mockSpaClient.logout).toHaveBeenCalledWith({ openUrl: false }); - expect(consoleWarnSpy).not.toHaveBeenCalled(); }); - it('should delegate to clearApiCredentials when audience is provided', async () => { - await credentialsManager.clearCredentials('https://api.example.com'); - - expect(mockSpaClient.logout).not.toHaveBeenCalled(); - 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 delegate to clearApiCredentials with scope when both audience and scope provided', async () => { - await credentialsManager.clearCredentials( - 'https://api.example.com', - 'read:data' - ); - - expect(mockSpaClient.logout).not.toHaveBeenCalled(); - expect(consoleWarnSpy).toHaveBeenCalledWith( - "'clearApiCredentials' for audience https://api.example.com and scope read:data is a no-op on the web. @auth0/auth0-spa-js handles credential storage automatically." - ); - }); - - it('should handle logout errors when clearing all credentials', async () => { + it('should handle logout errors', async () => { const logoutError = new Error('Logout failed'); mockSpaClient.logout.mockRejectedValue(logoutError); diff --git a/src/specs/NativeA0Auth0.ts b/src/specs/NativeA0Auth0.ts index 828ea828..575d3bcc 100644 --- a/src/specs/NativeA0Auth0.ts +++ b/src/specs/NativeA0Auth0.ts @@ -50,14 +50,9 @@ export interface Spec extends TurboModule { hasValidCredentials(minTTL: Int32): Promise; /** - * Clear credentials. Optionally filter by audience and scope. - * @param audience The audience to clear credentials for. If not provided, clears all credentials. - * @param scope The scope to clear credentials for. Only applicable when audience is provided. + * Clear credentials */ - clearCredentials( - audience: string | undefined, - scope: string | undefined - ): Promise; + clearCredentials(): Promise; /** * Get API credentials for a specific audience From c18120016bb1c9a8ea145cf7279cbd9d3457b1eb Mon Sep 17 00:00:00 2001 From: Subhankar Maiti Date: Wed, 17 Dec 2025 12:59:03 +0530 Subject: [PATCH 5/5] feat: update clearApiCredentials method documentation to recommend passing scope when clearing credentials --- src/core/interfaces/ICredentialsManager.ts | 4 ++-- src/platforms/native/bridge/INativeBridge.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/interfaces/ICredentialsManager.ts b/src/core/interfaces/ICredentialsManager.ts index 1d7eb138..936f1872 100644 --- a/src/core/interfaces/ICredentialsManager.ts +++ b/src/core/interfaces/ICredentialsManager.ts @@ -142,7 +142,7 @@ export interface ICredentialsManager { * `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 not provided, clears all credentials for the audience. + * @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. * @@ -151,7 +151,7 @@ export interface ICredentialsManager { * // Clear all credentials for an audience * await credentialsManager.clearApiCredentials('https://api.example.com'); * - * // Clear credentials for specific scope + * // Clear credentials for specific scope (recommended) * await credentialsManager.clearApiCredentials('https://api.example.com', 'read:data'); * ``` */ diff --git a/src/platforms/native/bridge/INativeBridge.ts b/src/platforms/native/bridge/INativeBridge.ts index 3a857a87..7a62ccdf 100644 --- a/src/platforms/native/bridge/INativeBridge.ts +++ b/src/platforms/native/bridge/INativeBridge.ts @@ -128,7 +128,7 @@ export interface INativeBridge { * 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 not provided, clears all credentials for the audience. + * @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;