Skip to content

Commit

Permalink
Merge pull request #1114 from JohnZoellerG/main
Browse files Browse the repository at this point in the history
Migrate OAuth PKCE sample to Compose
  • Loading branch information
JohnZoellerG authored May 31, 2024
2 parents d82b5ee + e7dca1b commit f6ea54e
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 149 deletions.
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)
.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

0 comments on commit f6ea54e

Please sign in to comment.