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

Migrate OAuth PKCE sample to Compose #1114

Merged
merged 3 commits into from
May 31, 2024
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
46 changes: 24 additions & 22 deletions WearOAuth/oauth-device-grant/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,6 @@ android {
versionName "1.0"
}

// Used to make the direct HTTP call for the token exchange. This will eventually be removed
// when the sample is updated to use server-side redirects.
useLibrary "org.apache.http.legacy"

buildTypes {
release {
minifyEnabled false
Expand All @@ -46,35 +42,41 @@ android {
targetCompatibility JavaVersion.VERSION_17
}

kotlinOptions { jvmTarget = JavaVersion.VERSION_17.majorVersion }
buildFeatures {compose true }
composeOptions {kotlinCompilerExtensionVersion compose_version }
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.majorVersion
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion compose_version
}
}

dependencies {
implementation (libs.wear.compose.foundation)
implementation(libs.wear.compose.foundation)

// For Wear Material Design UX guidelines and specifications
implementation (libs.wear.compose.material)
implementation (libs.androidx.compose.material3)
implementation(libs.wear.compose.material)
implementation(libs.androidx.compose.material3)

// For integration between Wear Compose and Androidx Navigation libraries
implementation (libs.wear.compose.navigation)
implementation(libs.wear.compose.navigation)

// For Wear preview annotations
implementation (libs.androidx.compose.ui.tooling)
implementation(libs.androidx.compose.ui.tooling)

// Horologist dependencies
implementation (libs.horologist.compose.layout)
implementation(libs.horologist.compose.layout)

// Standard android dependencies
implementation (projects.util)
implementation (libs.kotlin.stdlib)
implementation (libs.androidx.core.ktx)
implementation (libs.androidx.appcompat)
implementation (libs.androidx.lifecycle.viewmodel.ktx)
implementation (libs.androidx.fragment.ktx)
implementation (libs.androidx.wear)
implementation (libs.wear.remote.interactions)
implementation (libs.playservices.wearable)
implementation(projects.util)
implementation(libs.kotlin.stdlib)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.fragment.ktx)
implementation(libs.androidx.wear)
implementation(libs.wear.remote.interactions)
implementation(libs.playservices.wearable)
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ fun AuthenticateApp(deviceGrantViewModel: AuthDeviceGrantViewModel) {
val localContext = LocalContext.current
val columnState = rememberResponsiveColumnState(
contentPadding = ScalingLazyColumnDefaults.padding(
first = ItemType.Unspecified,
last = ItemType.Unspecified
first = ItemType.Text,
last = ItemType.Text
)
)
ScreenScaffold(scrollState = columnState) {
Expand Down
89 changes: 54 additions & 35 deletions WearOAuth/oauth-pkce/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,51 +14,70 @@
* limitations under the License.
*/
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}

android {
compileSdk 34
compileSdk 34
namespace "com.example.android.wearable.oauth.pkce"

namespace "com.example.android.wearable.oauth.pkce"
defaultConfig {
applicationId "com.example.android.wearable.oauth.pkce"
minSdk 26
targetSdk 33
versionCode 1
versionName "1.0"
}

defaultConfig {
applicationId "com.example.android.wearable.oauth.pkce"
minSdk 26
targetSdk 33
versionCode 1
versionName "1.0"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.majorVersion
}

kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.majorVersion
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
}
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
}
}
buildFeatures {
viewBinding true
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion compose_version
}
}

dependencies {
implementation projects.util
implementation libs.kotlin.stdlib
implementation libs.androidx.core.ktx
implementation libs.androidx.appcompat
implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.fragment.ktx)
implementation(libs.androidx.wear)
implementation(libs.wear.phone.interactions)
implementation libs.playservices.wearable
implementation(libs.wear.compose.foundation)

// For Wear Material Design UX guidelines and specifications
implementation(libs.wear.compose.material)
implementation(libs.androidx.compose.material3)

// For integration between Wear Compose and Androidx Navigation libraries
implementation(libs.wear.compose.navigation)

// For Wear preview annotations
implementation(libs.androidx.compose.ui.tooling)

// Horologist dependencies
implementation(libs.horologist.compose.layout)

// Standard android deps
implementation projects.util
implementation libs.kotlin.stdlib
implementation libs.androidx.core.ktx
implementation libs.androidx.appcompat
implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.fragment.ktx)
implementation(libs.androidx.wear)
implementation(libs.wear.phone.interactions)
implementation libs.playservices.wearable
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,26 @@
package com.example.android.wearable.oauth.pkce

import android.os.Bundle
import android.view.View
import android.widget.TextView
import androidx.activity.ComponentActivity
import androidx.activity.viewModels
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.wear.compose.material3.Button
import androidx.wear.compose.material3.ListHeader
import androidx.wear.compose.material3.Text
import com.google.android.horologist.annotations.ExperimentalHorologistApi
import com.google.android.horologist.compose.layout.AppScaffold
import com.google.android.horologist.compose.layout.ScalingLazyColumn
import com.google.android.horologist.compose.layout.ScalingLazyColumnDefaults
import com.google.android.horologist.compose.layout.ScreenScaffold
import com.google.android.horologist.compose.layout.rememberResponsiveColumnState

/**
* Demonstrates the OAuth flow on Wear OS. This sample currently handles the callback from the
Expand All @@ -35,22 +51,46 @@ class AuthPKCEActivity : ComponentActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_auth)
val viewModel by viewModels<AuthPKCEViewModel>()

// Start the OAuth flow when the user presses the button
findViewById<View>(R.id.authenticateButton).setOnClickListener {
viewModel.startAuthFlow()
}

// Show current status on the screen
viewModel.status.observe(this) { statusText ->
findViewById<TextView>(R.id.status_text_view).text = resources.getText(statusText)
}
setContent { PKCEApp(pkceViewModel = viewModel()) }
}
}

// Show dynamic content on the screen
viewModel.result.observe(this) { resultText ->
findViewById<TextView>(R.id.result_text_view).text = resultText
@OptIn(ExperimentalHorologistApi::class)
@Composable
fun PKCEApp(pkceViewModel: AuthPKCEViewModel) {
AppScaffold {
val uiState = pkceViewModel.uiState.collectAsState()
val localContext = LocalContext.current
val columnState = rememberResponsiveColumnState(
contentPadding = ScalingLazyColumnDefaults.padding(
first = ScalingLazyColumnDefaults.ItemType.Text,
last = ScalingLazyColumnDefaults.ItemType.Text
)
)
ScreenScaffold(scrollState = columnState) {
ScalingLazyColumn(columnState = columnState) {
item {
ListHeader {
Text(
stringResource(R.string.oauth_pkce),
textAlign = TextAlign.Center
)
}
}
item {
Button(
onClick = { pkceViewModel.startAuthFlow(localContext) },
modifier = Modifier.fillMaxSize()
) {
Text(
text = stringResource(R.string.authenticate),
modifier = Modifier.align(Alignment.CenterVertically)
)
}
}
item { Text(stringResource(id = uiState.value.statusCode)) }
item { Text(uiState.value.resultMessage) }
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,10 @@
*/
package com.example.android.wearable.oauth.pkce

import android.app.Application
import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.wear.phone.interactions.authentication.CodeChallenge
import androidx.wear.phone.interactions.authentication.CodeVerifier
Expand All @@ -32,29 +31,37 @@ import java.io.IOException
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

private const val TAG = "WearOAuthViewModel"

// TODO Add your client id & secret here (for dev purposes only).
// DO NOT MERGE
private const val CLIENT_ID = ""
private const val CLIENT_SECRET = ""

data class ProofKeyCodeExchangeState(
// Status to show on the Wear OS display
val statusCode: Int = R.string.start_auth_flow,
// Dynamic content to show on the Wear OS display
val resultMessage: String = ""
)

/**
* The viewModel that implements the OAuth flow. The method [startAuthFlow] implements the
* different steps of the flow. It first retrieves the OAuth code, uses it to exchange it for an
* access token, and uses the token to retrieve the user's name.
*/
class AuthPKCEViewModel(application: Application) : AndroidViewModel(application) {
// Status to show on the Wear OS display
val status: MutableLiveData<Int> by lazy { MutableLiveData<Int>() }
class AuthPKCEViewModel : ViewModel() {
private val _uiState = MutableStateFlow(ProofKeyCodeExchangeState())
val uiState: StateFlow<ProofKeyCodeExchangeState> = _uiState.asStateFlow()

// Dynamic content to show on the Wear OS display
val result: MutableLiveData<String> by lazy { MutableLiveData<String>() }

private fun showStatus(statusString: Int, resultString: String = "") {
status.postValue(statusString)
result.postValue(resultString)
private fun showStatus(statusCode: Int = R.string.start_auth_flow, resultString: String = "") {
_uiState.value =
_uiState.value.copy(statusCode = statusCode, resultMessage = resultString)
}

/**
Expand All @@ -67,7 +74,7 @@ class AuthPKCEViewModel(application: Application) : AndroidViewModel(application
* the phone. After the user consents on their phone, the wearable app is notified and can
* continue the authorization process.
*/
fun startAuthFlow() {
fun startAuthFlow(context: Context) {
viewModelScope.launch {
val codeVerifier = CodeVerifier()

Expand All @@ -78,16 +85,16 @@ class AuthPKCEViewModel(application: Application) : AndroidViewModel(application
.encodedPath("https://accounts.google.com/o/oauth2/v2/auth")
.appendQueryParameter("scope", "https://www.googleapis.com/auth/userinfo.profile")
.build()
val oauthRequest = OAuthRequest.Builder(getApplication())
val oauthRequest = OAuthRequest.Builder(context)
JohnZoellerG marked this conversation as resolved.
Show resolved Hide resolved
.setAuthProviderUrl(uri)
.setCodeChallenge(CodeChallenge(codeVerifier))
.setClientId(CLIENT_ID)
.build()

// Step 1: Retrieve the OAuth code
showStatus(R.string.status_switch_to_phone)
val code = retrieveOAuthCode(oauthRequest).getOrElse {
showStatus(R.string.status_failed)
showStatus(statusCode = R.string.status_switch_to_phone)
val code = retrieveOAuthCode(oauthRequest, context).getOrElse {
showStatus(statusCode = R.string.status_failed)
return@launch
}

Expand All @@ -114,13 +121,14 @@ class AuthPKCEViewModel(application: Application) : AndroidViewModel(application
* communication with the paired device, where the user can log in.
*/
private suspend fun retrieveOAuthCode(
oauthRequest: OAuthRequest
oauthRequest: OAuthRequest,
context: Context
): Result<String> {
Log.d(TAG, "Authorization requested. Request URL: ${oauthRequest.requestUrl}")

// Wrap the callback-based request inside a coroutine wrapper
return suspendCoroutine { c ->
RemoteAuthClient.create(getApplication()).sendAuthorizationRequest(
RemoteAuthClient.create(context).sendAuthorizationRequest(
request = oauthRequest,
executor = { command -> command?.run() },
clientCallback = object : RemoteAuthClient.Callback() {
Expand Down
Loading