Skip to content

Commit

Permalink
Support project based login (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
teelisyys authored Dec 29, 2022
1 parent 03904b9 commit cd3731b
Show file tree
Hide file tree
Showing 8 changed files with 157 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ class InvalidJWTException(message: String) : Throwable(message)
* The token is usually obtained from Speechly Identity Service and can be cached for its expiration period.
*
* @param appId the ID of Speechly application that will be accessed with this token.
* @param projectId the ID of Speechly project that will be accessed with this token.
* @param deviceId Speechly device identifier that is authorised to use this token.
* @param expiresAt the timestamp of token expiration.
* @param authScopes Speechly APIs that can be accessed with this token.
* @param tokenString the token value that should be passed to API when accessing it.
*/
data class AuthToken(
val appId: UUID,
val appId: UUID?,
val projectId: UUID?,
val deviceId: UUID,
val expiresAt: Instant,
val authScopes: Set<AuthScope>,
Expand Down Expand Up @@ -69,7 +71,8 @@ data class AuthToken(

return try {
AuthToken(
appId = UUID.fromString(payload.appId),
appId = payload.appId?.let(UUID::fromString),
projectId = payload.projectId?.let(UUID::fromString),
deviceId = UUID.fromString(payload.deviceId),
expiresAt = Instant.ofEpochSecond(payload.expiresAt),
authScopes = scopes,
Expand Down Expand Up @@ -104,7 +107,8 @@ data class AuthToken(
}

private data class TokenPayload(
val appId: String,
val appId: String? = null,
val projectId: String? = null,
val deviceId: String,

@Json(name = "scope")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.speechly.client.identity

import com.speechly.api.identity.v1.IdentityGrpcKt
import com.speechly.api.identity.v1.LoginRequest
import com.speechly.api.identity.v1.LoginResponse
import com.speechly.identity.v2.LoginRequest
import com.speechly.identity.v2.LoginResponse
import com.speechly.client.grpc.buildChannel
import com.speechly.identity.v2.IdentityAPIGrpcKt
import io.grpc.ManagedChannel
import io.grpc.Status
import java.io.Closeable
Expand Down Expand Up @@ -42,7 +42,7 @@ class GrpcIdentityClient(
private val channel: ManagedChannel,
private val shutdownTimeout: Long = 5
) : IdentityClient {
private val clientStub = IdentityGrpcKt.IdentityCoroutineStub(this.channel)
private val clientStub = IdentityAPIGrpcKt.IdentityAPICoroutineStub(this.channel)

companion object {
/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.speechly.client.identity

import com.speechly.api.identity.v1.LoginRequest
import com.speechly.identity.v2.LoginRequest
import com.speechly.client.cache.CacheService
import com.speechly.identity.v2.ApplicationScope
import com.speechly.identity.v2.ProjectScope
import java.io.Closeable
import java.time.Instant
import java.util.UUID
Expand All @@ -17,6 +19,14 @@ interface IdentityService : Closeable {
* @param deviceId Speechly device ID to use for authentication.
*/
suspend fun authenticate(appId: UUID, deviceId: UUID): AuthToken

/**
* Fetches a new project level authentication token and decodes it into `AuthToken`.
*
* @param projectId Speechly project ID to use for authentication.
* @param deviceId Speechly device ID to use for authentication.
*/
suspend fun authenticateProject(projectId: UUID, deviceId: UUID): AuthToken
}

/**
Expand Down Expand Up @@ -46,9 +56,27 @@ class BasicIdentityService(
}

override suspend fun authenticate(appId: UUID, deviceId: UUID): AuthToken {
val scope = ApplicationScope.newBuilder()
.setAppId(appId.toString())

val request = LoginRequest
.newBuilder()
.setAppId(appId.toString())
.setApplication(scope)
.setDeviceId(deviceId.toString())
.build()

val response = client.login(request)

return AuthToken.fromJWT(response.token)
}

override suspend fun authenticateProject(projectId: UUID, deviceId: UUID): AuthToken {
val scope = ProjectScope.newBuilder()
.setProjectId(projectId.toString())

val request = LoginRequest
.newBuilder()
.setProject(scope)
.setDeviceId(deviceId.toString())
.build()

Expand Down Expand Up @@ -107,6 +135,20 @@ class CachingIdentityService(
return this.reloadToken(appId, deviceId)
}

override suspend fun authenticateProject(projectId: UUID, deviceId: UUID): AuthToken {
// Try to load the token from the cache.
val token = this.loadProjectToken(projectId, deviceId)

// Make sure that cached token exists and won't expire too soon.
val expirationTime = Instant.now().plusSeconds(60)
if (token != null && token.validateExpiry(expirationTime)) {
return token
}

// Otherwise reload the token from the API and update the cached value.
return this.reloadProjectToken(projectId, deviceId)
}

override fun close() {
this.baseService.close()
}
Expand All @@ -121,10 +163,28 @@ class CachingIdentityService(
}
}

private fun loadProjectToken(projectId: UUID, deviceId: UUID): AuthToken? {
val cacheValue = this.cacheService?.loadString(this.makeCacheKey(projectId, deviceId)) ?: return null

return try {
AuthToken.fromJWT(cacheValue)
} catch (_: Throwable) {
null
}
}

private suspend fun reloadToken(appId: UUID, deviceId: UUID): AuthToken {
val token = this.baseService.authenticate(appId, deviceId)

this.cacheService?.storeString(this.makeCacheKey(token.appId, token.deviceId), token.tokenString)
this.cacheService?.storeString(this.makeCacheKey(token.appId!!, token.deviceId), token.tokenString)

return token
}

private suspend fun reloadProjectToken(projectId: UUID, deviceId: UUID): AuthToken {
val token = this.baseService.authenticateProject(projectId, deviceId)

this.cacheService?.storeString(this.makeCacheKey(token.projectId!!, token.deviceId), token.tokenString)

return token
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import io.grpc.stub.MetadataUtils
import java.io.Closeable
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.flow.Flow
import java.util.UUID
import kotlinx.coroutines.ExperimentalCoroutinesApi as ExperimentalCoroutinesApi

/**
Expand All @@ -22,7 +23,7 @@ interface SluClient : Closeable {
* @param streamConfig the configuration of the SLU stream.
*/
@ExperimentalCoroutinesApi
fun stream(authToken: AuthToken, streamConfig: StreamConfig, audioFlow: Flow<ByteArray>): SluStream
fun stream(authToken: AuthToken, streamConfig: StreamConfig, audioFlow: Flow<ByteArray>, contextAppId: UUID?): SluStream
}

/**
Expand Down Expand Up @@ -52,7 +53,7 @@ class GrpcSluClient(
}

@ExperimentalCoroutinesApi
override fun stream(authToken: AuthToken, streamConfig: StreamConfig, audioFlow: Flow<ByteArray>): GrpcSluStream {
override fun stream(authToken: AuthToken, streamConfig: StreamConfig, audioFlow: Flow<ByteArray>, contextAppId: UUID?): GrpcSluStream {
val metadata = Metadata()
metadata.put(
Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER),
Expand All @@ -62,7 +63,8 @@ class GrpcSluClient(
return GrpcSluStream(
MetadataUtils.attachHeaders(this.clientStub, metadata),
streamConfig,
audioFlow
audioFlow,
contextAppId
)
}

Expand Down
34 changes: 23 additions & 11 deletions android-client/src/main/kotlin/com/speechly/client/slu/SluStream.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,26 @@ import com.speechly.api.slu.v1.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import java.io.Closeable
import java.util.UUID

private val startReq: SLURequest = SLURequest
.newBuilder()
.setEvent(
SLUEvent
.newBuilder()
.setEvent(SLUEvent.Event.START)
.build()
).build()
private fun startReq(appId: UUID?): SLURequest {
val event = if (appId == null) {
SLUEvent
.newBuilder()
.setEvent(SLUEvent.Event.START)
.build()
} else {
SLUEvent
.newBuilder()
.setEvent(SLUEvent.Event.START)
.setAppId(appId.toString())
.build()
}
return SLURequest
.newBuilder()
.setEvent(event)
.build()
}

private val stopReq: SLURequest = SLURequest
.newBuilder()
Expand Down Expand Up @@ -65,7 +76,7 @@ data class StreamConfig(
/**
* This class represents an exception thrown when trying to interact with a closed stream.
*/
class StreamClosedException: Throwable("SLU stream is closed")
class StreamClosedException : Throwable("SLU stream is closed")

/**
* A SLU stream implementation backed by Speechly gRPC SLU API.
Expand All @@ -79,7 +90,8 @@ class StreamClosedException: Throwable("SLU stream is closed")
class GrpcSluStream(
clientStub: SLUGrpcKt.SLUCoroutineStub,
streamConfig: StreamConfig,
audioFlow: Flow<ByteArray>
audioFlow: Flow<ByteArray>,
streamAppId: UUID?
// private val responseChannel: Channel<Slu.SLUResponse> = Channel()
) : SluStream {
var responseFlow: Flow<SLUResponse>? = null
Expand All @@ -103,7 +115,7 @@ class GrpcSluStream(
}
}.onStart {
emit(configReq)
emit(startReq)
emit(startReq(streamAppId))
}.onCompletion {
emit(stopReq)
}
Expand Down
Loading

0 comments on commit cd3731b

Please sign in to comment.