Skip to content

Commit

Permalink
MBL-1169: Redirection step (#1947)
Browse files Browse the repository at this point in the history
* - Redirection code implemented
  • Loading branch information
Arkariang committed Feb 13, 2024
1 parent b196c74 commit 735c1f7
Show file tree
Hide file tree
Showing 7 changed files with 275 additions and 34 deletions.
19 changes: 8 additions & 11 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
Expand Down Expand Up @@ -148,17 +147,15 @@
android:theme="@style/Login" />
<activity
android:name=".ui.activities.OAuthActivity"
android:exported="true"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
android:launchMode="singleTop"
android:exported="true">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>

<data
android:scheme="ksrauth2"
android:host="authorize" />
<data android:scheme="ksrauth2" />
<data android:host="authenticate" />
</intent-filter>
</activity>
<activity
Expand Down
16 changes: 15 additions & 1 deletion app/src/main/java/com/kickstarter/libs/utils/CodeVerifier.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.kickstarter.libs.utils

import android.util.Base64
import com.kickstarter.libs.utils.CodeVerifier.Companion.DEFAULT_CODE_VERIFIER_ENTROPY
import timber.log.Timber
import java.io.UnsupportedEncodingException
import java.security.MessageDigest
Expand All @@ -13,7 +14,20 @@ import java.util.regex.Pattern
*
* @see [Proof Key for Code Exchange by OAuth Public Clients](https://datatracker.ietf.org/doc/html/rfc7636)
*/
class CodeVerifier {
interface PKCE {
fun generateRandomCodeVerifier(entropy: Int = DEFAULT_CODE_VERIFIER_ENTROPY): String
fun generateCodeChallenge(codeVerifier: String): String
}
open class CodeVerifier : PKCE {

override fun generateRandomCodeVerifier(entropy: Int): String {
return Companion.generateRandomCodeVerifier(entropyBytes = entropy)
}

override fun generateCodeChallenge(codeVerifier: String): String {
return Companion.generateCodeChallenge(codeVerifier)
}

companion object {
/**
* The minimum permitted length for a code verifier.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,16 @@ fun Intent.getPreLaunchProjectActivity(context: Context, slug: String?, project:
}

fun Intent.getStartLoginIntent(isOAuthEnabled: Boolean, context: Context): Intent {
return if (isOAuthEnabled)
return if (isOAuthEnabled) {
this.setClass(context, OAuthActivity::class.java)
else
} else
this.setClass(context, LoginActivity::class.java)
}

fun Intent.getSignupIntent(isOAuthEnabled: Boolean, context: Context): Intent {
return if (isOAuthEnabled)
return if (isOAuthEnabled) {
this.setClass(context, OAuthActivity::class.java)
else
} else
this.setClass(context, SignupActivity::class.java)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package com.kickstarter.models.chrome

import android.app.Activity
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
Expand Down Expand Up @@ -95,7 +94,7 @@ class ChromeTabsHelperActivity {
* navigation event has been detected
*/
class CustomTabSessionAndClientHelper(
context: Context,
context: Activity,
uri: Uri,
tabHiddenCallback: () -> Unit
) {
Expand All @@ -107,7 +106,7 @@ class ChromeTabsHelperActivity {

private val callback = object : CustomTabsCallback() {
override fun onNavigationEvent(navigationEvent: Int, extras: Bundle?) {
Timber.d("onNavigationEvent: Code = $navigationEvent")
Timber.d("OAuth onNavigationEvent: Code = $navigationEvent")
// - means the X button has been clicked, therefore ChromeTab has been dismissed
if (navigationEvent == TAB_HIDDEN) {
tabHiddenCallback()
Expand All @@ -132,6 +131,7 @@ class ChromeTabsHelperActivity {
sessionReady.tryEmit(session.isNotNull())
Timber.d("onCustomTabsServiceConnected")
}

override fun onServiceDisconnected(name: ComponentName) {
Timber.d("onServiceDisconnected")
customClient = null
Expand Down
70 changes: 55 additions & 15 deletions app/src/main/java/com/kickstarter/ui/activities/OAuthActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,71 @@ import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.browser.customtabs.CustomTabsIntent
import androidx.lifecycle.lifecycleScope
import com.kickstarter.libs.utils.CodeVerifier
import com.kickstarter.libs.utils.TransitionUtils
import com.kickstarter.libs.utils.extensions.getEnvironment
import com.kickstarter.models.chrome.ChromeTabsHelperActivity
import com.kickstarter.ui.IntentKey
import com.kickstarter.ui.extensions.setUpConnectivityStatusCheck
import com.kickstarter.viewmodels.OAuthViewModel
import com.kickstarter.viewmodels.OAuthViewModelFactory
import kotlinx.coroutines.launch
import timber.log.Timber

class OAuthActivity : AppCompatActivity() {

private lateinit var helper: ChromeTabsHelperActivity.CustomTabSessionAndClientHelper
private lateinit var viewModelFactory: OAuthViewModelFactory
private val viewModel: OAuthViewModel by viewModels {
viewModelFactory
}

val redirectUri = "ksrauth2://authorize"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setUpConnectivityStatusCheck(lifecycle)

// TODO: Will be moved to VM all the URI parameters building on MBL-1169
val codeVerifier = CodeVerifier.generateRandomCodeVerifier(entropyBytes = CodeVerifier.MAX_CODE_VERIFIER_ENTROPY)
val authParams = mapOf(
"redirect_uri" to redirectUri,
"response_type" to "code",
"code_challenge" to CodeVerifier.generateCodeChallenge(codeVerifier), // Set the code challenge
"code_challenge_method" to "S256"
).map { (k, v) -> "${(k)}=$v" }.joinToString("&")
val uri = Uri.parse("https://www.kickstarter.com/oauth/authorizations?$authParams")
Timber.d("OAuthActivity: onCreate Intent: $intent, onCreate data: ${intent.data}")

this.getEnvironment()?.let { env ->
viewModelFactory = OAuthViewModelFactory(environment = env)
}

viewModel.produceState(intent = intent)

lifecycleScope.launch {

viewModel.uiState.collect { state ->
// - Intent generated with onCreate
if (state.isAuthorizationStep && state.authorizationUrl.isNotEmpty()) {
openChromeTabWithUrl(state.authorizationUrl)
}

if (state.isTokenRetrieveStep && state.code.isNotEmpty()) {
// TODO WIP PHASE 3, VM call the endpoint with code & code_challenge, retrieve the token.
}
}
}
}

override fun onDestroy() {
Timber.d("OAuthActivity: onDestroy")
super.onDestroy()
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
Timber.d("OAuthActivity: onNewIntent Intent: $intent, data: ${intent?.data}")
// - Intent generated when the deepLink redirection takes place
intent?.let { viewModel.produceState(intent = it) }
}

private fun openChromeTabWithUrl(url: String) {
val authorizationUri = Uri.parse(url)

// BindCustomTabsService, obtain CustomTabsClient and Client, listens to navigation events
helper = ChromeTabsHelperActivity.CustomTabSessionAndClientHelper(this, uri) {
helper = ChromeTabsHelperActivity.CustomTabSessionAndClientHelper(this, authorizationUri) {
finish()
}

Expand All @@ -52,9 +85,16 @@ class OAuthActivity : AppCompatActivity() {

lifecycleScope.launch {
// - Once the session is ready and client warmed-up load the url
helper.isSessionReady().collect {
helper.isSessionReady().collect { ready ->
val tabIntent = CustomTabsIntent.Builder(helper.getSession()).build()
ChromeTabsHelperActivity.openCustomTab(this@OAuthActivity, tabIntent, uri, fallback)
tabIntent.intent.flags = Intent.FLAG_ACTIVITY_NO_HISTORY
// tabIntent.intent.addFlags(Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP)
ChromeTabsHelperActivity.openCustomTab(
this@OAuthActivity,
tabIntent,
authorizationUri,
fallback
)
}
}
}
Expand Down
106 changes: 106 additions & 0 deletions app/src/main/java/com/kickstarter/viewmodels/OAuthViewModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package com.kickstarter.viewmodels

import android.content.Intent
import android.net.Uri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.kickstarter.libs.ApiEndpoint
import com.kickstarter.libs.Environment
import com.kickstarter.libs.utils.CodeVerifier
import com.kickstarter.libs.utils.PKCE
import com.kickstarter.libs.utils.Secrets
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import timber.log.Timber

/**
* UiState for the OAuthScreen.
* @param authorizationUrl = Url to be loaded withing the ChromeTabs with all PKCE params
* @param code = code retrieved from the redirect deeplink, once the user has logged in successfully on ChromeTabs
*/
data class OAuthUiState(
val authorizationUrl: String = "",
val code: String = "",
val isAuthorizationStep: Boolean = false,
val isTokenRetrieveStep: Boolean = false
)
class OAuthViewModel(
private val environment: Environment,
private val verifier: PKCE
) : ViewModel() {

private val hostEndpoint = environment.webEndpoint()
private val clientID = if (hostEndpoint == ApiEndpoint.PRODUCTION.name) Secrets.Api.Client.PRODUCTION else Secrets.Api.Client.STAGING
private val codeVerifier = verifier.generateRandomCodeVerifier(entropy = CodeVerifier.MAX_CODE_VERIFIER_ENTROPY)

private var mutableUIState = MutableStateFlow(OAuthUiState())
val uiState: StateFlow<OAuthUiState>
get() = mutableUIState.asStateFlow()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue = OAuthUiState()
)
fun produceState(intent: Intent) {
viewModelScope.launch {
val uri = Uri.parse(intent.data.toString())
val scheme = uri.scheme
val host = uri.host
val code = uri.getQueryParameter("code")

if (scheme == REDIRECT_URI_SCHEMA && host == REDIRECT_URI_HOST && code != null) {
Timber.d("isTokenRetrieveStep after redirectionDeeplink: $code")
mutableUIState.emit(
OAuthUiState(
code = code,
isTokenRetrieveStep = true,
isAuthorizationStep = false
)
)
// TODO: will call with code and code_challenge once the backend is ready to call the tokenEndpoint
}

if (intent.data == null) {
val url = generateAuthorizationUrlWithParams()
Timber.d("isAuthorizationStep $url")
mutableUIState.emit(
OAuthUiState(
authorizationUrl = url,
isAuthorizationStep = true,
isTokenRetrieveStep = false
)
)
}
}
}
private fun generateAuthorizationUrlWithParams(): String {
val authParams = mapOf(
"redirect_uri" to REDIRECT_URI_SCHEMA,
"scope" to "1", // profile/email
"client_id" to clientID,
"response_type" to "1", // code
"code_challenge" to verifier.generateCodeChallenge(codeVerifier),
"code_challenge_method" to "S256"
).map { (k, v) -> "${(k)}=$v" }.joinToString("&")
return "$hostEndpoint/oauth/authorizations/new?$authParams"
}

private companion object {
const val REDIRECT_URI_SCHEMA = "ksrauth2"
const val REDIRECT_URI_HOST = "authenticate"
}
}

class OAuthViewModelFactory(
private val environment: Environment,
private val verifier: PKCE = CodeVerifier()
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return OAuthViewModel(environment, verifier = verifier) as T
}
}

0 comments on commit 735c1f7

Please sign in to comment.