Silhouette
Authentication Library
Silhouette
Overview
Silhouette is a comprehensive authentication library for Play Framework applications. It supports multiple authentication methods including OAuth1, OAuth2, OpenID, CAS, Multi-Factor Authentication, TOTP, Credentials Authentication, Basic Authentication, and custom authentication schemes.
Details
Silhouette is a robust and comprehensive authentication library for Play Framework, continuing active development as of 2024. Originally developed in the mohiva/play-silhouette repository, it is now actively maintained in the playframework/play-silhouette fork, providing the latest snapshot builds. The Authenticator system, which is the heart of authentication, allows unified handling of various authentication methods including JWT tokens, session-based, and cookie-based authentication. It features asynchronous and non-blocking processing, internationalization support, and comprehensive test coverage, making it ideal for enterprise-level application development. While Silhouette 7.0 is the final version for Scala 2.12/2.13 and Play 2.8, the current fork provides continuous updates and snapshot releases.
Pros and Cons
Pros
- Comprehensive Authentication Methods: Wide support for OAuth1/2, OpenID, CAS, JWT, TOTP, etc.
- Play Framework Integration: Complete Play Framework integration for consistent development experience
- Multi-Factor Authentication: Advanced security support with 2FA and TOTP
- Asynchronous Processing: Fully asynchronous and non-blocking architecture
- Customizable: Flexible implementation of custom authentication schemes
- Internationalization: Multi-language support and localization
- Test Support: Rich test utilities and coverage
- Documentation: Extensive documentation and sample code
Cons
- Learning Curve: Configuration and learning complexity due to rich features
- Play Framework Dependency: Play Framework exclusive with strong framework dependency
- Configuration Complexity: Complex initial setup and customization
- Maintenance Status: Mixture of official and unofficial fork versions
- Version Management: Stability considerations when using snapshot versions
- Resource Consumption: Slight memory usage due to rich features
- Dependencies: Dependencies on numerous external libraries
Key Links
- Silhouette Official Documentation
- Silhouette GitHub (Active Fork)
- Silhouette Rocks (Official Site)
- Silhouette Release Information
- Scaladex - Silhouette
- Play Framework Official Site
Usage Examples
Basic Silhouette Configuration
// Dependency configuration in build.sbt
libraryDependencies ++= Seq(
"org.playframework.silhouette" %% "play-silhouette" % "7.0.0",
"org.playframework.silhouette" %% "play-silhouette-password-bcrypt" % "7.0.0",
"org.playframework.silhouette" %% "play-silhouette-crypto-jca" % "7.0.0",
"org.playframework.silhouette" %% "play-silhouette-persistence" % "7.0.0",
"org.playframework.silhouette" %% "play-silhouette-testkit" % "7.0.0" % Test
)
// Configuration in application.conf
silhouette {
# JWT authentication settings
jwt.authenticator {
headerName = "X-Auth-Token"
issuerClaim = "play-silhouette"
encryptSubject = true
authenticatorExpiry = 12 hours
sharedSecret = "changeme"
}
# OAuth settings
oauth1TokenSecretProvider.cookieName="OAuth1TokenSecret"
oauth1TokenSecretProvider.cookiePath="/"
oauth1TokenSecretProvider.secureCookie=false // Set to true for HTTPS
oauth1TokenSecretProvider.httpOnlyCookie=true
}
User Model and Identity Definition
// User model
import play.silhouette.api.{Identity, LoginInfo}
import java.util.UUID
case class User(
userID: UUID,
loginInfo: LoginInfo,
firstName: Option[String],
lastName: Option[String],
fullName: Option[String],
email: Option[String],
avatarURL: Option[String],
activated: Boolean
) extends Identity
// UserService trait
import scala.concurrent.Future
trait UserService {
def retrieve(loginInfo: LoginInfo): Future[Option[User]]
def save(user: User): Future[User]
}
// UserService implementation example
class UserServiceImpl extends UserService {
def retrieve(loginInfo: LoginInfo): Future[Option[User]] = {
// Retrieve user from database
Future.successful(None) // Implementation example
}
def save(user: User): Future[User] = {
// Save user to database
Future.successful(user)
}
}
Silhouette Environment Configuration
// Silhouette Environment configuration
import play.silhouette.api._
import play.silhouette.api.util._
import play.silhouette.impl.authenticators._
import play.silhouette.impl.providers._
import play.silhouette.impl.providers.credentials._
import play.silhouette.impl.util._
import play.silhouette.password.BCryptPasswordHasher
import play.api.libs.ws.WSClient
// DI container configuration example
class SilhouetteModule extends AbstractModule {
def configure(): Unit = {
bind[Silhouette[DefaultEnv]].to[SilhouetteProvider[DefaultEnv]]
bind[UserService].to[UserServiceImpl]
bind[CacheLayer].to[PlayCacheLayer]
bind[IDGenerator].toInstance(new SecureRandomIDGenerator())
bind[PasswordHasher].toInstance(new BCryptPasswordHasher)
bind[FingerprintGenerator].toInstance(new DefaultFingerprintGenerator(false))
bind[EventBus].toInstance(EventBus())
bind[Clock].toInstance(Clock())
}
@Provides
def provideHTTPLayer(client: WSClient): HTTPLayer = new PlayHTTPLayer(client)
@Provides
def provideEnvironment(
userService: UserService,
authenticatorService: AuthenticatorService[JWTAuthenticator],
eventBus: EventBus
): Environment[DefaultEnv] = {
Environment[DefaultEnv](
userService,
authenticatorService,
Seq(),
eventBus
)
}
}
// Environment type alias
trait DefaultEnv extends Env {
type I = User
type A = JWTAuthenticator
}
JWT Authentication Implementation
// JWT Authenticator configuration
import play.silhouette.impl.authenticators._
import play.silhouette.api.util.Clock
import scala.concurrent.duration._
@Provides
def provideJWTAuthenticatorService(
settings: JWTAuthenticatorSettings,
repository: AuthenticatorRepository[JWTAuthenticator],
crypter: Crypter,
idGenerator: IDGenerator,
clock: Clock
): AuthenticatorService[JWTAuthenticator] = {
new JWTAuthenticatorService(settings, repository, crypter, idGenerator, clock)
}
@Provides
def provideJWTAuthenticatorSettings(): JWTAuthenticatorSettings = {
JWTAuthenticatorSettings(
fieldName = "X-Auth-Token",
issuerClaim = "play-silhouette",
authenticatorExpiry = 12.hours,
sharedSecret = "your-secret-key"
)
}
// JWT authentication usage in controller
class AuthController @Inject()(
silhouette: Silhouette[DefaultEnv],
userService: UserService,
credentialsProvider: CredentialsProvider,
passwordHasherRegistry: PasswordHasherRegistry
) extends AbstractController with SilhouetteController {
def signIn = silhouette.UnsecuredAction.async { implicit request =>
SignInForm.form.bindFromRequest.fold(
form => Future.successful(BadRequest(Json.obj("message" -> "Invalid form"))),
data => {
credentialsProvider.authenticate(Credentials(data.email, data.password)).flatMap { loginInfo =>
userService.retrieve(loginInfo).flatMap {
case Some(user) if user.activated =>
silhouette.env.authenticatorService.create(loginInfo).flatMap { authenticator =>
silhouette.env.eventBus.publish(LoginEvent(user, request))
silhouette.env.authenticatorService.init(authenticator).flatMap { token =>
silhouette.env.authenticatorService.embed(token, Ok(Json.obj(
"token" -> token,
"user" -> Json.obj("email" -> user.email)
)))
}
}
case Some(_) =>
Future.successful(Unauthorized(Json.obj("message" -> "Account not activated")))
case None =>
Future.successful(Unauthorized(Json.obj("message" -> "Invalid credentials")))
}
}.recover {
case _: ProviderException =>
Unauthorized(Json.obj("message" -> "Invalid credentials"))
}
}
)
}
}
OAuth2 Implementation
// OAuth2 provider configuration
import play.silhouette.impl.providers.oauth2._
@Provides
def provideGoogleProvider(
httpLayer: HTTPLayer,
stateHandler: OAuth2StateHandler
): GoogleProvider = {
new GoogleProvider(httpLayer, stateHandler, OAuth2Settings(
authorizationURL = Some("https://accounts.google.com/o/oauth2/auth"),
accessTokenURL = "https://accounts.google.com/o/oauth2/token",
redirectURL = Some("http://localhost:9000/authenticate/google"),
clientID = "your-google-client-id",
clientSecret = "your-google-client-secret",
scope = Some("profile email")
))
}
// OAuth2 authentication controller
def authenticateGoogle = silhouette.UnsecuredAction.async { implicit request =>
googleProvider.authenticate().flatMap {
case Left(result) => Future.successful(result)
case Right(authInfo) =>
for {
profile <- googleProvider.retrieveProfile(authInfo)
user <- userService.save(profile)
authenticator <- silhouette.env.authenticatorService.create(profile.loginInfo)
value <- silhouette.env.authenticatorService.init(authenticator)
result <- silhouette.env.authenticatorService.embed(value, Redirect("/dashboard"))
} yield {
silhouette.env.eventBus.publish(LoginEvent(user, request))
result
}
}
}
Multi-Factor Authentication (2FA/TOTP) Implementation
// TOTP authentication configuration
import play.silhouette.impl.providers.totp._
@Provides
def provideTOTPProvider(): TOTPProvider = {
new TOTPProvider(TOTPSettings(
qrCodeURL = "https://chart.googleapis.com/chart?chs=200x200&chld=M%%7C0&cht=qr&chl=",
issuer = "MyApp",
keyLength = 32,
window = 3
))
}
// Enable 2FA
def enableTwoFactor = silhouette.SecuredAction.async { implicit request =>
val totpInfo = totpProvider.createCredentials(request.identity.userID.toString)
for {
_ <- totpCredentialsDAO.save(totpInfo)
qrCodeURL = totpProvider.qrCodeURL(request.identity.email.getOrElse(""), totpInfo)
} yield Ok(Json.obj(
"qrCodeURL" -> qrCodeURL,
"sharedSecret" -> totpInfo.sharedSecret
))
}
// TOTP verification
def verifyTOTP = silhouette.SecuredAction.async { implicit request =>
VerifyTOTPForm.form.bindFromRequest.fold(
_ => Future.successful(BadRequest),
verificationCode => {
totpCredentialsDAO.find(request.identity.userID).flatMap {
case Some(totpInfo) =>
if (totpProvider.verify(verificationCode, totpInfo)) {
// 2FA authentication success
Future.successful(Ok(Json.obj("verified" -> true)))
} else {
Future.successful(Unauthorized(Json.obj("verified" -> false)))
}
case None =>
Future.successful(BadRequest(Json.obj("message" -> "TOTP not configured")))
}
}
)
}
Custom Authentication Provider
// API Key authentication provider implementation
import play.silhouette.api.Provider
import play.silhouette.api.util.{Credentials, ExtractableRequest}
case class APIKeyCredentials(apiKey: String) extends Credentials
class APIKeyProvider @Inject()(
apiKeyService: APIKeyService
) extends Provider {
override val id = APIKeyProvider.ID
def authenticate[B](credentials: APIKeyCredentials)
(implicit request: ExtractableRequest[B]): Future[LoginInfo] = {
apiKeyService.validate(credentials.apiKey).map { userID =>
LoginInfo(id, userID.toString)
}.getOrElse {
throw new InvalidCredentialsException("Invalid API key")
}
}
}
object APIKeyProvider {
val ID = "apikey"
}
// API Key authentication usage
def apiEndpoint = silhouette.UnsecuredAction.async { implicit request =>
request.headers.get("X-API-Key") match {
case Some(apiKey) =>
apiKeyProvider.authenticate(APIKeyCredentials(apiKey)).flatMap { loginInfo =>
userService.retrieve(loginInfo).map {
case Some(user) => Ok(Json.obj("data" -> "API response", "user" -> user.email))
case None => Unauthorized
}
}.recover {
case _: Exception => Unauthorized
}
case None =>
Future.successful(Unauthorized(Json.obj("message" -> "API key required")))
}
}
Password Reset Functionality
// Password reset token
case class PasswordResetToken(
id: UUID,
userID: UUID,
email: String,
expirationTime: DateTime,
isSignUp: Boolean = false
)
// Initiate password reset
def forgotPassword = silhouette.UnsecuredAction.async { implicit request =>
ForgotPasswordForm.form.bindFromRequest.fold(
_ => Future.successful(BadRequest),
email => {
userService.retrieve(LoginInfo(CredentialsProvider.ID, email)).flatMap {
case Some(user) =>
val token = PasswordResetToken(
id = UUID.randomUUID(),
userID = user.userID,
email = email,
expirationTime = DateTime.now.plusHours(1)
)
for {
_ <- passwordResetTokenService.save(token)
_ <- emailService.sendPasswordResetEmail(user, token)
} yield Ok(Json.obj("message" -> "Reset email sent"))
case None =>
// Return success response for security even if user doesn't exist
Future.successful(Ok(Json.obj("message" -> "Reset email sent")))
}
}
)
}
// Execute password reset
def resetPassword(tokenID: String) = silhouette.UnsecuredAction.async { implicit request =>
ResetPasswordForm.form.bindFromRequest.fold(
_ => Future.successful(BadRequest),
formData => {
passwordResetTokenService.find(UUID.fromString(tokenID)).flatMap {
case Some(token) if token.expirationTime.isAfterNow =>
val passwordInfo = passwordHasherRegistry.current.hash(formData.password)
for {
_ <- passwordInfoDAO.update(LoginInfo(CredentialsProvider.ID, token.email), passwordInfo)
_ <- passwordResetTokenService.remove(token.id)
} yield Ok(Json.obj("message" -> "Password reset successful"))
case Some(_) =>
Future.successful(BadRequest(Json.obj("message" -> "Token expired")))
case None =>
Future.successful(BadRequest(Json.obj("message" -> "Invalid token")))
}
}
)
}