Skip to content
This repository has been archived by the owner on Sep 12, 2021. It is now read-only.

Commit

Permalink
Merge b5caf92 into 1ff7b5f
Browse files Browse the repository at this point in the history
  • Loading branch information
bravegag committed Jun 23, 2019
2 parents 1ff7b5f + b5caf92 commit d2ed292
Show file tree
Hide file tree
Showing 6 changed files with 419 additions and 0 deletions.
1 change: 1 addition & 0 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,6 @@ object Dependencies {
val casClient = "org.jasig.cas.client" % "cas-client-core" % "3.4.1"
val casClientSupportSAML = "org.jasig.cas.client" % "cas-client-support-saml" % "3.4.1"
val apacheCommonLang = "org.apache.commons" % "commons-lang3" % "3.8.1"
val googleAuth = "com.warrenstrange" % "googleauth" % "1.2.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/**
* Original work: SecureSocial (https://github.com/jaliss/securesocial)
* Copyright 2013 Jorge Aliss (jaliss at gmail dot com) - twitter: @jaliss
*
* Derivative work: Silhouette (https://github.com/mohiva/play-silhouette)
* Modifications Copyright 2015 Mohiva Organisation (license at mohiva dot com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.mohiva.play.silhouette.impl.providers

import com.mohiva.play.silhouette.api._
import com.mohiva.play.silhouette.api.util._
import com.mohiva.play.silhouette.impl.providers.PasswordProvider.HasherIsNotRegistered
import com.mohiva.play.silhouette.impl.providers.TotpProvider._

import scala.concurrent.Future

/**
* TOTP authentication information intended to be stored in an authentication repository.
*
* @param sharedKey The key associated to an user that together with a verification
* code enables authentication.
* @param scratchCodes A sequence of hashed scratch (or recovery) codes, which can be
* used each once and as alternative to verification codes.
*/
case class TotpInfo(sharedKey: String, scratchCodes: Seq[PasswordInfo]) extends AuthInfo

/**
* TOTP authentication credentials data including plain recovery codes and URL to the
* QR-code for first-time activation of the TOTP.
*
* @param totpInfo The TOTP authentication info that will be persisted in an
* authentication repository.
* @param scratchCodesPlain A sequence of scratch codes in plain text. This variant
* is provided for the user to secure save the first time.
* @param qrUrl The QR-code that matches this shared key for first time activation
*/
case class TotpCredentials(totpInfo: TotpInfo, scratchCodesPlain: Seq[String], qrUrl: String)

/**
* The base interface for all TOTP (Time-based One-time Password) providers.
*/
trait TotpProvider extends Provider with ExecutionContextProvider with Logger {
/**
* The Password hasher registry to use.
*/
val passwordHasherRegistry: PasswordHasherRegistry

/**
* Returns TotpInfo containing the credentials data including sharedKey and scratch codes.
*
* @param providerKey A unique key which identifies a user on this provider (userID, email, ...).
* @param issuer The issuer name. This parameter cannot contain the colon character.
* @return TotpInfo containing the credentials data including sharedKey and scratch codes.
*/
def createCredentials(providerKey: String, issuer: Option[String] = None): TotpCredentials

/**
* Returns some login info when the TOTP authentication with verification code was successful,
* None otherwise.
*
* @param sharedKey A unique key which identifies a user on this provider (userID, email, ...).
* @param verificationCode the verification code generated using TOTP.
* @return Some login info if the authentication was successful, None otherwise.
*/
def authenticate(sharedKey: String, verificationCode: String): Future[Option[LoginInfo]] = {
Future(
if (isVerificationCodeValid(sharedKey, verificationCode)) {
Some(LoginInfo(ID, sharedKey))
} else {
logger.debug(VerificationCodeDoesNotMatch)
None
}
)
}

/**
* Some tuple consisting of (`PasswordInfo`, `TotpInfo`) if the authentication was successful,
* None otherwise. Authenticate the user using a TOTP scratch (or recovery) code. This method will
* check each of the previously hashed scratch codes and find the first one that
* matches the one entered by the user. The one found is removed from `totpInfo` and returned
* for easy client-side bookkeeping.
*
* @param totpInfo The original TOTP info containing the hashed scratch codes.
* @param plainScratchCode The plain scratch code entered by the user.
* @return Some tuple consisting of (`PasswordInfo`, `TotpInfo`) if the authentication was successful, None otherwise.
*/
def authenticate(totpInfo: TotpInfo, plainScratchCode: String): Future[Option[(PasswordInfo, TotpInfo)]] = Future {
Option(totpInfo).flatMap { totpInfo =>
Option(plainScratchCode).flatMap {
case plainScratchCode: String if plainScratchCode.nonEmpty => {
val found: Option[PasswordInfo] = totpInfo.scratchCodes.filter { passwordInfo =>
passwordHasherRegistry.find(passwordInfo) match {
case Some(hasher) => hasher.matches(passwordInfo, plainScratchCode)
case None => {
logger.error(HasherIsNotRegistered.format(id, passwordInfo.hasher, passwordHasherRegistry.all.map(_.id).mkString(", ")))
false
}
}
}.headOption

found.map { deleted =>
deleted -> totpInfo.copy(scratchCodes = totpInfo.scratchCodes.filterNot(_ == deleted))
}
}
case _ => None
}
}
}

/**
* Returns true in case the verification code is valid for given shared key, false otherwise.
*
* @param sharedKey The TOTP shared key associated with the user.
* @param verificationCode The verification code, presumably valid at this moment.
* @return true in case the verification code is valid for given shared key, false otherwise.
*/
protected def isVerificationCodeValid(sharedKey: String, verificationCode: String): Boolean
}

object TotpProvider {
/**
* The provider Id.
*/
val ID = "totp"

/**
* Messages
*/
val VerificationCodeDoesNotMatch = "[Silhouette][%s] TOTP verification code doesn't match"
val ScratchCodesMustBeClearedOut = "[Silhouette][%s] TOTP plain scratch codes must be cleared out"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/**
* Original work: SecureSocial (https://github.com/jaliss/securesocial)
* Copyright 2013 Jorge Aliss (jaliss at gmail dot com) - twitter: @jaliss
*
* Derivative work: Silhouette (https://github.com/mohiva/play-silhouette)
* Modifications Copyright 2015 Mohiva Organisation (license at mohiva dot com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.mohiva.play.silhouette.impl.providers.totp

import com.mohiva.play.silhouette.api.util.PasswordHasherRegistry
import com.mohiva.play.silhouette.impl.providers._
import com.mohiva.play.silhouette.impl.providers.totp.GoogleTotpProvider._
import com.warrenstrange.googleauth.{ GoogleAuthenticator, GoogleAuthenticatorQRGenerator }
import javax.inject.Inject

import scala.concurrent.ExecutionContext
import scala.collection.JavaConverters._

/**
* Google's TOTP authentication concrete provider implementation.
*
* @param injectedPasswordHasherRegistry used to hash the scratch (or recovery) codes.
* @param executionContext the execution context.
*/
class GoogleTotpProvider @Inject() (injectedPasswordHasherRegistry: PasswordHasherRegistry)(implicit val executionContext: ExecutionContext) extends TotpProvider {
/**
* Returns the provider ID.
*
* @return the provider ID.
*/
override def id: String = ID

/**
* The Password hasher registry to use
*/
override val passwordHasherRegistry: PasswordHasherRegistry = injectedPasswordHasherRegistry

/**
* Returns true when the verification code is valid for the related shared key, false otherwise.
*
* @param sharedKey TOTP shared key associated with the user.
* @param verificationCode Verification code, presumably valid at this moment.
* @return true when the verification code is valid for the related shared key, false otherwise.
*/
override protected def isVerificationCodeValid(sharedKey: String, verificationCode: String): Boolean = {
Option(sharedKey).exists {
case sharedKey: String if sharedKey.nonEmpty => {
Option(verificationCode).exists {
case verificationCode: String if verificationCode.nonEmpty && verificationCode.forall(_.isDigit) => {
try {
googleAuthenticator.authorize(sharedKey, verificationCode.toInt)
} catch {
case e: IllegalArgumentException => {
logger.debug(e.getMessage)
false
}
}
}
case verificationCode: String if verificationCode.nonEmpty => {
logger.debug(VerificationCodeMustBeANumber.format(id))
false
}
case _ => {
logger.debug(VerificationCodeMustNotBeNullOrEmpty.format(id))
false
}
}
}
case _ => {
logger.debug(SharedKeyMustNotBeNullOrEmpty.format(id))
false
}
}
}

/**
* Returns the generated TOTP credentials including the shared key along with hashed scratch codes
* for safe storage, plain text scratch codes for first time use and the url to the QR activation code.
*
* @param accountName A unique key which identifies a user on this provider (userID, email, ...).
* @param issuer The issuer name. This parameter cannot contain the colon
* @return the generated TOTP credentials including the shared key, scratch codes and qr url.
*/
override def createCredentials(accountName: String, issuer: Option[String]): TotpCredentials = {
val credentials = googleAuthenticator.createCredentials()
val qrUrl = GoogleAuthenticatorQRGenerator.getOtpAuthURL(issuer.orNull, accountName, credentials)
val currentHasher = passwordHasherRegistry.current
val scratchCodesPlain = credentials.getScratchCodes.asScala.map(_.toString)
val hashedScratchCodes = scratchCodesPlain.map { scratchCode =>
currentHasher.hash(scratchCode)
}
TotpCredentials(TotpInfo(credentials.getKey, hashedScratchCodes), scratchCodesPlain, qrUrl)
}
}

/**
* The companion object.
*/
object GoogleTotpProvider {
/**
* Actual Google authenticator provider.
*/
private val googleAuthenticator = new GoogleAuthenticator()

/**
* The provider Id.
*/
val ID = TotpProvider.ID

/**
* Messages
*/
val SharedKeyMustNotBeNullOrEmpty = "[Silhouette][%s] shared key must not be null or empty"
val VerificationCodeMustNotBeNullOrEmpty = "[Silhouette][%s] verification code must not be null or empty"
val VerificationCodeMustBeANumber = "[Silhouette][%s] Google's verification code must be a number"
}
1 change: 1 addition & 0 deletions silhouette/build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ libraryDependencies ++= Seq(
Library.jwtCore,
Library.jwtApi,
Library.apacheCommonLang,
Library.googleAuth,
Library.Play.specs2 % Test,
Library.Specs2.matcherExtra % Test,
Library.Specs2.mock % Test,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* Copyright 2015 Mohiva Organisation (license at mohiva dot com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.mohiva.play.silhouette.impl.providers

import org.specs2.mock.Mockito

/**
* Abstract test case for the [[com.mohiva.play.silhouette.impl.providers.TotpProvider]] based class.
*/
trait TotpProviderSpec extends PasswordProviderSpec with Mockito {
/**
* The context.
*/
trait Context extends BaseContext
}
Loading

0 comments on commit d2ed292

Please sign in to comment.