public
Authored by avatar CattenLinger :sparkles:

[Kotlin][Ktor]Authentication Extension: AuthenticationProvider base on RequestInfo

This is an extension of Ktor's Authentication feature.

It provides a way to extranct nesessary information from request for building customized credential and prncipal.

Example:

install(Authentication) {
    // Provide customized CredentialType and PrincipalType
    byRequestInfo<CustomizedCredential, CustomizedPrincipal>("customized") {
        
        // Required, extract credentials from request.
        challenge {
            // Can extract any information from the request.
            // In this example, I took the Basic Authentication token.
            call.request.headers["Authorization"]?.takeIf {
                it.isNotBlank()
            }?.let {
                CustomizedCredential(it.removePrefix("Basic ")).takeIf { c -> c.isValid }
            }
        }

        // Customized response when credential invalided.
        // By default it just return 401 and text 'Unauthorized'
        onInvalidCredential {
            call.respond(
                HttpStatusCode.Unauthorized, mapOf(
                    "error" to "unauthorized",
                    "message" to "invalid_credential"
                )
            )
        }

        //Required, do authentication here.
        authenticate {
            it?.toPrincipal()
        }

        // Customized response when authentication failed.
        // By default it just return 401 and text 'Unauthorized'
        onUnAuthenticated {
            call.respond(
                HttpStatusCode.Unauthorized, mapOf(
                    "error" to "unauthorized",
                    "message" to "session_invalided"
                )
            )
        }
    }
}
Edited
RequestInfoAuthenticationProvider.kt 3.76 KiB
import io.ktor.application.*
import io.ktor.auth.*
import io.ktor.http.*
import io.ktor.response.*

typealias CallContext = PipelineContext<*, ApplicationCall>

typealias RequestAuthChallengeHandler<TCredential> = suspend CallContext.() -> TCredential?
typealias RequestAuthInfoExtractor<TCredential, TPrincipal> = suspend CallContext.(TCredential?) -> TPrincipal?
typealias RequestAuthFailedHandler = suspend CallContext.(RequestInfoAuthenticationProvider<*, *>) -> Unit
typealias RequestAuthNoAuthHandler = suspend CallContext.(RequestInfoAuthenticationProvider<*, *>) -> Unit

class RequestInfoAuthenticationProvider<TCredential : Credential, TPrincipal : Principal> internal constructor(
    config: Configuration<TCredential, TPrincipal>
) :
    AuthenticationProvider(config) {

    val realm = config.realm

    internal val authenticator = config.authenticator
    internal val challenger = config.challenger

    internal val onInvalidCredential = config.onInvalidCredential
    internal val onUnauthenticated = config.onUnauthenticated

    class Configuration<TCredential, TPrincipal>(name: String?) : AuthenticationProvider.Configuration(name) {
        var realm: String = name ?: "NoxPay Lite"

        internal var challenger: RequestAuthChallengeHandler<TCredential> = {
            throw NotImplementedError(
                "Request auth info provider function is not specified. " +
                        "Use byRequestInfo<CredentialType, PrincipalType> { challenge { ... } } to fix."
            )
        }

        fun challenge(block: RequestAuthChallengeHandler<TCredential>) {
            challenger = block
        }

        internal var authenticator: RequestAuthInfoExtractor<TCredential, TPrincipal> = {
            throw NotImplementedError(
                "Request auth info provider function is not specified. use byRequestInfo { extractor { ... } } to fix."
            )
        }

        fun authenticate(block: RequestAuthInfoExtractor<TCredential, TPrincipal>) {
            authenticator = block
        }

        internal var onInvalidCredential: RequestAuthFailedHandler = {
            call.respond(HttpStatusCode.Unauthorized, "Unauthorized")
        }

        fun onInvalidCredential(block: RequestAuthFailedHandler) {
            onInvalidCredential = block
        }

        internal var onUnauthenticated: RequestAuthNoAuthHandler = {
            call.respond(HttpStatusCode.Unauthorized, "Unauthorized")
        }

        fun onUnAuthenticated(block: RequestAuthNoAuthHandler) {
            onUnauthenticated = block
        }
    }
}

private val RequestInfoAuthChallengeKey = "RequestInfoAuth"

fun <TCredential : Credential, TPrincipal : Principal> Authentication.Configuration.byRequestInfo(
    name: String? = null,
    configure: RequestInfoAuthenticationProvider.Configuration<TCredential, TPrincipal>.() -> Unit
) {
    val provider = RequestInfoAuthenticationProvider(RequestInfoAuthenticationProvider.Configuration<TCredential, TPrincipal>(name).apply(configure))

    provider.pipeline.intercept(AuthenticationPipeline.RequestAuthentication) { context ->
        val credential = provider.challenger(this)
        val principal = credential?.let { provider.authenticator(this, it) }

        if (credential == null) {
            context.challenge(RequestInfoAuthChallengeKey, AuthenticationFailedCause.NoCredentials) {
                provider.onInvalidCredential.invoke(this, provider)
                it.complete()
            }
        } else if (principal == null) {
            context.challenge(RequestInfoAuthChallengeKey, AuthenticationFailedCause.InvalidCredentials) {
                provider.onUnauthenticated(this, provider)
                it.complete()
            }
        }

        if (principal != null) context.principal(principal)
    }

    register(provider)
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment