swift-openapi-generator
swift-openapi-generator copied to clipboard
Support for security scheme/auth
https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#security-scheme-object
Let's do a proposal first, as there are a few ways this could be implemented.
For now, the workaround is to write your own middleware that injects the appropriate headers to every outgoing request.
Example: https://github.com/apple/swift-openapi-generator/tree/main/Examples/auth-client-middleware-example
@czechboy0 I'm able to get the client middleware to inject headers to the request, but whats the best way to extract that on the server?
What I have tried so far is having a server middleware that verifies the token:
func intercept(
_ request: Request,
metadata: ServerRequestMetadata,
operationID: String,
next: (Request, ServerRequestMetadata) async throws -> Response
) async throws -> Response {
if protectedOperations.contains(operationID) {
if let authToken = request.headerFields.first(where: { $0.name == "Authorization" })?.value
{
if validateJWTToken(authToken) {
var newHeaderFields = request.headerFields
newHeaderFields.append(HeaderField(name: "uid", value: "the-user-id"))
let newRequest = Request(
path: request.path,
query: request.query,
method: request.method,
headerFields: newHeaderFields,
body: request.body
)
let response = try await next(newRequest, metadata)
return response
All that works great, but from the auth jwt token I extract a uid, i'd like to pass that along to the subsequent handlers. How could I achieve this?
In the code above, I tried adding it to the headers, but then in my handler I don't seem to see it:
func authorizeUser(_ input: Operations.authorizeUser.Input) async throws -> Operations.authorizeUser.Output {
print("INPUT", input.headers)
return .ok(.init())
}
Any ideas? I might be doing it completely wrong
@Jonovono you could add the value to a TaskLocal value on the server, which allows you to access it from the handler. Or inject a token storage object both into the middleware and the object implementing APIProtocol on the server.
Thanks @czechboy0 , I have went the TaskLocal route and seems to be working good!
Right now, for default handlers generated by OpenAPI for Vapor. there's no way to access request object to authorize it. Is there any solution that being worked on?
Yes, there are two solutions to this, one you can use now, and one tracked by this issue that aims to generate some type-safe representation of the authorization rules from the OpenAPI document.
The first solution is to create a Vapor AsyncMiddleware
type and save whatever information you'd like to pass along into a task local value. Then in the handler (your type conforming to APIProtocol
, you extract the task local value and use it).
Something like this should work:
import OpenAPIVapor
import Vapor
enum MyTaskLocal {
@TaskLocal
static var vaporValue: String? // Or whatever value you're extracting.
}
struct TaskLocalVaporMiddleware: AsyncMiddleware {
func respond(
to request: Vapor.Request,
chainingTo next: Vapor.AsyncResponder
) async throws -> Vapor.Response {
try await MyTaskLocal.$vaporValue.withValue(request.<EXTRACT_VALUE_HERE>) {
try await next.respond(to: request)
}
}
}
struct Handler: APIProtocol {
func test(_ input: Operations.test.Input) async throws -> Operations.test.Output {
print("Task local from Vapor middleware: \(MyTaskLocal.vaporValue ?? "<nil>")")
return .noContent(.init())
}
}
@main
struct Tool {
static func main() async throws {
let app = Application()
app.middleware.use(TaskLocalVaporMiddleware())
let transport = VaporTransport(routesBuilder: app)
let handler = Handler()
try handler.registerHandlers(on: transport)
try app.run()
}
}
One caveat, the last time I tested this, the middleware had to be the last one in the chain (otherwise it doesn't work), so add it right before you start the server. This allows you to pass values from the concrete Vapor.Request type all the way to your type-safe handlers.
This looks great, thanks!
Something like this should work: ...
What's the go-to way (until we get proper support) for registering different middlewares for different routes? My current solution is to switch over request.route
in TaskLocalVaporMiddleware.respond
, but that seems far from ideal.
The ServerMiddleware protocol provides an operationId parameter, which you can use to change behavior based on the operation.
Alternatively, you can inspect the path, e.g. if your auth requirements can be derived from just some common path prefix.
I'd have to learn more about your use case to make more precise recommendations.
I essentially have two types of endpoints:
- Public endpoints: Don't need any middleware
- Private endpoints: Only authenticated requests should reach the request handler.
In Vapor we usually solve this using:
routes.get("overview", use: handler1)
routes.get("another-public-route": use: handler2)
// all routes registered under "private" will use AuthenticatorMiddleware
let private = routes.grouped(AuthenticatorMiddleware())
private.get("delete-me", use: handler3)
private.post("create-me", use: handler4)
And realistically there will be more endpoint-specific middlewares (e.g. some routes may require admin privileges etc.)
Gotcha. So if you don't want to have to maintain a manual list in your auth middleware that lets some operations through without auth and requires it for others, he's another idea.
Step 1: Use tags in your OpenAPI document to tag the operations that require authentication (e.g. called authed
).
Step 2: Tag all the unauthed ones with a different one (e.g. called unauthed
).
Step 3: Have 2 targets, e.g. called AuthedRoutes
and UnauthedRoutes
, and set up the generator for each of them, and in their config files, use the filter
functionality to only include the authed
and unauthed
ones, respectively. Each would have its own APIProtocol
protocol and its own handler implementation, e.g. called AuthedHandler
and UnauthedHandler
.
Step 4: In your server's executable, register them to the same server, such as:
import AuthedRoutes
import UnauthedRoutes
let app = Vapor.Application()
let transport = VaporTransport(routesBuilder: app)
// Register authed handler
let authedHandler = AuthedHandler()
try authedHandler.registerHandlers(on: transport, middlewares: [
AuthenticationMiddleware()
])
// Register unauthed handler
let unauthedHandler = UnauthedHandler()
try unauthedHandler.registerHandlers(on: transport)
try app.run()
Could this work for you?
// Edit: You wouldn't need two copies of the OpenAPI document, you can use symlinks for the openapi.yaml
required file in each target, but you can have only one copy of the document, e.g. at the root of your package.
@czechboy0 Thank you very much for your answer! While this does remove the additional route checking during runtime it is not very flexible. With each additional middleware this will require more symlinks and tags. Also it requires changing the OpenAPI yaml just to make something in Vapor work, which feels really wrong to me.
It is definitely a viable option for a small project, but for larger ones this will get out of control. I guess for now I'll stick to the additional path checking in the middleware!
Many thanks for you suggestions! 🙏🏼
Yeah it's not ideal, it's just a workaround for the lack of full support for security schemes in Swift OpenAPI Generator.
Once that's supported, you'd get all of the expected behavior almost for free.
But adding this feature to Swift OpenAPI Generator will likely need to be contributed by someone who needs it, with our guidance. Could be a fun project 🙂
On my side I am using the URLSessionTransport for an iOS application, how can I pass in the bearer token to my API calls? The API I am using does not explicitly specify the Authorization
header in its routes, but it does have the security field in the route for a bearer token security scheme.
I tried subclassing URLSessionTransport to hijack the send method to inject my token only to find out that it's a struct so it cannot be subclassed.
Is there any easier way to do this? Surely I am missing something here 🤷
This example should be a good blueprint for what you need 🙂
https://github.com/apple/swift-openapi-generator/tree/main/Examples/auth-client-middleware-example
Quick question: is this a valid example?
enum TaskLocals {
@TaskLocal
static var authorization: String?
}
public struct AuthMiddleware: ServerMiddleware {
public func intercept(
_ request: HTTPRequest,
body: HTTPBody?,
metadata: ServerRequestMetadata,
operationID: String,
next: @Sendable (HTTPRequest, HTTPBody?, ServerRequestMetadata)
async throws -> (HTTPResponse, HTTPBody?)
) async throws -> (HTTPResponse, HTTPBody?) {
let auth = request.headerFields[.authorization]
return try await TaskLocals.$authorization.withValue(auth) {
try await next(request, body, metadata)
}
}
}
extension APIGateway {
package func myHandler(
_ input: Operations.myHandler.Input
) async throws -> Operations.myHandler.Output {
print("--------------------------------------------------")
print(TaskLocals.authorization)
print("--------------------------------------------------")
fatalError()
}
}
I don't need to reach the HB / Vapor layer, but only the OAPI runtime.
What do you think? 🤔
Thanks, Tib
@tib yup, that's how it's expected to be used 👍
Hello,
We're using ClientMiddleware
to insert to correct auth info for the requests. However, when the user logs out / a different user logs in, those details change. To be able to set the new details on the client, the client object needs to be recreated as it doesn't seem to be a way to change the middleware once the client is instantiated. Is there a better way than this?
Thanks, Dalia
Yes, you can make your middleware an actor
, for example, or a class
that you make Sendable
and manually ensure safe access to internal mutable state. In this middleware stateful object, you can manage refreshing the credentials as needed, and always inject the current credentials in the intercept
method.
That way, you can still create a single Client at the start, and reuse it even as the credentials change.