Users и Roles

val userAdmin = User("admin","admin")
val userTutor = User("tutor", "tutor")
val userList = listOf(userAdmin, userTutor)

val roleAdmin = Role("admin")
val roleUser = Role("user")
val roleList = listOf(roleAdmin, roleUser)

val userRoles = mapOf(
    userAdmin to setOf(roleAdmin, roleUser),
    userTutor to setOf(roleUser)
)

UserPrincipal

class UserPrincipal(val user: User): Principal

Exception

open class AuthException(
    val _message: String
): Exception(_message){
    suspend fun handler(call: ApplicationCall){
        call.respond(HttpStatusCode.Unauthorized, _message)
    }
}

object PrincipalError:  AuthException("Missing principal")

object AccessDenied: AuthException("Access is denied")

Обработка ошибок

    install(StatusPages) {
        exception<AuthException> { call, cause ->
            cause.handler(call)
        }
    }

Плагин Authorization

class AuthorizationConfig(
    var getRole: (User) -> Set<Role> = { emptySet() } )
class Authorization(internal var config: AuthorizationConfig) {
    companion object : BaseApplicationPlugin
        <Application, AuthorizationConfig, Authorization> {
        override val key: AttributeKey<Authorization> = 
            AttributeKey("AuthorizationHolder")
        override fun install(
            pipeline: Application, 
            configure: AuthorizationConfig.() -> Unit) =
            Authorization(AuthorizationConfig().apply(configure))
    }
}

Плагин RouteAuthorization


class RouteAuthorizationConfig {
    var allowedRoles: () -> Set<Role> = { emptySet() }
}

val RouteAuthorization
    : RouteScopedPlugin<RouteAuthorizationConfig> = 
    createRouteScopedPlugin(
        "RouteAuthorization",
        ::RouteAuthorizationConfig
    ) { ... }

Плагин RouteAuthorization

val holderConfig = application.plugin(Authorization).config
val allowedRoles = pluginConfig.allowedRoles
val getRole = holderConfig.getRole
on(AuthenticationChecked) {
    val principal = it.authentication
        .principal<UserPrincipal>()?: throw PrincipalError
    if (allowedRoles()
        .intersect(getRole(principal.user))
        .isEmpty())
            throw AccessDenied
}

Вспомогательная функция

fun Route.authorization(
    roles: () -> Set<Role>,
    build: Route.() -> Unit
): Route {
    val name = roles().joinToString { it.name }
    val authenticatedRoute = 
        createChild(AuthorizationRouteSelector(name))
    authenticatedRoute.install(RouteAuthorization) {
        this.allowedRoles = roles
    }
    authenticatedRoute.build()
    return authenticatedRoute
}

Маршрут для авторизации

class AuthorizationRouteSelector(val name: String) : 
    RouteSelector() {
    override fun evaluate(
        context: RoutingResolveContext, 
        segmentIndex: Int): RouteSelectorEvaluation {
        return RouteSelectorEvaluation.Transparent
    }

    override fun toString(): String = "(authorize $name )"
}

Упрощенная вспомогательная функция

fun Route.authorization(
    roles: Set<Role>,
    build: Route.() -> Unit
): Route = authorization(
    { roles },
    build
)

Настройка аутентификации

install(Authentication) {
    jwt("auth-jwt") {
        realm = AuthConfig.myRealm
        verifier(
            JWT
                .require(Algorithm.HMAC256(AuthConfig.secret))
                .withAudience(AuthConfig.audience)
                .withIssuer(AuthConfig.issuer)
                .build()
        )

Настройка аутентификации

validate { credential ->
    userList.find {
        it.username == credential
            .payload.getClaim("username").asString()
    }?.let {
        UserPrincipal(it)
    }
}
challenge { defaultScheme, realm ->
    call.respond(
        HttpStatusCode.Unauthorized, 
        "Token is not valid or has expired")
}

Настройка авторизации

install(Authorization) {
    getRole = { user ->
        val user = userList.find { it.username == user.username }
        userRoles.getOrDefault(user, emptySet())
    }
}

Выдача токенов

post(Config.loginPath) {
    val user = call.receive<User>()
    val localUser = userList.find { it.username == user.username }
    if (localUser?.password != user.password)
        return@post call.respondText(...)
        val token = JWT.create()
            .withAudience(audience)
            ...
            .sign(Algorithm.HMAC256(secret))
        call.respond(hashMapOf("token" to token))
    }

Авторизация маршрутов

        authenticate("auth-jwt") {
            authorization(setOf(roleAdmin)) {
                get("ByStartName/{startName}") {
                    ...

Настройка маршрутов

enum class ApiPoint {
    GetAll, GetById, GetByIds, Post, Put, Delete;

    companion object {
        val all = setOf(GetAll, GetById, GetByIds, 
            Post, Put, Delete)
        val read = setOf(GetAll, GetById, GetByIds)
        val write = setOf(Post, Put, Delete)
    }
}

Настройка маршрутов

inline fun <reified T : Any> Route.repoRoutes(
    repo: Repo<T>,
    pointRoles: List<
        Pair<Set<ApiPoint>, () -> Set<Role>>
    > = emptyList()
) {

Функции для точки API

val getAll: Route.() -> Route = {
    get {
        val elemList: List<Item<T>> = repo.read()
        if (elemList.isEmpty()) {
            call.respondText("No element found", 
                status = HttpStatusCode.NotFound)
        } else {
            val elemJson = Json
                .encodeToString(listItemSerializer, elemList)
            call.respond(elemJson)
        }
    }
}

Выбор точки API

val points: (point: ApiPoint) -> Route.() -> Route = {
    when (it) {
        ApiPoint.GetAll -> getAll
        ApiPoint.GetById -> getById
        ApiPoint.GetByIds -> getByIds
        ApiPoint.Post -> post
        ApiPoint.Put -> put
        ApiPoint.Delete -> delete
    }
}

REST API

authenticate("auth-jwt") {
    pointRoles.forEach {
        authorization(it.second) {
            it.first.forEach { apiPoint ->
                points(apiPoint)()
            }
        }
    }
}

Создание репозитория

repoRoutes(
    lessonsRepo,
    listOf(
        ApiPoint.read to { roleList.toSet()},
        ApiPoint.write to { setOf(roleAdmin) }
    )
)