Skip to content

Just a working Jooby backend API example project in Kotlin

License

Notifications You must be signed in to change notification settings

jonaskahn/kooby-api-template

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

56 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Why Kooby?

  • Fast, light, easy to learn
  • I've been using Spring for almost my wokring time with Java/Kotlin. For large scale enterprise projects, Spring Stacks are undoubtedly the way to go, but for minimal projects, Kooby is a good choice.

Looking for Java version

What's included?

  • Support Default JWT
  • Support Role Access Layer
  • Hibernate, Flyway support by default
  • Add custom JPAQueryExecutor for the better querying
  • Add Jedis support instead of Lettuce
  • Using MapStruct for Object Mapper
  • Using Guice as Dependency Injection Framework
  • Multiple language support
  • Default admin user: admin@localhost/admin

Default API

  1. Create users
curl --location 'http://localhost:8080/api/auth/register' \
--header 'Content-Type: application/json' \
--data-raw '{
    "name": "test",
    "email": "test@localhost",
    "password": "test"
}'
  1. Generate token
curl --location 'http://localhost:8080/api/auth/generate-token' \
--header 'Content-Type: application/json' \
--data-raw '{
    "username": "test@localhost",
    "password": "test"
}'
  1. Get User Info
# With JPA Query
curl --location 'http://localhost:8080/api/secure/user/info' \
--header 'Accept-Language: vi'
--header 'Authorization: ••••••'
# With JPAQueryExecutor
curl --location 'http://localhost:8080/api/secure/user/info-with-executor' \
--header 'Accept-Language: vi'
--header 'Authorization: ••••••'
  1. Test Role
curl --location 'http://localhost:8080/api/secure/test/admin-role' \
--header 'Authorization: ••••••'
  1. Logout
curl --location --request DELETE 'http://localhost:8080/api/auth/secure/logout' \
--header 'Authorization: ••••••'

HOW

Implement JWT

  • Using Pac4j Module as Security Layer
install(
        Pac4jModule()
            .client("/api/secure/*") {
                HeaderClient(
                    "Authorization",
                    "Bearer ",
                    AdvancedJwtAuthenticator(
                        require(JedisPooled::class.java),
                        SecretSignatureConfiguration(it.getString("jwt.salt"))
                    )
                )
            }
    )
  1. Using HeaderClient to tell Jooby read Bearer token from header
  2. By default Jooby use the JwtAuthenticator from Pac4j, the problems are:
    • Token is completed stateless
    • What if user is lock/inactivated/deleted -> token may still valid by the exp -> user still can access to system
    • There is no truly logout

So, I solved these problems by store jid of JWT in Redis, after validate raw token, before createProfile I made a simple check to ensure the jid exists in redis. If no, token is invalid

See AdvancedJwtAuthenticator.kt

class AdvancedJwtAuthenticator(private val redis: JedisPooled, signatureConfiguration: SignatureConfiguration) :
    JwtAuthenticator(signatureConfiguration) {
    @Throws(ParseException::class)
    override fun createJwtProfile(
        credentials: TokenCredentials, jwt: JWT, context: WebContext?,
        sessionStore: SessionStore?
    ) {
        val jwtId = jwt.jwtClaimsSet.jwtid.toString()
        val uid = jwt.jwtClaimsSet.claims[Jwt.Attribute.UID].toString()
        if (!redis.exists(RedisNameSpace.getUserTokenExpirationKey(uid, jwtId))) {
            throw AuthorizationException()
        }
        super.createJwtProfile(credentials, jwt, context, sessionStore)
    }
}
  1. For now, when you want logout, just delete the related jid in redis.
internal class AccessVerifierImpl @Inject constructor(private val context: Context) : AccessVerifier {
    override fun hasRole(role: String): Boolean {
        val user = context.getUser<UserProfile>()
        return user?.roles?.contains(role) ?: false
    }

    override fun requireRole(role: String) {
        if (!hasRole(role)) {
            throw ForbiddenAccessException()
        }
    }

    override fun hasAnyRoles(vararg roles: String): Boolean {
        val user = context.getUser<UserProfile>()
        return user?.roles?.any { roles.contains(it) } ?: false
    }

    override fun requireAnyRoles(vararg roles: String) {
        if (!hasAnyRoles(*roles)) {
            throw ForbiddenAccessException()
        }
    }
}
  • hasRole or hasAnyRoles will check and return true/false, while requireRole and requireAnyRoles will explicitly throw exception if you do not have access.
  • Problem: Sometimes we want to retrieve data from database via native query, but we do not want manually map field's value from result to pojo class.
  • To solve this problem we have so many ways. With the usage of Hibernate, I create JPA Query Executor to parse sql result to object via Jackson
class UserRepositoryImpl @Inject constructor(
    private val em: EntityManager
) : UserRepository {
    override fun findCustomActivatedUserByPreferredUsername(preferredUsername: Long): UserInfoDto? {
        val query =
            em.createNativeQuery("select * from users where preferred_username = :preferredUsername and status = ${StatusCode.ACTIVATED}")
        return JpaQueryExecutor.builder<UserInfoDto>()
            .with(query, mutableMapOf("preferredUsername" to preferredUsername))
            .map(UserInfoDto::class.java)
            .getSingleResult()
    }
}

So, we directly map database field's value to Pojo object via Jackson, if Pojo class does not have correct field name, please using @JsonAlias (Like database field full_name, pojo class fullName)

Releases

No releases published

Packages

No packages published

Languages