OpenAPITools / openapi-generator

OpenAPI Generator allows generation of API client libraries (SDK generation), server stubs, documentation and configuration automatically given an OpenAPI Spec (v2, v3)
https://openapi-generator.tech
Apache License 2.0
21.71k stars 6.55k forks source link

[BUG] [JAVA] In webclient, SecurityRequirement is not required #19222

Open cbb330 opened 3 months ago

cbb330 commented 3 months ago

Bug Report Checklist

actual output has no required auth header required in the public interfaces of {name}Api.java. defining a securityscheme creates getters/setters. then securityRequirement creates a parser that will propagate the value from the setter if it is present.

This is the only thing added to {name}Api.java by the securityRequirement:

String[] localVarAuthNames = new String[] { "petstore_auth" };

to each route. This is then used later like this:

    protected void updateParamsForAuth(String[] authNames, MultiValueMap<String, String> queryParams, HttpHeaders headerParams, MultiValueMap<String, String> cookieParams) {
        for (String authName : authNames) { // from localVarAuthNames
            Authentication auth = authentications.get(authName); // this map is created during object init and codgen guarantees the securityRequirement auth methods are inserted to this map, it will never throw error
            if (auth == null) {
                throw new RestClientException("Authentication undefined: " + authName);
            }
            auth.applyToParams(queryParams, headerParams, cookieParams); // this is distinct per auth method with signature below
        }
    }

where applyToParams is an interface with this signature (note there is no exception or etc. compile time requirement for header presence):

    public void applyToParams(MultiValueMap<String, String> queryParams, HttpHeaders headerParams, MultiValueMap<String, String> cookieParams);

here is HttpBearerAuth.java's implementation of the interface as an example (they are all similar in webclient)

    @Override
    public void applyToParams(MultiValueMap<String, String> queryParams, HttpHeaders headerParams, MultiValueMap<String, String> cookieParams) {
        if (bearerToken == null) {
            return;
        }
        headerParams.add(HttpHeaders.AUTHORIZATION, (scheme != null ? upperCaseBearer(scheme) + " " : "") + bearerToken);
    }

I would expect a SecurityRequirement to be a compile time guarantee in the client because e.g. the auth header is not passed in parameters. This is exactly the case in "feign" see here:

the creation of auth in init

  public void addAuthorization(String authName, RequestInterceptor authorization) {
    if (apiAuthorizations.containsKey(authName)) {
      throw new RuntimeException("auth name \"" + authName + "\" already in api authorizations");
    }
    apiAuthorizations.put(authName, authorization);
    feignBuilder.requestInterceptor(authorization);
  }

then the requestInterceptor will throw an exception if auth parameters are null

  public BasicAuthRequestInterceptor(String username, String password, Charset charset) {
    checkNotNull(username, "username");
    checkNotNull(password, "password");
    this.headerValue = "Basic " + base64Encode((username + ":" + password).getBytes(charset));
  }
Description

Generated clients for any API which declare a global security requirement or route based security requirements, have no required auth at all. the requirement is simply present in the swagger UI, as well as is parsed on the client only if the auth is present, and exposes public getters/setters.

SecurityRequirement says as such the security is a requirement, so the expected outcome is the client should have compile time guarantee that the auth header be passed to the API public interfaces. e.g. just like a required header

openapi-generator version

6.6.0 7.7.0

OpenAPI declaration file content or url
{
  "openapi": "3.0.0",
  "servers": [
    {
      "url": "http://petstore.swagger.io/v2"
    }
  ],
  "info": {
    "description": "This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.",
    "version": "1.0.0",
    "title": "OpenAPI Petstore",
    "license": {
      "name": "Apache-2.0",
      "url": "https://www.apache.org/licenses/LICENSE-2.0.html"
    }
  },
  "tags": [
    {
      "name": "pet",
      "description": "Everything about your Pets"
    },
    {
      "name": "store",
      "description": "Access to Petstore orders"
    },
    {
      "name": "user",
      "description": "Operations about user"
    }
  ],
  "security": [
    {
      "api_key": []
    }
  ],
  "paths": {
    "/pet": {
      "post": {
        "tags": [
          "pet"
        ],
        "summary": "Add a new pet to the store",
        "description": "",
        "operationId": "addPet",
        "responses": {
          "200": {
            "description": "successful operation",
            "content": {
              "application/xml": {
                "schema": {
                  "$ref": "#/components/schemas/Pet"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Pet"
                }
              }
            }
          },
          "405": {
            "description": "Invalid input"
          }
        },
        "security": [
          {
            "petstore_auth": [
              "write:pets",
              "read:pets"
            ]
          }
        ],
        "requestBody": {
          "$ref": "#/components/requestBodies/Pet"
        }
      }
    }
  },
  "components": {
    "requestBodies": {
      "Pet": {
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Pet"
            }
          },
          "application/xml": {
            "schema": {
              "$ref": "#/components/schemas/Pet"
            }
          }
        },
        "description": "Pet object that needs to be added to the store",
        "required": true
      }
    },
    "securitySchemes": {
      "petstore_auth": {
        "type": "oauth2",
        "flows": {
          "implicit": {
            "authorizationUrl": "http://petstore.swagger.io/api/oauth/dialog",
            "scopes": {
              "write:pets": "modify pets in your account",
              "read:pets": "read your pets"
            }
          }
        }
      },
      "api_key": {
        "type": "apiKey",
        "name": "api_key",
        "in": "header"
      }
    },
    "schemas": {
      "Order": {
        "title": "Pet Order",
        "description": "An order for a pets from the pet store",
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "petId": {
            "type": "integer",
            "format": "int64"
          },
          "quantity": {
            "type": "integer",
            "format": "int32"
          },
          "shipDate": {
            "type": "string",
            "format": "date-time"
          },
          "status": {
            "type": "string",
            "description": "Order Status",
            "enum": [
              "placed",
              "approved",
              "delivered"
            ]
          },
          "complete": {
            "type": "boolean",
            "default": false
          }
        },
        "xml": {
          "name": "Order"
        }
      },
      "Category": {
        "title": "Pet category",
        "description": "A category for a pet",
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "name": {
            "type": "string",
            "pattern": "^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$"
          }
        },
        "xml": {
          "name": "Category"
        }
      },
      "User": {
        "title": "a User",
        "description": "A User who is purchasing from the pet store",
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "username": {
            "type": "string"
          },
          "firstName": {
            "type": "string"
          },
          "lastName": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "password": {
            "type": "string"
          },
          "phone": {
            "type": "string"
          },
          "userStatus": {
            "type": "integer",
            "format": "int32",
            "description": "User Status"
          }
        },
        "xml": {
          "name": "User"
        }
      },
      "Tag": {
        "title": "Pet Tag",
        "description": "A tag for a pet",
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "name": {
            "type": "string"
          }
        },
        "xml": {
          "name": "Tag"
        }
      },
      "Pet": {
        "title": "a Pet",
        "description": "A pet for sale in the pet store",
        "type": "object",
        "required": [
          "name",
          "photoUrls"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "category": {
            "$ref": "#/components/schemas/Category"
          },
          "name": {
            "type": "string",
            "example": "doggie"
          },
          "photoUrls": {
            "type": "array",
            "xml": {
              "name": "photoUrl",
              "wrapped": true
            },
            "items": {
              "type": "string"
            }
          },
          "tags": {
            "type": "array",
            "xml": {
              "name": "tag",
              "wrapped": true
            },
            "items": {
              "$ref": "#/components/schemas/Tag"
            }
          },
          "status": {
            "type": "string",
            "description": "pet status in the store",
            "deprecated": true,
            "enum": [
              "available",
              "pending",
              "sold"
            ]
          }
        },
        "xml": {
          "name": "Pet"
        }
      },
      "ApiResponse": {
        "title": "An uploaded response",
        "description": "Describes the result of uploading an image resource",
        "type": "object",
        "properties": {
          "code": {
            "type": "integer",
            "format": "int32"
          },
          "type": {
            "type": "string"
          },
          "message": {
            "type": "string"
          }
        }
      }
    }
  }
}
Generation Details

java -jar $jar/openapi-generator-cli.jar generate \ -i $input \ --api-package com.linkedin.openhouse.$name.client.api \ --model-package com.linkedin.openhouse.$name.client.model \ --invoker-package com.linkedin.openhouse.$name.client.invoker \ --group-id com.linkedin.openhouse.$name \ --artifact-id generated-$name-client \ --artifact-version 0.0.1-SNAPSHOT \ -g java \ -p java8=true \ --library webclient \ -o $output

Steps to reproduce

use my input above, the 6.6.0 jar, java8, and webclient library

Related issues/PRs

https://github.com/OpenAPITools/openapi-generator/issues?q=is%3Aissue+label%3A%22Client%3A+Java%22+security+

^ none of above cover this issue.

Suggest a fix

when SecurityRequirement is present,

any of:

MaurizioCasciano commented 3 months ago

Similar to https://github.com/OpenAPITools/openapi-generator/issues/19168 with RestTemplate.

cbb330 commented 3 months ago

@MaurizioCasciano no, it is not. That issue is for a missing auth in the init function -- it is missing code that should be there.

here we have something else, that securityRequirement doesn't result in any client-side requirement at all. auth is still optional on the client at build time.