com.gu.identity.play.IdentityPlayAuthService.scala Maven / Gradle / Ivy
The newest version!
package com.gu.identity.play
import cats.effect.IO
import cats.implicits.catsSyntaxApplicativeError
import cats.syntax.either._
import com.gu.identity.auth._
import play.api.mvc.{Cookie, RequestHeader}
import scala.concurrent.ExecutionContext
// Class that builds upon functionality of IdentityAuthService
// to make user authentication convenient in the context of a Play application.
// It handles extracting the user credentials from a Play request before authenticating them
// using the underlying IdentityAuthService.
// Target client should be set if this service is being used to authenticate using crypto access tokens
// in addition to SC_GU_U cookies. See comments on UserCredentials.CryptoAccessToken for more context.
class IdentityPlayAuthService(
identityAuthService: IdentityAuthService,
targetClient: Option[String]
) {
// just convenience vals to avoid having to repeat the same okta or idapi specific methods here or asking clients to initialise the services themselves
lazy val idapiPlayAuthService = new IdapiPlayAuthService(identityAuthService.idapiAuthService, targetClient)
lazy val oktaPlayAuthService = new OktaPlayAuthService(identityAuthService.oktaAuthService)
import IdentityPlayAuthService._
// -----
def getOktaCredentialsFromRequest(requestHeader: RequestHeader): IO[UserCredentials] = IO.fromEither {
Helpers
.fetchBearerTokenFromAuthHeader(requestHeader.headers.get)
.map(OktaUserCredentials)
.left
.map(OktaValidationException)
}
def getCredentialsFromRequest(request: RequestHeader): IO[UserCredentials] =
getOktaCredentialsFromRequest(request) recoverWith { case OktaValidationException(MissingHeader) =>
getIdapiUserCredentialsFromRequest(request, targetClient)
}
def validateCredentialsFromRequest[I <: IdentityClaims, A <: AccessClaims](
request: RequestHeader,
requiredScopes: List[AccessScope] = List.empty,
)(implicit
accessClaimsParser: AccessClaimsParser[A],
userInfoResponseParser: UserInfoResponseParser[I]
): IO[(UserCredentials, AuthenticatedUserData[I, A])] = {
for {
credentials <- getCredentialsFromRequest(request)
user <- identityAuthService.validateCredentials(credentials, requiredScopes)
} yield (credentials, user)
}
}
object IdentityPlayAuthService {
case class UserCredentialsMissingError(message: String) extends Exception {
override def getMessage: String = message
}
def getSCGUUCookieFromRequest(request: RequestHeader): IO[Cookie] =
IO.fromEither(
Either.fromOption(
request.cookies.get("SC_GU_U"),
UserCredentialsMissingError("SC_GU_U cookie not set")
)
)
def getCryptoAccessTokenFromRequest(
request: RequestHeader,
targetClient: String
): IO[IdapiUserCredentials.CryptoAccessToken] =
IO.fromEither(
Either.fromOption(
request.headers
.get("GU-IdentityToken")
.map(token => IdapiUserCredentials.CryptoAccessToken(token, targetClient)),
UserCredentialsMissingError("GU-IdentityToken header not set")
)
)
def getIdapiUserCredentialsFromRequest(
request: RequestHeader,
targetClient: Option[String]
): IO[IdapiUserCredentials] =
getSCGUUCookieFromRequest(request)
.redeemWith(
// If no SC_GU_U cookie set, attempt to get crypto access token if target client is defined.
err => {
// If target client isn't set, then this indicates authentication with crypto access tokens is not supported.
// In this case just return the original error.
targetClient.fold(IO.raiseError[IdapiUserCredentials](err)) { client =>
// Otherwise attempt to extract a crypto access toke for the target client.
getCryptoAccessTokenFromRequest(request, client)
.handleErrorWith { _ =>
// At this point we know attempts were made to extract both credential types,
// and that both attempts were unsuccessful due to the respective credentials not being present.
IO.raiseError(UserCredentialsMissingError("neither SC_GU_U cookie or GU-IdentityToken header set"))
}
}
},
cookie => IO(IdapiUserCredentials.SCGUUCookie(cookie.value))
)
// Supports creating an IdentityPlayAuthService outside of an IO context e.g. a Play application (typically)
def unsafeInit(idapiAuthConfig: IdapiAuthConfig, oktaTokenValidationConfig: OktaTokenValidationConfig)(implicit
ec: ExecutionContext
): IdentityPlayAuthService = {
val identityAuthService = IdentityAuthService.unsafeInit(
IdapiAuthConfig(idapiAuthConfig.identityApiUri, idapiAuthConfig.accessToken),
oktaTokenValidationConfig
)
new IdentityPlayAuthService(identityAuthService, idapiAuthConfig.targetClient)
}
}