Skip to content

Commit 8ddb21f

Browse files
authored
Create authentication snippets (#745)
* Create authentication snippets * Apply Spotless
1 parent 4479663 commit 8ddb21f

File tree

10 files changed

+430
-6
lines changed

10 files changed

+430
-6
lines changed

gradle/libs.versions.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
accompanist = "0.36.0"
33
activityKtx = "1.12.1"
44
android-googleid = "1.1.1"
5-
androidGradlePlugin = "8.13.1"
5+
androidGradlePlugin = "8.13.2"
66
androidx-activity-compose = "1.12.1"
77
androidx-appcompat = "1.7.0"
88
androidx-compose-bom = "2025.12.00"
@@ -96,6 +96,7 @@ wearOngoing = "1.1.0"
9696
wearToolingPreview = "1.0.0"
9797
webkit = "1.14.0"
9898
wearPhoneInteractions = "1.1.0"
99+
wearRemoteInteractions = "1.1.0"
99100

100101
[libraries]
101102
accompanist-adaptive = "com.google.accompanist:accompanist-adaptive:0.37.3"
@@ -237,6 +238,7 @@ validator-push = { module = "com.google.android.wearable.watchface.validator:val
237238
wear-compose-material = { module = "androidx.wear.compose:compose-material", version.ref = "wearComposeMaterial" }
238239
wear-compose-material3 = { module = "androidx.wear.compose:compose-material3", version.ref = "wearComposeMaterial3" }
239240
androidx-wear-phone-interactions = { group = "androidx.wear", name = "wear-phone-interactions", version.ref = "wearPhoneInteractions" }
241+
androidx-wear-remote-interactions = { group = "androidx.wear", name = "wear-remote-interactions", version.ref = "wearRemoteInteractions" }
240242

241243
[plugins]
242244
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }

wear/.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
/build
1+
/build
2+
local.properties

wear/build.gradle.kts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ android {
3838
}
3939
kotlin {
4040
jvmToolchain(21)
41+
compilerOptions {
42+
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21)
43+
}
4144
}
4245

4346
buildFeatures {
@@ -49,10 +52,6 @@ android {
4952
excludes += "/META-INF/{AL2.0,LGPL2.1}"
5053
}
5154
}
52-
kotlinOptions {
53-
jvmTarget = "21"
54-
}
55-
5655
testOptions {
5756
unitTests {
5857
isIncludeAndroidResources = true
@@ -62,9 +61,14 @@ android {
6261

6362
dependencies {
6463
implementation(libs.androidx.core.ktx)
64+
implementation(libs.androidx.credentials)
65+
implementation((libs.androidx.credentials.play.services.auth))
6566
implementation(libs.androidx.media3.exoplayer)
6667
implementation(libs.androidx.media3.ui)
6768
implementation(libs.androidx.wear.input)
69+
implementation(libs.androidx.wear.phone.interactions)
70+
implementation(libs.android.identity.googleid)
71+
implementation(libs.androidx.wear.remote.interactions)
6872
val composeBom = platform(libs.androidx.compose.bom)
6973
implementation(composeBom)
7074
androidTestImplementation(composeBom)

wear/src/main/AndroidManifest.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,18 @@
376376
</service>
377377
<!-- [END android_wear_datalayerlistener_intent_filter] -->
378378

379+
<service
380+
android:name=".snippets.datalayer.AuthDataListenerService"
381+
android:exported="true">
382+
<intent-filter>
383+
<action android:name="com.google.android.gms.wearable.DATA_CHANGED" />
384+
<data
385+
android:scheme="wear"
386+
android:host="*"
387+
android:pathPrefix="/auth" />
388+
</intent-filter>
389+
</service>
390+
379391
</application>
380392

381393
</manifest>
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/*
2+
* Copyright 2025 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example.wear.snippets.auth
18+
19+
import android.app.Activity
20+
import android.content.Context
21+
import android.os.Bundle
22+
import androidx.credentials.Credential
23+
import androidx.credentials.CredentialManager
24+
import androidx.credentials.CustomCredential
25+
import androidx.credentials.GetCredentialRequest
26+
import androidx.credentials.GetCredentialResponse
27+
import androidx.credentials.GetPasswordOption
28+
import androidx.credentials.GetPublicKeyCredentialOption
29+
import androidx.credentials.PasswordCredential
30+
import androidx.credentials.PublicKeyCredential
31+
import androidx.credentials.exceptions.GetCredentialCancellationException
32+
import com.google.android.libraries.identity.googleid.GetGoogleIdOption
33+
34+
/**
35+
* Handles authentication operations using the Android Credential Manager API.
36+
*
37+
* This class interacts with an [AuthenticationServer] to facilitate sign-in processes
38+
* using Passkeys, Passwords, and Sign-In With Google credentials.
39+
*
40+
* @param context The Android [Context] used to create the [CredentialManager].
41+
* @param authenticationServer The [AuthenticationServer] responsible for handling authentication requests.
42+
*/
43+
class CredentialManagerAuthenticator(
44+
applicationContext: Context,
45+
private val authenticationServer: AuthenticationServer,
46+
) {
47+
private val credentialManager: CredentialManager = CredentialManager.create(applicationContext)
48+
49+
internal suspend fun signInWithCredentialManager(activity: Activity): Boolean {
50+
// [START android_wear_credential_manager_secondary_fallback]
51+
try {
52+
val getCredentialResponse: GetCredentialResponse =
53+
credentialManager.getCredential(activity, createGetCredentialRequest())
54+
return authenticate(getCredentialResponse.credential)
55+
} catch (_: GetCredentialCancellationException) {
56+
navigateToSecondaryAuthentication()
57+
}
58+
// [END android_wear_credential_manager_secondary_fallback]
59+
return false
60+
}
61+
62+
/**
63+
* Creates a [GetCredentialRequest] with standard Wear Credential types.
64+
*
65+
* @return A configured [GetCredentialRequest] ready to be used with [CredentialManager.getCredential].
66+
*/
67+
private fun createGetCredentialRequest(): GetCredentialRequest {
68+
return GetCredentialRequest(
69+
credentialOptions = listOf(
70+
GetPublicKeyCredentialOption(authenticationServer.getPublicKeyRequestOptions()),
71+
GetPasswordOption(),
72+
GetGoogleIdOption.Builder()
73+
.setServerClientId("<Your Google Sign in Server Client ID here.").build(),
74+
),
75+
)
76+
}
77+
78+
/**
79+
* Routes the credential received from `getCredential` to the appropriate authentication
80+
* type handler on the [AuthenticationServer].
81+
*
82+
* @param credential The selected cre
83+
* @return `true` if the credential was successfully processed and authenticated, else 'false'.
84+
*/
85+
private fun authenticate(credential: Credential): Boolean {
86+
when (credential) {
87+
is PublicKeyCredential -> {
88+
return authenticationServer.loginWithPasskey(credential.authenticationResponseJson)
89+
}
90+
91+
is PasswordCredential -> {
92+
return authenticationServer.loginWithPassword(
93+
credential.id,
94+
credential.password,
95+
)
96+
}
97+
98+
is CustomCredential -> {
99+
return authenticationServer.loginWithCustomCredential(
100+
credential.type,
101+
credential.data,
102+
)
103+
}
104+
105+
else -> {
106+
return false
107+
}
108+
}
109+
}
110+
}
111+
112+
/** Placeholder authentication server would make network calls to your authentication server.*/
113+
class AuthenticationServer {
114+
115+
/** Retrieves the public key credential request options from the authentication server.*/
116+
internal fun getPublicKeyRequestOptions(): String {
117+
return "result of network call"
118+
}
119+
120+
fun loginWithPasskey(passkeyResponseJSON: String): Boolean {
121+
return true
122+
}
123+
124+
fun loginWithPassword(username: String, password: String): Boolean {
125+
return true
126+
}
127+
128+
fun loginWithCustomCredential(type: String, data: Bundle): Boolean {
129+
return true
130+
}
131+
}
132+
133+
/** placeholder navigation function. */
134+
fun navigateToSecondaryAuthentication() {
135+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright 2025 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example.wear.snippets.auth
18+
19+
import android.content.Context
20+
import android.content.Intent
21+
import android.net.Uri
22+
import androidx.wear.remote.interactions.RemoteActivityHelper
23+
24+
class DeviceGrantManager(private val context: Context) {
25+
26+
/** Executes the full Device Grant flow. */
27+
suspend fun startAuthFlow(): Result<String> {
28+
// 1: Get device info from server
29+
val deviceVerificationInfo = getFakeVerificationInfo()
30+
31+
// 2: Show user code UI and open URL on phone
32+
verifyDeviceAuthGrant(deviceVerificationInfo.verificationUri)
33+
34+
// 3: Fetch for the DAG token
35+
val token = fetchToken(deviceVerificationInfo.deviceCode)
36+
37+
// Step 3: Use token to get profile
38+
return retrieveUserProfile(token)
39+
}
40+
41+
// The response data when retrieving the verification
42+
data class VerificationInfo(
43+
val verificationUri: String,
44+
val userCode: String,
45+
val deviceCode: String,
46+
)
47+
48+
/* A fake server call to retrieve */
49+
private fun getFakeVerificationInfo(): VerificationInfo {
50+
// An example of a verificationURI w
51+
return VerificationInfo(
52+
"your client backend",
53+
userCode = "placeholderUser",
54+
deviceCode = "myDeviceCode",
55+
)
56+
}
57+
58+
// [START android_wear_auth_oauth_dag_authorize_device]
59+
// Request access from the authorization server and receive Device Authorization Response.
60+
private fun verifyDeviceAuthGrant(verificationUri: String) {
61+
RemoteActivityHelper(context).startRemoteActivity(
62+
Intent(Intent.ACTION_VIEW).apply {
63+
addCategory(Intent.CATEGORY_BROWSABLE)
64+
data = Uri.parse(verificationUri)
65+
},
66+
null
67+
)
68+
}
69+
// [END android_wear_auth_oauth_dag_authorize_device]
70+
71+
private fun fetchToken(deviceCode: String): Result<String> {
72+
return Result.success("placeholderToken")
73+
}
74+
75+
private fun retrieveUserProfile(token: Result<String>): Result<String> {
76+
return Result.success("placeholderProfile")
77+
}
78+
}

0 commit comments

Comments
 (0)