From 20da349694b5104cf6f51ca39be472d2e9e01f4c Mon Sep 17 00:00:00 2001 From: Colin Frick Date: Thu, 22 Feb 2024 09:35:27 +0100 Subject: [PATCH] feat: add (experimental) Device Authorization Grant --- app/build.gradle.kts | 1 + app/src/main/AndroidManifest.xml | 43 ++-- .../io/fusionauth/sdk/DeviceLoginActivity.kt | 120 ++++++++++ .../java/io/fusionauth/sdk/LoginActivity.kt | 5 +- .../main/res/layout/activity_device_login.xml | 210 ++++++++++++++++++ app/src/main/res/layout/activity_login.xml | 13 +- app/src/main/res/values/strings.xml | 7 + .../fusionauth/mobilesdk/ExperimentalApi.kt | 12 + .../oauth/OAuthAuthorizationService.kt | 136 ++++++++++++ .../oauth/OAuthDeviceAuthorizationResponse.kt | 17 ++ .../mobilesdk/oauth/OAuthErrorResponse.kt | 20 ++ .../mobilesdk/oauth/OAuthTokenResponse.kt | 30 +++ 12 files changed, 588 insertions(+), 26 deletions(-) create mode 100644 app/src/main/java/io/fusionauth/sdk/DeviceLoginActivity.kt create mode 100644 app/src/main/res/layout/activity_device_login.xml create mode 100644 library/src/main/java/io/fusionauth/mobilesdk/ExperimentalApi.kt create mode 100644 library/src/main/java/io/fusionauth/mobilesdk/oauth/OAuthDeviceAuthorizationResponse.kt create mode 100644 library/src/main/java/io/fusionauth/mobilesdk/oauth/OAuthErrorResponse.kt create mode 100644 library/src/main/java/io/fusionauth/mobilesdk/oauth/OAuthTokenResponse.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 711cadf..9878cf2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -55,6 +55,7 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") implementation("androidx.browser:browser:1.7.0") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0") + implementation("com.journeyapps:zxing-android-embedded:4.1.0") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index edc567d..46af155 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,32 +1,35 @@ + xmlns:tools="http://schemas.android.com/tools"> - + + android:allowBackup="true" + android:dataExtractionRules="@xml/data_extraction_rules" + android:fullBackupContent="@xml/backup_rules" + android:icon="@mipmap/ic_launcher" + android:label="@string/app_name_short" + android:roundIcon="@mipmap/ic_launcher_round" + android:supportsRtl="true" + android:theme="@style/AppTheme" + android:usesCleartextTraffic="true" + tools:targetApi="31"> + android:name=".DeviceLoginActivity" + android:exported="false"/> + - + - + - - + - \ No newline at end of file + diff --git a/app/src/main/java/io/fusionauth/sdk/DeviceLoginActivity.kt b/app/src/main/java/io/fusionauth/sdk/DeviceLoginActivity.kt new file mode 100644 index 0000000..6d06af4 --- /dev/null +++ b/app/src/main/java/io/fusionauth/sdk/DeviceLoginActivity.kt @@ -0,0 +1,120 @@ +package io.fusionauth.sdk + +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.annotation.MainThread +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope +import com.google.zxing.BarcodeFormat +import com.journeyapps.barcodescanner.BarcodeEncoder +import io.fusionauth.mobilesdk.AuthorizationManager +import io.fusionauth.mobilesdk.ExperimentalApi +import io.fusionauth.mobilesdk.exceptions.AuthorizationException +import kotlinx.coroutines.launch + +/** + * Demonstrates the usage of the FusionAuth SDK to authorize a user utilizing the Device Authorization + * Grant. This is useful for devices that do not have a browser or other input mechanism. + */ +class DeviceLoginActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(R.layout.activity_device_login) + + findViewById(R.id.device_login).setOnClickListener { + startAuth() + } + + displayAuthOptions() + } + + @OptIn(ExperimentalApi::class) + @MainThread + fun startAuth() { + displayLoading("Starting device authorization request") + + lifecycleScope.launch { + try { + val deviceCodeResponse = AuthorizationManager + .oAuth(this@DeviceLoginActivity) + .deviceAuthorize() + + findViewById(R.id.device_code_code).let { + (it as TextView).text = deviceCodeResponse.user_code + } + findViewById(R.id.device_code_link).let { + (it as TextView).text = deviceCodeResponse.verification_uri + } + + BarcodeEncoder().encodeBitmap( + deviceCodeResponse.verification_uri_complete, + BarcodeFormat.QR_CODE, + QR_CODE_DIMENSION, QR_CODE_DIMENSION + ) + .let { bitmap -> + findViewById(R.id.device_code_qr).setImageBitmap(bitmap) + } + + displayDeviceCode() + + displayLoading("Polling for authorization") + + val authState = AuthorizationManager + .oAuth(this@DeviceLoginActivity) + .getDeviceFusionAuthState(deviceCodeResponse) + + // Is logged in! + startActivity(Intent(this@DeviceLoginActivity, TokenActivity::class.java)) + } catch (e: AuthorizationException) { + Log.e(DeviceLoginActivity.TAG, "Error while authorizing", e) + displayError(e.message ?: "Error while authorizing", true) + } + } + } + + @MainThread + private fun displayLoading(loadingMessage: String) { + findViewById(R.id.loading_container).visibility = View.VISIBLE + findViewById(R.id.auth_container).visibility = View.GONE + findViewById(R.id.error_container).visibility = View.GONE + + (findViewById(R.id.loading_description) as TextView).text = loadingMessage + } + + @MainThread + private fun displayError(error: String, recoverable: Boolean) { + findViewById(R.id.error_container).visibility = View.VISIBLE + findViewById(R.id.loading_container).visibility = View.GONE + findViewById(R.id.auth_container).visibility = View.GONE + + (findViewById(R.id.error_description) as TextView).text = error + findViewById(R.id.retry).visibility = if (recoverable) View.VISIBLE else View.GONE + } + + @MainThread + private fun displayAuthOptions() { + findViewById(R.id.device_code_container).visibility = View.GONE + findViewById(R.id.auth_container).visibility = View.VISIBLE + findViewById(R.id.loading_container).visibility = View.GONE + findViewById(R.id.error_container).visibility = View.GONE + } + + @MainThread + private fun displayDeviceCode() { + findViewById(R.id.device_code_container).visibility = View.VISIBLE + findViewById(R.id.auth_container).visibility = View.GONE + findViewById(R.id.loading_container).visibility = View.GONE + findViewById(R.id.error_container).visibility = View.GONE + } + + companion object { + private const val TAG = "DeviceLoginActivity" + private const val QR_CODE_DIMENSION = 512 + } + +} diff --git a/app/src/main/java/io/fusionauth/sdk/LoginActivity.kt b/app/src/main/java/io/fusionauth/sdk/LoginActivity.kt index 4636482..27be324 100644 --- a/app/src/main/java/io/fusionauth/sdk/LoginActivity.kt +++ b/app/src/main/java/io/fusionauth/sdk/LoginActivity.kt @@ -24,8 +24,8 @@ import androidx.lifecycle.lifecycleScope import com.google.android.material.snackbar.Snackbar import io.fusionauth.mobilesdk.AuthorizationConfiguration import io.fusionauth.mobilesdk.AuthorizationManager -import io.fusionauth.mobilesdk.oauth.OAuthAuthorizeOptions import io.fusionauth.mobilesdk.exceptions.AuthorizationException +import io.fusionauth.mobilesdk.oauth.OAuthAuthorizeOptions import io.fusionauth.mobilesdk.storage.SharedPreferencesStorage import kotlinx.coroutines.launch @@ -69,6 +69,9 @@ class LoginActivity : AppCompatActivity() { findViewById(R.id.start_auth).setOnClickListener { startAuth() } + findViewById(R.id.device_login).setOnClickListener { + startActivity(Intent(this, DeviceLoginActivity::class.java)) + } if (AuthorizationManager.oAuth(this@LoginActivity).isCancelled(intent)) { displayAuthCancelled() diff --git a/app/src/main/res/layout/activity_device_login.xml b/app/src/main/res/layout/activity_device_login.xml new file mode 100644 index 0000000..dfa81dc --- /dev/null +++ b/app/src/main/res/layout/activity_device_login.xml @@ -0,0 +1,210 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +