Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/actions/rl-scanner/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ runs:
pip install boto3 requests

- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # pin@v4.3.0
uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # pin@v4.3.0
with:
role-to-assume: ${{ env.PRODSEC_TOOLS_ARN }}
aws-region: 'us-east-1'
Expand Down
2 changes: 1 addition & 1 deletion .github/actions/setup-darwin/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ runs:
shell: bash

- name: Set up Ruby
uses: ruby/setup-ruby@3ff19f5e2baf30647122352b96108b1fbe250c64 # pin@v1.299.0
uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # pin@v1.300.0
with:
ruby-version: ${{ inputs.ruby }}
bundler-cache: true
Expand Down
2 changes: 1 addition & 1 deletion .github/actions/setup-publish/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ runs:

steps:
- name: Install Flutter
uses: subosito/flutter-action@0ca7a949e71ae44c8e688a51c5e7e93b2c87e295 # pin@v2.22.0
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # pin@v2.23.0
with:
flutter-version: ${{ inputs.flutter }}
channel: stable
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -556,28 +556,28 @@ jobs:
path: coverage/windows

- name: Upload coverage report for auth0_flutter
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2
with:
name: Auth0 Flutter
flags: auth0_flutter
directory: coverage/auth0_flutter

- name: Upload coverage report for auth0_flutter_platform_interface
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2
with:
name: Auth0 Flutter
flags: auth0_flutter_platform_interface
directory: coverage/auth0_flutter_platform_interface

- name: Upload coverage report for iOS
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2
with:
name: Auth0 Flutter
flags: auth0_flutter_ios
directory: coverage/ios

- name: Upload coverage report for Android
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2
with:
name: Auth0 Flutter
flags: auth0_flutter_android
Expand Down
13 changes: 13 additions & 0 deletions .github/workflows/sca_scan.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: SCA

on:
pull_request:
branches: ["main"]
workflow_dispatch:

jobs:
snyk-cli:
uses: auth0/devsecops-tooling/.github/workflows/sca-scan.yml@main
with:
java-version: "17"
secrets: inherit
12 changes: 6 additions & 6 deletions appium-test/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 35 additions & 2 deletions auth0_flutter/EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -382,10 +382,16 @@ try {
final credentials = await auth0.webAuthentication().login();
// ...
} on WebAuthenticationException catch (e) {
print(e);
if (e.isRetryable) {
// Transient error (e.g. network issue) — safe to retry
} else {
print(e);
}
}
```

The `isRetryable` property indicates whether the error is transient (e.g. a network outage) and the operation can be retried.

</details>

<details>
Expand Down Expand Up @@ -750,6 +756,27 @@ print(e);
}
```

#### Retryable errors

The `isRetryable` property on `CredentialsManagerException` indicates whether the error is transient and the operation can be retried. When `true`, the failure is likely due to a temporary condition such as a network outage. When `false`, the failure is permanent (e.g. an invalid refresh token) and retrying will not help — you should log the user out instead.

```dart
try {
final credentials = await auth0.credentialsManager.credentials();
// ...
} on CredentialsManagerException catch (e) {
if (e.isRetryable) {
// Transient error (e.g. network issue) — safe to retry
print("Temporary error, retrying...");
} else {
// Permanent error — log the user out
print("Credentials cannot be renewed: ${e.message}");
}
}
```

> The `isRetryable` property is available on all exception types (`CredentialsManagerException`, `ApiException`, `WebAuthenticationException`) across Android and iOS/macOS. It returns `true` when the underlying failure is network-related, indicating the operation may succeed on retry.

### Native to Web SSO

Native to Web SSO allows authenticated users in your native mobile application to seamlessly transition to your web application without requiring them to log in again. This is achieved by exchanging a refresh token for a Session Transfer Token, which can then be used to establish a session in the web application.
Expand Down Expand Up @@ -1121,10 +1148,16 @@ try {
connectionOrRealm: connection);
// ...
} on ApiException catch (e) {
print(e);
if (e.isRetryable) {
// Transient error (e.g. network issue) — safe to retry
} else {
print(e);
}
}
```

The `isRetryable` property indicates whether the error is transient (e.g. a network outage) and the operation can be retried. It returns `true` when `isNetworkError` is `true`.

[Go up ⤴](#examples)

## 🌐📱 Organizations
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.auth0.auth0_flutter

import com.auth0.android.authentication.AuthenticationException
import com.auth0.android.authentication.storage.CredentialsManagerException

fun CredentialsManagerException.toMap(): Map<String, Any> {
val exceptionCause = this.cause
val isRetryable = when (exceptionCause) {
is AuthenticationException -> exceptionCause.isNetworkError
else -> false
}

val map = mutableMapOf<String, Any>("_isRetryable" to isRetryable)
if (exceptionCause != null) {
map["cause"] = exceptionCause.toString()
map["causeStackTrace"] = exceptionCause.stackTraceToString()
}
return map
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class GetCredentialsRequestHandler : CredentialsManagerRequestHandler {
credentialsManager.getCredentials(scope, minTtl, parameters, object:
Callback<Credentials, CredentialsManagerException> {
override fun onFailure(exception: CredentialsManagerException) {
result.error(exception.message ?: "UNKNOWN ERROR", exception.message, exception)
result.error(exception.message ?: "UNKNOWN ERROR", exception.message, exception.toMap())
}

override fun onSuccess(credentials: Credentials) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.auth0.android.authentication.storage.SecureCredentialsManager
import com.auth0.android.callback.Callback
import com.auth0.android.result.SSOCredentials
import com.auth0.auth0_flutter.request_handlers.MethodCallRequest
import com.auth0.auth0_flutter.toMap
import io.flutter.plugin.common.MethodChannel

class GetSSOCredentialsRequestHandler : CredentialsManagerRequestHandler {
Expand All @@ -22,7 +23,7 @@ class GetSSOCredentialsRequestHandler : CredentialsManagerRequestHandler {
credentialsManager.getSsoCredentials(parameters, object :
Callback<SSOCredentials, CredentialsManagerException> {
override fun onFailure(exception: CredentialsManagerException) {
result.error(exception.message ?: "UNKNOWN ERROR", exception.message, exception)
result.error(exception.message ?: "UNKNOWN ERROR", exception.message, exception.toMap())
}

override fun onSuccess(credentials: SSOCredentials) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class RenewCredentialsRequestHandler : CredentialsManagerRequestHandler {
credentialsManager.getCredentials(null, 0, parameters, true, object :
Callback<Credentials, CredentialsManagerException> {
override fun onFailure(exception: CredentialsManagerException) {
result.error(exception.message ?: "UNKNOWN ERROR", exception.message, exception)
result.error(exception.message ?: "UNKNOWN ERROR", exception.message, exception.toMap())
}

override fun onSuccess(credentials: Credentials) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,12 @@ class LoginWebAuthRequestHandler(

builder.start(context, object : Callback<Credentials, AuthenticationException> {
override fun onFailure(exception: AuthenticationException) {
result.error(exception.getCode(), exception.getDescription(), exception)
val details = mutableMapOf<String, Any>("_isRetryable" to exception.isNetworkError)
exception.cause?.let {
details["cause"] = it.toString()
details["causeStackTrace"] = it.stackTraceToString()
}
result.error(exception.getCode(), exception.getDescription(), details)
}

override fun onSuccess(credentials: Credentials) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,12 @@ class LogoutWebAuthRequestHandler(private val builderResolver: (MethodCallReques

builder.start(context, object : Callback<Void?, AuthenticationException> {
override fun onFailure(exception: AuthenticationException) {
result.error(exception.getCode(), exception.getDescription(), exception)
val details = mutableMapOf<String, Any>("_isRetryable" to exception.isNetworkError)
exception.cause?.let {
details["cause"] = it.toString()
details["causeStackTrace"] = it.stackTraceToString()
}
result.error(exception.getCode(), exception.getDescription(), details)
}

override fun onSuccess(res: Void?) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package com.auth0.auth0_flutter

import com.auth0.android.authentication.AuthenticationException
import com.auth0.android.authentication.storage.CredentialsManagerException
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.`when`
import org.mockito.Mockito.mock
import org.robolectric.RobolectricTestRunner

@RunWith(RobolectricTestRunner::class)
class CredentialsManagerExceptionExtensionsTest {

@Test
fun `should set isRetryable to false when cause is null`() {
val exception = mock(CredentialsManagerException::class.java)
`when`(exception.cause).thenReturn(null)

val map = exception.toMap()

assertThat(map["_isRetryable"], equalTo(false))
}

@Test
fun `should set isRetryable to true when cause is a network AuthenticationException`() {
val authException = mock(AuthenticationException::class.java)
`when`(authException.isNetworkError).thenReturn(true)

val exception = mock(CredentialsManagerException::class.java)
`when`(exception.cause).thenReturn(authException)

val map = exception.toMap()

assertThat(map["_isRetryable"], equalTo(true))
}

@Test
fun `should set isRetryable to false when cause is a non-network AuthenticationException`() {
val authException = mock(AuthenticationException::class.java)
`when`(authException.isNetworkError).thenReturn(false)

val exception = mock(CredentialsManagerException::class.java)
`when`(exception.cause).thenReturn(authException)

val map = exception.toMap()

assertThat(map["_isRetryable"], equalTo(false))
}

@Test
fun `should set isRetryable to false when cause is a generic exception`() {
val exception = mock(CredentialsManagerException::class.java)
`when`(exception.cause).thenReturn(RuntimeException("generic error"))

val map = exception.toMap()

assertThat(map["_isRetryable"], equalTo(false))
}

@Test
fun `should include cause in map when cause is present`() {
val cause = RuntimeException("network error")
val exception = mock(CredentialsManagerException::class.java)
`when`(exception.cause).thenReturn(cause)

val map = exception.toMap()

assertThat(map["cause"], equalTo(cause.toString()))
}

@Test
fun `should include causeStackTrace in map when cause is present`() {
val cause = RuntimeException("network error")
val exception = mock(CredentialsManagerException::class.java)
`when`(exception.cause).thenReturn(cause)

val map = exception.toMap()

assertThat(map["causeStackTrace"], equalTo(cause.stackTraceToString()))
}

@Test
fun `should not include cause or causeStackTrace in map when cause is null`() {
val exception = mock(CredentialsManagerException::class.java)
`when`(exception.cause).thenReturn(null)

val map = exception.toMap()

assertThat(map.containsKey("cause"), equalTo(false))
assertThat(map.containsKey("causeStackTrace"), equalTo(false))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,36 @@ class LoginWebAuthRequestHandlerTest {
val mockRequest = MethodCallRequest(mockAccount, hashMapOf<String, Any>())
handler.handle(mock(), mockRequest, mockResult)

verify(mockResult).error("code", "description", exception)
verify(mockResult).error(eq("code"), eq("description"), eq(mapOf("_isRetryable" to false)))
}

@Test
fun `returns cause and causeStackTrace in error details when cause is present`() {
val builder = mock<WebAuthProvider.Builder>()
val mockResult = mock<Result>()
val cause = RuntimeException("network error")
val exception = mock<AuthenticationException>()
whenever(exception.getCode()).thenReturn("code")
whenever(exception.getDescription()).thenReturn("description")
whenever(exception.isNetworkError).thenReturn(true)
whenever(exception.cause).thenReturn(cause)

doAnswer { invocation ->
val cb = invocation.getArgument<Callback<Credentials, AuthenticationException>>(1)
cb.onFailure(exception)
}.`when`(builder).start(any(), any())

val handler = LoginWebAuthRequestHandler { _ -> builder }
val mockAccount = mock<Auth0>()
val mockRequest = MethodCallRequest(mockAccount, hashMapOf<String, Any>())
handler.handle(mock(), mockRequest, mockResult)

verify(mockResult).error(eq("code"), eq("description"), check {
val map = it as Map<*, *>
assertThat(map["_isRetryable"], equalTo(true))
assertThat(map["cause"], equalTo(cause.toString()))
assertThat(map["causeStackTrace"], equalTo(cause.stackTraceToString()))
})
}

@Test
Expand Down
Loading
Loading