Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
build/
.gradle/
.idea
/local.properties
.shade-config.properties
14 changes: 14 additions & 0 deletions auth/src/main/kotlin/inkapplications/shade/auth/HueAuthApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ package inkapplications.shade.auth

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import inkapplications.shade.constructs.HueError
import inkapplications.shade.constructs.HueResponse
import inkapplications.shade.serialization.converter.FirstInCollection
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Path

/**
* Hue Bridge Authentication endpoints.
Expand All @@ -27,6 +31,16 @@ internal interface HueAuthApi {
@POST("api/")
@FirstInCollection
suspend fun createToken(@Body devicetype: DeviceType): AuthToken

/**
* Validate token.
*
* Send request to a non-existing endpoint to validate token based on error type:
* Invalid token returns error type 1 (unauthorized user)
* Valid token returns error type 4 (method not available)
*/
@GET("api/{token}/connected")
suspend fun validateToken(@Path("token") token: String): HueResponse<HueError>
}

/**
Expand Down
33 changes: 32 additions & 1 deletion auth/src/main/kotlin/inkapplications/shade/auth/ShadeAuth.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,16 @@ package inkapplications.shade.auth
import inkapplications.shade.constructs.ErrorCodes
import inkapplications.shade.constructs.ShadeApiError
import inkapplications.shade.constructs.ShadeException
import inkapplications.shade.constructs.throwOnFailure
import inkapplications.shade.serialization.parse
import kotlinx.coroutines.delay
import retrofit2.HttpException

/**
* Error type returned by Hue Bridge for a non-existing endpoint.
*/
const val METHOD_NOT_AVAILABLE = 4

/**
* Authentication for the Phillips Hue bridge.
*/
Expand All @@ -21,6 +27,11 @@ interface ShadeAuth {
* These do not appear to expire. Store it safely.
*/
suspend fun awaitToken(retries: Int = 50, timeout: Long = 5000)

/**
* Validate token.
*/
suspend fun validateToken(token: String): Boolean
}

/**
Expand All @@ -30,7 +41,7 @@ internal class ApiAuth(
private val authApi: HueAuthApi,
private val appId: String,
private val storage: TokenStorage
): ShadeAuth {
) : ShadeAuth {
override suspend fun awaitToken(retries: Int, timeout: Long) {
repeat(retries) {
try {
Expand All @@ -48,4 +59,24 @@ internal class ApiAuth(
}
throw ShadeException("Auth timed out")
}

/**
* Validate token.
*
* Send request to a non-existing endpoint to validate token based on error type:
* Invalid token returns error type 1 (unauthorized user)
* Valid token returns error type 4 (method not available)
*/
override suspend fun validateToken(token: String): Boolean {
if (token.isNotBlank()) {
try {
authApi.validateToken(token).throwOnFailure()
} catch (error: ShadeApiError) {
return error.hueError.type == METHOD_NOT_AVAILABLE
} catch (error: HttpException) {
throw error.parse()
}
}
return false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ abstract class AuthModule {
@IntoSet
abstract fun discover(command: Discover): CliktCommand

@Binds
@IntoSet
abstract fun validate(command: Validate): CliktCommand

@Binds
abstract fun storage(fileStorage: FileStorage): TokenStorage
}
28 changes: 28 additions & 0 deletions cli/src/main/kotlin/inkapplications/shade/cli/auth/Validate.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package inkapplications.shade.cli.auth

import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.parameters.options.option
import dagger.Reusable
import inkapplications.shade.Shade
import kotlinx.coroutines.runBlocking
import javax.inject.Inject

@Reusable
class Validate @Inject constructor(
private val shade: Shade
): CliktCommand(
name = "auth:validate",
help = "Check validity of stored token"
) {
private val token by option(
"--token",
help = "Token value to validate, ex: yOXvTj16z5qx1TWazPXBgZa8vAlgebBmpl5wbxXb"
)

override fun run() {
runBlocking {
val valid = shade.auth.validateToken(token.orEmpty())
echo("Valid token: $valid")
}
}
}
3 changes: 2 additions & 1 deletion http/src/main/kotlin/shade/http/RateLimitInterceptor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ object RateLimitInterceptor: Interceptor {

override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val limit = request.header(RATE_LIMIT)?.toLong() ?: 0
val limit = request.header(RATE_LIMIT)?.toLong()
?: return chain.proceed(request)

semaphore.acquire()
val response = request.newBuilder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ internal class AuthDelegate(
}

override suspend fun awaitToken(retries: Int, timeout: Long) = delegate.awaitToken(retries, timeout)
override suspend fun validateToken(token: String) = delegate.validateToken(token)
}