-
Notifications
You must be signed in to change notification settings - Fork 233
feat: added support for MRRT on Android/iOS #1258
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
…0Client and WebCredentialsManager
…into SDK-6263_mrrt_support
This reverts commit 20ea213.
…oP key management
…n across platforms
…into feat/dpop-support
…te auth0 dependency to 3.10.0
…ate URL format in NativeBridge
…dance in FAQ and examples
…into feat/dpop-support
…tive-auth0 into SDK-6263_mrrt_support
…into SDK-6263_mrrt_support
… and documentation
…into SDK-6263_mrrt_support
…Credentials methods
|
Claude finished @subhankarmaiti's task —— View job Code Review Progress
Overall Assessment❌ This PR implements MRRT (Multi-Resource Refresh Token) support for Android and iOS, but has several issues that should be addressed before merging: Critical Issues:
Major Issues:
Minor Issues:
The implementation is well-structured with good test coverage and comprehensive documentation. Once the critical token expiration issue is fixed and error handling is made consistent, this will be a solid feature addition. |
| map.putString(TOKEN_TYPE_KEY, credentials.type) | ||
| return map | ||
| } | ||
| } No newline at end of file |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing newline at end of file
The file should end with a newline character for POSIX compliance and consistency with the rest of the codebase.
| } | |
| } | |
| } |
| override fun clearApiCredentials(audience: String, promise: Promise) { | ||
| secureCredentialsManager.clearApiCredentials(audience) | ||
| promise.resolve(true) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing blank line before closing brace
For consistency with the rest of the class structure, there should be a blank line before the closing method brace and the next method/property.
| } | |
| secureCredentialsManager.clearApiCredentials(audience) | |
| promise.resolve(true) | |
| } | |
| } | ||
| } | ||
|
|
||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inconsistent blank line spacing
There's an extra blank line here that's inconsistent with the rest of the file. Remove one blank line to match the spacing pattern used elsewhere in the class.
| @objc public func clearApiCredentials(audience: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { |
| const claims = await this.client.getIdTokenClaims(); | ||
| if (!claims || !claims.exp) { | ||
| throw new AuthError( | ||
| 'IdTokenMissing', | ||
| 'ID token or expiration claim is missing.' | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Incorrect token expiration used in getApiCredentials
The code uses the ID token's expiration (claims.exp) as the expiresAt value for API credentials, but API credentials should use the access token's expiration, not the ID token's. These expiration times can be different, and using the wrong one could cause:
- Premature token refresh if ID token expires before access token
- Using expired access tokens if ID token expires after access token
- Incorrect
isExpired()checks when callingapiCredentials.isExpired()
The native implementations correctly use the access token's expiration from the APICredentials object. The web implementation should calculate the access token expiration from tokenResponse or use a different approach to get the actual access token expiration.
Note: The @auth0/auth0-spa-js library's getTokenSilently doesn't directly provide the access token expiration in the response. You may need to:
- Decode the access token if it's a JWT to get its expiration
- Or request this feature from the
@auth0/auth0-spa-jslibrary - Or document this limitation in the web platform implementation
| <Header title="Class-Based Profile & Credentials" /> | ||
| <ScrollView contentContainerStyle={styles.content}> | ||
| <UserInfo user={user} /> | ||
| <Result title="Last Action Result" result={result} error={error} /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing error handling for getCredentials
The runTest wrapper catches errors, but calling getCredentials() without parameters may not provide the necessary context. When testing getCredentials(), consider passing the scope and audience parameters that were used during login to ensure the test accurately reflects real-world usage. This is especially important for MRRT support where audience matters.
| 'refreshToken' in result | ||
| ) { | ||
| const token = (result as Credentials).refreshToken; | ||
| if (token) { | ||
| runTest( | ||
| () => revokeRefreshToken({ refreshToken: token }), | ||
| 'Revoke Refresh Token' | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unsafe type narrowing and null checking
The type checking logic here is fragile:
- It doesn't verify that
resultis actually aCredentialsobject with a validrefreshToken - The
tokencould beundefinedeven after the check - The disabled state doesn't account for an empty string token
| 'refreshToken' in result | |
| ) { | |
| const token = (result as Credentials).refreshToken; | |
| if (token) { | |
| runTest( | |
| () => revokeRefreshToken({ refreshToken: token }), | |
| 'Revoke Refresh Token' | |
| ); | |
| } | |
| onPress={() => { | |
| if ( | |
| typeof result === 'object' && | |
| result && | |
| 'refreshToken' in result && | |
| result.refreshToken | |
| ) { | |
| runTest( | |
| () => revokeRefreshToken({ refreshToken: result.refreshToken! }), | |
| 'Revoke Refresh Token' | |
| ); | |
| } | |
| }} | |
| disabled={ | |
| !( | |
| typeof result === 'object' && | |
| result && | |
| 'refreshToken' in result && | |
| result.refreshToken | |
| ) | |
| } |
| async clearCredentials(): Promise<void> { | ||
| await this.handleError(this.bridge.clearCredentials()); | ||
| // Also clear the DPoP key when clearing credentials | ||
| await this.handleError(this.bridge.clearDPoPKey()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential error in clearCredentials: DPoP key clearing may fail silently
The clearCredentials method attempts to clear the DPoP key, but if clearDPoPKey() fails, the entire operation fails. This could be problematic because:
- The credentials might have already been cleared before the DPoP key clearing fails
- The user would see an error even though their primary credentials were cleared successfully
- This creates an inconsistent state
Consider handling the DPoP key clearing error more gracefully, similar to how it's done in the native Android/iOS code where DPoP clearing errors are logged but don't fail the operation.
| await this.handleError(this.bridge.clearDPoPKey()); | |
| await this.handleError(this.bridge.clearCredentials()); | |
| // Also clear the DPoP key when clearing credentials | |
| // If DPoP key clearing fails, log it but don't fail the operation | |
| try { | |
| await this.bridge.clearDPoPKey(); | |
| } catch (e) { | |
| // Log the error but don't propagate it | |
| console.warn('Failed to clear DPoP key:', e); | |
| } |
| parameters?: Record<string, any> | ||
| ): Promise<ApiCredentials> { | ||
| const nativeCredentials = await this.handleError( | ||
| this.bridge.getApiCredentials(audience, scope, 0, parameters) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing minTtl parameter in getApiCredentials
The getApiCredentials method always passes minTtl: 0 to the bridge, ignoring the typical pattern used in getCredentials. This means:
- The method will always return cached tokens even if they're about to expire
- Users cannot specify a minimum TTL for API credentials
- This is inconsistent with the
getCredentialsmethod which accepts aminTtlparameter
Consider adding a minTtl parameter to getApiCredentials for consistency and flexibility:
| this.bridge.getApiCredentials(audience, scope, 0, parameters) | |
| async getApiCredentials( | |
| audience: string, | |
| scope?: string, | |
| minTtl?: number, | |
| parameters?: Record<string, any> | |
| ): Promise<ApiCredentials> { | |
| const nativeCredentials = await this.handleError( | |
| this.bridge.getApiCredentials(audience, scope, minTtl ?? 0, parameters) | |
| ); |
Note: This would also require updating the interface definition in ICredentialsManager.ts and all other implementations.
| scope, | ||
| minTtl.toInt(), | ||
| cleanedParameters, | ||
| emptyMap(), // headers not supported from JS yet |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Empty headers map comment is misleading
The comment says "headers not supported from JS yet", but headers are not being passed because the React Native bridge doesn't expose this parameter. Consider either:
- Adding header support if it's a planned feature
- Or updating the comment to clarify this is by design for security reasons (headers from JS could pose security risks)
| async getApiCredentials( | ||
| audience: string, | ||
| scope?: string, | ||
| parameters?: Record<string, any> | ||
| ): Promise<ApiCredentials> { | ||
| try { | ||
| const tokenResponse = await this.client.getTokenSilently({ | ||
| authorizationParams: { | ||
| ...parameters, | ||
| audience: audience, | ||
| scope: scope, | ||
| }, | ||
| detailedResponse: true, | ||
| }); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inconsistent parameter handling between getCredentials and getApiCredentials
In getCredentials (line 28), parameters are spread first: { ...parameters, scope }, but in getApiCredentials (line 67), audience and scope come first: { ...parameters, audience, scope }.
The spread order matters - properties defined after the spread will override properties in the spread object. For consistency and to allow parameter overrides, consider using the same pattern in both methods:
| async getApiCredentials( | |
| audience: string, | |
| scope?: string, | |
| parameters?: Record<string, any> | |
| ): Promise<ApiCredentials> { | |
| try { | |
| const tokenResponse = await this.client.getTokenSilently({ | |
| authorizationParams: { | |
| ...parameters, | |
| audience: audience, | |
| scope: scope, | |
| }, | |
| detailedResponse: true, | |
| }); | |
| async getApiCredentials( | |
| audience: string, | |
| scope?: string, | |
| parameters?: Record<string, any> | |
| ): Promise<ApiCredentials> { | |
| try { | |
| const tokenResponse = await this.client.getTokenSilently({ | |
| authorizationParams: { | |
| ...parameters, | |
| audience, | |
| scope, | |
| }, | |
| detailedResponse: true, | |
| }); |
This ensures that explicit audience/scope parameters always take precedence over any values in the parameters object.
| onPress={() => | ||
| runTest( | ||
| () => getApiCredentials(audience, scope), | ||
| 'Get API Credentials' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing scope parameter in getApiCredentials call
The test passes both audience and scope to getApiCredentials, but based on the FAQ and best practices, scope should match what was requested during login. Consider adding a comment or validation to ensure users understand that the scope must be compatible with the API's configuration.
…into SDK-6263_mrrt_support
…into SDK-6263_mrrt_support
…into SDK-6263_mrrt_support
No description provided.