Skip to content
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

API support for fetching session token #802

Merged
merged 3 commits into from
Feb 24, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.auth0.android.result.Credentials
import com.auth0.android.result.DatabaseUser
import com.auth0.android.result.PasskeyChallenge
import com.auth0.android.result.PasskeyRegistrationChallenge
import com.auth0.android.result.SSOCredentials
import com.auth0.android.result.UserProfile
import com.google.gson.Gson
import okhttp3.HttpUrl.Companion.toHttpUrl
Expand Down Expand Up @@ -921,6 +922,44 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
return factory.get(url.toString(), jwksAdapter)
}

/**
* Creates a new request to fetch a session token in exchange for a refresh token.
*
* @param refreshToken A valid refresh token obtained as part of Auth0 authentication
* @return a request to fetch a session token
*/
public fun fetchSessionToken(refreshToken: String): Request<SSOCredentials, AuthenticationException> {
val params = ParameterBuilder.newBuilder()
.setClientId(clientId)
.setGrantType(ParameterBuilder.GRANT_TYPE_TOKEN_EXCHANGE)
.set(SUBJECT_TOKEN_KEY, refreshToken)
.set(SUBJECT_TOKEN_TYPE_KEY, ParameterBuilder.TOKEN_TYPE_REFRESH_TOKEN)
.set(REQUESTED_TOKEN_TYPE_KEY, ParameterBuilder.TOKEN_TYPE_SESSION_TOKEN)
.asDictionary()
return loginWithTokenGeneric<SSOCredentials>(params)
}

/**
* Helper function to make a request to the /oauth/token endpoint with a custom response type.
*/
private inline fun <reified T> loginWithTokenGeneric(parameters: Map<String, String>): Request<T,AuthenticationException> {
val url = auth0.getDomainUrl().toHttpUrl().newBuilder()
.addPathSegment(OAUTH_PATH)
.addPathSegment(TOKEN_PATH)
.build()
val requestParameters =
ParameterBuilder.newBuilder()
.setClientId(clientId)
.addAll(parameters)
.asDictionary()
val adapter: JsonAdapter<T> = GsonAdapter(
T::class.java, gson
)
val request = factory.post(url.toString(), adapter)
request.addParameters(requestParameters)
return request
}

/**
* Helper function to make a request to the /oauth/token endpoint.
*/
Expand Down Expand Up @@ -989,6 +1028,7 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
private const val RECOVERY_CODE_KEY = "recovery_code"
private const val SUBJECT_TOKEN_KEY = "subject_token"
private const val SUBJECT_TOKEN_TYPE_KEY = "subject_token_type"
private const val REQUESTED_TOKEN_TYPE_KEY = "requested_token_type"
private const val USER_METADATA_KEY = "user_metadata"
private const val AUTH_SESSION_KEY = "auth_session"
private const val AUTH_RESPONSE_KEY = "authn_response"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ public class ParameterBuilder private constructor(parameters: Map<String, String
public const val GRANT_TYPE_TOKEN_EXCHANGE: String =
"urn:ietf:params:oauth:grant-type:token-exchange"
public const val GRANT_TYPE_PASSKEY :String = "urn:okta:params:oauth:grant-type:webauthn"
public const val TOKEN_TYPE_REFRESH_TOKEN :String = "urn:ietf:params:oauth:token-type:refresh_token"
public const val TOKEN_TYPE_SESSION_TOKEN :String = "urn:auth0:params:oauth:token-type:session_token"
public const val SCOPE_OPENID: String = "openid"
public const val SCOPE_OFFLINE_ACCESS: String = "openid offline_access"
public const val SCOPE_KEY: String = "scope"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.annotation.VisibleForTesting
import com.auth0.android.authentication.AuthenticationAPIClient
import com.auth0.android.callback.Callback
import com.auth0.android.result.Credentials
import com.auth0.android.result.SSOCredentials
import com.auth0.android.util.Clock
import java.util.*

Expand All @@ -29,7 +30,9 @@ public abstract class BaseCredentialsManager internal constructor(

@Throws(CredentialsManagerException::class)
public abstract fun saveCredentials(credentials: Credentials)
public abstract fun saveSsoCredentials(ssoCredentials: SSOCredentials)
Copy link
Contributor

@Widcket Widcket Feb 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this method is needed. We're already providing a method for getting fresh SSOCredentials, which automaticaly saves the refresh token.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same functionality can also be achieved by simply retrieving the credentials, replacing the refresh token, and saving them back.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the case we have it for internal code reuse, then we should not have it be public.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this method for the scenario where we get a new refresh token when calling the API directly from the AuthenticationAPI class. The user would have to call this to replace the existing one else the following getSsoCredentials call will fail.

public abstract fun getCredentials(callback: Callback<Credentials, CredentialsManagerException>)
public abstract fun getSsoCredentials(callback: Callback<SSOCredentials, CredentialsManagerException>)
public abstract fun getCredentials(
scope: String?,
minTtl: Int,
Expand Down Expand Up @@ -60,6 +63,10 @@ public abstract class BaseCredentialsManager internal constructor(
callback: Callback<Credentials, CredentialsManagerException>
)

@JvmSynthetic
@Throws(CredentialsManagerException::class)
public abstract suspend fun awaitSsoCredentials(): SSOCredentials

@JvmSynthetic
@Throws(CredentialsManagerException::class)
public abstract suspend fun awaitCredentials(): Credentials
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.auth0.android.authentication.AuthenticationAPIClient
import com.auth0.android.authentication.AuthenticationException
import com.auth0.android.callback.Callback
import com.auth0.android.result.Credentials
import com.auth0.android.result.SSOCredentials
import kotlinx.coroutines.suspendCancellableCoroutine
import java.util.*
import java.util.concurrent.Executor
Expand Down Expand Up @@ -53,6 +54,85 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
storage.store(LEGACY_KEY_CACHE_EXPIRES_AT, credentials.expiresAt.time)
}


/**
* Stores the given [SSOCredentials] refresh token in the storage.
* This method must be called if the SSOCredentials are obtained by directly invoking [AuthenticationAPIClient.fetchSessionToken] api and
* [rotating refresh token](https://auth0.com/docs/secure/tokens/refresh-tokens/refresh-token-rotation) are enabled for
* the client. Method will silently return ,if the passed credentials has no refresh token.
*
* @param ssoCredentials the credentials to save in the storage.
*/
override fun saveSsoCredentials(ssoCredentials: SSOCredentials) {
if (ssoCredentials.refreshToken.isNullOrEmpty())
return // No refresh token to save
serialExecutor.execute {
val existingRefreshToken = storage.retrieveString(KEY_REFRESH_TOKEN)
// Checking if the existing one needs to be replaced with the new one
if (ssoCredentials.refreshToken == existingRefreshToken)
return@execute
storage.store(KEY_REFRESH_TOKEN, ssoCredentials.refreshToken)
}
}

/**
* Fetches a new [SSOCredentials] . It will fail with [CredentialsManagerException]
* if the existing refresh_token is null or no longer valid. This method will handle saving the refresh_token,
* if a new one is issued
*/
override fun getSsoCredentials(callback: Callback<SSOCredentials, CredentialsManagerException>) {
serialExecutor.execute {
val refreshToken = storage.retrieveString(KEY_REFRESH_TOKEN)
if (refreshToken.isNullOrEmpty()) {
callback.onFailure(CredentialsManagerException.NO_REFRESH_TOKEN)
return@execute
}

try {
val sessionCredentials =
authenticationClient.fetchSessionToken(refreshToken)
.execute()
saveSsoCredentials(sessionCredentials)
callback.onSuccess(sessionCredentials)
} catch (error: AuthenticationException) {
val exception = when {
error.isRefreshTokenDeleted ||
error.isInvalidRefreshToken -> CredentialsManagerException.Code.RENEW_FAILED

error.isNetworkError -> CredentialsManagerException.Code.NO_NETWORK
else -> CredentialsManagerException.Code.API_ERROR
}
callback.onFailure(
CredentialsManagerException(
exception,
error
)
)
}
}
}

/**
* Fetches a new [SSOCredentials] . It will fail with [CredentialsManagerException]
* if the existing refresh_token is null or no longer valid. This method will handle saving the refresh_token,
* if a new one is issued
*/
@JvmSynthetic
@Throws(CredentialsManagerException::class)
override suspend fun awaitSsoCredentials(): SSOCredentials {
return suspendCancellableCoroutine { continuation ->
getSsoCredentials(object : Callback<SSOCredentials, CredentialsManagerException> {
override fun onSuccess(result: SSOCredentials) {
continuation.resume(result)
}

override fun onFailure(error: CredentialsManagerException) {
continuation.resumeWithException(error)
}
})
}
}

/**
* Retrieves the credentials from the storage and refresh them if they have already expired.
* It will throw [CredentialsManagerException] if the saved access_token or id_token is null,
Expand Down Expand Up @@ -144,8 +224,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
forceRefresh: Boolean
): Credentials {
return suspendCancellableCoroutine { continuation ->
getCredentials(
scope,
getCredentials(scope,
minTtl,
parameters,
headers,
Expand Down Expand Up @@ -299,8 +378,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
if (willAccessTokenExpire) {
val tokenLifetime = (expiresAt - currentTimeInMillis - minTtl * 1000) / -1000
val wrongTtlException = CredentialsManagerException(
CredentialsManagerException.Code.LARGE_MIN_TTL,
String.format(
CredentialsManagerException.Code.LARGE_MIN_TTL, String.format(
Locale.getDefault(),
"The lifetime of the renewed Access Token (%d) is less than the minTTL requested (%d). Increase the 'Token Expiration' setting of your Auth0 API in the dashboard, or request a lower minTTL.",
tokenLifetime,
Expand All @@ -326,15 +404,14 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
callback.onSuccess(credentials)
} catch (error: AuthenticationException) {
val exception = when {
error.isRefreshTokenDeleted ||
error.isInvalidRefreshToken -> CredentialsManagerException.Code.RENEW_FAILED
error.isRefreshTokenDeleted || error.isInvalidRefreshToken -> CredentialsManagerException.Code.RENEW_FAILED

error.isNetworkError -> CredentialsManagerException.Code.NO_NETWORK
else -> CredentialsManagerException.Code.API_ERROR
}
callback.onFailure(
CredentialsManagerException(
exception,
error
exception, error
)
)
}
Expand Down Expand Up @@ -364,8 +441,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
val emptyCredentials =
TextUtils.isEmpty(accessToken) && TextUtils.isEmpty(idToken) || expiresAt == null
return !(emptyCredentials || willExpire(
expiresAt!!,
minTtl
expiresAt!!, minTtl
) && refreshToken == null)
}

Expand Down
Loading