|
|
@@ -14,7 +14,6 @@ import org.mongodb.scala.bson.DefaultBsonTransformers
|
|
|
import org.mongodb.scala.bson.conversions.Bson
|
|
|
import scala.language.implicitConversions
|
|
|
|
|
|
-// TOOD: Migrate to SecureSocial OAuth lib
|
|
|
@Singleton
|
|
|
class OAuth2Service @Inject()(db: MongoDBService, config: Configuration)
|
|
|
extends AuthorizationHandler[User]
|
|
|
@@ -26,10 +25,9 @@ class OAuth2Service @Inject()(db: MongoDBService, config: Configuration)
|
|
|
import db.withCollection
|
|
|
|
|
|
override def createAccessToken(auth: AuthInfo[User]) = {
|
|
|
- println("Create token")
|
|
|
val newAuth = Authorization(auth.user)
|
|
|
- withCollection(Authorization) {auths =>
|
|
|
- auths.insertOne(newAuth).head().map(_ => newAuth.toToken())
|
|
|
+ withCollection(Authorization) { (auths) =>
|
|
|
+ auths.insertOne(newAuth).head().map((_) => newAuth.toToken())
|
|
|
}.flatten
|
|
|
}
|
|
|
|
|
|
@@ -39,39 +37,43 @@ class OAuth2Service @Inject()(db: MongoDBService, config: Configuration)
|
|
|
cred: Option[ClientCredential],
|
|
|
requ: AuthorizationRequest
|
|
|
): Future[Boolean] = {
|
|
|
- val cc = cred.getOrElse(throw new UnauthorizedClient("username required"))
|
|
|
- requ.grantType match {
|
|
|
- case OAuthGrantType.PASSWORD => validateUsernamePassword(
|
|
|
- cc.clientId,
|
|
|
- cc.clientSecret.getOrElse(throw new AccessDenied("password required"))
|
|
|
- )
|
|
|
- case OAuthGrantType.REFRESH_TOKEN => validateUsernameRefresh(
|
|
|
- cc.clientId,
|
|
|
- Authorization.decodeToken(RefreshTokenRequest(requ).refreshToken)
|
|
|
- )
|
|
|
- case OAuthGrantType.AUTHORIZATION_CODE => throw new UnsupportedGrantType()
|
|
|
- case OAuthGrantType.CLIENT_CREDENTIALS => throw new UnsupportedGrantType()
|
|
|
- case OAuthGrantType.IMPLICIT => throw new UnsupportedGrantType()
|
|
|
+ cred.fold[Future[Boolean]](Future.failed(new UnauthorizedClient("username required"))) { (cc) =>
|
|
|
+ requ.grantType match {
|
|
|
+ case OAuthGrantType.PASSWORD =>
|
|
|
+ cc.clientSecret.fold[Future[Boolean]](Future.failed(new UnauthorizedClient("password required"))) { (sec) =>
|
|
|
+ validateUsernamePassword(cc.clientId, sec).map(_.isDefined)
|
|
|
+ }
|
|
|
+ case OAuthGrantType.REFRESH_TOKEN => validateUsernameRefresh(
|
|
|
+ cc.clientId,
|
|
|
+ Authorization.decodeToken(RefreshTokenRequest(requ).refreshToken)
|
|
|
+ )
|
|
|
+ case OAuthGrantType.AUTHORIZATION_CODE => Future.failed(new UnsupportedGrantType())
|
|
|
+ case OAuthGrantType.CLIENT_CREDENTIALS => Future.failed(new UnsupportedGrantType())
|
|
|
+ case OAuthGrantType.IMPLICIT => Future.failed(new UnsupportedGrantType())
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private def withDevModeCond(bson: Bson) =
|
|
|
- if (devMode) bson else and(bson, not(equal("devOnly", true)))
|
|
|
-
|
|
|
- private def validateUsernamePassword(
|
|
|
+ def validateUsernamePassword(
|
|
|
username: String,
|
|
|
pass: String
|
|
|
- ): Future[Boolean] = withCollection(User) {users =>
|
|
|
+ ): Future[Option[User]] = withCollection(User) { (users) =>
|
|
|
users.find(withDevModeCond(equal("email", username)))
|
|
|
.first()
|
|
|
.toFutureOption()
|
|
|
- .map(res => res.map { x => pass.isBcryptedSafeBounded(x.password)}
|
|
|
- .getOrElse(Success(false))
|
|
|
- ).transform ({ _.flatten })
|
|
|
+ // TODO: Replace bcrypt with something that doesn't use String objects.
|
|
|
+ .filter {
|
|
|
+ case Some(user) => pass.isBcryptedSafeBounded(user.password) == Success(true)
|
|
|
+ case None => false
|
|
|
+ }
|
|
|
+
|
|
|
}.flatten
|
|
|
|
|
|
+ private def withDevModeCond(bson: Bson) =
|
|
|
+ if (devMode) bson else and(bson, not(equal("devOnly", true)))
|
|
|
+
|
|
|
private def validateUsernameRefresh(username: String, refresh: Array[Byte]) =
|
|
|
- withCollection(Authorization) {auths =>
|
|
|
+ withCollection(Authorization) { (auths) =>
|
|
|
auths.find(and(
|
|
|
equal("email", username),
|
|
|
equal("refreshToken", refresh)
|
|
|
@@ -81,11 +83,11 @@ class OAuth2Service @Inject()(db: MongoDBService, config: Configuration)
|
|
|
override def findUser(
|
|
|
cred: Option[ClientCredential],
|
|
|
requ: AuthorizationRequest
|
|
|
- ): Future[Option[User]] = withCollection(User) {users =>
|
|
|
- users.find(withDevModeCond(equal("email", cred.getOrElse(
|
|
|
- throw new UnsupportedGrantType("client_id required")
|
|
|
- ).clientId))).first().toFutureOption()
|
|
|
- }.flatten.map({x => {println(s"findUser: $x"); x}})
|
|
|
+ ): Future[Option[User]] = withCollection(User) { (users) =>
|
|
|
+ cred.fold[Future[Option[User]]](Future.failed(new UnsupportedGrantType("client_id required"))) { (cc) =>
|
|
|
+ users.find(withDevModeCond(equal("email", cc.clientId))).first().toFutureOption()
|
|
|
+ }
|
|
|
+ }.flatten
|
|
|
|
|
|
/* 2020-07-25: Never re-issue the same authorization token. Always generate a
|
|
|
* new one.
|
|
|
@@ -94,62 +96,67 @@ class OAuth2Service @Inject()(db: MongoDBService, config: Configuration)
|
|
|
Future.successful(None)
|
|
|
}
|
|
|
|
|
|
- private def freshAccessToken(token: Array[Byte]) = and(
|
|
|
- gt("created", Instant.now() - Authorization.accessFreshTime),
|
|
|
- equal("accessToken", token)
|
|
|
- )
|
|
|
-
|
|
|
override def findAccessToken(token: String): Future[Option[AccessToken]] =
|
|
|
- withCollection(Authorization) {auths =>
|
|
|
+ withCollection(Authorization) { (auths) =>
|
|
|
auths.find(
|
|
|
freshAccessToken(Authorization.decodeToken(token))
|
|
|
).first().toFutureOption()
|
|
|
.map(_.map(_.toToken()))
|
|
|
- }.flatten.map({x => {println(s"findAccessToken: $x"); x}})
|
|
|
+ }.flatten
|
|
|
|
|
|
override def findAuthInfoByAccessToken(token: AccessToken) =
|
|
|
- withCollection(Authorization) {collection =>
|
|
|
- collection.find(freshAccessToken(Authorization.decodeToken(token.token)))
|
|
|
+ withCollection(Authorization) { (auths) =>
|
|
|
+ auths.find(freshAccessToken(Authorization.decodeToken(token.token)))
|
|
|
.first()
|
|
|
.toFutureOption()
|
|
|
.map(_.map(auth => AuthInfo(auth, Some(auth.email), None, None)))
|
|
|
}.flatten
|
|
|
|
|
|
+ private def freshAccessToken(token: Array[Byte]) = and(
|
|
|
+ gt("created", Instant.now() - Authorization.accessFreshTime),
|
|
|
+ equal("accessToken", token)
|
|
|
+ )
|
|
|
+
|
|
|
implicit def optFut2FutOpt[A](
|
|
|
x: Option[Future[A]]
|
|
|
- )(implicit ec: ExecutionContext): Future[Option[A]] = x match {
|
|
|
- case Some(f) => f.map(Some(_))
|
|
|
- case None => Future.successful(None)
|
|
|
+ )(implicit ec: ExecutionContext): Future[Option[A]] = x.fold[Future[Option[A]]](Future.successful(None)) {
|
|
|
+ _.map(Some(_))
|
|
|
}
|
|
|
|
|
|
override def findAuthInfoByRefreshToken(token: String) =
|
|
|
- withCollection(Authorization) {auths =>
|
|
|
- auths.find(equal("refreshToken", Authorization.decodeToken(token)))
|
|
|
+ withCollection(Authorization) { (auths) =>
|
|
|
+ auths.find(freshRefreshToken(Authorization.decodeToken(token)))
|
|
|
.first()
|
|
|
.toFutureOption()
|
|
|
}.flatten
|
|
|
- .map(authOpt => withCollection(User) {users =>
|
|
|
- optFut2FutOpt(authOpt.map({ auth => users.find(
|
|
|
+ .map(authOpt => withCollection(User) { (users) =>
|
|
|
+ optFut2FutOpt(authOpt.map({ (auth) => users.find(
|
|
|
withDevModeCond(equal("_id", auth.userId))
|
|
|
).first()
|
|
|
.toFuture()
|
|
|
- .map({user => AuthInfo(user, Some(user.email), None, None)})
|
|
|
+ .map({ (user) => AuthInfo(user, Some(user.email), None, None)})
|
|
|
}))
|
|
|
}.flatten).flatten
|
|
|
-
|
|
|
+
|
|
|
+ private def freshRefreshToken(token: Array[Byte]) = and(
|
|
|
+ gt("created", Instant.now() - Authorization.refreshFreshTime),
|
|
|
+ equal("refreshToken", token)
|
|
|
+ )
|
|
|
+
|
|
|
override def refreshAccessToken(
|
|
|
auth: AuthInfo[User],
|
|
|
refreshToken: String
|
|
|
): Future[AccessToken] = {
|
|
|
val newAuth = Authorization(auth.user)
|
|
|
- withCollection(Authorization) {auths =>
|
|
|
+ withCollection(Authorization) { (auths) =>
|
|
|
auths.replaceOne(
|
|
|
and(
|
|
|
equal("userId", auth.user._id),
|
|
|
- equal("refreshToken", Authorization.decodeToken(refreshToken))
|
|
|
+ equal("refreshToken", Authorization.decodeToken(refreshToken)),
|
|
|
+ gt("created", Instant.now() - Authorization.refreshFreshTime),
|
|
|
),
|
|
|
newAuth
|
|
|
- ).head().map(_ => newAuth.toToken())
|
|
|
+ ).head().map( (_) => newAuth.toToken())
|
|
|
}.flatten
|
|
|
}
|
|
|
|
|
|
@@ -162,18 +169,18 @@ class OAuth2Service @Inject()(db: MongoDBService, config: Configuration)
|
|
|
Future.failed(
|
|
|
new InvalidClient("Invalid client or client is not authorized")
|
|
|
)
|
|
|
- else withCollection(Authorization) {auths =>
|
|
|
+ else withCollection(Authorization) { (auths) =>
|
|
|
auths.deleteOne(and(
|
|
|
equal("userId", auth.user.userId),
|
|
|
equal("refreshToken", Authorization.decodeToken(refresh))
|
|
|
- )).head().map { _ => {} }
|
|
|
+ )).head().map { (_) => {} }
|
|
|
}.flatten
|
|
|
|
|
|
override def deleteAuthCode(token: String): Future[Unit] =
|
|
|
- throw new UnsupportedGrantType("Code grant authorizations are not supported.")
|
|
|
+ Future.failed(new UnsupportedGrantType("Code grant authorizations are not supported."))
|
|
|
|
|
|
override def findAuthInfoByCode(
|
|
|
code: String
|
|
|
): Future[Option[AuthInfo[User]]] =
|
|
|
- throw new UnsupportedGrantType("Code grant authorizations are not supported.")
|
|
|
+ Future.failed(new UnsupportedGrantType("Code grant authorizations are not supported."))
|
|
|
}
|