Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support requiring 2FA for authentication, check isEnrolledIn2Sv #204

Merged
merged 1 commit into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def projectWithPlayVersion(playVersion: PlayVersion) =
"org.typelevel" %% "cats-core" % "2.9.0",
commonsCodec,
"org.scalatest" %% "scalatest" % "3.2.16" % Test,
"software.amazon.awssdk" % "ssm" % "2.21.7" % Test
Copy link
Member Author

@rtyley rtyley Nov 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new dependency is only for the test scope, as it's only needed for the new TwoFactorAuthCheckerCLITester, only used for double-checking Google Credentials are correctly setup.

) ++ googleDirectoryAPI ++ playVersion.playLibs,

sonatypeReleaseSettings
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.gu.googleauth

import com.google.api.services.directory.DirectoryScopes.ADMIN_DIRECTORY_USER_READONLY
import com.google.auth.oauth2.GoogleCredentials
import com.gu.googleauth.internal.DirectoryService

import scala.concurrent.{ExecutionContext, Future, blocking}

/**
* Uses the `isEnrolledIn2Sv` field on https://developers.google.com/admin-sdk/directory/reference/rest/v1/users
* to check the 2FA status of a user.
*
* @param googleCredentials must have read-only access to retrieve a User using the Admin SDK Directory API
*/
class TwoFactorAuthChecker(googleCredentials: GoogleCredentials) {

private val usersApi = DirectoryService(googleCredentials, ADMIN_DIRECTORY_USER_READONLY).users()

def check(userEmail: String)(implicit ec: ExecutionContext): Future[Boolean] = Future { blocking {
usersApi.get(userEmail).execute().getIsEnrolledIn2Sv
}}
}
11 changes: 11 additions & 0 deletions play-v27/src/main/scala/com/gu/googleauth/actions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,20 @@ trait LoginSupport extends Logging {
}
logWarn(desc, e)
Future.successful(redirectWithError(failureRedirectTarget, message))
}.flatMap { userIdentity =>
authConfig.twoFactorAuthChecker.map(requireTwoFactorAuthFor(userIdentity)).getOrElse(EitherT.pure(userIdentity))
}
}

private def requireTwoFactorAuthFor(userIdentity: UserIdentity)(checker: TwoFactorAuthChecker)(
implicit ec: ExecutionContext
): EitherT[Future, Result, UserIdentity] = EitherT {
checker.check(userIdentity.email).map(userHas2FA => if (userHas2FA) Right(userIdentity) else {
logger.warn(s"failed-oauth-callback : user-does-not-have-2fa")
Left(redirectWithError(failureRedirectTarget, "You do not have 2FA enabled"))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This message will be displayed to the user if they don't have 2FA - as in this Ophan PR here:

image

})
}

/**
* Looks up user's Google Groups and ensures they belong to any that are required. Redirects to
* `failureRedirectTarget` if the user is not a member of any required group.
Expand Down
4 changes: 3 additions & 1 deletion play-v27/src/main/scala/com/gu/googleauth/auth.scala
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import play.api.libs.ws.WSBodyWritables._
* @param prompt An optional space delimited, case sensitive list of ASCII string values that specifies whether the
* Authorization Server prompts the End-User for reauthentication and consent
* @param antiForgeryChecker configuration for the checks that ensure the OAuth callback can't be forged
* @param twoFactorAuthChecker only allow users to authenticate if they have 2FA enabled
*/
case class GoogleAuthConfig(
clientId: String,
Expand All @@ -43,7 +44,8 @@ case class GoogleAuthConfig(
maxAuthAge: Option[Duration] = GoogleAuthConfig.defaultMaxAuthAge,
enforceValidity: Boolean = GoogleAuthConfig.defaultEnforceValidity,
prompt: Option[String] = GoogleAuthConfig.defaultPrompt,
antiForgeryChecker: AntiForgeryChecker
antiForgeryChecker: AntiForgeryChecker,
twoFactorAuthChecker: Option[TwoFactorAuthChecker] = None
)

object GoogleAuthConfig {
Expand Down
4 changes: 3 additions & 1 deletion play-v27/src/main/scala/com/gu/googleauth/groups.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import scala.jdk.CollectionConverters._
*
* You can use a Service Account to access the Directory API (in fact, non-Service access, ie web-user,
* doesn't seem to work?). The Service Account needs the following scope:
* https://www.googleapis.com/auth/admin.directory.group.readonly
* https://www.googleapis.com/auth/admin.directory.group.readonly - note that if you're using
* [[TwoFactorAuthChecker]] it requires a different scope:
* https://www.googleapis.com/auth/admin.directory.user.readonly
*
* So long as you have the Service Account certificate as a string, you can easily make
* an instance of com.google.auth.oauth2.ServiceAccountCredentials with
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.gu.googleauth

import com.google.auth.oauth2.GoogleCredentials
import software.amazon.awssdk.auth.credentials.{AwsCredentialsProvider, ProfileCredentialsProvider}
import software.amazon.awssdk.http.apache.ApacheHttpClient
import software.amazon.awssdk.regions.Region.EU_WEST_1
import software.amazon.awssdk.services.ssm.SsmClient
import software.amazon.awssdk.services.ssm.model.GetParameterRequest

import scala.concurrent.Await
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.Duration

/**
* This is a non-production piece of code, only intended for checking that Google Credentials are correctly set up,
* and that we're able to get a good response from the Google Admin SDK Directory API.
*/
object TwoFactorAuthCheckerCLITester extends App {

val checker = new TwoFactorAuthChecker(GoogleCredentialsInDev.loadGuardianGoogleCredentials())

val userEmail = ??? // eg "firstname.lastname@guardian.co.uk"

val has2FA: Boolean = Await.result(checker.check(userEmail), Duration.Inf)

println(s"User $userEmail has 2FA enabled: $has2FA")
}

object GoogleCredentialsInDev {
/**
* Only intended for use by Guardian developers, in development. Requires Janus credentials.
*/
def loadGuardianGoogleCredentials(): GoogleCredentials = {
val aws = new AWS("ophan")
val serviceAccountCert = aws.loadSecureString("/Ophan/Dashboard/GoogleCloudPlatform/OphanOAuthServiceAccountCert")
val impersonatedUser = aws.loadSecureString("/Ophan/Dashboard/GoogleCloudPlatform/ImpersonatedUser")

ServiceAccountHelper.credentialsFrom(serviceAccountCert).createDelegated(impersonatedUser)
}
}

class AWS(profileName: String) {
val credentials: AwsCredentialsProvider = ProfileCredentialsProvider.builder().profileName(profileName).build()

val SSM: SsmClient =
SsmClient.builder().httpClientBuilder(ApacheHttpClient.builder()).credentialsProvider(credentials).region(EU_WEST_1).build()

def loadSecureString(paramName: String): String = SSM.getParameter(
GetParameterRequest.builder().name(paramName).withDecryption(true).build()
).parameter().value()
}
Loading