Private
Public Access
1
0

Backend: UI: Implements authentication.

This commit is contained in:
2024-03-02 00:38:17 -08:00
parent 222ec84855
commit d0be011053
12 changed files with 193 additions and 26 deletions

View File

@@ -46,6 +46,7 @@ dependencies {
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.google.code.gson:gson:2.9.0'
implementation 'com.auth0.android:jwtdecode:2.0.2'
// Realm
implementation "io.realm.kotlin:library-base:${realm_version}"

View File

@@ -62,7 +62,7 @@ class CachedChatDatabase (private val realmConfig: RealmConfiguration) {
// Flow for watching for message changes for a given conversation
fun messagesChanged(conversation: ModelConversation): Flow<List<ModelMessage>> {
return realm.query(Conversation::class, "guid == '${conversation.guid}'")
return realm.query(Conversation::class, "guid == $0", conversation.guid)
.find()
.first()
.messages
@@ -142,7 +142,7 @@ class CachedChatDatabase (private val realmConfig: RealmConfiguration) {
}
private fun getManagedConversationByGuid(guid: GUID): Conversation {
return realm.query(Conversation::class, "guid == '$guid'")
return realm.query(Conversation::class, "guid == $0", guid)
.find()
.first()
}

View File

@@ -1,12 +1,19 @@
package net.buzzert.kordophone.backend.server
import com.google.gson.Gson
import kotlinx.coroutines.runBlocking
import okhttp3.Authenticator
import okhttp3.HttpUrl
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.Route
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.create
import java.net.URL
interface APIClient {
@@ -18,12 +25,86 @@ interface APIClient {
): WebSocket
}
class RetrofitAPIClient(private val baseURL: URL): APIClient {
data class Authentication (
val username: String,
val password: String,
)
class TokenStore(val authentication: Authentication) {
var authenticationToken: String? = null
}
class AuthenticationInterceptor(
val tokenStore: TokenStore
): Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
// If empty, allow the 401 to occur so we renew our token.
val token = tokenStore.authenticationToken ?:
return chain.proceed(chain.request())
val newRequest = chain.request().newBuilder()
.header("Authorization", "Bearer $token")
.build()
return chain.proceed(newRequest)
}
}
class TokenAuthenticator(
private val tokenStore: TokenStore,
private val baseURL: URL
) : Authenticator {
private val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(baseURL)
.addConverterFactory(GsonConverterFactory.create())
.build()
private val apiInterface: APIInterface
get() = retrofit.create(APIInterface::class.java)
override fun authenticate(route: Route?, response: Response): Request? {
// Fetch new token
val request = AuthenticationRequest(
username = tokenStore.authentication.username,
password = tokenStore.authentication.password
)
val token = runBlocking {
apiInterface.authenticate(request).body()
}
if (token == null) {
// Auth failure.
// TODO: How to bubble this up?
return null
}
// Update token store
tokenStore.authenticationToken = token.serializedToken
return response.request().newBuilder()
.header("Authorization", "Bearer ${token.serializedToken}")
.build()
}
}
class RetrofitAPIClient(
private val baseURL: URL,
private val authentication: Authentication,
): APIClient {
private val tokenStore: TokenStore = TokenStore(authentication)
private val client: OkHttpClient = OkHttpClient.Builder()
.addInterceptor(AuthenticationInterceptor(tokenStore))
.authenticator(TokenAuthenticator(tokenStore, baseURL))
.build()
private val retrofit: Retrofit = Retrofit.Builder()
.client(client)
.baseUrl(baseURL)
.addConverterFactory(GsonConverterFactory.create())
.build()
override fun getAPIInterface(): APIInterface {
return retrofit.create(APIInterface::class.java)
}

View File

@@ -1,5 +1,6 @@
package net.buzzert.kordophone.backend.server
import com.auth0.android.jwt.JWT
import com.google.gson.annotations.SerializedName
import net.buzzert.kordophone.backend.model.Conversation
import net.buzzert.kordophone.backend.model.GUID
@@ -30,6 +31,23 @@ data class SendMessageResponse(
val sentMessageGUID: String,
)
data class AuthenticationRequest(
@SerializedName("username")
val username: String,
@SerializedName("password")
val password: String,
)
data class AuthenticationResponse(
@SerializedName("jwt")
val serializedToken: String,
) {
fun decodeToken(): JWT {
return JWT(serializedToken)
}
}
interface APIInterface {
@GET("/version")
suspend fun getVersion(): ResponseBody
@@ -50,6 +68,9 @@ interface APIInterface {
@POST("/markConversation")
suspend fun markConversation(@Query("guid") conversationGUID: String): Response<Void>
@POST("/authenticate")
suspend fun authenticate(@Body request: AuthenticationRequest): Response<AuthenticationResponse>
}
class ResponseDecodeError(val response: ResponseBody): Exception(response.string())

View File

@@ -32,7 +32,7 @@ const val REPO_LOG: String = "ChatRepository"
const val CONVERSATION_MESSAGE_SYNC_COUNT = 10
class ChatRepository(
private val apiClient: APIClient,
apiClient: APIClient,
private val database: CachedChatDatabase,
) {
sealed class Error {
@@ -84,14 +84,26 @@ class ChatRepository(
val requestGuid: GUID,
)
private val apiInterface = apiClient.getAPIInterface()
private var apiInterface = apiClient.getAPIInterface()
private val outgoingMessageQueue: ArrayBlockingQueue<OutgoingMessageInfo> = ArrayBlockingQueue(16)
private var outgoingMessageThread: Thread? = null
private val _messageDeliveredChannel = MutableSharedFlow<MessageDeliveredEvent>()
private val _errorEncounteredChannel = MutableSharedFlow<Error>()
private val updateMonitor = UpdateMonitor(apiClient)
private var updateMonitor = UpdateMonitor(apiClient)
private var updateWatchJob: Job? = null
private var updateWatchScope: CoroutineScope? = null
fun updateAPIClient(client: APIClient) {
this.apiInterface = client.getAPIInterface()
this.updateMonitor = UpdateMonitor(client)
// Restart update watch job, if necessary.
if (this.updateWatchJob != null) {
stopWatchingForUpdates()
beginWatchingForUpdates(updateWatchScope!!)
}
}
suspend fun getVersion(): String {
return apiInterface.getVersion().string()
@@ -111,6 +123,7 @@ class ChatRepository(
}
}
updateWatchScope = scope
updateMonitor.beginMonitoringUpdates()
}

View File

@@ -14,6 +14,8 @@ import net.buzzert.kordophone.backend.model.Message
import net.buzzert.kordophone.backend.model.UpdateItem
import net.buzzert.kordophone.backend.server.APIClient
import net.buzzert.kordophone.backend.server.APIInterface
import net.buzzert.kordophone.backend.server.AuthenticationRequest
import net.buzzert.kordophone.backend.server.AuthenticationResponse
import net.buzzert.kordophone.backend.server.SendMessageRequest
import net.buzzert.kordophone.backend.server.SendMessageResponse
import net.buzzert.kordophone.backend.server.authenticatedWebSocketURL
@@ -257,4 +259,15 @@ class MockServerInterface(private val server: MockServer): APIInterface {
server.markConversationAsRead(conversationGUID)
return Response.success(null)
}
override suspend fun authenticate(request: AuthenticationRequest): Response<AuthenticationResponse> {
// Anything goes!
val response = AuthenticationResponse(
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." +
"eyJ1c2VybmFtZSI6InRlc3QiLCJleHAiOjE3MDk3OTQ5NjB9." +
"82UcI1gB4eARmgrKwAY6JnbEdWLXou1GWp29scnUhi8"
)
return Response.success(response)
}
}