Marko Kunic
12/16/2021, 12:34 PMKamil Kalisz
12/16/2021, 1:09 PMpackage com.bngdev.common.config.auth
import com.google.firebase.FirebaseApp
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseAuthException
import io.ktor.application.*
import io.ktor.auth.*
import io.ktor.http.auth.*
import io.ktor.response.*
class FirebaseAuthenticationProvider internal constructor(
config: Configuration,
internal val firebaseApp: FirebaseApp
) : AuthenticationProvider(config) {
companion object{
const val FirebaseAuthKey = "FirebaseAuth"
const val configurationName = "bearer"
val firebaseAuthProvider = FirebaseAuthProvider()
}
internal val realm: String = config.realm
internal val schemes = config.schemes
internal val authHeader: (ApplicationCall) -> HttpAuthHeader? = config.authHeader
internal val authenticationFunction: AuthenticationFunction<FirebaseCredential> = config.authenticationFunction
class Configuration internal constructor(
name: String?,
private val firebaseApp: FirebaseApp
) : AuthenticationProvider.Configuration(name) {
internal var authenticationFunction: AuthenticationFunction<FirebaseCredential> = {
throw NotImplementedError(
"Firebase auth validate function is not specified. Use firebaseAuth { validate { ... } } to fix."
)
}
internal var schemes = FirebaseAuthSchemes("Bearer")
internal var authHeader: (ApplicationCall) -> HttpAuthHeader? = { call ->
try {
call.request.parseAuthorizationHeader()
} catch (e: IllegalArgumentException) {
null
}
}
/**
* JWT realm name that will be used during auth challenge
*/
var realm: String = "FirebaseIdToken"
/**
* Http auth header retrieval function. By default it does parse `Authorization` header content.
*/
fun authHeader(block: (ApplicationCall) -> HttpAuthHeader?) {
authHeader = block
}
/**
* @param [defaultScheme] default scheme that will be used to challenge the client when no valid auth is provided
* @param [additionalSchemes] additional schemes that will be accepted when validating the authentication
*/
fun authSchemes(defaultScheme: String = "Bearer", vararg additionalSchemes: String) {
schemes = FirebaseAuthSchemes(defaultScheme, *additionalSchemes)
}
fun validate(validate: AuthenticationFunction<FirebaseCredential>) {
authenticationFunction = validate
}
internal fun build() = FirebaseAuthenticationProvider(this, firebaseApp)
}
}
fun Authentication.Configuration.firebase(
name: String? = null,
firebaseApp: FirebaseApp,
configure: FirebaseAuthenticationProvider.Configuration.() -> Unit
) {
val provider = FirebaseAuthenticationProvider.Configuration(name, firebaseApp).apply(configure).build()
val realm = provider.realm
val schemes = provider.schemes
val firebaseAuth = FirebaseAuth.getInstance(provider.firebaseApp)
provider.pipeline.intercept(AuthenticationPipeline.RequestAuthentication) { context ->
val authHeader = provider.authHeader(call) ?: run {
context.bearerChallenge(AuthenticationFailedCause.NoCredentials, realm, schemes)
return@intercept
}
val token = authHeader.getBlob(provider.schemes)?.takeIf { it.isNotBlank() } ?: run {
context.bearerChallenge(AuthenticationFailedCause.InvalidCredentials, realm, schemes)
return@intercept
}
val firebaseToken = try {
firebaseAuth.verifyIdToken(token)
} catch (e: FirebaseAuthException) {
context.bearerChallenge(AuthenticationFailedCause.InvalidCredentials, realm, schemes)
return@intercept
} catch (cause: Throwable) {
val message = cause.message ?: cause.javaClass.simpleName
context.error(FirebaseAuthenticationProvider.FirebaseAuthKey, AuthenticationFailedCause.Error(message))
return@intercept
}
val principal = provider.authenticationFunction(call, FirebaseCredential(firebaseToken)) ?: run {
context.bearerChallenge(AuthenticationFailedCause.InvalidCredentials, realm, schemes)
return@intercept
}
context.principal(principal)
}
register(provider)
}
private fun AuthenticationContext.bearerChallenge(
cause: AuthenticationFailedCause,
realm: String,
schemes: FirebaseAuthSchemes
) = challenge(FirebaseAuthenticationProvider.FirebaseAuthKey, cause) {
call.respond(
UnauthorizedResponse(
HttpAuthHeader.Parameterized(
schemes.defaultScheme,
mapOf(HttpAuthHeader.Parameters.Realm to realm)
)
)
)
it.complete()
}
private fun HttpAuthHeader.getBlob(schemes: FirebaseAuthSchemes) = when {
this is HttpAuthHeader.Single && authScheme.toLowerCase() in schemes -> blob
else -> null
}
Marko Kunic
12/16/2021, 1:10 PMMarko Kunic
12/16/2021, 1:10 PMMarko Kunic
12/16/2021, 1:10 PM