diff --git a/README.md b/README.md index bb598916..8338967e 100644 --- a/README.md +++ b/README.md @@ -359,7 +359,9 @@ and your service. ChangeLog ========= -## Version 0.20.14 (2018-01-09) +## Version 0.21.15 (2018-01-25) +* [[I] #183 - Accessibility of views bases on Pool 1 connection](https://github.com/Viva-con-Agua/drops/issues/183) +* [[F] #180 - Allow webservice secrets](https://github.com/Viva-con-Agua/drops/issues/180) * [[F] #106 - Send new registered user to Pool 1](https://github.com/Viva-con-Agua/drops/issues/106) ## Version 0.19.14 (2017-12-14) diff --git a/app/api/ApiAction.scala b/app/api/ApiAction.scala index c57ad05c..f374e7f6 100644 --- a/app/api/ApiAction.scala +++ b/app/api/ApiAction.scala @@ -25,10 +25,10 @@ class ApiAction @Inject()( def invokeBlock[A](request: Request[A], block: (ApiRequest[A]) => Future[Result]) = { implicit val messages : Messages = messagesApi.preferred(request) Try(apiRequestProvider.get[A](request)) match { - case Success(apiRequest) => block(apiRequest)/*apiRequest.getClient.flatMap(_ match { - case Some(oauthClient) => block(apiRequest) - case _ => Future.successful(BadRequest(Json.obj("error" -> Messages("rest.api.noValidAPIClient")))) - })*/ + case Success(apiRequest) => apiRequest.getClient.flatMap(_ match { + case Left(oauthClient) => block(apiRequest) + case Right(e) => Future.successful(BadRequest(Json.obj("error" -> Messages(e.getMessage)))) + }) case Failure(f) => Future.successful(BadRequest(Json.obj("error" -> Messages("rest.api.noValidAPIRequest", f.getMessage)))) } } diff --git a/app/api/ApiRequest.scala b/app/api/ApiRequest.scala index 7351a985..bf4540c2 100644 --- a/app/api/ApiRequest.scala +++ b/app/api/ApiRequest.scala @@ -5,30 +5,31 @@ import javax.inject.Inject import api.query._ import daos.{OauthClientDao, UserDao} import models.OauthClient +import play.api.Configuration import play.api.libs.json._ import play.api.mvc.Request import play.api.mvc.AnyContent +import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future import scala.util.{Failure, Success, Try} class ApiRequestProvider @Inject() ( + configuration: Configuration, oauthClientDao : OauthClientDao, userDao: UserDao ) { - def get[A](request: Request[A]) = ApiRequest[A](request, oauthClientDao, userDao) + def get[A](request: Request[A]) = ApiRequest[A](request, oauthClientDao, userDao, configuration) } /** * Created by johann on 21.12.16. */ -case class ApiRequest[A](request : Request[A], oauthClientDao : OauthClientDao, userDao: UserDao){ -// val cĺientId = request.queryString("client_id").headOption.getOrElse( -// throw new Exception // Todo: Meaningful Exception -// ) -// val clientSecret = request.queryString("client_secret").headOption.getOrElse( -// throw new Exception // Todo: Meaningful Exception -// ) +case class ApiRequest[A](request : Request[A], oauthClientDao : OauthClientDao, userDao: UserDao, config: Configuration){ + val cĺientId = request.queryString("client_id").headOption.getOrElse( + throw new Exception // Todo: Meaningful Exception + ) + val clientSecret = request.queryString("client_secret").headOption val version = request.queryString.getOrElse("version", request.queryString.getOrElse("v", Seq("1.1.0") // change this, if a new version of the webservice was implemented (so it uses the new version by default) @@ -57,6 +58,22 @@ case class ApiRequest[A](request : Request[A], oauthClientDao : OauthClientDao, case _ => None // all other possible contents are unknown to me } -// def getClient : Future[Option[OauthClient]] = -// oauthClientDao.find(cĺientId, clientSecret) + def getClient : Future[Either[OauthClient, Exception]] = + config.getString("drops.ws.security").getOrElse("none") match { + case "none" => oauthClientDao.find(cĺientId).map(_ match { + case Some(client) => Left(client) + case _ => Right(new Exception("rest.api.givenClientNotFound")) + }) + case "secret" => clientSecret match { + case Some(secret) => oauthClientDao.find(cĺientId, secret).map(_ match { + case Some(client) => Left(client) + case _ => Right(new Exception("rest.api.givenClientNotFound")) + }) + case _ => Future.successful(Right(new Exception("rest.api.noClientSecretGiven"))) + } + case "sluice" => { + // Todo: Implement integration for using sluice in intra-microservice communication + Future.successful(Right(new Exception("rest.api.securityMethodNotImplemented"))) + } + } } diff --git a/app/controllers/Application.scala b/app/controllers/Application.scala index 7b193470..13b50df2 100644 --- a/app/controllers/Application.scala +++ b/app/controllers/Application.scala @@ -19,7 +19,7 @@ import services.UserService import daos.{CrewDao, OauthClientDao, TaskDao} import play.api.libs.json.{JsPath, JsValue, Json, Reads} import play.api.libs.ws._ -import utils.{WithAlternativeRoles, WithRole} +import utils.authorization.{Pool1Restriction, WithAlternativeRoles, WithRole} import scala.collection.JavaConversions._ import scala.concurrent.ExecutionContext.Implicits.global @@ -36,17 +36,19 @@ class Application @Inject() ( configuration: Configuration, socialProviderRegistry: SocialProviderRegistry) extends Silhouette[User,CookieAuthenticator] { - def index = SecuredAction.async { implicit request => + val pool1Export = configuration.getBoolean("pool1.export").getOrElse(false) + + def index = SecuredAction(Pool1Restriction(pool1Export)).async { implicit request => Future.successful(Ok(views.html.index(request.identity, request.authenticator.loginInfo))) } - def profile = SecuredAction.async { implicit request => + def profile = SecuredAction(Pool1Restriction(pool1Export)).async { implicit request => crewDao.list.map(l => Ok(views.html.profile(request.identity, request.authenticator.loginInfo, socialProviderRegistry, UserForms.userForm, CrewForms.geoForm, l.toSet, PillarForms.define)) ) } - def updateBase = SecuredAction.async { implicit request => + def updateBase = SecuredAction(Pool1Restriction(pool1Export)).async { implicit request => UserForms.userForm.bindFromRequest.fold( bogusForm => crewDao.list.map(l => BadRequest(views.html.profile(request.identity, request.authenticator.loginInfo, socialProviderRegistry, bogusForm, CrewForms.geoForm, l.toSet, PillarForms.define))), userData => request.identity.profileFor(request.authenticator.loginInfo) match { @@ -67,7 +69,7 @@ class Application @Inject() ( ) } - def updateCrew = SecuredAction.async { implicit request => + def updateCrew = SecuredAction(Pool1Restriction(pool1Export)).async { implicit request => CrewForms.geoForm.bindFromRequest.fold( bogusForm => crewDao.list.map(l => BadRequest(views.html.profile(request.identity, request.authenticator.loginInfo, socialProviderRegistry, UserForms.userForm, bogusForm, l.toSet, PillarForms.define))), crewData => { @@ -90,7 +92,7 @@ class Application @Inject() ( ) } - def updatePillar = SecuredAction.async { implicit request => + def updatePillar = SecuredAction(Pool1Restriction(pool1Export)).async { implicit request => PillarForms.define.bindFromRequest.fold( bogusForm => crewDao.list.map(l => BadRequest(views.html.profile(request.identity, request.authenticator.loginInfo, socialProviderRegistry, UserForms.userForm, CrewForms.geoForm, l.toSet, bogusForm))), pillarData => request.identity.profileFor(request.authenticator.loginInfo) match { @@ -108,12 +110,12 @@ class Application @Inject() ( ) } - def task = SecuredAction{ implicit request => + def task = SecuredAction(Pool1Restriction(pool1Export)) { implicit request => val resultingTasks: Future[Seq[Task]] = taskDao.all() Ok(views.html task(request.identity, request.authenticator.loginInfo, resultingTasks)) } - def initCrews = Action.async { request => + def initCrews = SecuredAction(WithRole(RoleAdmin) && Pool1Restriction(pool1Export)).async { request => configuration.getConfigList("crews").map(_.toList.map(c => crewDao.find(c.getString("name").get).map(_ match { case Some(crew) => crew @@ -125,7 +127,7 @@ class Application @Inject() ( Future.successful(Redirect("/")) } - def fixCrewsID = SecuredAction.async { request => + def fixCrewsID = SecuredAction(WithRole(RoleAdmin) && Pool1Restriction(pool1Export)).async { request => val crews = crewDao.listOfStubs.flatMap(l => Future.sequence(l.map(oldCrew => crewDao.update(oldCrew.toCrew)))) val users = crews.flatMap(l => userService.listOfStubs.flatMap(ul => Future.sequence(ul.map(user => { val profiles = user.profiles.map(profile => { @@ -141,7 +143,7 @@ class Application @Inject() ( res.map(pair => Ok(Json.arr(Json.toJson(pair._1), Json.toJson(pair._2)))) } - def initUsers(number: Int, specialRoleUsers : Int = 1) = SecuredAction(WithRole(RoleAdmin)).async { request => { + def initUsers(number: Int, specialRoleUsers : Int = 1) = SecuredAction(WithRole(RoleAdmin) && Pool1Restriction(pool1Export)).async { request => { val wsRequest = ws.url("https://randomuser.me/api/") .withHeaders("Accept" -> "application/json") .withRequestTimeout(10000) @@ -173,11 +175,11 @@ class Application @Inject() ( ) }} - def registration = SecuredAction(WithAlternativeRoles(RoleAdmin, RoleEmployee)) { implicit request => + def registration = SecuredAction((WithRole(RoleAdmin) || WithRole(RoleEmployee)) && Pool1Restriction(pool1Export)) { implicit request => Ok(views.html.oauth2.register(request.identity, request.authenticator.loginInfo, socialProviderRegistry, OAuth2ClientForms.register)) } - def registerOAuth2Client = SecuredAction(WithAlternativeRoles(RoleAdmin, RoleEmployee)).async { implicit request => + def registerOAuth2Client = SecuredAction((WithRole(RoleAdmin) || WithRole(RoleEmployee)) && Pool1Restriction(pool1Export)).async { implicit request => OAuth2ClientForms.register.bindFromRequest.fold( bogusForm => Future.successful(BadRequest(views.html.oauth2.register(request.identity, request.authenticator.loginInfo, socialProviderRegistry, bogusForm))), registerData => { diff --git a/app/controllers/Files.scala b/app/controllers/Files.scala index 0e528ee8..d36f5cbf 100644 --- a/app/controllers/Files.scala +++ b/app/controllers/Files.scala @@ -17,6 +17,7 @@ import models._ import play.modules.reactivemongo.json.collection.JSONCollection import services.UserService import reactivemongo.api.gridfs.{DefaultFileToSave, FileToSave, GridFS, ReadFile} +import utils.authorization.Pool1Restriction import scala.collection.JavaConversions._ import scala.concurrent.ExecutionContext.Implicits.global @@ -31,6 +32,9 @@ class Files @Inject() ( configuration: Configuration, val reactiveMongoApi: ReactiveMongoApi ) extends Silhouette[User,CookieAuthenticator] with MongoController with ReactiveMongoComponents { + + val pool1Export = configuration.getBoolean("pool1.export").getOrElse(false) + // gridFSBodyParser from `MongoController` import MongoController.readFileReads @@ -47,7 +51,7 @@ class Files @Inject() ( val files = reactiveMongoApi.db.collection[JSONCollection]("fs.files") - def uploadProfileImage = SecuredAction.async(fsParser) { implicit request => + def uploadProfileImage = SecuredAction(Pool1Restriction(pool1Export)).async(fsParser) { implicit request => val futureFile: Future[ReadFile[JSONSerializationPack.type, JsValue]] = request.body.files.head.ref diff --git a/app/controllers/OAuth2Controller.scala b/app/controllers/OAuth2Controller.scala index 1c460b60..14a24a29 100644 --- a/app/controllers/OAuth2Controller.scala +++ b/app/controllers/OAuth2Controller.scala @@ -30,6 +30,7 @@ class OAuth2Controller @Inject() ( oauthClientDao: OauthClientDao, oauthDataHandler: OAuthDataHandler, val messagesApi: MessagesApi, + configuration: Configuration, val env:Environment[User,CookieAuthenticator] ) extends Silhouette[User,CookieAuthenticator] with OAuth2Provider { override val tokenEndpoint = new DropsTokenEndpoint() @@ -38,14 +39,38 @@ class OAuth2Controller @Inject() ( issueAccessToken(oauthDataHandler) } - def getCode(clientId : String) = SecuredAction.async { implicit request => - oauthClientDao.find(clientId, None, "authorization_code").flatMap(_ match { + /** + * If a valid client was submitted, a new OAuth code will be generated and send to the clients redirect URI. + * + * Different possibilities to secure webservice communication are supported. First, you can use no security ('none'). + * Secondly, you can use a secret ('secret') and last the microservices can be identified using Sluice. Method in use + * will be defined in your application.conf + * + * @author Johann Sell + * @param clientId identifies the client + * @param clientSecret secures the communication, if this method is configured. + * @return + */ + def getCode(clientId : String, clientSecret : String) = SecuredAction.async { implicit request => { + + def bodyWithSecret(secret : Option[String]) = oauthClientDao.find(clientId, secret, "authorization_code").flatMap(_ match { case Some(client) => oauthCodeDao.save(OauthCode(request.identity, client)).map( - code => code.client.redirectUri.map( (uri) => Redirect( uri + code.code)).getOrElse( + code => code.client.redirectUri.map((uri) => Redirect(uri + code.code)).getOrElse( BadRequest(Messages("oauth2server.clientHasNoRedirectURI")) ) ) case _ => Future.successful(BadRequest(Messages("oauth2server.clientId.notFound"))) }) - } + + configuration.getString("drops.ws.security").getOrElse("secret") match { + case "none" => bodyWithSecret(None) + case "secret" if clientSecret != "" => bodyWithSecret(Some(clientSecret)) + case "sluice" => { + // TODO: Implement integration for using sluice in intra-microservice communication + Future.successful(BadRequest(Messages("oauth2server.security.method.notImplemented", "sluice"))) + } + case _ => Future.successful(BadRequest(Messages("oauth2server.clientSecret.missing"))) + } + + }} } diff --git a/app/controllers/Roles.scala b/app/controllers/Roles.scala index 44f981e8..c0f830ef 100644 --- a/app/controllers/Roles.scala +++ b/app/controllers/Roles.scala @@ -13,7 +13,7 @@ import models._ import play.api.data.Form import play.api.data.Forms._ import services.UserService -import utils.WithRole +import utils.authorization.{Pool1Restriction, WithRole} import scala.concurrent.ExecutionContext.Implicits.global /** @@ -22,13 +22,16 @@ import scala.concurrent.ExecutionContext.Implicits.global class Roles @Inject() ( userService: UserService, val messagesApi: MessagesApi, + configuration: Configuration, val env:Environment[User,CookieAuthenticator]) extends Silhouette[User,CookieAuthenticator] { - def index = SecuredAction(WithRole(RoleAdmin)).async { request => + val pool1Export = configuration.getBoolean("pool1.export").getOrElse(false) + + def index = SecuredAction(WithRole(RoleAdmin) && Pool1Restriction(pool1Export)).async { request => userService.list.map(users => Ok(views.html.roles.index(request.identity, request.authenticator.loginInfo, RolesForms.setUsers(users))(request, messagesApi.preferred(request)))) //RolesForms.setUsers(users) } - def update = SecuredAction(WithRole(RoleAdmin)).async { request => + def update = SecuredAction(WithRole(RoleAdmin) && Pool1Restriction(pool1Export)).async { request => RolesForms.set.bindFromRequest()(request).fold( bogusForm => Future.successful(BadRequest( views.html.roles.index(request.identity, request.authenticator.loginInfo, bogusForm)(request, messagesApi.preferred(request)) diff --git a/app/utils/authorization/CombinableRestriction.scala b/app/utils/authorization/CombinableRestriction.scala new file mode 100644 index 00000000..2b9ebfab --- /dev/null +++ b/app/utils/authorization/CombinableRestriction.scala @@ -0,0 +1,41 @@ +package utils.authorization + +import com.mohiva.play.silhouette.api.Authorization +import com.mohiva.play.silhouette.impl.authenticators.CookieAuthenticator +import models.User +import play.api.i18n.Messages +import play.api.mvc.Request +import scala.concurrent.ExecutionContext.Implicits.global + +import scala.concurrent.Future + +case class AuthAndCombination(one: CombinableRestriction, two: CombinableRestriction) extends Authorization[User,CookieAuthenticator] { + override def isAuthorized[B](identity: User, authenticator: CookieAuthenticator)(implicit request: Request[B], messages: Messages): Future[Boolean] = + one.isAuthorized(identity, authenticator).flatMap( + (first) => two.isAuthorized(identity, authenticator).map( + (second) => first && second + ) + ) +} + + +case class AuthOrCombination(one: CombinableRestriction, two: CombinableRestriction) extends Authorization[User,CookieAuthenticator] { + override def isAuthorized[B](identity: User, authenticator: CookieAuthenticator)(implicit request: Request[B], messages: Messages): Future[Boolean] = + one.isAuthorized(identity, authenticator).flatMap( + (first) => two.isAuthorized(identity, authenticator).map( + (second) => first || second + ) + ) +} + +trait CombinableRestriction extends Authorization[User,CookieAuthenticator] { + def isAuthorized[B](identity: User, authenticator: CookieAuthenticator)(implicit request: Request[B], messages: Messages): Future[Boolean] + + def &&(other: CombinableRestriction) : Authorization[User,CookieAuthenticator] = { + AuthAndCombination(this, other) + } + + def ||(other: CombinableRestriction) : Authorization[User,CookieAuthenticator] = { + AuthOrCombination(this, other) + } +} diff --git a/app/utils/authorization/Pool1Restriction.scala b/app/utils/authorization/Pool1Restriction.scala new file mode 100644 index 00000000..d8934324 --- /dev/null +++ b/app/utils/authorization/Pool1Restriction.scala @@ -0,0 +1,25 @@ +package utils.authorization + +import com.mohiva.play.silhouette.api.Authorization +import com.mohiva.play.silhouette.impl.authenticators.CookieAuthenticator +import models.{Role, RoleAdmin, User} +import play.api.i18n.Messages +import play.api.mvc.Request + +import scala.concurrent.Future + +/** + * Gets a parameter indicating if an active connection to Pool 1 exists. If the parameter is true (connection exists): + * Only admins have permissions to access the requested resource; all other users are rejected. If it's false, there is + * no need to consider this restriction. + * + * @author Johann Sell + * @param active indicates if a connection does exists + */ +case class Pool1Restriction(active: Boolean) extends Authorization[User,CookieAuthenticator] with CombinableRestriction { + def isAuthorized[B](user: User, authenticator: CookieAuthenticator)(implicit request : Request[B], messages: Messages) = + user.roles match { + case list: Set[Role] => Future.successful(list.contains(RoleAdmin) || !active) + case _ => Future.successful(!active) + } +} diff --git a/app/utils/WithRole.scala b/app/utils/authorization/WithRole.scala similarity index 90% rename from app/utils/WithRole.scala rename to app/utils/authorization/WithRole.scala index c25162cb..42fe08f1 100644 --- a/app/utils/WithRole.scala +++ b/app/utils/authorization/WithRole.scala @@ -1,17 +1,17 @@ -package utils +package utils.authorization import com.mohiva.play.silhouette.api.Authorization import com.mohiva.play.silhouette.impl.authenticators.CookieAuthenticator import models.{Role, User} import play.api.i18n._ -import play.api.mvc.{Request, RequestHeader} +import play.api.mvc.Request import scala.concurrent.Future /** * Check for authorization */ -case class WithRole(role: Role) extends Authorization[User,CookieAuthenticator] { +case class WithRole(role: Role) extends Authorization[User,CookieAuthenticator] with CombinableRestriction { def isAuthorized[B](user: User, authenticator: CookieAuthenticator)(implicit request : Request[B], messages: Messages) = user.roles match { case list: Set[Role] => Future.successful(list.contains(role)) diff --git a/build.sbt b/build.sbt index f16f186b..a4754274 100644 --- a/build.sbt +++ b/build.sbt @@ -2,7 +2,7 @@ import com.typesafe.sbt.packager.docker.Cmd name := """Drops""" -version := "0.20.14" +version := "0.21.15" lazy val root = (project in file(".")).enablePlugins(PlayScala).enablePlugins(DockerPlugin) diff --git a/conf/application.conf b/conf/application.conf index d1401004..b8d33a45 100644 --- a/conf/application.conf +++ b/conf/application.conf @@ -49,6 +49,8 @@ play.mailer { # connectiontimeout (defaults to 60s in milliseconds) } +drops.ws.security="secret" // other options: "sluice" or "none" + include "silhouette.conf" include "admin.conf" include "crews.conf" diff --git a/conf/messages b/conf/messages index 78cfdb9d..5687a49c 100644 --- a/conf/messages +++ b/conf/messages @@ -141,7 +141,9 @@ crew.update.noProfileForLogin=Internal Server Error: Found no profile for the cu crew.select.one=Please, select a crew oauth2server.clientId.notFound=No client application is registered with the given client ID. +oauth2server.clientSecret.missing=No client secret is given. oauth2server.clientId.missing=No client ID is given. +oauth2server.security.method.notImplemented=Configured security method ''{0}'' is not implemented yet. Please change ''drops.ws.security'' in your application.conf. oauth2.register.title=Register Oauth2 Client oauth2.register.legend=Register Oauth2 Client @@ -175,6 +177,9 @@ rest.api.syntaxError=The query has an syntax error rest.api.syntaxGrammar=The query has an grammar error rest.api.missingFilterValue=There is no filter value for one or more query parts rest.api.queryFunctionsNotImplementedYet=One or more query functions are not implemented yet +rest.api.givenClientNotFound=Found no valid client for the given ID. +rest.api.noClientSecretGiven=Security method ''secret'' is currently configured, but no client secret was given. +rest.api.securityMethodNotImplemented=The currently configured security method is not implemented yet, please change ''drops.ws.security'' in your application.conf pool1.error.config.noURI=No Pool 1 system configured (missing URL) pool1.debug.not.activated=Export not activated in configuration! Set `pool1.export=true` in your applications conf file. diff --git a/conf/messages.de b/conf/messages.de index 8e6740f6..ea8eead8 100644 --- a/conf/messages.de +++ b/conf/messages.de @@ -141,7 +141,9 @@ crew.update.noProfileForLogin=Ooohh!! Kein Tropfenprofil für den eingeloggten N crew.select.one=Bitte wähle deine Crew aus oauth2server.clientId.notFound=Keinen OAuth 2 Client gefunden, der zu der angegebenen ID passt. +oauth2server.clientSecret.missing=Kein OAuth 2 Client Secret angegeben! oauth2server.clientId.missing=Keine OAuth 2 Client ID angegeben! +oauth2server.security.method.notImplemented=Konfigurierte Methode zur Absicherung der Webservices ''{0}'' ist bisher nicht implementiert. Bitte ändere den Wert der Variablen ''drops.ws.security'' in deiner application.conf. oauth2.register.title=Registriere Oauth2 Client oauth2.register.legend=Registriere Oauth2 Client @@ -173,4 +175,7 @@ rest.api.noValidAPIClient=Das ist kein valider API Client rest.api.syntaxError=Die Anfrage hat einen syntaktischen Fehler rest.api.syntaxGrammar=Die Anfrage hat einen grammatikalischen Fehler rest.api.missingFilterValue=Für Teile der Filteranfrage ist kein Wert gesetzt -rest.api.queryFunctionsNotImplementedYet=Mindestens eine Funktion in der Anfrage ist noch nicht implementiert. \ No newline at end of file +rest.api.queryFunctionsNotImplementedYet=Mindestens eine Funktion in der Anfrage ist noch nicht implementiert. +rest.api.givenClientNotFound=Es konnte der angebenen ID kein valider Client zugeordnet werden. +rest.api.noClientSecretGiven=Methode zur Absicherung der Webservices ist momentan ''secret'', aber es wurde kein Client Secret angegeben. +rest.api.securityMethodNotImplemented=Konfigurierte Methode zur Absicherung der Webservices ist derzeit nicht implementiert, bitte ändere ''drops.ws.security'' in deiner application.conf \ No newline at end of file diff --git a/conf/routes b/conf/routes index 131caf9f..78853de5 100644 --- a/conf/routes +++ b/conf/routes @@ -40,7 +40,8 @@ POST /rest/access controllers.RestApi. # OAuth2 Rest API GET /oauth2/rest/profile controllers.Oauth2RestApi.profile -GET /oauth2/code/get/:clientId controllers.OAuth2Controller.getCode(clientId : String) +GET /oauth2/code/get/:clientId controllers.OAuth2Controller.getCode(clientId : String, clientSecret = "") +GET /oauth2/code/get/:clientId/:clientSecret controllers.OAuth2Controller.getCode(clientId : String, clientSecret : String) # Authentication GET /auth/init controllers.Auth.init