Skip to content

Commit

Permalink
feat: support for OpenID connect login
Browse files Browse the repository at this point in the history
  • Loading branch information
Bnyro committed Nov 10, 2024
1 parent 65758e6 commit 9733983
Show file tree
Hide file tree
Showing 12 changed files with 170 additions and 56 deletions.
19 changes: 17 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,23 @@

<activity
android:name=".ui.activities.SettingsActivity"
android:exported="true"
android:label="@string/settings"
android:screenOrientation="locked" />
android:launchMode="singleTop"
android:screenOrientation="locked">

<intent-filter>
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data
android:host="callback"
android:scheme="${applicationId}" />
</intent-filter>

</activity>

<activity
android:name=".ui.activities.AboutActivity"
Expand Down Expand Up @@ -115,8 +130,8 @@
<activity
android:name=".ui.activities.OfflinePlayerActivity"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
android:launchMode="singleTop"
android:label="@string/player"
android:launchMode="singleTop"
android:supportsPictureInPicture="true" />

<activity
Expand Down
34 changes: 32 additions & 2 deletions app/src/main/java/com/github/libretube/LibreTubeApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@ import android.app.Application
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationManagerCompat
import androidx.work.ExistingPeriodicWorkPolicy
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.helpers.ImageHelper
import com.github.libretube.helpers.NotificationHelper
import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.helpers.ProxyHelper
import com.github.libretube.helpers.ShortcutHelper
import com.github.libretube.util.ExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

class LibreTubeApp : Application() {
override fun onCreate() {
Expand Down Expand Up @@ -42,7 +46,7 @@ class LibreTubeApp : Application() {
/**
* Fetch the image proxy URL for local playlists and the watch history
*/
ProxyHelper.fetchProxyUrl()
fetchInstanceConfig()

/**
* Handler for uncaught exceptions
Expand All @@ -57,6 +61,32 @@ class LibreTubeApp : Application() {
ShortcutHelper.createShortcuts(this)
}

fun fetchInstanceConfig() {
val isAuthSameApi = RetrofitInstance.apiUrl == RetrofitInstance.authUrl

CoroutineScope(Dispatchers.IO).launch {
runCatching {
val config = RetrofitInstance.api.getConfig()
config.imageProxyUrl?.let {
PreferenceHelper.putString(PreferenceKeys.IMAGE_PROXY_URL, it)
}
if (isAuthSameApi) PreferenceHelper
.putStringSet(
PreferenceKeys.INSTANCE_OIDC_PROVIDERS,
config.oidcProviders.toSet()
)
}
if (!isAuthSameApi) runCatching {
val config = RetrofitInstance.authApi.getConfig()
PreferenceHelper
.putStringSet(
PreferenceKeys.INSTANCE_OIDC_PROVIDERS,
config.oidcProviders.toSet()
)
}
}
}

/**
* Initializes the required notification channels for the app.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ import kotlinx.serialization.Serializable
data class PipedConfig(
val donationUrl: String? = null,
val statusPageUrl: String? = null,
val imageProxyUrl: String? = null
val imageProxyUrl: String? = null,
val oidcProviders: List<String> = emptyList()
)
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ object PreferenceKeys {
// Authentications
const val TOKEN = "token"
const val USERNAME = "username"
const val OIDC = "oidc"

// General
const val LANGUAGE = "language"
Expand Down Expand Up @@ -143,6 +144,7 @@ object PreferenceKeys {
const val LAST_WATCHED_FEED_TIME = "last_watched_feed_time"
const val AUTH_PREF_FILE = "auth"
const val IMAGE_PROXY_URL = "image_proxy_url"
const val INSTANCE_OIDC_PROVIDERS = "instance_oidc_providers"
const val SELECTED_CHANNEL_GROUP = "selected_channel_group"
const val SELECTED_DOWNLOAD_SORT_TYPE = "selected_download_sort_type"
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,15 @@ object PreferenceHelper {
return authSettings.getString(PreferenceKeys.TOKEN, "")!!
}

fun setToken(newValue: String) {
authSettings.edit { putString(PreferenceKeys.TOKEN, newValue) }
fun isLoggedInWithOidc(): Boolean {
return authSettings.getBoolean(PreferenceKeys.OIDC, false)
}

fun setToken(token: String, isOidc: Boolean) {
authSettings.edit {
putString(PreferenceKeys.TOKEN, token)
putBoolean(PreferenceKeys.OIDC, isOidc)
}
}

fun getUsername(): String {
Expand Down
14 changes: 0 additions & 14 deletions app/src/main/java/com/github/libretube/helpers/ProxyHelper.kt
Original file line number Diff line number Diff line change
@@ -1,23 +1,9 @@
package com.github.libretube.helpers

import com.github.libretube.api.RetrofitInstance
import com.github.libretube.constants.PreferenceKeys
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull

object ProxyHelper {
fun fetchProxyUrl() {
CoroutineScope(Dispatchers.IO).launch {
runCatching {
RetrofitInstance.api.getConfig().imageProxyUrl?.let {
PreferenceHelper.putString(PreferenceKeys.IMAGE_PROXY_URL, it)
}
}
}
}

fun rewriteUrl(url: String?): String? {
if (url == null) return null

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package com.github.libretube.ui.activities

import android.content.Intent
import android.os.Bundle
import androidx.activity.addCallback
import androidx.fragment.app.Fragment
import androidx.fragment.app.commit
import androidx.fragment.app.replace
import com.github.libretube.R
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.databinding.ActivitySettingsBinding
import com.github.libretube.extensions.toastFromMainThread
import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.ui.preferences.InstanceSettings
import com.github.libretube.ui.preferences.MainSettings
Expand Down Expand Up @@ -42,6 +46,20 @@ class SettingsActivity : BaseActivity() {
handleRedirect()
}

override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)

val sessionId = intent.data?.getQueryParameter("session")
if (sessionId == null) {
this.toastFromMainThread(R.string.error)
return

}

PreferenceHelper.setToken(sessionId, true)
recreate()
}

private fun handleRedirect() {
val redirectKey = intent.extras?.getString(REDIRECT_KEY)

Expand Down
59 changes: 49 additions & 10 deletions app/src/main/java/com/github/libretube/ui/dialogs/LoginDialog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,50 +16,66 @@ import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.obj.Login
import com.github.libretube.api.obj.Token
import com.github.libretube.constants.IntentData
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.databinding.DialogLoginBinding
import com.github.libretube.extensions.TAG
import com.github.libretube.extensions.toastFromMainDispatcher
import com.github.libretube.helpers.ContextHelper
import com.github.libretube.helpers.IntentHelper
import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.ui.preferences.InstanceSettings.Companion.INSTANCE_DIALOG_REQUEST_KEY
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import retrofit2.HttpException
import java.net.URLEncoder
import java.nio.charset.StandardCharsets

class LoginDialog : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val binding = DialogLoginBinding.inflate(layoutInflater)

return MaterialAlertDialogBuilder(requireContext())
val dialogBuilder = MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.login)
.setPositiveButton(R.string.login, null)
.setNegativeButton(R.string.register, null)
.setView(binding.root)

val oidcProviders = PreferenceHelper.getStringSet(PreferenceKeys.INSTANCE_OIDC_PROVIDERS, setOf())
if (oidcProviders.isNotEmpty()) {
dialogBuilder.setNeutralButton(R.string.oidc, null)
}

return dialogBuilder
.show()
.apply {
getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener {
val email = binding.username.text?.toString()
val username = binding.username.text?.toString()
val password = binding.password.text?.toString()

if (!email.isNullOrEmpty() && !password.isNullOrEmpty()) {
signIn(email, password)
if (!username.isNullOrEmpty() && !password.isNullOrEmpty()) {
signIn(username, password)
} else {
Toast.makeText(context, R.string.empty, Toast.LENGTH_SHORT).show()
}
}
getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener {
val email = binding.username.text?.toString().orEmpty()
val username = binding.username.text?.toString().orEmpty()
val password = binding.password.text?.toString().orEmpty()

if (isEmail(email)) {
showPrivacyAlertDialog(email, password)
} else if (email.isNotEmpty() && password.isNotEmpty()) {
signIn(email, password, true)
if (isEmail(username)) {
showPrivacyAlertDialog(username, password)
} else if (username.isNotEmpty() && password.isNotEmpty()) {
signIn(username, password, true)
} else {
Toast.makeText(context, R.string.empty, Toast.LENGTH_SHORT).show()
}
}
getButton(DialogInterface.BUTTON_NEUTRAL)?.setOnClickListener {
showOidcProviderDialog(oidcProviders.toList())
}
}
}

Expand Down Expand Up @@ -94,7 +110,7 @@ class LoginDialog : DialogFragment() {
if (createNewAccount) R.string.registered else R.string.loggedIn
)

PreferenceHelper.setToken(response.token)
PreferenceHelper.setToken(response.token, false)
PreferenceHelper.setUsername(login.username)

withContext(Dispatchers.Main) {
Expand All @@ -118,6 +134,29 @@ class LoginDialog : DialogFragment() {
.show()
}

private fun showOidcProviderDialog(oidcProviders: List<String>) {
var selectedProviderIndex = 0

val appContext = requireContext().applicationContext
val fragmentManager = ContextHelper.unwrapActivity<BaseActivity>(requireContext())
.supportFragmentManager
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.oidc_login)
.setSingleChoiceItems(oidcProviders.toTypedArray(), selectedProviderIndex) { _, selected ->
selectedProviderIndex = selected
}
.setPositiveButton(R.string.login) { _, _ ->
val provider = oidcProviders[selectedProviderIndex]
val redirectUrl = URLEncoder.encode("${appContext.packageName}://callback", StandardCharsets.UTF_8)
val oidcUrl = "${RetrofitInstance.authUrl}/oidc/${provider}/login?redirect=${redirectUrl}"
IntentHelper.openLinkFromHref(appContext, fragmentManager, oidcUrl, forceDefaultOpen = true)

this@LoginDialog.dismiss()
}
.setNegativeButton(R.string.cancel, null)
.show()
}

private fun isEmail(text: String): Boolean {
return Patterns.EMAIL_ADDRESS.toRegex().matches(text)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
class LogoutDialog : DialogFragment() {
@SuppressLint("SetTextI18n")
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val user = PreferenceHelper.getUsername()
val username = PreferenceHelper.getUsername().takeIf { it.isNotEmpty() }

return MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.logout)
.setMessage(getString(R.string.already_logged_in) + " ($user)")
.setMessage(getString(R.string.already_logged_in) + username?.let { " ($it)" }.orEmpty())
.setPositiveButton(R.string.logout) { _, _ ->
Toast.makeText(context, R.string.loggedout, Toast.LENGTH_SHORT).show()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class AdvancedSettings : BasePreferenceFragment() {
PreferenceHelper.clearPreferences()

// clear login token
PreferenceHelper.setToken("")
PreferenceHelper.setToken("", false)

ActivityCompat.recreate(requireActivity())
}
Expand Down
Loading

0 comments on commit 9733983

Please sign in to comment.