-
Notifications
You must be signed in to change notification settings - Fork 421
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
Authentication / security enhancements #1167
Comments
Not sure if intentional but to me a val d = (e: Endpoint[(A, I), E, O, R]).deferredServerLogic[A, User] { (user: User, i: I) => sth: F[Either[E, O]] }
d: DefferedServerEndpoint[A, User, I, E, O, R] The authenticated user may be necessary to perform user-related operations for instance. |
@markarasev Yes of course, thanks for pointing this out! Fixed :) |
Btw.: suggestions on alternate syntax, improvements to the existing methods or ideas for new ones welcome - even if not fully fleshed out and maybe impossible to implement :) |
I find that I have several endpoints sharing the same authentication method and logic but each endpoint has specific errors. I don't want to use a too generic error type, that would imply an endpoint could return some concrete error although it cannot and should never do so (a "Not Found" error defined for getting an entity would also be allowed when PUTting an entity at an existing path for instance). At the moment, I'm fine with duplicating authentication logic (which can be factorized to a single method call) but it feels a little bit odd. Actually, I don't understand this restriction on error types while some other types can be refined further, but I did not have a closer look to the code to find the reason. If sealed trait GetEntityError
sealed trait Endpoint2Error
case class EntityNotFound(id: String) extends GetEntityError
case object ConcreteEndpoint2Error extends Endpoint2Error
case class Unauthorized(token: String) extends GetEntityError with Endpoint2Error
case class User(name: String)
def auth(token: String): Future[Either[Unauthorized, User]] = Future {
if (token == "secret") Right(User("Spock"))
else Left(Unauthorized(token)) // shared authorization error
}
val secureEndpoint: PartialServerEndpoint[String, User, Unit, Unauthorized, Unit, Any, Future] = endpoint
.in(header[String]("X-AUTH-TOKEN"))
.errorOut(jsonBody[Unauthorized])
.serverLogicForCurrent(auth)
val entities = Seq(...)
val getEntityEndpoint = secureEndpoint.get
.in("entity")
.in(path[String]))
.out(stringBody)
.errorOut(jsonBody[GetEntityError]) // GetEntityError is a supertype of Unauthorized
.serverLogic { case (user, entityId) =>
val result = entities.find(_.id == entityId) match {
case Some(entity) => Right(entity.toString)
case None => Left(EntityNotFound(entityId)) // Here I can use an endpoint-specific error
}
Future.successful[Either[GetEntityError, String]](result)
}
val endpoint2 = secureEndpoint.errorOut(jsonBody[Endpoint2Error]) // Endpoint2Error is a supertype of Unauthorized
// ... Also, an alternative could be to provide a mapping function from the previous error type to the new one when redefining While I understand the idea behind |
Not sure if it's possible, but it may be more useful if the output of val groupEndpoint = endpoint
.in(header[String]("X-AUTH-TOKEN"))
.in("group" / path[Int]("group-id"))
.errorOut(jsonBody[Error])
.serverLogicForCurrent { case (token, groupId) =>
val res = for {
user <- EitherT(auth(token))
_ <- userInGroup(user, groupId)
} yield {
(user, groupId)
}
res.value
}
val itemEndpoint = groupEndpoint
.in("item" / path[Int]("item-id"))
.serverLogicForCurrent { case ((user, groupId), itemId) =>
EitherT(itemInGroup(itemId, groupId)) // need groupId again here
.map(_ => itemId)
.value
}
val finalEndpoint = itemEndpoint
.in("buy")
.serverLogic { case ((user, groupId), itemId) =>
...
} |
Thanks for the feedback so far! I'd like to share an updated proposal for security-related features in the upcoming versions of Tapir. As always, more feedback welcome :) Describing security inputs
Server logic & security
Removals
I think the proposed solution meets the requirements mentioned here:
|
There's one more problem unfortunately ;) We now end up with two unrelated endpoint types: However, after a closer look, One solution is to create a mirror hierarchy: But maybe there's a simple solution: add a type parameter to There's one huge drawback - an additional type parameter for all endpoints. I don't like that, but the alternative - having a hierarchy of endpoints - doesn't look like being easier to work with. It would be hard to preserve type-safety, given that the inputs of both types of endpoints differ. Let me know what you think :) |
This would also simplify the Hmm we'll need some type aliases to make this more user-friendly :) Maybe following the changes proposed in #1157 after all:
|
First portion of the changes is implemented in 0.19.0-M14: https://github.com/softwaremill/tapir/releases/tag/v0.19.0-M14 Feedback welcome :) |
Extending the error output in partial endpoints is now possible using |
First, thank you the recent changes in this area! I am trying to make use of dedicated security input to implement a basic session-based (cookie) authentication, and I am not sure what would be the right way to proceed. Before tapir, I would use tsec for http4s (https://jmcardon.github.io/tsec/docs/http4s/auth-scookie.html), where one would define an authed endpoint as case request@GET -> Root / "api" asAuthed user => and all the plumbing (cookie authentication, session updates etc.) would happen automatically. Tapir version of the endpoint would look as follows: endpoint.securityIn(auth.apiKey(cookie[String]("api-session"))) But now, |
@kamilkloch Hm I would have to make some experiments in code (not sure what the types involved are), but on a first quick glance, if I recall correctly, It would probably be challenging to reuse this directly, as here the It's probably a good idea to look into integration with libraries such as tsec, but that's a second (or third ;) ) step after having the basics in place. |
@adamw The logic component is beneath Now, general question for all http4s middleware, how to retrofit |
@kamilkloch In general this won't be possible - as a middleware contains both the metadata on what it reads (which headers etc.) and the logic in a single opaque function. In tapir this is separated. You could try with an interceptor, though. The shape is similar :). And I guess the type of a middleware is |
@kamilkloch in fact, maybe create an issue with a feature request to create a tsec integration? |
@adamw Yes, spot on with the type of the middleware :)
In fact, it looks like a lot of work has been poured into https://github.com/softwaremill/akka-http-session. Perhaps it would be possible to re-use it and abstract over the backend. |
@adamw would just like to say: really, really nice job on the security and error improvements. We were doing some hacks similar to people in this thread, and the 0.19.0 release totally cleans them up and enables better type safety. Could not be happier! |
@dvgica Thank you! |
Just refactored my code using the new security features and it seems to work well, thanks a lot! |
Current state
Tapir currently has some support for authentication / security:
auth.apiKey[String]
,auth.bearer[Token]
etc. These are then marked as authentication inputs in OpenAPI docs, but otherwise extract raw auth tokens, such as a string api key, just as normal inputs do.serverLogicForCurrent
method. Such an endpoint has logic for transforming e.g. aString
token into aUser
instance built-in, and can be extended with additional inputs/outputs (error outputs are fixed).serverLogicPart
. That way, the same authentication function can be used in multiple endpoints, without the need for manual compositionMissing features
Still, some functionalities are missing. Some examples of features that would be nice to have:
User
instance isn't usedHow to improve Tapir's support for authentication / security? I don't have a definite answer, but I would surely appreciate feedback on what people use right now, and what are their experiences.
Survey
Please fill this short survey: https://forms.gle/WWzEP78vpmQdkHyp8 (I'll post anonymous results here in some time), or if you prefer leave a comment beneath:
.serverLogic
, or.serverLogicForCurrent
,.serverLogicPart
?.serverLogicForCurrent
or.serverLogicPart
for other purposes?DefferedServerEndpoint
as described below?Thank you!
Possible solutions
What are some possible solutions?
A. It might be tempting, to define an interceptor which would perform authentication, and possibly authorisation as well. An interceptor could inspect the endpoint, and if there are supported authentication inputs, run the authentication logic. Moreover, if in the endpoints tags (a yet-non-existent, but easy to add field
Endpoint.tags: Map[String, Any]
) contain required roles, we could perform authorisation here as well. Interceptors could be extended with a callback allowing them to run when the endpoint is determined to match the method/path, but before the body is decoded.There are some major problems here, however. How to pass the result of authentication to the endpoint's logic? This would have to be somehow attached to the request (again to an untyped
tags
map), and then extracted by an appropriate input on the endpoint (usingextractFromRequest
). Doable, but not a clean & typesafe solution. Also, the "supported" authentication inputs would have to be passed exactly as defined in the endpoint, which would open the door to yet another possible runtime error.B. Create server endpoints with part of the logic deferred. This is an extension (or maybe replacement?) to what is currently possible using the two partial server logic variants. Having an
Endpoint[(A, I), E, O, R]
- note that the input is intentionally written down as a tuple containing authentication inputsA
and other inputsI
- we could define a method:Hence we would provide the server logic for the endpoint, except providing the authentication part. Then, we could complete the server logic, giving the authentication logic once, in bulk:
What do you think? Maybe you have some alternative ideas? What other security-related features are missing from tapir? Let us know - thanks :)
The text was updated successfully, but these errors were encountered: