Authentication

The authentication module allows to restrict access to sections provided by handlers to authenticated users only. Independent from the kind of authentication used, the identified user can be read from the request in the following manner:

Kotlin
import org.genhttp.api.content.authentication.User
import org.genhttp.api.protocol.Request

import org.genhttp.modules.authentication.getUser

fun getDisplayName(request: Request): String? = request.getUser<User>()?.displayName

The user object for the current request will be determined by the installed authentication concern. The concern will allow you to add your custom user object there.

For frameworks such as webservices or controllers, you can also inject the user instance in your function calls (see user injection) or use role-based authorization.

API Key Authentication

This kind of authentication requires your clients to authenticate themselves using a key sent with an additional HTTP header (named X-API-Key).

Kotlin
val securedContent = Layout.create()

val auth = ApiKeyAuthentication.create()
    .keys("ABC-123", "BCD-234")

securedContent.add(auth)

You may customize both where the key is read from and what keys are valid for authentication:

Kotlin
import org.genhttp.api.content.authentication.User
import org.genhttp.api.protocol.Request
import org.genhttp.api.protocol.getEntry

import org.genhttp.modules.authentication.ApiKeyAuthentication
import org.genhttp.modules.authentication.apikey.ApiKeyUser

val auth = ApiKeyAuthentication.create()
    .withQueryParameter("key") // read the key from the query ...
    .withHeader("key") // ... or a custom header ...
    .extractor { request -> request.header.headers.getEntry("Authorization")?.removePrefix("Key ") } // ... or anywhere else
    .authenticator(::authenticate)

suspend fun authenticate(request: Request, key: String): User? {
    // do something to check the key (e.g. query a database)
    return ApiKeyUser(key) // or null, if no access is granted
}

Requests without a key are answered with 401 Unauthorized, requests with a key that is not accepted with 403 Forbidden.

Bearer Authentication

Bearer authentication expects the clients to send an access token that they obtained from a trusted issuer with every request. The concern will validate the tokens against the signing keys of the issuer and will optionally perform additional validation steps to authorize the client.

Kotlin
val securedContent = Layout.create()

val auth = BearerAuthentication.create()
    .issuer("https://sso.mycompany.org/auth/")
    .audience("https://myapi.company.org") // optional

securedContent.add(auth)

Please note, that the issuer url needs to exactly match the issued value (including trailing slashes etc.), while a trailing slash is ignored when comparing the audience. To validate the issuer, the concern needs to download the signing keys from the server on first request (via .well-known/openid-configuration below the issuer url). Subsequent requests will use the cached keys.

genhttp-kt validates tokens with Nimbus JOSE + JWT instead of System.IdentityModel: the token handed to your functions is a com.nimbusds.jwt.SignedJWT, signing keys are java.security.Key instances. Unsigned (alg: none) and encrypted tokens are rejected.

If you would like to perform additional checks, you can pass a custom validator:

Kotlin
import com.nimbusds.jwt.SignedJWT

import org.genhttp.api.content.ProviderException
import org.genhttp.api.protocol.ResponseStatus

import org.genhttp.modules.authentication.BearerAuthentication

val auth = BearerAuthentication.create()
    .issuer("https://sso.mycompany.org/auth/")
    .validation(::validateClient)

fun validateClient(token: SignedJWT) {
    if (token.jwtClaimsSet.subject != "my-client") {
        throw ProviderException(ResponseStatus.Forbidden, "Only 'my-client' is allowed to access this service")
    }
}

Similar to the validator, you can pass a custom logic to fetch the signing keys to validate against:

Kotlin
import com.nimbusds.jwt.SignedJWT

import org.genhttp.modules.authentication.BearerAuthentication

import java.nio.file.Files
import java.nio.file.Path
import java.security.Key
import java.security.KeyFactory
import java.security.spec.X509EncodedKeySpec
import java.util.Base64

val auth = BearerAuthentication.create()
    .issuer("https://sso.mycompany.org/auth/")
    .keyResolver(::fetchSigningKeys)

fun fetchSigningKeys(token: SignedJWT): Collection<Key> {
    val pem = Files.readAllLines(Path.of("signing-key.pem"))
        .filterNot { it.startsWith("-----") }
        .joinToString("")

    val spec = X509EncodedKeySpec(Base64.getDecoder().decode(pem))

    return listOf(KeyFactory.getInstance("RSA").generatePublic(spec))
}

If you would like to map incoming tokens to user accounts, you can add a mapper which will resolve a User instance. This mechanism can be combined with user injection to allow you to directly access the user instance in your service functions.

Kotlin
import com.nimbusds.jwt.SignedJWT

import org.genhttp.api.content.authentication.User
import org.genhttp.api.protocol.Request

import org.genhttp.modules.authentication.*
import org.genhttp.modules.layouting.*
import org.genhttp.modules.reflection.*
import org.genhttp.modules.reflection.injectors.UserInjector
import org.genhttp.modules.webservices.*

val auth = BearerAuthentication.create()
    .issuer("https://sso.mycompany.org/auth/")
    .userMapping(::mapUser)
    .allowExpired()

val injection = Injection.default()
    .add(UserInjector(ServiceUser::class))

val api = Layout.create()
    .addService<MyService>("service", injectors = injection)
    .add(auth)

class ServiceUser(val subject: String) : User {

    override val displayName: String
        get() = subject

}

fun mapUser(request: Request, token: SignedJWT): User? {
    // e.g. look up the account of the user in your database
    return ServiceUser(token.jwtClaimsSet.subject)
}

class MyService {

    @ResourceMethod
    fun doWork(user: ServiceUser): String = "Hello ${user.displayName}"

}

Returning null will not deny access for the requesting client. If you want to do so, throw a ProviderException with status Forbidden.

Client Certificate Authentication

This concern can be used to secure a section of your web application by analyzing the certificate the client presented during the SSL/TLS handshake when connecting to the server. The configuration for this feature can be found here.

Kotlin
import org.genhttp.api.content.authentication.User
import org.genhttp.api.protocol.Request

import org.genhttp.modules.authentication.ClientCertificateAuthentication
import org.genhttp.modules.authentication.clientcertificate.ClientCertificateUser
import org.genhttp.modules.layouting.Layout

import java.security.cert.X509Certificate

val securedContent = Layout.create()

val auth = ClientCertificateAuthentication.create()
    .authorization(::authorize)
    .userMapping(::mapUser)

securedContent.add(auth)

fun authorize(request: Request, certificate: X509Certificate?): Boolean {
    // return true to allow the request, false to deny it
    return certificate != null
}

fun mapUser(request: Request, certificate: X509Certificate?): User? {
    // create the user record you would like to use for this request
    return certificate?.let { ClientCertificateUser(it) }
}

Requests that are not authorized are answered with 403 Forbidden.

Basic Authentication

Basic authentication can be used by either specifying a list of users and their passwords or by implementing your custom logic to authenticate and load users (e.g. from a database).

Kotlin
import org.genhttp.modules.authentication.*
import org.genhttp.modules.authentication.basic.BasicAuthenticationUser
import org.genhttp.modules.layouting.Layout

val securedContent = Layout.create()

// specify all users and their password ...
val auth = BasicAuthentication.create()
    .add("Bob", "pw123")
    .add("Alice", "123pw")

securedContent.add(auth)

// ... or implement your custom logic
securedContent.authentication({ user: String, password: String ->
    // validate the given credentials here and return your custom user object which needs to implement User
    BasicAuthenticationUser(user)
})

Both the API key and the basic authentication offer an authentication(...) variant taking a lambda with two parameters. Kotlin cannot choose between them by the body of the lambda, so the types of its parameters need to be given explicitly ({ user: String, password: String -> ... } for basic authentication, { request: Request, key: String -> ... } for API keys). The variant of the basic authentication takes the name of the realm as its last parameter (authentication({ ... }, realm = "My App")), so its lambda is passed in parentheses.

Mixed Authentication

The MultiAuthentication concern allows you to use multiple authentication methods at the same time, so for example you can authenticate your API both with API keys or basic authentication. The concern will try the authentication mechanisms in the order you added them to the builder.

Kotlin
val keyAuth = ApiKeyAuthentication.create()
    .keys("ABC-123")

val basicAuth = BasicAuthentication.create()
    .add("Bob", "pw123")

val mixedAuth = MultiAuthentication.create()
    .add(keyAuth)
    .add(basicAuth)

val api = Layout.create()
    .addService<MyService>("service")
    .add(mixedAuth)

class MyService {

    @ResourceMethod
    fun hello(): String = "Hello World"

}

Please note that if you would like to support basic authentication via browser, you need to add the BasicAuthentication last, as the response generated by this concern will contain the challenge that instructs the browser to ask the user for credentials.

Role-based Authorization

If you are using one of the service frameworks, you probably want to authorize users on function level (e.g. an admin is allowed to delete something whereas regular users are not). This can be achieved by setting the roles of the user on creation in combination with the @RequireRole interceptor:

Webservices

Kotlin
import org.genhttp.api.content.authentication.User
import org.genhttp.api.protocol.Request
import org.genhttp.engine.internal.Host

import org.genhttp.modules.authentication.*
import org.genhttp.modules.authentication.apikey.*
import org.genhttp.modules.layouting.*
import org.genhttp.modules.webservices.*

suspend fun main() {
    val auth = ApiKeyAuthentication.create()
        .withQueryParameter("apiKey")
        .authenticator(::authenticateRequest)

    val app = Layout.create()
        .addService<MyService>("service")
        .authentication(auth)

    Host.create()
        .handler(app)
        .run()
}

suspend fun authenticateRequest(request: Request, apiKey: String): User? = when (apiKey) {
    "abc" -> ApiKeyUser(apiKey, "ADMIN", "USER")
    "bcd" -> ApiKeyUser(apiKey, "USER")
    else -> null
}

class MyService {

    @ResourceMethod(path = "/admin")
    @RequireRole("ADMIN")
    fun checkAdmin(): String = "You are an administrator"

    @ResourceMethod(path = "/user")
    @RequireRole("USER")
    fun checkUser(): String = "You are a user"

}

Functional

Kotlin
import org.genhttp.api.content.authentication.User
import org.genhttp.api.protocol.Request
import org.genhttp.engine.internal.Host

import org.genhttp.modules.authentication.*
import org.genhttp.modules.authentication.apikey.*
import org.genhttp.modules.functional.*

suspend fun main() {
    val auth = ApiKeyAuthentication.create()
        .withQueryParameter("apiKey")
        .authenticator(::authenticateRequest)

    val app = Inline.create()
        .get("/admin", ::checkAdmin)
        .get("/user", ::checkUser)
        .authentication(auth)

    Host.create()
        .handler(app)
        .run()
}

suspend fun authenticateRequest(request: Request, apiKey: String): User? = when (apiKey) {
    "abc" -> ApiKeyUser(apiKey, "ADMIN", "USER")
    "bcd" -> ApiKeyUser(apiKey, "USER")
    else -> null
}

// handlers that need annotations are functions, added as function references
@RequireRole("ADMIN")
fun checkAdmin(): String = "You are an administrator"

@RequireRole("USER")
fun checkUser(): String = "You are a user"

Controllers

Kotlin
import org.genhttp.api.content.authentication.User
import org.genhttp.api.protocol.Request
import org.genhttp.engine.internal.Host

import org.genhttp.modules.authentication.*
import org.genhttp.modules.authentication.apikey.*
import org.genhttp.modules.controllers.*
import org.genhttp.modules.layouting.*
import org.genhttp.modules.reflection.*

suspend fun main() {
    val auth = ApiKeyAuthentication.create()
        .withQueryParameter("apiKey")
        .authenticator(::authenticateRequest)

    val app = Layout.create()
        .addController<MyController>("service")
        .authentication(auth)

    Host.create()
        .handler(app)
        .run()
}

suspend fun authenticateRequest(request: Request, apiKey: String): User? = when (apiKey) {
    "abc" -> ApiKeyUser(apiKey, "ADMIN", "USER")
    "bcd" -> ApiKeyUser(apiKey, "USER")
    else -> null
}

class MyController {

    @ControllerAction(Method.Get)
    @RequireRole("ADMIN")
    fun checkAdmin(): String = "You are an administrator"

    @ControllerAction(Method.Get)
    @RequireRole("USER")
    fun checkUser(): String = "You are a user"

}

Requests without an authenticated user are answered with 401 Unauthorized, users that lack one of the required roles with 403 Forbidden. Roles are compared ignoring their case.