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:
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>()?.displayNameThe 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).
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:
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.
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:
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:
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.
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.
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).
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.
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
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
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
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.