Skip to content

Commit 643d9dc

Browse files
authored
API support for fetching session token (#802)
1 parent e3afbe2 commit 643d9dc

File tree

10 files changed

+975
-15
lines changed

10 files changed

+975
-15
lines changed

auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import com.auth0.android.result.Credentials
1414
import com.auth0.android.result.DatabaseUser
1515
import com.auth0.android.result.PasskeyChallenge
1616
import com.auth0.android.result.PasskeyRegistrationChallenge
17+
import com.auth0.android.result.SSOCredentials
1718
import com.auth0.android.result.UserProfile
1819
import com.google.gson.Gson
1920
import okhttp3.HttpUrl.Companion.toHttpUrl
@@ -921,6 +922,44 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
921922
return factory.get(url.toString(), jwksAdapter)
922923
}
923924

925+
/**
926+
* Creates a new request to fetch a session token in exchange for a refresh token.
927+
*
928+
* @param refreshToken A valid refresh token obtained as part of Auth0 authentication
929+
* @return a request to fetch a session token
930+
*/
931+
public fun fetchSessionToken(refreshToken: String): Request<SSOCredentials, AuthenticationException> {
932+
val params = ParameterBuilder.newBuilder()
933+
.setClientId(clientId)
934+
.setGrantType(ParameterBuilder.GRANT_TYPE_TOKEN_EXCHANGE)
935+
.set(SUBJECT_TOKEN_KEY, refreshToken)
936+
.set(SUBJECT_TOKEN_TYPE_KEY, ParameterBuilder.TOKEN_TYPE_REFRESH_TOKEN)
937+
.set(REQUESTED_TOKEN_TYPE_KEY, ParameterBuilder.TOKEN_TYPE_SESSION_TOKEN)
938+
.asDictionary()
939+
return loginWithTokenGeneric<SSOCredentials>(params)
940+
}
941+
942+
/**
943+
* Helper function to make a request to the /oauth/token endpoint with a custom response type.
944+
*/
945+
private inline fun <reified T> loginWithTokenGeneric(parameters: Map<String, String>): Request<T,AuthenticationException> {
946+
val url = auth0.getDomainUrl().toHttpUrl().newBuilder()
947+
.addPathSegment(OAUTH_PATH)
948+
.addPathSegment(TOKEN_PATH)
949+
.build()
950+
val requestParameters =
951+
ParameterBuilder.newBuilder()
952+
.setClientId(clientId)
953+
.addAll(parameters)
954+
.asDictionary()
955+
val adapter: JsonAdapter<T> = GsonAdapter(
956+
T::class.java, gson
957+
)
958+
val request = factory.post(url.toString(), adapter)
959+
request.addParameters(requestParameters)
960+
return request
961+
}
962+
924963
/**
925964
* Helper function to make a request to the /oauth/token endpoint.
926965
*/
@@ -989,6 +1028,7 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
9891028
private const val RECOVERY_CODE_KEY = "recovery_code"
9901029
private const val SUBJECT_TOKEN_KEY = "subject_token"
9911030
private const val SUBJECT_TOKEN_TYPE_KEY = "subject_token_type"
1031+
private const val REQUESTED_TOKEN_TYPE_KEY = "requested_token_type"
9921032
private const val USER_METADATA_KEY = "user_metadata"
9931033
private const val AUTH_SESSION_KEY = "auth_session"
9941034
private const val AUTH_RESPONSE_KEY = "authn_response"

auth0/src/main/java/com/auth0/android/authentication/ParameterBuilder.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,8 @@ public class ParameterBuilder private constructor(parameters: Map<String, String
160160
public const val GRANT_TYPE_TOKEN_EXCHANGE: String =
161161
"urn:ietf:params:oauth:grant-type:token-exchange"
162162
public const val GRANT_TYPE_PASSKEY :String = "urn:okta:params:oauth:grant-type:webauthn"
163+
public const val TOKEN_TYPE_REFRESH_TOKEN :String = "urn:ietf:params:oauth:token-type:refresh_token"
164+
public const val TOKEN_TYPE_SESSION_TOKEN :String = "urn:auth0:params:oauth:token-type:session_token"
163165
public const val SCOPE_OPENID: String = "openid"
164166
public const val SCOPE_OFFLINE_ACCESS: String = "openid offline_access"
165167
public const val SCOPE_KEY: String = "scope"

auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import androidx.annotation.VisibleForTesting
44
import com.auth0.android.authentication.AuthenticationAPIClient
55
import com.auth0.android.callback.Callback
66
import com.auth0.android.result.Credentials
7+
import com.auth0.android.result.SSOCredentials
78
import com.auth0.android.util.Clock
89
import java.util.*
910

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

3031
@Throws(CredentialsManagerException::class)
3132
public abstract fun saveCredentials(credentials: Credentials)
33+
public abstract fun saveSsoCredentials(ssoCredentials: SSOCredentials)
3234
public abstract fun getCredentials(callback: Callback<Credentials, CredentialsManagerException>)
35+
public abstract fun getSsoCredentials(callback: Callback<SSOCredentials, CredentialsManagerException>)
3336
public abstract fun getCredentials(
3437
scope: String?,
3538
minTtl: Int,
@@ -60,6 +63,10 @@ public abstract class BaseCredentialsManager internal constructor(
6063
callback: Callback<Credentials, CredentialsManagerException>
6164
)
6265

66+
@JvmSynthetic
67+
@Throws(CredentialsManagerException::class)
68+
public abstract suspend fun awaitSsoCredentials(): SSOCredentials
69+
6370
@JvmSynthetic
6471
@Throws(CredentialsManagerException::class)
6572
public abstract suspend fun awaitCredentials(): Credentials

auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt

Lines changed: 86 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.auth0.android.authentication.AuthenticationAPIClient
66
import com.auth0.android.authentication.AuthenticationException
77
import com.auth0.android.callback.Callback
88
import com.auth0.android.result.Credentials
9+
import com.auth0.android.result.SSOCredentials
910
import kotlinx.coroutines.suspendCancellableCoroutine
1011
import java.util.*
1112
import java.util.concurrent.Executor
@@ -53,6 +54,85 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
5354
storage.store(LEGACY_KEY_CACHE_EXPIRES_AT, credentials.expiresAt.time)
5455
}
5556

57+
58+
/**
59+
* Stores the given [SSOCredentials] refresh token in the storage.
60+
* This method must be called if the SSOCredentials are obtained by directly invoking [AuthenticationAPIClient.fetchSessionToken] api and
61+
* [rotating refresh token](https://auth0.com/docs/secure/tokens/refresh-tokens/refresh-token-rotation) are enabled for
62+
* the client. Method will silently return ,if the passed credentials has no refresh token.
63+
*
64+
* @param ssoCredentials the credentials to save in the storage.
65+
*/
66+
override fun saveSsoCredentials(ssoCredentials: SSOCredentials) {
67+
if (ssoCredentials.refreshToken.isNullOrEmpty())
68+
return // No refresh token to save
69+
serialExecutor.execute {
70+
val existingRefreshToken = storage.retrieveString(KEY_REFRESH_TOKEN)
71+
// Checking if the existing one needs to be replaced with the new one
72+
if (ssoCredentials.refreshToken == existingRefreshToken)
73+
return@execute
74+
storage.store(KEY_REFRESH_TOKEN, ssoCredentials.refreshToken)
75+
}
76+
}
77+
78+
/**
79+
* Fetches a new [SSOCredentials] . It will fail with [CredentialsManagerException]
80+
* if the existing refresh_token is null or no longer valid. This method will handle saving the refresh_token,
81+
* if a new one is issued
82+
*/
83+
override fun getSsoCredentials(callback: Callback<SSOCredentials, CredentialsManagerException>) {
84+
serialExecutor.execute {
85+
val refreshToken = storage.retrieveString(KEY_REFRESH_TOKEN)
86+
if (refreshToken.isNullOrEmpty()) {
87+
callback.onFailure(CredentialsManagerException.NO_REFRESH_TOKEN)
88+
return@execute
89+
}
90+
91+
try {
92+
val sessionCredentials =
93+
authenticationClient.fetchSessionToken(refreshToken)
94+
.execute()
95+
saveSsoCredentials(sessionCredentials)
96+
callback.onSuccess(sessionCredentials)
97+
} catch (error: AuthenticationException) {
98+
val exception = when {
99+
error.isRefreshTokenDeleted ||
100+
error.isInvalidRefreshToken -> CredentialsManagerException.Code.RENEW_FAILED
101+
102+
error.isNetworkError -> CredentialsManagerException.Code.NO_NETWORK
103+
else -> CredentialsManagerException.Code.API_ERROR
104+
}
105+
callback.onFailure(
106+
CredentialsManagerException(
107+
exception,
108+
error
109+
)
110+
)
111+
}
112+
}
113+
}
114+
115+
/**
116+
* Fetches a new [SSOCredentials] . It will fail with [CredentialsManagerException]
117+
* if the existing refresh_token is null or no longer valid. This method will handle saving the refresh_token,
118+
* if a new one is issued
119+
*/
120+
@JvmSynthetic
121+
@Throws(CredentialsManagerException::class)
122+
override suspend fun awaitSsoCredentials(): SSOCredentials {
123+
return suspendCancellableCoroutine { continuation ->
124+
getSsoCredentials(object : Callback<SSOCredentials, CredentialsManagerException> {
125+
override fun onSuccess(result: SSOCredentials) {
126+
continuation.resume(result)
127+
}
128+
129+
override fun onFailure(error: CredentialsManagerException) {
130+
continuation.resumeWithException(error)
131+
}
132+
})
133+
}
134+
}
135+
56136
/**
57137
* Retrieves the credentials from the storage and refresh them if they have already expired.
58138
* It will throw [CredentialsManagerException] if the saved access_token or id_token is null,
@@ -144,8 +224,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
144224
forceRefresh: Boolean
145225
): Credentials {
146226
return suspendCancellableCoroutine { continuation ->
147-
getCredentials(
148-
scope,
227+
getCredentials(scope,
149228
minTtl,
150229
parameters,
151230
headers,
@@ -299,8 +378,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
299378
if (willAccessTokenExpire) {
300379
val tokenLifetime = (expiresAt - currentTimeInMillis - minTtl * 1000) / -1000
301380
val wrongTtlException = CredentialsManagerException(
302-
CredentialsManagerException.Code.LARGE_MIN_TTL,
303-
String.format(
381+
CredentialsManagerException.Code.LARGE_MIN_TTL, String.format(
304382
Locale.getDefault(),
305383
"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.",
306384
tokenLifetime,
@@ -326,15 +404,14 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
326404
callback.onSuccess(credentials)
327405
} catch (error: AuthenticationException) {
328406
val exception = when {
329-
error.isRefreshTokenDeleted ||
330-
error.isInvalidRefreshToken -> CredentialsManagerException.Code.RENEW_FAILED
407+
error.isRefreshTokenDeleted || error.isInvalidRefreshToken -> CredentialsManagerException.Code.RENEW_FAILED
408+
331409
error.isNetworkError -> CredentialsManagerException.Code.NO_NETWORK
332410
else -> CredentialsManagerException.Code.API_ERROR
333411
}
334412
callback.onFailure(
335413
CredentialsManagerException(
336-
exception,
337-
error
414+
exception, error
338415
)
339416
)
340417
}
@@ -364,8 +441,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
364441
val emptyCredentials =
365442
TextUtils.isEmpty(accessToken) && TextUtils.isEmpty(idToken) || expiresAt == null
366443
return !(emptyCredentials || willExpire(
367-
expiresAt!!,
368-
minTtl
444+
expiresAt!!, minTtl
369445
) && refreshToken == null)
370446
}
371447

0 commit comments

Comments
 (0)