Skip to content

Commit

Permalink
Revoke Session (#155)
Browse files Browse the repository at this point in the history
  • Loading branch information
itaihanski authored Nov 17, 2024
1 parent eb1e0ba commit 141ab88
Show file tree
Hide file tree
Showing 10 changed files with 154 additions and 25 deletions.
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ written for Android. You can read more on the [Descope Website](https://descope.
Add the following to your `build.gradle` dependencies:

```groovy
implementation 'com.descope:descope-kotlin:0.12.0'
implementation 'com.descope:descope-kotlin:0.12.1'
```

## Quickstart
Expand Down Expand Up @@ -151,11 +151,13 @@ active session and clear it from the session manager:

```kotlin
Descope.sessionManager.session?.refreshJwt?.run {
Descope.auth.logout(this)
Descope.auth.revokeSessions(RevokeType.CurrentSession, this)
Descope.sessionManager.clearSession()
}
```

It is also possible to revoke all sessions by providing the appropriate `RevokeType` parameter.

You can customize how the `DescopeSessionManager` behaves by using
your own `storage` and `lifecycle` objects. See the documentation
for more details.
Expand Down Expand Up @@ -531,5 +533,5 @@ If you need help you can email [Descope Support](mailto:[email protected])

## License

The Descope SDK for Flutter is licensed for use under the terms and conditions
The Descope SDK for Android is licensed for use under the terms and conditions
of the [MIT license Agreement](https://github.com/descope/descope-android/blob/main/LICENSE).
8 changes: 8 additions & 0 deletions descopesdk/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,12 @@
android:fitsSystemWindows="true"
android:theme="@style/Theme.Hidden" />
</application>

<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
</intent>
</queries>
</manifest>
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.descope.android

import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Handler
Expand Down Expand Up @@ -185,12 +188,13 @@ internal class DescopeFlowCoordinator(private val webView: WebView) {
view?.run {
val isWebAuthnSupported = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
val origin = if (isWebAuthnSupported) getPackageOrigin(context) else ""
val useCustomSchemeFallback = shouldUseCustomSchemeUrl(context)
evaluateJavascript(
setupScript(
origin = origin,
oauthNativeProvider = flow.oauthProvider?.name ?: "",
oauthNativeRedirect = flow.oauthRedirect ?: "",
ssoRedirect = flow.ssoRedirect ?: "",
oauthRedirect = pickRedirectUrl(flow.oauthRedirect, flow.oauthRedirectCustomScheme, useCustomSchemeFallback),
ssoRedirect = pickRedirectUrl(flow.ssoRedirect, flow.ssoRedirectCustomScheme, useCustomSchemeFallback),
magicLinkRedirect = flow.magicLinkRedirect ?: "",
isWebAuthnSupported = isWebAuthnSupported,
)
Expand Down Expand Up @@ -267,7 +271,7 @@ private enum class State {
private fun setupScript(
origin: String,
oauthNativeProvider: String,
oauthNativeRedirect: String,
oauthRedirect: String,
ssoRedirect: String,
magicLinkRedirect: String,
isWebAuthnSupported: Boolean
Expand Down Expand Up @@ -298,7 +302,7 @@ function prepareWebComponent(wc) {
bridgeVersion: 1,
platform: 'android',
oauthProvider: '$oauthNativeProvider',
oauthRedirect: '$oauthNativeRedirect',
oauthRedirect: '$oauthRedirect',
ssoRedirect: '$ssoRedirect',
magicLinkRedirect: '$magicLinkRedirect',
origin: '$origin',
Expand Down Expand Up @@ -367,3 +371,23 @@ private fun String.toUri(): Uri? {
null
}
}

// Default Browser

private fun shouldUseCustomSchemeUrl(context: Context): Boolean {
val browserIntent = Intent("android.intent.action.VIEW", Uri.parse("http://"))
val resolveInfo = context.packageManager.resolveActivity(browserIntent, PackageManager.MATCH_DEFAULT_ONLY)
return when(resolveInfo?.loadLabel(context.packageManager).toString().lowercase()){
"opera",
"opera mini",
"duckduckgo",
"mi browser" -> true
else -> false
}
}

private fun pickRedirectUrl(main: String?, fallback: String?, useFallback: Boolean): String {
var url = main
if (useFallback && fallback != null) url = fallback
return url ?: ""
}
32 changes: 26 additions & 6 deletions descopesdk/src/main/java/com/descope/android/DescopeFlowView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,21 +36,41 @@ data class DescopeFlow(val uri: Uri) {
/**
* An optional deep link link URL to use when performing OAuth authentication, overriding
* whatever is configured in the flow or project.
* - **IMPORTANT NOTE**: even the Application links are the recommended way to configure
* deep links, some browsers, such as Opera, do not honor them and open the URLs inline.
* It is possible to circumvent this issue by using a custom scheme, albeit less secure.
* - **IMPORTANT NOTE**: even though App Links are the recommended way to configure
* deep links, some browsers, such as Opera, do not respect them and open the URLs inline.
* It is possible to circumvent this issue by providing a custom scheme based URL via [oauthRedirectCustomScheme].
*/
var oauthRedirect: String? = null

/**
* An optional custom scheme based URL, e.g. `mycustomscheme://myhost`,
* to use when performing OAuth authentication overriding whatever is configured in the flow or project.
* Functionally, this URL is exactly the same as [oauthRedirect], and will be used in its stead, only
* when the user has a default browser that does not honor App Links by default.
* That means the `https` based App Links are opened inline in the browser, instead
* of being handled by the application.
*/
var oauthRedirectCustomScheme: String? = null

/**
* An optional deep link link URL to use performing SSO authentication, overriding
* whatever is configured in the flow or project
* - **IMPORTANT NOTE**: even the Application links are the recommended way to configure
* deep links, some browsers, such as Opera, do not honor them and open the URLs inline.
* It is possible to circumvent this issue by using a custom scheme, albeit less secure.
* - **IMPORTANT NOTE**: even though App Links are the recommended way to configure
* deep links, some browsers, such as Opera, do not respect them and open the URLs inline.
* It is possible to circumvent this issue by providing a custom scheme via [ssoRedirectCustomScheme]
*/
var ssoRedirect: String? = null

/**
* An optional custom scheme based URL, e.g. `mycustomscheme://myhost`,
* to use when performing SSO authentication overriding whatever is configured in the flow or project.
* Functionally, this URL is exactly the same as [ssoRedirect], and will be used in its stead, only
* when the user has a default browser that does not honor App Links by default.
* That means the `https` based App Links are opened inline in the browser, instead
* of being handled by the application.
*/
var ssoRedirectCustomScheme: String? = null

/**
* An optional deep link link URL to use when sending magic link emails, overriding
* whatever is configured in the flow or project
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import com.descope.sdk.DescopeConfig
import com.descope.sdk.DescopeSdk
import com.descope.types.DeliveryMethod
import com.descope.types.OAuthProvider
import com.descope.types.RevokeType
import com.descope.types.RevokeType.AllSessions
import com.descope.types.RevokeType.CurrentSession
import com.descope.types.SignInOptions
import com.descope.types.SignUpDetails
import com.descope.types.UpdateOptions
Expand Down Expand Up @@ -469,8 +472,11 @@ internal open class DescopeClient(internal val config: DescopeConfig) : HttpClie
headers = authorization(refreshJwt),
)

suspend fun logout(refreshJwt: String) = post(
route = "auth/logout",
suspend fun logout(refreshJwt: String, revokeType: RevokeType) = post(
route = when (revokeType) {
CurrentSession -> "auth/logout"
AllSessions -> "auth/logoutall"
},
decoder = emptyResponse,
headers = authorization(refreshJwt),
)
Expand Down Expand Up @@ -538,6 +544,7 @@ private fun List<SignInOptions>.toMap(): Map<String, Any> {
is SignInOptions.CustomClaims -> map["customClaims"] = it.claims
is SignInOptions.Mfa -> map["mfa"] = true
is SignInOptions.StepUp -> map["stepup"] = true
is SignInOptions.RevokeOtherSessions -> map["revokeOtherSessions"] = true
}
}
return map.toMap()
Expand Down
22 changes: 17 additions & 5 deletions descopesdk/src/main/java/com/descope/internal/routes/Auth.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.descope.internal.routes
import com.descope.internal.http.DescopeClient
import com.descope.sdk.DescopeAuth
import com.descope.types.DescopeUser
import com.descope.types.RevokeType
import com.descope.types.RefreshResponse
import com.descope.types.Result

Expand All @@ -18,15 +19,26 @@ internal class Auth(private val client: DescopeClient) : DescopeAuth {
override suspend fun refreshSession(refreshJwt: String): RefreshResponse =
client.refresh(refreshJwt).toRefreshResponse()

override suspend fun refreshSession(refreshJwt: String, callback: (Result<RefreshResponse>) -> Unit) = wrapCoroutine(callback) {
override fun refreshSession(refreshJwt: String, callback: (Result<RefreshResponse>) -> Unit) = wrapCoroutine(callback) {
refreshSession(refreshJwt)
}

override suspend fun logout(refreshJwt: String) =
client.logout(refreshJwt)
override suspend fun revokeSessions(revokeType: RevokeType, refreshJwt: String) {
client.logout(refreshJwt, revokeType)
}

override suspend fun logout(refreshJwt: String, callback: (Result<Unit>) -> Unit) = wrapCoroutine(callback) {
logout(refreshJwt)
override fun revokeSessions(revoke: RevokeType, refreshJwt: String, callback: (Result<Unit>) -> Unit) = wrapCoroutine(callback) {
revokeSessions(revoke, refreshJwt)
}

// Deprecated

@Deprecated(message = "Use revokeSessions instead", replaceWith = ReplaceWith("revokeSessions(RevokeType.CurrentSession, refreshJwt)"))
override suspend fun logout(refreshJwt: String) =
revokeSessions(RevokeType.CurrentSession, refreshJwt)

@Deprecated(message = "Use revokeSessions instead", replaceWith = ReplaceWith("revokeSessions(RevokeType.CurrentSession, refreshJwt, callback)"))
override fun logout(refreshJwt: String, callback: (Result<Unit>) -> Unit) =
revokeSessions(RevokeType.CurrentSession, refreshJwt, callback)

}
39 changes: 35 additions & 4 deletions descopesdk/src/main/java/com/descope/sdk/Routes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.descope.types.AuthenticationResponse
import com.descope.types.DeliveryMethod
import com.descope.types.DescopeUser
import com.descope.types.EnchantedLinkResponse
import com.descope.types.RevokeType
import com.descope.types.OAuthProvider
import com.descope.types.PasswordPolicy
import com.descope.types.RefreshResponse
Expand Down Expand Up @@ -51,17 +52,47 @@ interface DescopeAuth {
suspend fun refreshSession(refreshJwt: String): RefreshResponse

/** @see refreshSession */
suspend fun refreshSession(refreshJwt: String, callback: (Result<RefreshResponse>) -> Unit)
fun refreshSession(refreshJwt: String, callback: (Result<RefreshResponse>) -> Unit)

/**
* Logs out from an active [DescopeSession].
* It's a good security practice to remove refresh JWTs from the Descope servers if
* they become redundant before expiry. This function will usually be called with `.currentSession`
* when the user wants to sign out of the application. For example:
*
*
* fun onSignOut() {
* // clear the session locally from the app and revoke the refreshJWT
* // from the Descope servers in a coroutine scope without waiting for the call to finish
* Descope.sessionManager.session?.refreshJwt?.run {
* Descope.sessionManager.clearSession()
* GlobalScope.launch(Dispatchers.Main) { // This can be whatever scope makes sense for your app
* try {
* Descope.auth.revokeSessions(RevokeType.CurrentSession, refreshJwt)
* } catch (e: Exception){
* }
* }
* }
* showLaunchScreen()
* }
*
* - Important: When called with `RevokeType.AllSessions` the provided refresh JWT will not
* be usable anymore and the user will need to sign in again.
*
* @param revokeType which sessions should be removed by this call.
* - `CurrentSession`: log out of the current session (the one provided by this refresh JWT)
* - `AllSessions`: log out of all sessions for the user
* @param refreshJwt the refreshJwt from an active [DescopeSession].
*/
suspend fun revokeSessions(revokeType: RevokeType, refreshJwt: String)

/** @see revokeSessions*/
fun revokeSessions(revoke: RevokeType, refreshJwt: String, callback: (Result<Unit>) -> Unit)

@Deprecated(message = "Use revokeSessions instead", replaceWith = ReplaceWith("revokeSessions(RevokeType.CurrentSession, refreshJwt)"))
suspend fun logout(refreshJwt: String)

/** @see logout */
suspend fun logout(refreshJwt: String, callback: (Result<Unit>) -> Unit)
@Deprecated(message = "Use revokeSessions instead", replaceWith = ReplaceWith("revokeSessions(RevokeType.CurrentSession, refreshJwt, callback)"))
fun logout(refreshJwt: String, callback: (Result<Unit>) -> Unit)
}

/**
Expand Down
2 changes: 1 addition & 1 deletion descopesdk/src/main/java/com/descope/sdk/Sdk.kt
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,6 @@ class DescopeSdk(context: Context, projectId: String, configure: DescopeConfig.(
const val name = "DescopeAndroid"

/** The Descope SDK version */
const val version = "0.12.0"
const val version = "0.12.1"
}
}
21 changes: 21 additions & 0 deletions descopesdk/src/main/java/com/descope/types/Others.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ import com.descope.session.DescopeSession

// Enums

/**
* Which sessions to revoke when calling `DescopeAuth.revokeSessions()`
*/
enum class RevokeType {
/** Revokes the provided refresh JWT. */
CurrentSession,
/**
* Revokes the provided refresh JWT and all other active sessions for the user.
*
* - Important: This causes all sessions for the user to be removed, and the provided
* refresh JWT will not be usable after the logout call completes.
*/
AllSessions,
}

/** The delivery method for an OTP or Magic Link message. */
enum class DeliveryMethod {
Email,
Expand Down Expand Up @@ -94,6 +109,12 @@ sealed class SignInOptions {
* and refresh JWTs will be an array with an entry for each authentication method used.
*/
class Mfa(val refreshJwt: String) : SignInOptions()


/**
* Revokes all other active sessions for the user besides the new session being created.
*/
data object RevokeOtherSessions : SignInOptions()
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ internal fun List<SignInOptions>.validate(body: Map<String, Any?>) {
is SignInOptions.StepUp -> {
assertTrue(loginOptions["stepup"] as Boolean)
}

is SignInOptions.RevokeOtherSessions -> {
assertTrue(loginOptions["revokeOtherSessions"] as Boolean)
}
}
}
}
Expand Down

0 comments on commit 141ab88

Please sign in to comment.