Backend: UI: Implements authentication.
This commit is contained in:
@@ -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}"
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user