auth.basic.scala Maven / Gradle / Ivy
package otoroshi.auth
import java.security.SecureRandom
import java.util.{Base64, Optional}
import akka.http.scaladsl.model.Uri
import akka.http.scaladsl.util.FastFuture
import com.fasterxml.jackson.annotation.JsonInclude.Include
import com.fasterxml.jackson.databind.{ObjectMapper, SerializationFeature}
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module
import com.google.common.base.Charsets
import com.yubico.webauthn._
import com.yubico.webauthn.data._
import otoroshi.controllers.{routes, LocalCredentialRepository}
import otoroshi.env.Env
import otoroshi.models._
import org.joda.time.DateTime
import org.mindrot.jbcrypt.BCrypt
import otoroshi.models.{OtoroshiAdminType, UserRight, UserRights, WebAuthnOtoroshiAdmin}
import otoroshi.utils.syntax.implicits._
import play.api.Logger
import play.api.libs.json._
import play.api.mvc._
import otoroshi.security.{IdGenerator, OtoroshiClaim}
import otoroshi.utils.{JsonPathValidator, JsonValidator}
import java.nio.charset.StandardCharsets
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success, Try}
case class WebAuthnDetails(handle: String, credentials: Map[String, JsValue]) {
def asJson: JsValue = WebAuthnDetails.fmt.writes(this)
}
object WebAuthnDetails {
def fmt =
new Format[WebAuthnDetails] {
override def writes(o: WebAuthnDetails) =
Json.obj(
"handle" -> o.handle,
"credentials" -> o.credentials
)
override def reads(json: JsValue) =
Try {
JsSuccess(
WebAuthnDetails(
handle = (json \ "handle").as[String],
credentials = (json \ "credentials").asOpt[Map[String, JsValue]].getOrElse(Map.empty)
)
)
} recover { case e =>
JsError(e.getMessage)
} get
}
}
case class BasicAuthUser(
name: String,
password: String,
email: String,
webauthn: Option[WebAuthnDetails] = None,
metadata: JsObject = Json.obj(),
tags: Seq[String],
rights: UserRights,
adminEntityValidators: Map[String, Seq[JsonValidator]]
) {
def asJson: JsValue = BasicAuthUser.fmt.writes(this)
}
object BasicAuthUser {
def fmt =
new Format[BasicAuthUser] {
override def writes(o: BasicAuthUser) =
Json.obj(
"name" -> o.name,
"password" -> o.password,
"email" -> o.email,
"metadata" -> o.metadata,
"tags" -> o.tags,
"webauthn" -> o.webauthn.map(_.asJson).getOrElse(JsNull).as[JsValue],
"rights" -> o.rights.json,
"adminEntityValidators" -> o.adminEntityValidators.mapValues(v => JsArray(v.map(_.json)))
)
override def reads(json: JsValue) =
Try {
JsSuccess(
BasicAuthUser(
name = (json \ "name").as[String],
password = (json \ "password").as[String],
email = (json \ "email").as[String],
webauthn = (json \ "webauthn").asOpt(WebAuthnDetails.fmt),
metadata = (json \ "metadata").asOpt[JsObject].getOrElse(Json.obj()),
tags = (json \ "tags").asOpt[Seq[String]].getOrElse(Seq.empty),
rights = UserRights.readFromObject(json),
adminEntityValidators = json
.select("adminEntityValidators")
.asOpt[JsObject]
.map { obj =>
obj.value.mapValues { arr =>
arr.asArray.value
.map { item =>
JsonValidator.format.reads(item)
}
.collect { case JsSuccess(v, _) =>
v
}
}.toMap
}
.getOrElse(Map.empty[String, Seq[JsonValidator]])
)
)
} recover { case e =>
JsError(e.getMessage)
} get
}
}
object BasicAuthModuleConfig extends FromJson[AuthModuleConfig] {
lazy val logger = Logger("otoroshi-basic-auth-config")
def fromJsons(value: JsValue): BasicAuthModuleConfig =
try {
_fmt.reads(value).get
} catch {
case e: Throwable => {
logger.error(s"Try to deserialize ${Json.prettyPrint(value)}")
throw e
}
}
val _fmt = new Format[BasicAuthModuleConfig] {
override def reads(json: JsValue) =
fromJson(json) match {
case Left(e) => JsError(e.getMessage)
case Right(v) => JsSuccess(v.asInstanceOf[BasicAuthModuleConfig])
}
override def writes(o: BasicAuthModuleConfig) = o.asJson
}
override def fromJson(json: JsValue): Either[Throwable, AuthModuleConfig] =
Try {
Right(
BasicAuthModuleConfig(
location = otoroshi.models.EntityLocation.readFromKey(json),
id = (json \ "id").as[String],
name = (json \ "name").as[String],
desc = (json \ "desc").asOpt[String].getOrElse("--"),
clientSideSessionEnabled = (json \ "clientSideSessionEnabled").asOpt[Boolean].getOrElse(true),
sessionMaxAge = (json \ "sessionMaxAge").asOpt[Int].getOrElse(86400),
basicAuth = (json \ "basicAuth").asOpt[Boolean].getOrElse(false),
webauthn = (json \ "webauthn").asOpt[Boolean].getOrElse(false),
users = (json \ "users").asOpt(Reads.seq(BasicAuthUser.fmt)).getOrElse(Seq.empty[BasicAuthUser]),
metadata = (json \ "metadata").asOpt[Map[String, String]].getOrElse(Map.empty),
tags = (json \ "tags").asOpt[Seq[String]].getOrElse(Seq.empty[String]),
sessionCookieValues =
(json \ "sessionCookieValues").asOpt(SessionCookieValues.fmt).getOrElse(SessionCookieValues()),
userValidators = (json \ "userValidators")
.asOpt[Seq[JsValue]]
.map(_.flatMap(v => JsonPathValidator.format.reads(v).asOpt))
.getOrElse(Seq.empty),
remoteValidators = (json \ "remoteValidators")
.asOpt[Seq[JsValue]]
.map(_.flatMap(v => RemoteUserValidatorSettings.format.reads(v).asOpt))
.getOrElse(Seq.empty),
allowedUsers = json.select("allowedUsers").asOpt[Seq[String]].getOrElse(Seq.empty),
deniedUsers = json.select("deniedUsers").asOpt[Seq[String]].getOrElse(Seq.empty)
)
)
} recover { case e =>
Left(e)
} get
}
case class BasicAuthModuleConfig(
id: String,
name: String,
desc: String,
users: Seq[BasicAuthUser] = Seq.empty[BasicAuthUser],
clientSideSessionEnabled: Boolean,
sessionMaxAge: Int = 86400,
userValidators: Seq[JsonPathValidator] = Seq.empty,
remoteValidators: Seq[RemoteUserValidatorSettings] = Seq.empty,
basicAuth: Boolean = false,
webauthn: Boolean = false,
tags: Seq[String],
metadata: Map[String, String],
sessionCookieValues: SessionCookieValues,
location: otoroshi.models.EntityLocation = otoroshi.models.EntityLocation(),
allowedUsers: Seq[String] = Seq.empty,
deniedUsers: Seq[String] = Seq.empty
) extends AuthModuleConfig {
def `type`: String = "basic"
def humanName: String = "In memory auth. provider"
override def form: Option[Form] = None
override def withLocation(location: EntityLocation): AuthModuleConfig = copy(location = location)
override def authModule(config: GlobalConfig): AuthModule = BasicAuthModule(this)
override def asJson =
location.jsonWithKey ++ Json.obj(
"type" -> "basic",
"id" -> this.id,
"name" -> this.name,
"desc" -> this.desc,
"basicAuth" -> this.basicAuth,
"webauthn" -> this.webauthn,
"clientSideSessionEnabled" -> this.clientSideSessionEnabled,
"sessionMaxAge" -> this.sessionMaxAge,
"metadata" -> this.metadata,
"tags" -> JsArray(tags.map(JsString.apply)),
"users" -> Writes.seq(BasicAuthUser.fmt).writes(this.users),
"sessionCookieValues" -> SessionCookieValues.fmt.writes(this.sessionCookieValues),
"userValidators" -> JsArray(userValidators.map(_.json)),
"allowedUsers" -> this.allowedUsers,
"deniedUsers" -> this.deniedUsers,
"remoteValidators" -> JsArray(remoteValidators.map(_.json))
)
def save()(implicit ec: ExecutionContext, env: Env): Future[Boolean] = env.datastores.authConfigsDataStore.set(this)
override def cookieSuffix(desc: ServiceDescriptor) = s"basic-auth-$id"
def theDescription: String = desc
def theMetadata: Map[String, String] = metadata
def theName: String = name
def theTags: Seq[String] = tags
override def _fmt()(implicit env: Env): Format[AuthModuleConfig] = AuthModuleConfig._fmt(env)
}
object BasicAuthModule {
def defaultConfig = BasicAuthModuleConfig(
id = IdGenerator.namedId("auth_mod", IdGenerator.uuid),
name = "New auth. module",
desc = "New auth. module",
tags = Seq.empty,
metadata = Map.empty,
sessionCookieValues = SessionCookieValues(),
clientSideSessionEnabled = true
)
// def apply(): BasicAuthModule = BasicAuthModule(defaultConfig)
}
case class BasicAuthModule(authConfig: BasicAuthModuleConfig) extends AuthModule {
def this() = this(BasicAuthModule.defaultConfig)
def decodeBase64(encoded: String): String = new String(OtoroshiClaim.decoder.decode(encoded), Charsets.UTF_8)
def extractUsernamePassword(header: String): Option[(String, String)] = {
val base64 = header.replace("Basic ", "").replace("basic ", "")
Option(base64)
.map(decodeBase64)
.map(_.split(":").toSeq)
.filter(v => v.nonEmpty && v.length > 1)
.flatMap(a => a.headOption.map(head => (head, a.tail.mkString(":"))))
}
def bindUser(username: String, password: String, descriptor: ServiceDescriptor)(implicit
env: Env,
ec: ExecutionContext
): Future[Either[ErrorReason, PrivateAppsUser]] = {
authConfig.users
.find(u => u.email == username)
.filter(u => BCrypt.checkpw(password, u.password)) match {
case Some(user) =>
PrivateAppsUser(
randomId = IdGenerator.token(64),
name = user.name,
email = user.email,
profile = Json.obj(
"name" -> user.name,
"email" -> user.email,
"metadata" -> user.metadata,
"tags" -> user.tags
),
realm = authConfig.cookieSuffix(descriptor),
otoroshiData = Some(user.metadata),
authConfigId = authConfig.id,
tags = Seq.empty,
metadata = Map.empty,
location = authConfig.location
).validate(descriptor, isRoute = true, authConfig)
case None => Left(ErrorReason(s"You're not authorized here")).vfuture
}
}
def bindAdminUser(username: String, password: String, descriptor: ServiceDescriptor)(implicit
env: Env,
ec: ExecutionContext
): Future[Either[ErrorReason, BackOfficeUser]] = {
authConfig.users
.find(u => u.email == username)
.filter(u => BCrypt.checkpw(password, u.password)) match {
case Some(user) =>
BackOfficeUser(
randomId = IdGenerator.token(64),
name = user.name,
email = user.email,
profile = Json.obj(
"name" -> user.name,
"email" -> user.email,
"metadata" -> user.metadata,
"tags" -> user.tags
),
simpleLogin = false,
authConfigId = authConfig.id,
tags = Seq.empty,
metadata = Map.empty,
rights = user.rights,
adminEntityValidators = user.adminEntityValidators,
location = authConfig.location
).validate(descriptor, isRoute = true, authConfig)
case None => Left(ErrorReason(s"You're not authorized here")).vfuture
}
}
override def paLoginPage(
request: RequestHeader,
config: GlobalConfig,
descriptor: ServiceDescriptor,
isRoute: Boolean
)(implicit
ec: ExecutionContext,
env: Env
): Future[Result] = {
implicit val req = request
val redirect = request
.getQueryString("redirect")
.filter(redirect =>
request.getQueryString("hash").contains(env.sign(s"desc=${descriptor.id}&redirect=${redirect}"))
)
.map(redirectBase64Encoded =>
new String(Base64.getUrlDecoder.decode(redirectBase64Encoded), StandardCharsets.UTF_8)
)
val hash = env.sign(s"${authConfig.id}:::${descriptor.id}")
env.datastores.authConfigsDataStore.generateLoginToken().flatMap { token =>
if (authConfig.basicAuth) {
def unauthorized() =
Results
.Unauthorized("")
.withHeaders("WWW-Authenticate" -> s"""Basic realm="${authConfig.cookieSuffix(descriptor)}"""")
.addingToSession(
s"pa-redirect-after-login-${authConfig.cookieSuffix(descriptor)}" -> redirect.getOrElse(
routes.PrivateAppsController.home.absoluteURL(env.exposedRootSchemeIsHttps)
)
)
.future
req.headers.get("Authorization") match {
case Some(auth) if auth.startsWith("Basic ") =>
extractUsernamePassword(auth) match {
case None => Results.Forbidden(otoroshi.views.html.oto.error("Forbidden access", env)).future
case Some((username, password)) =>
bindUser(username, password, descriptor) flatMap {
case Left(_) => Results.Forbidden(otoroshi.views.html.oto.error("Forbidden access", env)).future
case Right(user) =>
env.datastores.authConfigsDataStore.setUserForToken(token, user.toJson).map { _ =>
if (isRoute) {
Results.Redirect(
s"/privateapps/generic/callback?route=true&ref=${authConfig.id}&desc=${descriptor.id}&token=$token&hash=$hash"
)
} else {
Results.Redirect(s"/privateapps/generic/callback?desc=${descriptor.id}&token=$token&hash=$hash")
}
}
}
}
case _ => unauthorized()
}
} else {
Results
.Ok(
otoroshi.views.html.oto
.login(
if (isRoute)
s"/privateapps/generic/callback?route=true&ref=${authConfig.id}&desc=${descriptor.id}&hash=$hash"
else
s"/privateapps/generic/callback?desc=${descriptor.id}&hash=$hash",
"POST",
token,
authConfig.webauthn,
env
)
)
.addingToSession(
s"pa-redirect-after-login-${authConfig.cookieSuffix(descriptor)}" -> redirect.getOrElse(
routes.PrivateAppsController.home.absoluteURL(env.exposedRootSchemeIsHttps)
)
)
.future
}
}
}
override def paLogout(
request: RequestHeader,
user: Option[PrivateAppsUser],
config: GlobalConfig,
descriptor: ServiceDescriptor
)(implicit
ec: ExecutionContext,
env: Env
) = FastFuture.successful(Right(None))
override def paCallback(request: Request[AnyContent], config: GlobalConfig, descriptor: ServiceDescriptor)(implicit
ec: ExecutionContext,
env: Env
): Future[Either[ErrorReason, PrivateAppsUser]] = {
implicit val req = request
if (req.method == "GET" && authConfig.basicAuth) {
req.getQueryString("token") match {
case Some(token) =>
env.datastores.authConfigsDataStore
.getUserForToken(token)
.map(_.flatMap(a => PrivateAppsUser.fmt.reads(a).asOpt))
.flatMap {
case Some(user) =>
user.validate(
descriptor,
isRoute = true,
authConfig
)
case None => Left(ErrorReason("No user found")).vfuture
}
case _ => FastFuture.successful(Left(ErrorReason("Forbidden access")))
}
} else {
request.body.asFormUrlEncoded match {
case None => FastFuture.successful(Left(ErrorReason("No Authorization form here")))
case Some(form) => {
(form.get("username").map(_.last), form.get("password").map(_.last), form.get("token").map(_.last)) match {
case (Some(username), Some(password), Some(token)) => {
env.datastores.authConfigsDataStore.validateLoginToken(token).flatMap {
case false => Left(ErrorReason("Bad token")).vfuture
case true =>
authConfig.users
.find(u => u.email == username)
.filter(u => BCrypt.checkpw(password, u.password)) match {
case Some(user) =>
PrivateAppsUser(
randomId = IdGenerator.token(64),
name = user.name,
email = user.email,
profile = Json.obj(
"name" -> user.name,
"email" -> user.email,
"metadata" -> user.metadata,
"tags" -> user.tags
),
realm = authConfig.cookieSuffix(descriptor),
otoroshiData = Some(user.metadata),
authConfigId = authConfig.id,
tags = Seq.empty,
metadata = Map.empty,
location = authConfig.location
).validate(
descriptor,
isRoute = true,
authConfig
)
case None => Left(ErrorReason(s"You're not authorized here")).vfuture
}
}
}
case _ => {
FastFuture.successful(Left(ErrorReason("Authorization form is not complete")))
}
}
}
}
}
}
override def boLoginPage(request: RequestHeader, config: GlobalConfig)(implicit
ec: ExecutionContext,
env: Env
): Future[Result] = {
implicit val req = request
val redirect = request.getQueryString("redirect")
val hash = env.sign(s"${authConfig.id}:::backoffice")
env.datastores.authConfigsDataStore.generateLoginToken().flatMap { token =>
if (authConfig.basicAuth) {
def unauthorized() =
Results
.Unauthorized(otoroshi.views.html.oto.error("You are not authorized here", env))
.withHeaders("WWW-Authenticate" -> "otoroshi-admin-realm")
.addingToSession(
"bo-redirect-after-login" -> redirect.getOrElse(
routes.PrivateAppsController.home.absoluteURL(env.exposedRootSchemeIsHttps)
)
)
.future
req.headers.get("Authorization") match {
case Some(auth) if auth.startsWith("Basic ") =>
extractUsernamePassword(auth) match {
case None => Results.Forbidden(otoroshi.views.html.oto.error("Forbidden access", env)).future
case Some((username, password)) =>
bindAdminUser(username, password, env.backOfficeServiceDescriptor) flatMap {
case Left(_) => Results.Forbidden(otoroshi.views.html.oto.error("Forbidden access", env)).future
case Right(user) =>
env.datastores.authConfigsDataStore.setUserForToken(token, user.toJson).map { _ =>
Results.Redirect(s"/backoffice/auth0/callback?token=$token&hash=$hash")
}
}
}
case _ => unauthorized()
}
} else {
Results
.Ok(
otoroshi.views.html.oto
.login(s"/backoffice/auth0/callback?hash=$hash", "POST", token, authConfig.webauthn, env)
)
.addingToSession(
"bo-redirect-after-login" -> redirect.getOrElse(
routes.BackOfficeController.dashboard.absoluteURL(env.exposedRootSchemeIsHttps)
)
)
.future
}
}
}
override def boLogout(request: RequestHeader, user: BackOfficeUser, config: GlobalConfig)(implicit
ec: ExecutionContext,
env: Env
) =
FastFuture.successful(Right(None))
override def boCallback(
request: Request[AnyContent],
config: GlobalConfig
)(implicit ec: ExecutionContext, env: Env): Future[Either[ErrorReason, BackOfficeUser]] = {
implicit val req = request
if (req.method == "GET" && authConfig.basicAuth) {
req.getQueryString("token") match {
case Some(token) =>
env.datastores.authConfigsDataStore
.getUserForToken(token)
.map(_.flatMap(a => BackOfficeUser.fmt.reads(a).asOpt))
.map {
case Some(user) => Right(user)
case None => Left(ErrorReason("No user found"))
}
case _ => FastFuture.successful(Left(ErrorReason("Forbidden access")))
}
} else {
request.body.asFormUrlEncoded match {
case None => FastFuture.successful(Left(ErrorReason("No Authorization form here")))
case Some(form) => {
(form.get("username").map(_.last), form.get("password").map(_.last), form.get("token").map(_.last)) match {
case (Some(username), Some(password), Some(token)) => {
env.datastores.authConfigsDataStore.validateLoginToken(token).flatMap {
case false => Left(ErrorReason("Bad token")).vfuture
case true => bindAdminUser(username, password, env.backOfficeServiceDescriptor)
}
}
case _ => {
FastFuture.successful(Left(ErrorReason("Authorization form is not complete")))
}
}
}
}
}
}
/////////// Webauthn
private val base64Encoder = java.util.Base64.getUrlEncoder
private val base64Decoder = java.util.Base64.getUrlDecoder
private val random = new SecureRandom()
private val jsonMapper = new ObjectMapper()
.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
.setSerializationInclusion(Include.NON_ABSENT)
.registerModule(new Jdk8Module())
def webAuthnLoginStart(
body: JsValue,
descriptor: ServiceDescriptor
)(implicit env: Env, ec: ExecutionContext): Future[Either[String, JsValue]] = {
import collection.JavaConverters._
val usernameOpt = (body \ "username").asOpt[String]
val passwordOpt = (body \ "password").asOpt[String]
val reqOrigin = (body \ "origin").as[String]
val reqOriginHost = Uri(reqOrigin).authority.host.address()
val reqOriginDomain: String = reqOriginHost.split("\\.").toList.reverse match {
case tld :: domain :: _ => s"$domain.$tld"
case value => value.mkString(".")
}
val users = authConfig.users.filter(_.webauthn.isDefined).map { usr =>
WebAuthnOtoroshiAdmin(
username = usr.email,
password = "foo",
label = "foo",
handle = usr.webauthn.get.handle,
credentials = usr.webauthn.get.credentials,
createdAt = DateTime.now(),
typ = OtoroshiAdminType.WebAuthnAdmin,
metadata = Map.empty,
rights = usr.rights,
location = authConfig.location,
adminEntityValidators = usr.adminEntityValidators
)
}
(usernameOpt, passwordOpt) match {
case (Some(username), Some(password)) => {
bindUser(username, password, descriptor).map(_.toOption) flatMap {
case Some(_) => {
val rpIdentity: RelyingPartyIdentity =
RelyingPartyIdentity.builder.id(reqOriginDomain).name("Otoroshi").build
val rp: RelyingParty = RelyingParty.builder
.identity(rpIdentity)
.credentialRepository(new LocalCredentialRepository(users, jsonMapper, base64Decoder))
.origins(Seq(reqOrigin, reqOriginDomain).toSet.asJava)
.build
val request: AssertionRequest =
rp.startAssertion(StartAssertionOptions.builder.username(Optional.of(username)).build)
val registrationRequestId = IdGenerator.token(32)
val jsonRequest: String = jsonMapper.writeValueAsString(request)
val finalRequest = Json.obj(
"requestId" -> registrationRequestId,
"request" -> Json.parse(jsonRequest),
"username" -> username,
"label" -> "--"
)
env.datastores.webAuthnRegistrationsDataStore
.setRegistrationRequest(registrationRequestId, finalRequest)
.map { _ =>
Right(finalRequest)
}
}
case _ => FastFuture.successful(Left("bad request"))
}
}
case (_, _) => {
FastFuture.successful(Left("bad request"))
}
}
}
def webAuthnAdminLoginStart(
body: JsValue
)(implicit env: Env, ec: ExecutionContext): Future[Either[String, JsValue]] = {
import collection.JavaConverters._
val usernameOpt = (body \ "username").asOpt[String]
val passwordOpt = (body \ "password").asOpt[String]
val reqOrigin = (body \ "origin").as[String]
val reqOriginHost = Uri(reqOrigin).authority.host.address()
val reqOriginDomain: String = reqOriginHost.split("\\.").toList.reverse match {
case tld :: domain :: _ => s"$domain.$tld"
case value => value.mkString(".")
}
val users = authConfig.users.filter(_.webauthn.isDefined).map { usr =>
WebAuthnOtoroshiAdmin(
username = usr.email,
password = "foo",
label = "foo",
handle = usr.webauthn.get.handle,
credentials = usr.webauthn.get.credentials,
createdAt = DateTime.now(),
typ = OtoroshiAdminType.WebAuthnAdmin,
metadata = Map.empty,
rights = usr.rights,
location = authConfig.location,
adminEntityValidators = usr.adminEntityValidators
)
}
(usernameOpt, passwordOpt) match {
case (Some(username), Some(password)) => {
bindAdminUser(username, password, env.backOfficeServiceDescriptor).map(_.toOption) flatMap {
case Some(_) => {
val rpIdentity: RelyingPartyIdentity =
RelyingPartyIdentity.builder.id(reqOriginDomain).name("Otoroshi").build
val rp: RelyingParty = RelyingParty.builder
.identity(rpIdentity)
.credentialRepository(new LocalCredentialRepository(users, jsonMapper, base64Decoder))
.origins(Seq(reqOrigin, reqOriginDomain).toSet.asJava)
.build
val request: AssertionRequest =
rp.startAssertion(StartAssertionOptions.builder.username(Optional.of(username)).build)
val registrationRequestId = IdGenerator.token(32)
val jsonRequest: String = jsonMapper.writeValueAsString(request)
val finalRequest = Json.obj(
"requestId" -> registrationRequestId,
"request" -> Json.parse(jsonRequest),
"username" -> username,
"label" -> "--"
)
env.datastores.webAuthnRegistrationsDataStore
.setRegistrationRequest(registrationRequestId, finalRequest)
.map { _ =>
Right(finalRequest)
}
}
case _ => FastFuture.successful(Left("bad request"))
}
}
case (_, _) => {
FastFuture.successful(Left("bad request"))
}
}
}
def webAuthnLoginFinish(
body: JsValue,
descriptor: ServiceDescriptor
)(implicit env: Env, ec: ExecutionContext): Future[Either[ErrorReason, PrivateAppsUser]] = {
import collection.JavaConverters._
val json = body
val webauthn = (json \ "webauthn").as[JsObject]
val otoroshi = (json \ "otoroshi").as[JsObject]
val reqOrigin = (otoroshi \ "origin").as[String]
val reqId = (json \ "requestId").as[String]
val reqOriginHost = Uri(reqOrigin).authority.host.address()
val reqOriginDomain: String = reqOriginHost.split("\\.").toList.reverse match {
case tld :: domain :: _ => s"$domain.$tld"
case value => value.mkString(".")
}
val users = authConfig.users.filter(_.webauthn.isDefined).map { usr =>
WebAuthnOtoroshiAdmin(
username = usr.email,
password = "foo",
label = "foo",
handle = usr.webauthn.get.handle,
credentials = usr.webauthn.get.credentials,
createdAt = DateTime.now(),
typ = OtoroshiAdminType.WebAuthnAdmin,
metadata = Map.empty,
rights = usr.rights,
location = authConfig.location,
adminEntityValidators = usr.adminEntityValidators
)
}
val usernameOpt = (otoroshi \ "username").asOpt[String]
val passwordOpt = (otoroshi \ "password").asOpt[String]
(usernameOpt, passwordOpt) match {
case (Some(username), Some(pass)) => {
users.find(u => u.username == username) match {
case None => FastFuture.successful(Left(ErrorReason("Bad user")))
case Some(user) => {
env.datastores.webAuthnRegistrationsDataStore.getRegistrationRequest(reqId).flatMap {
case None => FastFuture.successful(Left(ErrorReason("bad request")))
case Some(rawRequest) => {
val request =
jsonMapper.readValue(Json.stringify((rawRequest \ "request").as[JsValue]), classOf[AssertionRequest])
bindUser(username, pass, descriptor) flatMap {
case Left(err) => FastFuture.successful(Left(err))
case Right(user) => {
val rpIdentity: RelyingPartyIdentity =
RelyingPartyIdentity.builder.id(reqOriginDomain).name("Otoroshi").build
val rp: RelyingParty = RelyingParty.builder
.identity(rpIdentity)
.credentialRepository(new LocalCredentialRepository(users, jsonMapper, base64Decoder))
.origins(Seq(reqOrigin, reqOriginDomain).toSet.asJava)
.build
val pkc = PublicKeyCredential.parseAssertionResponseJson(Json.stringify(webauthn))
Try(
rp.finishAssertion(
FinishAssertionOptions
.builder()
.request(request)
.response(pkc)
.build()
)
) match {
case Failure(e) =>
FastFuture.successful(Left(ErrorReason("bad request")))
case Success(result) if !result.isSuccess =>
FastFuture.successful(Left(ErrorReason("bad request")))
case Success(result) if result.isSuccess => {
FastFuture.successful(Right(user))
}
}
}
}
}
}
}
}
}
case (_, _) => FastFuture.successful(Left(ErrorReason("Not Authorized")))
}
}
def webAuthnAdminLoginFinish(
body: JsValue
)(implicit env: Env, ec: ExecutionContext): Future[Either[ErrorReason, BackOfficeUser]] = {
import collection.JavaConverters._
val json = body
val webauthn = (json \ "webauthn").as[JsObject]
val otoroshi = (json \ "otoroshi").as[JsObject]
val reqOrigin = (otoroshi \ "origin").as[String]
val reqId = (json \ "requestId").as[String]
val reqOriginHost = Uri(reqOrigin).authority.host.address()
val reqOriginDomain: String = reqOriginHost.split("\\.").toList.reverse match {
case tld :: domain :: _ => s"$domain.$tld"
case value => value.mkString(".")
}
val users = authConfig.users.filter(_.webauthn.isDefined).map { usr =>
WebAuthnOtoroshiAdmin(
username = usr.email,
password = "foo",
label = "foo",
handle = usr.webauthn.get.handle,
credentials = usr.webauthn.get.credentials,
createdAt = DateTime.now(),
typ = OtoroshiAdminType.WebAuthnAdmin,
metadata = Map.empty,
rights = usr.rights,
location = authConfig.location,
adminEntityValidators = usr.adminEntityValidators
)
}
val usernameOpt = (otoroshi \ "username").asOpt[String]
val passwordOpt = (otoroshi \ "password").asOpt[String]
(usernameOpt, passwordOpt) match {
case (Some(username), Some(pass)) => {
users.find(u => u.username == username) match {
case None => FastFuture.successful(Left(ErrorReason("Bad user")))
case Some(user) => {
env.datastores.webAuthnRegistrationsDataStore.getRegistrationRequest(reqId).flatMap {
case None => FastFuture.successful(Left(ErrorReason("bad request")))
case Some(rawRequest) => {
val request =
jsonMapper.readValue(Json.stringify((rawRequest \ "request").as[JsValue]), classOf[AssertionRequest])
bindAdminUser(username, pass, env.backOfficeServiceDescriptor) flatMap {
case Left(err) => FastFuture.successful(Left(err))
case Right(user) => {
val rpIdentity: RelyingPartyIdentity =
RelyingPartyIdentity.builder.id(reqOriginDomain).name("Otoroshi").build
val rp: RelyingParty = RelyingParty.builder
.identity(rpIdentity)
.credentialRepository(new LocalCredentialRepository(users, jsonMapper, base64Decoder))
.origins(Seq(reqOrigin, reqOriginDomain).toSet.asJava)
.build
val pkc = PublicKeyCredential.parseAssertionResponseJson(Json.stringify(webauthn))
Try(
rp.finishAssertion(
FinishAssertionOptions
.builder()
.request(request)
.response(pkc)
.build()
)
) match {
case Failure(e) =>
FastFuture.successful(Left(ErrorReason("bad request")))
case Success(result) if !result.isSuccess =>
FastFuture.successful(Left(ErrorReason("bad request")))
case Success(result) if result.isSuccess => {
FastFuture.successful(Right(user))
}
}
}
}
}
}
}
}
}
case (_, _) => FastFuture.successful(Left(ErrorReason("Not Authorized")))
}
}
def webAuthnRegistrationStart(
body: JsValue
)(implicit env: Env, ec: ExecutionContext): Future[Either[String, JsValue]] = {
import collection.JavaConverters._
val username = (body \ "username").as[String]
val label = (body \ "label").as[String]
val reqOrigin = (body \ "origin").as[String]
val reqOriginHost = Uri(reqOrigin).authority.host.address()
val reqOriginDomain: String = reqOriginHost.split("\\.").toList.reverse match {
case tld :: domain :: _ => s"$domain.$tld"
case value => value.mkString(".")
}
val users = authConfig.users.filter(_.webauthn.isDefined).map { usr =>
WebAuthnOtoroshiAdmin(
username = usr.email,
password = "foo",
label = "foo",
handle = usr.webauthn.get.handle,
credentials = usr.webauthn.get.credentials,
createdAt = DateTime.now(),
typ = OtoroshiAdminType.WebAuthnAdmin,
metadata = Map.empty,
rights = usr.rights,
location = authConfig.location,
adminEntityValidators = usr.adminEntityValidators
)
}
val rpIdentity: RelyingPartyIdentity = RelyingPartyIdentity.builder.id(reqOriginDomain).name("Otoroshi").build
val rp: RelyingParty = RelyingParty.builder
.identity(rpIdentity)
.credentialRepository(new LocalCredentialRepository(users, jsonMapper, base64Decoder))
.origins(Seq(reqOrigin, reqOriginDomain).toSet.asJava)
.build
val userHandle = new Array[Byte](64)
random.nextBytes(userHandle)
val registrationRequestId = IdGenerator.token(32)
val request: PublicKeyCredentialCreationOptions = rp.startRegistration(
StartRegistrationOptions.builder
.user(
UserIdentity.builder
.name(username)
.displayName(label)
.id(new ByteArray(userHandle))
.build
)
.build
)
val jsonRequest = jsonMapper.writeValueAsString(request)
val finalRequest = Json.obj(
"requestId" -> registrationRequestId,
"request" -> Json.parse(jsonRequest),
"username" -> username,
"label" -> label,
"handle" -> base64Encoder.encodeToString(userHandle)
)
env.datastores.webAuthnRegistrationsDataStore.setRegistrationRequest(registrationRequestId, finalRequest).map { _ =>
Right(finalRequest)
}
}
def webAuthnRegistrationFinish(
body: JsValue
)(implicit env: Env, ec: ExecutionContext): Future[Either[String, JsValue]] = {
import collection.JavaConverters._
val json = body
val responseJson = Json.stringify((json \ "webauthn").as[JsValue])
val otoroshi = (json \ "otoroshi").as[JsObject]
val reqOrigin = (otoroshi \ "origin").as[String]
val reqId = (json \ "requestId").as[String]
val handle = (otoroshi \ "handle").as[String]
val reqOriginHost = Uri(reqOrigin).authority.host.address()
val reqOriginDomain: String = reqOriginHost.split("\\.").toList.reverse match {
case tld :: domain :: _ => s"$domain.$tld"
case value => value.mkString(".")
}
val users = authConfig.users.filter(_.webauthn.isDefined).map { usr =>
WebAuthnOtoroshiAdmin(
username = usr.email,
password = "foo",
label = "foo",
handle = usr.webauthn.get.handle,
credentials = usr.webauthn.get.credentials,
createdAt = DateTime.now(),
typ = OtoroshiAdminType.WebAuthnAdmin,
metadata = Map.empty,
rights = usr.rights,
location = authConfig.location,
adminEntityValidators = usr.adminEntityValidators
)
}
val rpIdentity: RelyingPartyIdentity = RelyingPartyIdentity.builder.id(reqOriginDomain).name("Otoroshi").build
val rp: RelyingParty = RelyingParty.builder
.identity(rpIdentity)
.credentialRepository(new LocalCredentialRepository(users, jsonMapper, base64Decoder))
.origins(Seq(reqOrigin, reqOriginDomain).toSet.asJava)
.build
val pkc = PublicKeyCredential.parseRegistrationResponseJson(responseJson)
env.datastores.webAuthnRegistrationsDataStore.getRegistrationRequest(reqId).flatMap {
case None => FastFuture.successful(Left("bad request"))
case Some(rawRequest) => {
val request = jsonMapper
.readValue(Json.stringify((rawRequest \ "request").as[JsValue]), classOf[PublicKeyCredentialCreationOptions])
Try(
rp.finishRegistration(
FinishRegistrationOptions
.builder()
.request(request)
.response(pkc)
.build()
)
) match {
case Failure(e) =>
e.printStackTrace()
FastFuture.successful(Left("bad request"))
case Success(result) => {
val username = (otoroshi \ "username").as[String]
val password = (otoroshi \ "password").as[String]
val label = (otoroshi \ "label").as[String]
val saltedPassword = BCrypt.hashpw(password, BCrypt.gensalt())
val credential = Json.parse(jsonMapper.writeValueAsString(result))
val user = authConfig.users.find(_.email == username).get
val newUser = user.webauthn match {
case None => {
user.copy(
webauthn = Some(
WebAuthnDetails(
handle = handle,
credentials = Map((credential \ "keyId" \ "id").as[String] -> credential)
)
)
)
}
case Some(wbathn) => {
user.copy(
webauthn = Some(
WebAuthnDetails(
handle = wbathn.handle,
credentials = wbathn.credentials + ((credential \ "keyId" \ "id").as[String] -> credential)
)
)
)
}
}
val conf = authConfig.copy(users = authConfig.users.filterNot(_.email == username) :+ newUser)
conf.save().map { _ =>
Right(Json.obj("username" -> username))
}
}
}
}
}
}
def webAuthnRegistrationDelete(
user: BasicAuthUser
)(implicit env: Env, ec: ExecutionContext): Future[Either[String, JsValue]] = {
val conf = authConfig.copy(users = authConfig.users.filterNot(_.email == user.email) :+ user.copy(webauthn = None))
conf.save().map { _ =>
Right(Json.obj("username" -> user.email))
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy