Token-based Authentication Flow
This flow allows to implement the common signup, login and authentication functionalities of a web application.
Authentication domains
The flow defines and orchestrates two authentication domains:
LoginDomain
: an authentication domain whose credential is theLogin
data type.AccessTokenDomain
: an authentication domain whose credential is theAccessToken
data type.
The Login
data type is defined by a username and a password, while the
AccessToken
data type is defined by a value -- which is usually a randomly
generated string -- and an expiration date.
Functionalities
The flow is implemented by the TokenBasedAuthenticationFlow
class, which
exposes these functionalities:
registerSubjectLogin
: registers theLogin
of aSubject
in theLoginDomain
. This is typically used when signing up a new user.exchangeForTokens
: exchanges aLogin
for anAccessToken
. This is typically used when a user logs into the system providing username and password, and they receive anAccessToken
that they will use for authenticating further requests.validateToken
: checks whether anAccessToken
is valid and returns the authenticatedSubject
if true.unregisterToken
: invalidates anAccessToken
. This is typically used when a user logs out from a device.unregisterAllSubjectTokens
: invalidates allAccessToken
of aSubject
. This can be used to implement security features, such as logging out from all active sessions in case the password is suspected to be compromised or upon a change password request.unregisterLogin
: invalidates aLogin
. This can be used in case of a change password request in an app that wants to guarantee a single login at the time per user.unregisterAllSubjectLogins
: invalidates allLogin
of aSubject
. This is typically used when removing a user from the system.
How to use it
You can create a TokenBasedAuthenticationFlow
by providing the two
authentication domains discussed above.
toctoc
provides some specialized implementations of such domains combinations
of DBMS and data-access library. For this example we'll be using Postgres and
Slick, leveraging the toctoc-slick-postgresql
module (see the
installation docs for instructions on how to add it as
a dependency).
// The generic flow
import io.buildo.toctoc.core.authentication.TokenBasedAuthentication.TokenBasedAuthenticationFlow
// The specialized AccessToken domain
import io.buildo.toctoc.slick.authentication.token.PostgreSqlSlickAccessTokenAuthenticationDomain
// The specialized Login domain
import io.buildo.toctoc.slick.authentication.login.PostgreSqlSlickLoginAuthenticationDomain
import slick.jdbc.JdbcBackend.Database
import java.time.Duration
object PostgreSqlSlickTokenBasedAuthenticationFlow {
def create(
db: Database,
tokenDuration: Duration
): TokenBasedAuthenticationFlow = new TokenBasedAuthenticationFlow(
loginD = new PostgreSqlSlickLoginAuthenticationDomain(db),
accessTokenD = new PostgreSqlSlickAccessTokenAuthenticationDomain(db),
tokenDuration = tokenDuration,
)
}
Now that we have it, we can integrate it in our app.
Login and Logout
Here's a possible implementation of a controller that provides login
and
logout
functionalities:
import io.buildo.toctoc.core.authentication.TokenBasedAuthentication.TokenBasedAuthenticationFlow
import io.buildo.toctoc.core.authentication.TokenBasedAuthentication.AccessToken
import io.buildo.toctoc.core.authentication.TokenBasedAuthentication.Login
import io.buildo.toctoc.core.authentication.AuthenticationError
import zio.IO
trait AuthenticationController {
def login(credentials: Login): IO[AuthenticationError, AccessToken]
def logout(accessToken: AccessToken): IO[AuthenticationError, Unit]
}
object AuthenticationController {
def create(authFlow: TokenBasedAuthenticationFlow): AuthenticationController = new AuthenticationController {
override def login(credentials: Login): IO[AuthenticationError, AccessToken] =
authFlow.exchangeForTokens(credentials)
override def logout(accessToken: AccessToken): IO[AuthenticationError, Unit] =
authFlow.unregisterToken(accessToken)
}
}
Signup
For implementing a signup we need to know how to create users in our app.
Let's introduce a UserService
which manages our users:
import java.util.UUID
import zio.UIO
case class User(id: UUID, firstName: String, lastName: String)
case class UserCreate(firstName: String, lastName: String)
trait UserService {
def create(user: UserCreate): UIO[UUID]
def read(id: UUID): UIO[Option[User]]
}
We can now implement a UserController
which uses the service and the
authentication flow to provide the signup
functionality:
import io.buildo.toctoc.core.authentication.TokenBasedAuthentication.TokenBasedAuthenticationFlow
import io.buildo.toctoc.core.authentication.TokenBasedAuthentication.Login
import io.buildo.toctoc.core.authentication.TokenBasedAuthentication.UserSubject
import io.buildo.toctoc.core.authentication.AuthenticationError
import zio.IO
trait UserController {
def signup(credentials: Login, user: UserCreate): IO[AuthenticationError, Unit]
}
object UserController {
def create(userService: UserService, authFlow: TokenBasedAuthenticationFlow): UserController = new UserController {
override def signup(credentials: Login, user: UserCreate): IO[AuthenticationError, Unit] =
for {
// create the user
userId <- userService.create(user)
// create the login
res <- authFlow.registerSubjectLogin(UserSubject(userId.toString), credentials)
} yield res
}
}
Sequence diagram
Here's a sequence diagram that shows the common interactions we've seen above