-
Notifications
You must be signed in to change notification settings - Fork 3
Authentication and authorization
The Authentication plugin to handle authentication and authorization. Typical usage scenarios include logging in users, granting access to specific resources, and securely transmitting information between parties. You can also use Authentication with Sessions to keep a user's information between routes.
sourceSets {
commonMain.dependencies {
implementation("dev.programadorthi.routing:auth:$version")
}
}
The following authentication and authorization schemes:
Sessions provide a mechanism to persist data between different routes. Typical use cases include storing a logged-in user's ID, the contents of a shopping basket, or keeping user preferences on the client. An user that already has an associated session can be authenticated using the session provider.
There is an API for creating custom plugins, which can be used to implement your own plugin for handling authentication and authorization. For example, the AuthenticationChecked hook is executed after authentication credentials are checked, and it allows you to implement authorization.
After installing Authentication, you can configure and use Authentication as follows:
To use a specific authentication provider, you need to call the corresponding function inside the install block. For example, to use the session authentication, call the session function:
val router = routing {
install(Authentication) {
session<Principal> {
// Configure session authentication
}
}
}
Inside this function, you can configure settings specific to this provider.
A function for using a specific provider optionally allows you to specify a provider name. You can use different providers for different purposes:
val router = routing {
install(Authentication) {
session<Principal>("user-session") {
// Configure session authentication
}
session<Principal>("cart-session") {
// Configure session authentication
}
}
}
These names can be used later to authenticate different routes using different providers.
Important
Note that a provider name should be unique, and you can define only one provider without a name.
Each provider type has its own configuration. For instance, the SessionAuthenticationProvider.Config
class contains options passed to the session
function. The most important function exposed by this class is validate
that receives the credentials and must returns a Principal
or null
. A code sample below shows how it can look:
val router = routing {
install(Authentication) {
session<Principal>("user-session") {
validate { credentials ->
// your custom validation and returns a Principal or null
}
}
}
}
To understand how the validate
function works, we need to introduce two terms:
- A principal is an entity that can be authenticated: a user, a computer, a service, etc.
- A credential is a set of properties to authenticate a principal: a user/password pair, an API key, and so on.
Important
You can also create a custom principal by implementing the Principal
interface. This might be useful in the following cases:
- Mapping the credentials to a custom principal allows you to have additional information about the authenticated principal inside a route handler.
- If you use session authentication, a principal might be a data class that stores session data.
So, the validate function checks a specified Credential and returns a Principal
in the case of successful authentication or null if authentication fails.
Important
To skip authentication based on specific criteria, use skipWhen
. For example, you can skip authentication if a session already exists:
session<Principal>("user-session") {
skipWhen { call -> call.sessions.get<UserSession>() != null }
}
The final step is to protect specific resources in our application. You can do this by using the authenticate
function. This function accepts two optional parameters:
- A name of a provider used to authenticate nested routes. The code snippet below uses a provider with the user-session name to protect the /home and /orders routes:
val router = routing {
install(Authentication) {
// ...
}
authenticate("user-session") {
handle(path = "/home") {
// ...
}
handle(path = "/orders") {
// ...
}
}
// a 'public' route
handle(path = "/login") {
// ...
}
}
- A strategy used to resolve nested authentication providers. This strategy is represented by the
AuthenticationStrategy
enumeration value. For instance, the client should provide authentication data for all providers registered with theAuthenticationStrategy.Required
strategy. In the code snippet below, only a user that passed session authentication can try to access the /admin route using custom authentication:
val router = routing {
install(Authentication) {
// ...
}
authenticate("user-session", strategy = AuthenticationStrategy.Required) {
handle(path = "/home") {
// ...
}
authenticate("custom-provider", strategy = AuthenticationStrategy.Required) {
handle(path = "/admin") {
// ...
}
}
}
}
In the case of successful authentication, you can retrieve an authenticated Principal
inside a route handler using the call.principal
function. This function accepts a specific principal type returned by the configured authentication provider. In a code sample below, call.principal
is used to obtain UserIdPrincipal
and get a name of an authenticated user.
val router = routing {
install(Authentication) {
// ...
}
authenticate("user-session") {
handle(path = "/home") {
println("User name: ${call.principal<UserIdPrincipal>()?.name}")
}
}
}
If you use session authentication, a principal might be a data class that stores session data. So, you need to pass this data class to call.principal
:
val router = routing {
install(Authentication) {
// ...
}
authenticate("user-session") {
handle(path = "/home") {
val userSession = call.principal<UserSession>()
}
}
}
In the case of nested authentication providers, you can pass a provider name to call.principal
to get a principal for the desired provider. In the example below, the user-session value is passed to get a principal for a topmost session provider:
val router = routing {
install(Authentication) {
// ...
}
authenticate("user-session", strategy = AuthenticationStrategy.Required) {
authenticate("custom-provider", strategy = AuthenticationStrategy.Required) {
handle(path = "/home") {
val userSession = call.principal<UserSession>("user-session")
}
}
}
}
This section demonstrates how to authenticate a user with a session-based authentication, save information about this user to a session, and then authorize this user on subsequent requests using the session provider.
First, you need to create a data class for storing session data. Note that this class should inherit Principal
since the validate
function should return a Principal in the case of successful authentication.
data class UserSession(val name: String, val count: Int) : Principal
After creating a data class, you need to install and configure the Sessions plugin.
val router = routing {
install(Sessions) {
session<UserSession>()
}
}
The session
authentication provider exposes its settings via the SessionAuthenticationProvider.Config
class. In the example below, the following settings are specified:
- The
validate
function checks the session instance and returns Principal in the case of successful authentication. - The
challenge
function specifies an action performed if authentication fails and returns aChallengeStatus
. For instance, you can redirect back to a login route or something else.
val router = routing {
install(Sessions) {
session<UserSession>()
}
install(Authentication) {
session<UserSession>("user-session") {
validate { credentials ->
// simulating a credentials validation
if (credentials.isValid()) {
credentials // a valid authentication
} else {
null // user not authorized
}
}
challenge {
// ask for credentials and returns a ChallengeStatus
}
}
}
}
Important
There are some challenge
function overloads to help redirecting to another route
To save information about a logged-in user to a session, use the call.sessions.set
function.
val router = routing {
install(Sessions) {
session<UserSession>()
}
install(Authentication) {
// ...
}
authenticate("user-session") {
handle(path = "/home") {
val userName = call.principal<UserIdPrincipal>()?.name.toString()
call.sessions.set(UserSession(name = userName, count = 1))
}
}
}
There are some other authentication plugins on Ktor that is for server application only but you can lookup on them to how create your custom authentication behavior.