funkwhale-app-android/app/src/main/java/audio/funkwhale/ffa/utils/OAuth.kt

250 lines
7.2 KiB
Kotlin
Raw Normal View History

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
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 {
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
}
2021-08-02 13:24:12 +02:00
}
return false
}
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 {
Log.i("OAuth", "refreshAccessToken()")
2021-08-26 08:33:12 +02:00
return if (state.refreshToken != null) {
val refreshRequest = state.createTokenRefreshRequest()
val auth = ClientSecretPost(state.clientSecret)
2022-06-11 16:37:38 +02:00
val refreshService = service(context)
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))
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()
}
}
}
}
2022-06-11 16:37:38 +02:00
refreshService.dispose()
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
fun register(authState: AuthState? = null, callback: () -> Unit): FuelResult {
2021-08-02 13:24:12 +02:00
(authState ?: state()).authorizationServiceConfiguration?.let { config ->
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-07-23 14:10:13 +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
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
state().apply {
update(response)
save()
callback()
return FuelResult.ok()
2021-07-23 14:10:13 +02:00
}
}
2021-07-23 14:10:13 +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
}
}
}
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"
)
}
fun authorizeIntent(activity: Activity): Intent? {
2021-08-03 10:28:17 +02:00
val authService = service(activity)
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,
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()
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()
}
}
}