-
Notifications
You must be signed in to change notification settings - Fork 523
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
OpenAPI: implement oidc discovery support #1858
Comments
I have a bit of a struggle to understand how I could be able to add a SockJSHandler (really, the bridge), when using the Vert.x v4 RouterBuilder from |
@thced more than adding a Router, you can add more than one handler to each operation. |
Is there any action to take here @pmlopes ? |
@slinkydeveloper Yes, I understand the handler chaining, but the operation does not take a Router instance. In essence, it does not allow you to mount a subRouter to an operation. |
@slinkydeveloper I think we need an overload to allow sub routers like we allow handlers, the tricky part is getting the order right and see if the subrouter doesn't shadow the remaining of the router. Alternatively we may need to define a specific |
Initial proposal to address the security limitations: /cc @slinkydeveloper @vietj @photomorre RouteBuilder::createRouter()This method should become asynchronous. Currently the method assumes that the user handlers and missing security handlers are created in a blocking fashion. For OIDC (security) this isn't true, for example, we need to execute the discovery function that will do a network download to configure itself. RouteBuilder::securityHandlerFactory(...)<T> RouterBuilder securityHandlerFactory(
String securitySchemaName,
Function<T, Future<AuthenticationHandler>> securityHandlerFactory); With this in mind we should create a auth factory that is asynchronous and returns a Future The (OAuth2Options securitySchemaOptions) -> Future.succeeded(result); This factory should be called for all types: It shall not be the responsibility of the builder to call the discovery asynchronous method. Reasons:
In the end such a usage would look like: RouterBuilder.create(vertx, yaml_location)
.onSuccess(routerBuilder -> {
routerBuilder.setOptions(FACTORY_OPTIONS);
routerBuilder.operation("candy")
.handler(rc -> rc.end("Chocolate"));
// here the new API
routerBuilder.securityHandlerFactory(
// id:
"oauth2",
// factory:
(oauth2options) -> {
return OpenIDConnectAuth.discover(
vertx,
// enhance the options if needed
oauth2options
// Azure OpenId does not return the same url where the request was sent to
.setValidateIssuer(false)
.setSite(site)
.setJWTOptions(new JWTOptions()
.setNonceAlgorithm("SHA-256")
.addAudience(config.getClientID()))
.setExtraParameters(extraParameters))
});
// since the createRouter() is not async because it may call the factory above
// the usage will become:
routerBuilder.createRouter()
.onSuccess(router -> {
// use the generated router now!
... We will need to issue a deprecation to the blocking createRouter() ASAP to inform that we will replace it with the async version. So we may need to discuss with the upstream to see how we should address this with the minimum impact. We may avoid having issues with the current /**
* @deprecated use {@link #securityHandlerFactory} instead.
*/
@deprecated
default RouterBuilder securityHandler(String securitySchemaName, AuthenticationHandler handler) {
return securityHandlerFactory(securitySchemaName, options -> Future.succeededFuture(handler));
} |
That is a huge breaking change 😄 Can this be done before the user starts dealing with openapi at all? Can we move this somehow to Also can you show a sample contract for that?
I guess that answers my above proposal... Can we give flow and parameters in the |
Following offsite discussion to avoid breaking the API we may achieve the same result by adding an extra static factory: RouterBuilder.create(
vertx,
"charlie_chocolate_factory.yaml",
loaderOptions,
// here a new argument to provide AuthenticationHandler's
(securitySchemaName, oauth2options) -> {
switch(securitySchemaName) {
case "openid":
case "oauth2":
return OpenIDConnectAuth.discover(
vertx,
// enhance the options if needed
oauth2options
// Azure OpenId does not return the same url where the request was sent to
.setValidateIssuer(false)
.setSite(site)
.setJWTOptions(new JWTOptions()
.setNonceAlgorithm("SHA-256")
.addAudience(config.getClientID()))
.setExtraParameters(extraParameters));
default:
return null;
}
}).onSuccess(routerBuilder -> {
routerBuilder.setOptions(FACTORY_OPTIONS);
routerBuilder.operation("candy")
.handler(rc -> rc.end("Chocolate"));
Router result = routerBuilder.createRouter(); Now the @VertxGen
public interface AuthenticationHandlerProvider {
@Fluent
<T> AuthenticationHandlerProvider add(
String securitySchemaName,
Function<T, Future<AuthenticationHandler>> securityHandlerFactory);
} Which would be usable as: RouterBuilder.create(
vertx,
"charlie_chocolate_factory.yaml",
loaderOptions,
// here a new argument to provide AuthenticationHandler's
AuthenticationHandlerProvider.create()
.add("openid", oauth2options ->
OpenIDConnectAuth.discover(
vertx,
// enhance the options if needed
oauth2options
// Azure OpenId does not return the same url where the request was sent to
.setValidateIssuer(false)
.setSite(site)
.setJWTOptions(new JWTOptions()
.setNonceAlgorithm("SHA-256")
.addAudience(config.getClientID()))
.setExtraParameters(extraParameters)))
).onSuccess(routerBuilder -> {
routerBuilder.setOptions(FACTORY_OPTIONS);
routerBuilder.operation("candy")
.handler(rc -> rc.end("Chocolate"));
Router result = routerBuilder.createRouter(); This option would allow adding an arbitrary number of providers, avoiding the switch statement. Making it probably more readable and extractable to a external variable if the list is long, etc... |
Renamed the issue to reflect the discussion in the comments. This issue shall track the implementation of |
@thced I've repurposed this issue to track OpenID Connect support, perhaps we should create a different one for sockjs bridge. Maybe we could define a template openapi document with all the sockjs endpoints? and have some way to mount a sub router instead of a handler? However mounting a subrouter is tricky because it can shade the generated router so I'm inclined to say we need to carefully see how to handle this. |
I've been reviewing the OpenAPI module regarding security, currently we lack support for When a API document defines: components:
securitySchemes:
openId:
type: openIdConnect
openIdConnectUrl: https://example.com/.well-known/openid-configuration The router generator should perform a "discovery" call to configure the
It is assumed that RouterBuilder.create(
vertx,
"some_api.yaml",
loaderOptions,
// here a new argument to provide AuthenticationProviders's
AuthenticationProviderFactory.create()
.add("openid", config ->
OpenIDConnectAuth.discover(
vertx,
// enhance the config if needed
// received config: {openIdConnectUrl: "https://example.com"}
new Oauth2Options()
.setSite(config.getString("openIdConnectUrl")) This ensures that the creation of OAuth2Handler.create(authProvider, "http://localhost/callback") It is interesting to see that the handler requires an optional second argument which isn't defined anywhere. So we may need to change the original idea to create an asynchronous RouterBuilder.create(
vertx,
"some_api.yaml",
loaderOptions,
// here a new argument to provide AuthenticationHandler's
AuthenticationHandlerFactory.create()
.add("openid", config ->
OpenIDConnectAuth.discover(
vertx,
// enhance the config if needed
new Oauth2Options()
.setSite(config.getString("openIdConnectUrl"))
.compose(provider -> Future.succeeded(Oauth2Handler.create(provider, "http://localhost/callback") We could try to get the server value from the
Then the config json passed would contain 2 values: {openIdConnectUrl: "https://example.com", servers: [{url: "http://localhost"}]} Which would reduce the duplication on configuration: RouterBuilder.create(
vertx,
"some_api.yaml",
loaderOptions,
// here a new argument to provide AuthenticationHandler's
AuthenticationHandlerFactory.create()
.add("openid", config ->
OpenIDConnectAuth.discover(
vertx,
// enhance the config if needed
new Oauth2Options()
.setSite(config.getString("openIdConnectUrl"))
.compose(provider -> Future.succeeded(
Oauth2Handler.create(
provider,
config.getJsonArray("servers").get(0).getString("url") + "/callback") This would solve the creation of the handler. However we havent' dealt with request scopes. An api document can contain: # top level global security
security:
- openId:
- pets_read
- pets_write
- admin Which assumes that the token request should ask for those scopes at least, so we probably should also pass the global scopes to the factory config {
openIdConnectUrl: "https://example.com",
servers: [{url: "http://localhost"]},
scopes: ["pets_read", "pets_write", "admin"]
} Note that the global ones should be filtered by security identity, so we don't need to have it as an object as we return the With this the user may choose to use them or not in the factory: RouterBuilder.create(
vertx,
"some_api.yaml",
loaderOptions,
// here a new argument to provide AuthenticationHandler's
AuthenticationHandlerFactory.create()
.add("openid", config ->
OpenIDConnectAuth.discover(
vertx,
// enhance the config if needed
new Oauth2Options()
.setSite(config.getString("openIdConnectUrl"))
.compose(provider -> Future.succeeded(
Oauth2Handler.create(
provider,
config.getJsonArray("servers").get(0).getString("url") + "/callback")
.withScope(config.getJsonArray("scopes").getString(0)
... Now the handler is fully complete to be used in the router. It is important to notice that since some API's require security and other not the handler is only mounted before the user handler requiring it. There's just one flaw here. Web users won't be able to login ever, because we didn't handle the Oauth2 "callback" route. This means that the route builder need to mount as soon as possible (say after the static handler, session handler, etc...) the callbacks for each constructed handler: for (Oauth2Handler h : securityHandlers) {
Route callbackRoute = router.route();
h.setupCallback(r);
} This will ensure that the login handshake will work. We should only Have I missed anything here @slinkydeveloper ? PS: The same proposal should also work for security type |
Task breakout to accomplish this feature:
|
That is perfectly fine; to be honest, we can wait till this issue arise again, and then take it to the drawing board. |
To avoid OpenAPI to handle the clone of OAauth2Handler's because of different scopes see: #1905 |
This issue should also be compatible with: JWTAuth as used here: Reference: https://swagger.io/docs/specification/authentication/bearer-authentication/ openapi: 3.0.0
...
# 1) Define the security scheme type (HTTP bearer)
components:
securitySchemes:
bearerAuth: # arbitrary name for the security scheme
type: http
scheme: bearer
bearerFormat: JWT # optional, arbitrary value for documentation purposes
# 2) Apply the security globally to all operations
security:
- bearerAuth: [] # use the same name as above
paths:
/something:
get:
security:
- bearerAuth: [] From the document it seems that the security scopes here are expected to always be an empty list as scopes are an Oauth2/OIDC feature |
OpenAPI 3.1.0 also states: http://spec.openapis.org/oas/v3.1.0#patterned-fields-2
This means that JWT auth should also consider validating the claims. We need to add an |
OIDC + JWT scopes have been merged to master. |
Currently, there are a couple of limitations known to
openapi
module.It is not possible to combine
oauth2
config withoidc discovery
Scopes required per route are not checked.
This issue is a tracking issue to address this issues and more.
This is a follow up to: https://github.com/vert-x3/vertx-auth/issues/450
The text was updated successfully, but these errors were encountered: