Skip to content
Draft
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
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
43 changes: 23 additions & 20 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,32 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.INTERNET"/>

<application
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: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">
<activity
android:name=".LoginActivity"
android:exported="true">
android:name=".DeviceLoginActivity"
android:exported="false"/>
<activity
android:name=".LoginActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.MAIN"/>

<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>

<activity android:name=".TokenActivity"
android:windowSoftInputMode="stateHidden" />
<activity
android:name=".TokenActivity"
android:windowSoftInputMode="stateHidden"/>
</application>

</manifest>
</manifest>
120 changes: 120 additions & 0 deletions app/src/main/java/io/fusionauth/sdk/DeviceLoginActivity.kt
Original file line number Diff line number Diff line change
@@ -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<View>(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<TextView>(R.id.device_code_code).let {
(it as TextView).text = deviceCodeResponse.user_code
}
findViewById<TextView>(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<ImageView>(R.id.device_code_qr).setImageBitmap(bitmap)
}

displayDeviceCode()

displayLoading("Polling for authorization")

val authState = AuthorizationManager

Check warning

Code scanning / detekt

Property is unused and should be removed.

Private property `authState` is unused.

Check notice

Code scanning / CodeQL

Unread local variable

Variable 'FusionAuthState authState' is never read.
.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)

Check notice

Code scanning / mobsfscan

The App logs information. Sensitive information should never be logged.

The App logs information. Sensitive information should never be logged.
displayError(e.message ?: "Error while authorizing", true)
}
}
}

@MainThread
private fun displayLoading(loadingMessage: String) {
findViewById<View>(R.id.loading_container).visibility = View.VISIBLE
findViewById<View>(R.id.auth_container).visibility = View.GONE

Check failure

Code scanning / mobsfscan

Hidden elements in view can be used to hide data from user. But this data can be leaked.

Hidden elements in view can be used to hide data from user. But this data can be leaked.
findViewById<View>(R.id.error_container).visibility = View.GONE

(findViewById<View>(R.id.loading_description) as TextView).text = loadingMessage
}

@MainThread
private fun displayError(error: String, recoverable: Boolean) {
findViewById<View>(R.id.error_container).visibility = View.VISIBLE
findViewById<View>(R.id.loading_container).visibility = View.GONE
findViewById<View>(R.id.auth_container).visibility = View.GONE

(findViewById<View>(R.id.error_description) as TextView).text = error
findViewById<View>(R.id.retry).visibility = if (recoverable) View.VISIBLE else View.GONE
}

@MainThread
private fun displayAuthOptions() {
findViewById<View>(R.id.device_code_container).visibility = View.GONE
findViewById<View>(R.id.auth_container).visibility = View.VISIBLE
findViewById<View>(R.id.loading_container).visibility = View.GONE
findViewById<View>(R.id.error_container).visibility = View.GONE
}

@MainThread
private fun displayDeviceCode() {
findViewById<View>(R.id.device_code_container).visibility = View.VISIBLE
findViewById<View>(R.id.auth_container).visibility = View.GONE
findViewById<View>(R.id.loading_container).visibility = View.GONE
findViewById<View>(R.id.error_container).visibility = View.GONE
}

companion object {
private const val TAG = "DeviceLoginActivity"
private const val QR_CODE_DIMENSION = 512
}

}
5 changes: 4 additions & 1 deletion app/src/main/java/io/fusionauth/sdk/LoginActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -69,6 +69,9 @@ class LoginActivity : AppCompatActivity() {
findViewById<View>(R.id.start_auth).setOnClickListener {
startAuth()
}
findViewById<View>(R.id.device_login).setOnClickListener {
startActivity(Intent(this, DeviceLoginActivity::class.java))
}

if (AuthorizationManager.oAuth(this@LoginActivity).isCancelled(intent)) {
displayAuthCancelled()
Expand Down
Loading