Skip to content

Authentication and authorization

Thiago Santos edited this page Aug 14, 2024 · 2 revisions

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")
    }
}

Supported authentication types

The following authentication and authorization schemes:

Session

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.

Custom

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.

Configure Authentication

After installing Authentication, you can configure and use Authentication as follows:

Step 1: Choose an authentication provider

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.

Step 2: Specify a provider name

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.

Step 3: Configure a provider

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 }
}

Step 4: Protect specific resources

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 the AuthenticationStrategy.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") {
                // ...
            }
        }
    }
}

Step 5: Get a principal inside a route handler

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")
            }
        }
    }
}

Session authentication

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.

Step 1: Create a data class

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

Step 2: Install and configure a session

After creating a data class, you need to install and configure the Sessions plugin.

val router = routing {
    install(Sessions) {
        session<UserSession>()
    }
}

Step 3: Configure session authentication

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 a ChallengeStatus. 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

Step 4: Save user data in a session

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))
        }
    }
}

Custom authentication

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.