2021-07-23 14:10:13 +02:00
|
|
|
package audio.funkwhale.ffa.utils
|
|
|
|
|
|
|
|
import android.app.Activity
|
|
|
|
import android.content.Context
|
|
|
|
import android.content.Intent
|
|
|
|
import android.net.Uri
|
2021-08-09 20:04:33 +02:00
|
|
|
import android.util.Log
|
2021-07-23 14:10:13 +02:00
|
|
|
import com.github.kittinunf.fuel.Fuel
|
2021-08-03 10:28:17 +02:00
|
|
|
import com.github.kittinunf.fuel.core.FuelError
|
2021-07-23 14:10:13 +02:00
|
|
|
import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult
|
|
|
|
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
|
|
|
import com.github.kittinunf.fuel.gson.jsonBody
|
|
|
|
import com.github.kittinunf.result.Result
|
|
|
|
import com.preference.PowerPreference
|
|
|
|
import kotlinx.coroutines.runBlocking
|
2021-09-09 09:56:15 +02:00
|
|
|
import net.openid.appauth.AuthState
|
|
|
|
import net.openid.appauth.AuthorizationException
|
|
|
|
import net.openid.appauth.AuthorizationRequest
|
|
|
|
import net.openid.appauth.AuthorizationResponse
|
|
|
|
import net.openid.appauth.AuthorizationService
|
|
|
|
import net.openid.appauth.AuthorizationServiceConfiguration
|
|
|
|
import net.openid.appauth.ClientSecretPost
|
|
|
|
import net.openid.appauth.RegistrationRequest
|
|
|
|
import net.openid.appauth.RegistrationResponse
|
|
|
|
import net.openid.appauth.ResponseTypeValues
|
2021-07-23 14:10:13 +02:00
|
|
|
|
|
|
|
fun AuthState.save() {
|
|
|
|
PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).apply {
|
2021-08-09 20:04:33 +02:00
|
|
|
setString("state", jsonSerializeString())
|
2021-07-23 14:10:13 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-02 13:24:12 +02:00
|
|
|
class AuthorizationServiceFactory {
|
|
|
|
|
|
|
|
fun create(context: Context): AuthorizationService {
|
|
|
|
return AuthorizationService(context)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-09 07:08:47 +02:00
|
|
|
class OAuth(private val authorizationServiceFactory: AuthorizationServiceFactory) {
|
2021-07-30 10:57:49 +02:00
|
|
|
|
|
|
|
companion object {
|
|
|
|
private val REDIRECT_URI =
|
|
|
|
Uri.parse("urn:/audio.funkwhale.funkwhale-android/oauth/callback")
|
|
|
|
}
|
|
|
|
|
2021-07-23 14:10:13 +02:00
|
|
|
data class App(val client_id: String, val client_secret: String)
|
|
|
|
|
2021-08-09 07:08:47 +02:00
|
|
|
fun tryState(): AuthState? {
|
2021-07-30 10:57:49 +02:00
|
|
|
|
|
|
|
val savedState = PowerPreference
|
|
|
|
.getFileByName(AppContext.PREFS_CREDENTIALS)
|
|
|
|
.getString("state")
|
|
|
|
|
|
|
|
return if (savedState != null && savedState.isNotEmpty()) {
|
|
|
|
return AuthState.jsonDeserialize(savedState)
|
|
|
|
} else {
|
|
|
|
null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-23 09:17:06 +02:00
|
|
|
fun state(): AuthState {
|
|
|
|
return tryState() ?: throw IllegalStateException("Couldn't find saved state")
|
|
|
|
}
|
2021-07-23 14:10:13 +02:00
|
|
|
|
2021-08-09 07:08:47 +02:00
|
|
|
fun isAuthorized(context: Context): Boolean {
|
2021-07-23 14:10:13 +02:00
|
|
|
val state = tryState()
|
2021-09-09 09:56:15 +02:00
|
|
|
return (
|
|
|
|
if (state != null) {
|
|
|
|
state.validAuthorization() || refreshAccessToken(state, context)
|
|
|
|
} else {
|
|
|
|
false
|
|
|
|
}
|
2022-08-26 14:06:41 +02:00
|
|
|
)
|
|
|
|
.also { it.logInfo("isAuthorized()") }
|
2021-07-23 14:10:13 +02:00
|
|
|
}
|
|
|
|
|
2021-08-26 08:33:12 +02:00
|
|
|
private fun AuthState.validAuthorization() = this.isAuthorized && !this.needsTokenRefresh
|
|
|
|
|
2021-08-09 07:08:47 +02:00
|
|
|
fun tryRefreshAccessToken(context: Context): Boolean {
|
2021-07-23 14:10:13 +02:00
|
|
|
tryState()?.let { state ->
|
2021-08-23 09:17:06 +02:00
|
|
|
return if (state.needsTokenRefresh && state.refreshToken != null) {
|
2021-08-26 08:33:12 +02:00
|
|
|
refreshAccessToken(state, context)
|
2021-08-23 09:17:06 +02:00
|
|
|
} else {
|
|
|
|
state.isAuthorized
|
2023-12-16 02:45:47 +01:00
|
|
|
}
|
2021-08-02 13:24:12 +02:00
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2021-08-22 09:12:57 +02:00
|
|
|
fun refreshAccessToken(context: Context): Boolean {
|
2021-08-26 08:33:12 +02:00
|
|
|
return tryState()?.let { refreshAccessToken(it, context) } ?: false
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun refreshAccessToken(state: AuthState, context: Context): Boolean {
|
2021-08-22 09:12:57 +02:00
|
|
|
Log.i("OAuth", "refreshAccessToken()")
|
2021-08-26 08:33:12 +02:00
|
|
|
return if (state.refreshToken != null) {
|
2021-08-22 09:12:57 +02:00
|
|
|
val refreshRequest = state.createTokenRefreshRequest()
|
|
|
|
val auth = ClientSecretPost(state.clientSecret)
|
2022-06-11 16:37:38 +02:00
|
|
|
val refreshService = service(context)
|
2021-08-22 09:12:57 +02:00
|
|
|
runBlocking {
|
2022-06-11 16:37:38 +02:00
|
|
|
refreshService.performTokenRequest(refreshRequest, auth) { response, e ->
|
|
|
|
if (e != null) {
|
2022-08-26 14:06:41 +02:00
|
|
|
Log.e("OAuth", "performTokenRequest failed: $e")
|
2022-06-11 16:37:38 +02:00
|
|
|
Log.e("OAuth", Log.getStackTraceString(e))
|
2023-12-16 02:55:07 +01:00
|
|
|
EventBus.send(Event.LogOut)
|
2022-06-11 16:37:38 +02:00
|
|
|
} else {
|
|
|
|
state.apply {
|
|
|
|
Log.i("OAuth", "applying new authState")
|
|
|
|
update(response, e)
|
|
|
|
save()
|
|
|
|
}
|
2021-08-22 09:12:57 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-06-11 16:37:38 +02:00
|
|
|
refreshService.dispose()
|
2021-08-22 09:12:57 +02:00
|
|
|
true
|
|
|
|
} else {
|
|
|
|
false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-09 07:08:47 +02:00
|
|
|
fun init(hostname: String): AuthState {
|
2021-08-02 13:24:12 +02:00
|
|
|
return AuthState(
|
|
|
|
AuthorizationServiceConfiguration(
|
|
|
|
Uri.parse("$hostname/authorize"),
|
|
|
|
Uri.parse("$hostname/api/v1/oauth/token/"),
|
|
|
|
Uri.parse("$hostname/api/v1/oauth/apps/")
|
|
|
|
)
|
2021-08-09 07:08:47 +02:00
|
|
|
).also {
|
|
|
|
it.save()
|
|
|
|
}
|
2021-07-23 14:10:13 +02:00
|
|
|
}
|
|
|
|
|
2021-08-09 07:08:47 +02:00
|
|
|
fun service(context: Context): AuthorizationService =
|
2021-08-02 13:24:12 +02:00
|
|
|
authorizationServiceFactory.create(context)
|
2021-07-23 14:10:13 +02:00
|
|
|
|
2021-08-10 14:54:37 +02:00
|
|
|
fun register(authState: AuthState? = null, callback: () -> Unit): FuelResult {
|
2021-08-02 13:24:12 +02:00
|
|
|
(authState ?: state()).authorizationServiceConfiguration?.let { config ->
|
2021-08-10 14:54:37 +02:00
|
|
|
|
|
|
|
val (_, _, result: Result<App, FuelError>) = runBlocking {
|
|
|
|
Fuel.post(config.registrationEndpoint.toString())
|
2021-07-23 14:10:13 +02:00
|
|
|
.header("Content-Type", "application/json")
|
|
|
|
.jsonBody(registrationBody())
|
|
|
|
.awaitObjectResponseResult(gsonDeserializerOf(App::class.java))
|
2021-08-10 14:54:37 +02:00
|
|
|
}
|
2021-07-23 14:10:13 +02:00
|
|
|
|
2021-08-10 14:54:37 +02:00
|
|
|
when (result) {
|
|
|
|
is Result.Success -> {
|
|
|
|
Log.i("OAuth", "OAuth client app created.")
|
|
|
|
val app = result.get()
|
2021-07-23 14:10:13 +02:00
|
|
|
|
2021-08-10 14:54:37 +02:00
|
|
|
val response = RegistrationResponse.Builder(registration()!!)
|
|
|
|
.setClientId(app.client_id)
|
|
|
|
.setClientSecret(app.client_secret)
|
|
|
|
.setClientIdIssuedAt(0)
|
|
|
|
.setClientSecretExpiresAt(null)
|
|
|
|
.build()
|
2021-07-23 14:10:13 +02:00
|
|
|
|
2021-08-10 14:54:37 +02:00
|
|
|
state().apply {
|
|
|
|
update(response)
|
|
|
|
save()
|
|
|
|
callback()
|
|
|
|
return FuelResult.ok()
|
2021-07-23 14:10:13 +02:00
|
|
|
}
|
2021-08-10 14:54:37 +02:00
|
|
|
}
|
2021-07-23 14:10:13 +02:00
|
|
|
|
2021-08-10 14:54:37 +02:00
|
|
|
is Result.Failure -> {
|
|
|
|
Log.i(
|
|
|
|
"OAuth", "Couldn't register client application ${result.error.formatResponseMessage()}"
|
|
|
|
)
|
|
|
|
return FuelResult.from(result)
|
2021-07-23 14:10:13 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-08-10 14:54:37 +02:00
|
|
|
Log.i("OAuth", "Missing AuthorizationServiceConfiguration")
|
|
|
|
return FuelResult.failure()
|
2021-07-23 14:10:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private fun registrationBody(): Map<String, String> {
|
|
|
|
return mapOf(
|
|
|
|
"name" to "Funkwhale for Android (${android.os.Build.MODEL})",
|
|
|
|
"redirect_uris" to REDIRECT_URI.toString(),
|
|
|
|
"scopes" to "read write"
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-08-26 23:19:53 +02:00
|
|
|
fun authorizeIntent(activity: Activity): Intent? {
|
2021-08-03 10:28:17 +02:00
|
|
|
val authService = service(activity)
|
2022-08-26 23:19:53 +02:00
|
|
|
return authorizationRequest()?.let { it ->
|
|
|
|
authService.getAuthorizationRequestIntent(it)
|
2021-07-23 14:10:13 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-09 07:08:47 +02:00
|
|
|
fun exchange(
|
2021-08-03 10:28:17 +02:00
|
|
|
context: Context,
|
2021-07-30 10:57:49 +02:00
|
|
|
authorization: Intent,
|
2021-08-13 10:55:26 +02:00
|
|
|
success: () -> Unit
|
2021-07-30 10:57:49 +02:00
|
|
|
) {
|
2021-07-23 14:10:13 +02:00
|
|
|
state().let { state ->
|
|
|
|
state.apply {
|
|
|
|
update(
|
|
|
|
AuthorizationResponse.fromIntent(authorization),
|
|
|
|
AuthorizationException.fromIntent(authorization)
|
|
|
|
)
|
|
|
|
save()
|
|
|
|
}
|
|
|
|
|
|
|
|
AuthorizationResponse.fromIntent(authorization)?.let {
|
|
|
|
val auth = ClientSecretPost(state().clientSecret)
|
2022-06-11 16:37:38 +02:00
|
|
|
val requestService = service(context)
|
|
|
|
|
|
|
|
requestService.performTokenRequest(it.createTokenExchangeRequest(), auth) { response, e ->
|
|
|
|
if (e != null) {
|
2022-08-26 14:06:41 +02:00
|
|
|
Log.e("FFA", "performTokenRequest failed: $e")
|
2022-06-11 16:37:38 +02:00
|
|
|
Log.e("FFA", Log.getStackTraceString(e))
|
|
|
|
} else {
|
|
|
|
state.apply {
|
2022-08-26 14:06:41 +02:00
|
|
|
update(response, e)
|
|
|
|
save()
|
|
|
|
}
|
2022-06-11 16:37:38 +02:00
|
|
|
}
|
2021-07-23 14:10:13 +02:00
|
|
|
|
|
|
|
if (response != null) success()
|
2021-08-13 10:55:26 +02:00
|
|
|
else Log.e("FFA", "performTokenRequest() not successful")
|
2021-07-23 14:10:13 +02:00
|
|
|
}
|
2022-06-11 16:37:38 +02:00
|
|
|
requestService.dispose()
|
2021-07-23 14:10:13 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun registration() =
|
|
|
|
state().authorizationServiceConfiguration?.let { config ->
|
|
|
|
RegistrationRequest.Builder(config, listOf(REDIRECT_URI)).build()
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun authorizationRequest() = state().let { state ->
|
|
|
|
state.authorizationServiceConfiguration?.let { config ->
|
|
|
|
AuthorizationRequest.Builder(
|
|
|
|
config,
|
|
|
|
state.lastRegistrationResponse?.clientId ?: "",
|
|
|
|
ResponseTypeValues.CODE,
|
|
|
|
REDIRECT_URI
|
|
|
|
)
|
|
|
|
.setScopes("read", "write")
|
|
|
|
.build()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|