From 8569fafb1dae5af7d46f9d4bb2e650987ee748b7 Mon Sep 17 00:00:00 2001 From: Anton Kurako Date: Mon, 10 Nov 2025 12:44:35 +0300 Subject: [PATCH 1/4] Added OpenAPI generator HTTP client multi auth support --- openapi/openapi-generator/build.gradle | 8 + .../kora/openapi/generator/KoraCodegen.java | 128 ++++++----- .../kora/javaClientSecuritySchema.mustache | 62 ++++-- .../kora/javaServerSecuritySchema.mustache | 7 +- .../kora/kotlinClientSecuritySchema.mustache | 41 ++-- .../kora/kotlinServerSecuritySchema.mustache | 13 +- .../openapi/generator/KoraCodegenTest.java | 1 + .../example/petstoreV3_security_multi.yaml | 204 ++++++++++++++++++ 8 files changed, 361 insertions(+), 103 deletions(-) create mode 100644 openapi/openapi-generator/src/test/resources/example/petstoreV3_security_multi.yaml diff --git a/openapi/openapi-generator/build.gradle b/openapi/openapi-generator/build.gradle index 9fd819828..b794cad50 100644 --- a/openapi/openapi-generator/build.gradle +++ b/openapi/openapi-generator/build.gradle @@ -92,11 +92,19 @@ sourceSets { addOpenapiDir("petstoreV3_request_parameters") addOpenapiDir("petstoreV3_responses") addOpenapiDir("petstoreV3_security_all") + addOpenapiDir("petstoreV3_security_all_auth_arg") + addOpenapiDir("petstoreV3_security_multi") + addOpenapiDir("petstoreV3_security_multi_auth_arg") addOpenapiDir("petstoreV3_security_api_key") + addOpenapiDir("petstoreV3_security_api_key_auth_arg") addOpenapiDir("petstoreV3_security_basic") + addOpenapiDir("petstoreV3_security_basic_auth_arg") addOpenapiDir("petstoreV3_security_bearer") + addOpenapiDir("petstoreV3_security_bearer_auth_arg") addOpenapiDir("petstoreV3_security_oauth") + addOpenapiDir("petstoreV3_security_oauth_auth_arg") addOpenapiDir("petstoreV3_security_cookie") + addOpenapiDir("petstoreV3_security_cookie_auth_arg") addOpenapiDir("petstoreV3_single_response") addOpenapiDir("petstoreV3_types") addOpenapiDir("petstoreV3_validation") diff --git a/openapi/openapi-generator/src/main/java/ru/tinkoff/kora/openapi/generator/KoraCodegen.java b/openapi/openapi-generator/src/main/java/ru/tinkoff/kora/openapi/generator/KoraCodegen.java index 98fa2b376..c34839f0e 100644 --- a/openapi/openapi-generator/src/main/java/ru/tinkoff/kora/openapi/generator/KoraCodegen.java +++ b/openapi/openapi-generator/src/main/java/ru/tinkoff/kora/openapi/generator/KoraCodegen.java @@ -160,6 +160,7 @@ record CodegenParams( String jsonAnnotation, boolean enableValidation, boolean authAsMethodArgument, + boolean authAllowMultiple, @Nullable String primaryAuth, @Nullable String clientConfigPrefix, String securityConfigPrefix, @@ -203,6 +204,7 @@ static CodegenParams parse(Map additionalProperties) { var jsonAnnotation = "ru.tinkoff.kora.json.common.annotation.Json"; var enableServerValidation = false; var authAsMethodArgument = false; + var authAllowMultiple = false; var primaryAuth = (String) null; var clientConfigPrefix = (String) null; var securityConfigPrefix = (String) null; @@ -259,6 +261,9 @@ static CodegenParams parse(Map additionalProperties) { if (additionalProperties.containsKey(AUTH_AS_METHOD_ARGUMENT)) { authAsMethodArgument = Boolean.parseBoolean(additionalProperties.get(AUTH_AS_METHOD_ARGUMENT).toString()); } + if (additionalProperties.containsKey(AUTH_ALLOW_MULTIPLE)) { + authAllowMultiple = Boolean.parseBoolean(additionalProperties.get(AUTH_ALLOW_MULTIPLE).toString()); + } if (additionalProperties.containsKey(CLIENT_CONFIG_PREFIX)) { clientConfigPrefix = additionalProperties.get(CLIENT_CONFIG_PREFIX).toString(); } @@ -299,7 +304,7 @@ static CodegenParams parse(Map additionalProperties) { forceIncludeNonRequired = Boolean.parseBoolean(additionalProperties.get(FORCE_INCLUDE_NON_REQUIRED).toString()); } - return new CodegenParams(codegenMode, jsonAnnotation, enableServerValidation, authAsMethodArgument, primaryAuth, clientConfigPrefix, + return new CodegenParams(codegenMode, jsonAnnotation, enableServerValidation, authAsMethodArgument, authAllowMultiple, primaryAuth, clientConfigPrefix, securityConfigPrefix, clientTags, interceptors, additionalContractAnnotations, requestInDelegateParams, enableJsonNullable, filterWithModels, prefixPath, delegateMethodBodyMode, implicitHeaders, implicitHeadersRegex, forceIncludeOptional, forceIncludeNonRequired); } @@ -361,6 +366,7 @@ void processAdditionalProperties(Map additionalProperties) { public static final String INTERCEPTORS = "interceptors"; public static final String ADDITIONAL_CONTRACT_ANNOTATIONS = "additionalContractAnnotations"; public static final String AUTH_AS_METHOD_ARGUMENT = "authAsMethodArgument"; + public static final String AUTH_ALLOW_MULTIPLE = "authAllowMultiple"; public static final String ENABLE_JSON_NULLABLE = "enableJsonNullable"; public static final String FILTER_WITH_MODELS = "filterWithModels"; public static final String PREFIX_PATH = "prefixPath"; @@ -2244,10 +2250,11 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List(); - record AuthMethodGroup(String name, List methods) {} + record AuthMethodGroup(String name, int index, List methods) {} var authMethods = (List) this.vendorExtensions.computeIfAbsent("authMethods", k -> new ArrayList()); - var tags = (Set) this.vendorExtensions.computeIfAbsent("tags", k -> new TreeSet()); + var authMethodTags = (Set) this.vendorExtensions.computeIfAbsent("authMethodTags", k -> new TreeSet()); + var authTags = (Set) this.vendorExtensions.computeIfAbsent("authTags", k -> new TreeSet()); var operations = (Map) objs.get("operations"); if (params.clientConfigPrefix != null) { httpClientAnnotationParams.put("configPath", "\"" + params.clientConfigPrefix + "." + operations.get("classname") + "\""); @@ -2505,67 +2512,88 @@ record AuthMethodGroup(String name, List methods) {} response.vendorExtensions.put("singleResponse", op.responses.size() == 1); } if (op.hasAuthMethods) { - if (params.codegenMode.isServer()) { - var operationAuthMethods = new TreeSet(); - for (var authMethod : op.authMethods) { - tags.add(upperCase(toVarName(authMethod.name))); - final String authName; - if (authMethod.isApiKey || authMethod.isBasic || authMethod.isBasicBearer) { - authName = upperCase(toVarName(authMethod.name)); - } else if (authMethod.isOAuth) { - if (authMethod.scopes == null || authMethod.scopes.isEmpty()) { - authName = upperCase(toVarName(authMethod.name) + "NoScopes"); - } else { - var scopes = authMethod.scopes.stream() - .map(it -> this.upperCase(toVarName(it.get("scope").toString()))) - .sorted() - .collect(Collectors.joining("With")); - authName = upperCase(toVarName(authMethod.name) + "With" + scopes); - } + var operationAuthMethods = new TreeSet(); + for (var authMethod : op.authMethods) { + authTags.add(upperCase(toVarName(authMethod.name))); + final String authName; + if (authMethod.isApiKey || authMethod.isBasic || authMethod.isBasicBearer) { + authName = upperCase(toVarName(authMethod.name)); + } else if (authMethod.isOAuth) { + if (authMethod.scopes == null || authMethod.scopes.isEmpty()) { + authName = upperCase(toVarName(authMethod.name) + "NoScopes"); } else { - throw new IllegalStateException(); + var scopes = authMethod.scopes.stream() + .map(it -> this.upperCase(toVarName(it.get("scope").toString()))) + .sorted() + .collect(Collectors.joining("With")); + authName = upperCase(toVarName(authMethod.name) + "With" + scopes); } - operationAuthMethods.add(authName); - tags.add(upperCase(authName)); - } - var authInterceptorTag = String.join("With", operationAuthMethods); - var security = new ArrayList(); - for (int i = 0; i < op.authMethods.size(); i++) { - var source = op.authMethods.get(i); - var scopes = Objects.requireNonNullElse(source.scopes, List.>of()).stream().map(m -> m.get("scope").toString()).toList(); - var copy = source.filterByScopeNames(scopes); - security.add(copy); - copy.vendorExtensions.put("isLast", i == op.authMethods.size() - 1); - copy.vendorExtensions.put("isFirst", i == 0); - copy.vendorExtensions.put("hasScopes", copy.scopes != null && !copy.scopes.isEmpty()); + } else { + throw new IllegalStateException(); } - - tags.add(authInterceptorTag); - if (authMethods.stream().noneMatch(a -> a.name.equals(authInterceptorTag))) { - authMethods.add(new AuthMethodGroup(authInterceptorTag, security)); + operationAuthMethods.add(authName); + authTags.add(upperCase(authName)); + } + var authInterceptorTag = String.join("With", operationAuthMethods); + var security = new ArrayList(); + for (int i = 0; i < op.authMethods.size(); i++) { + var source = op.authMethods.get(i); + var scopes = Objects.requireNonNullElse(source.scopes, List.>of()).stream().map(m -> m.get("scope").toString()).toList(); + var copy = source.filterByScopeNames(scopes); + security.add(copy); + copy.vendorExtensions.put("isLast", i == op.authMethods.size() - 1); + copy.vendorExtensions.put("isFirst", i == 0); + copy.vendorExtensions.put("index", i + 1); + copy.vendorExtensions.put("revertIndex", op.authMethods.size() - i); + copy.vendorExtensions.put("hasScopes", copy.scopes != null && !copy.scopes.isEmpty()); + } + + if (params.codegenMode().isServer()) { + authTags.add(authInterceptorTag); + if (!authMethodTags.contains(authInterceptorTag)) { + authMethodTags.add(authInterceptorTag); + authMethods.add(new AuthMethodGroup(authInterceptorTag, authMethods.size() + 1, security)); } - op.vendorExtensions.put("authInterceptorTag", authInterceptorTag); } else { + if (!operationAuthMethods.contains(authInterceptorTag)) { + authTags.add(authInterceptorTag); + if (!authMethodTags.contains(authInterceptorTag)) { + authMethodTags.add(authInterceptorTag); + authMethods.add(new AuthMethodGroup(authInterceptorTag, authMethods.size() + 1, security)); + } + } + if (op.authMethods.size() == 1 || params.primaryAuth == null) { if (op.authMethods.size() > 1) { Set secSchemes = op.authMethods.stream() .map(s -> s.name) .collect(Collectors.toSet()); - LOGGER.warn("Found multiple securitySchemes {} for {} {} it is recommended to specify preferred securityScheme using `primaryAuth` property, or the first random will be used", - secSchemes, op.httpMethod, op.path); + LOGGER.warn("Found multiple securitySchemes {} for {} {} it is recommended to specify preferred securityScheme using `primaryAuth` property, using first random: {}", + secSchemes, op.httpMethod, op.path, op.authMethods.get(0).name); } - CodegenSecurity authMethod = op.authMethods.get(0); - if (params.authAsMethodArgument) { - CodegenParameter fakeAuthParameter = getAuthArgumentParameter(authMethod, op.allParams); - op.allParams.add(fakeAuthParameter); + if (params.authAllowMultiple) { + if (params.authAsMethodArgument) { + for (CodegenSecurity authMethod : op.authMethods) { + CodegenParameter fakeAuthParameter = getAuthArgumentParameter(authMethod, op.allParams); + op.allParams.add(fakeAuthParameter); + } + } else { + op.vendorExtensions.put("authInterceptorTag", authInterceptorTag); + } } else { - var authName = camelize(toVarName(authMethod.name)); - tags.add(upperCase(authName)); - op.vendorExtensions.put("authInterceptorTag", authName); + CodegenSecurity authMethod = op.authMethods.get(0); + if (params.authAsMethodArgument) { + CodegenParameter fakeAuthParameter = getAuthArgumentParameter(authMethod, op.allParams); + op.allParams.add(fakeAuthParameter); + } else { + var authName = camelize(toVarName(authMethod.name)); + authTags.add(upperCase(authName)); + op.vendorExtensions.put("authInterceptorTag", authName); + } } } else { CodegenSecurity authMethod = op.authMethods.stream() @@ -2578,7 +2606,7 @@ record AuthMethodGroup(String name, List methods) {} op.allParams.add(fakeAuthParameter); } else { var authName = camelize(toVarName(authMethod.name)); - tags.add(upperCase(authName)); + authTags.add(upperCase(authName)); op.vendorExtensions.put("authInterceptorTag", authName); } } @@ -2763,7 +2791,7 @@ private CodegenParameter getAuthArgumentParameter(CodegenSecurity authMethod, Li String authName = getAuthName(authMethod.name, parameters); fakeAuthParameter.paramName = authName; - fakeAuthParameter.baseName = authName; + fakeAuthParameter.baseName = authMethod.keyParamName; fakeAuthParameter.nameInLowerCase = authName.toLowerCase(Locale.ROOT); if (authMethod.isKeyInQuery) { fakeAuthParameter.isQueryParam = true; diff --git a/openapi/openapi-generator/src/main/resources/openapi/templates/kora/javaClientSecuritySchema.mustache b/openapi/openapi-generator/src/main/resources/openapi/templates/kora/javaClientSecuritySchema.mustache index 8152026c0..60bafbb88 100644 --- a/openapi/openapi-generator/src/main/resources/openapi/templates/kora/javaClientSecuritySchema.mustache +++ b/openapi/openapi-generator/src/main/resources/openapi/templates/kora/javaClientSecuritySchema.mustache @@ -5,10 +5,6 @@ */ package {{apiPackage}}; -import ru.tinkoff.kora.common.Module; -import ru.tinkoff.kora.common.Tag; -import ru.tinkoff.kora.common.DefaultComponent; -import ru.tinkoff.kora.config.common.Config; import ru.tinkoff.kora.config.common.extractor.ConfigValueExtractor; import ru.tinkoff.kora.http.client.common.interceptor.*; @@ -25,9 +21,9 @@ public interface ApiSecurity { static final class {{#lambda.classname}}{{name}}{{/lambda.classname}} {} {{#isApiKey}} - @DefaultComponent - @Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}.class) - default String {{name}}Config(Config config, ConfigValueExtractor extractor) { + @ru.tinkoff.kora.common.DefaultComponent + @ru.tinkoff.kora.common.Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}.class) + default String {{name}}Config(ru.tinkoff.kora.config.common.Config config, ConfigValueExtractor extractor) { var configPath = "{{#hasSecurityConfigPrefix}}{{securityConfigPrefix}}.{{/hasSecurityConfigPrefix}}{{name}}"; var configValue = config.get(configPath); var parsed = extractor.extract(configValue); @@ -37,17 +33,18 @@ public interface ApiSecurity { return parsed; } - @DefaultComponent - @Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}.class) - default ApiKeyHttpClientInterceptor {{name}}HttpClientAuthInterceptor(@Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}.class) String apiKey) { + @ru.tinkoff.kora.common.DefaultComponent + @ru.tinkoff.kora.common.Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}.class) + default HttpClientInterceptor {{name}}HttpClientAuthInterceptor(@ru.tinkoff.kora.common.Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}.class) String apiKey) { var paramLocation = ApiKeyHttpClientInterceptor.ApiKeyLocation.{{#isKeyInQuery}}QUERY{{/isKeyInQuery}}{{#isKeyInHeader}}HEADER{{/isKeyInHeader}}{{#isKeyInCookie}}COOKIE{{/isKeyInCookie}}; return new ApiKeyHttpClientInterceptor(paramLocation, "{{keyParamName}}", apiKey); } {{/isApiKey}}{{#isBasicBasic}} @ru.tinkoff.kora.common.annotation.Generated("openapi generator kora client") - static record {{#lambda.classname}}{{name}}{{/lambda.classname}}Config(String username, String password){} + static record {{#lambda.classname}}{{name}}{{/lambda.classname}}Config(String username, String password) {} - default {{#lambda.classname}}{{name}}{{/lambda.classname}}Config {{name}}Config(Config config, ConfigValueExtractor<{{#lambda.classname}}{{name}}{{/lambda.classname}}Config> extractor) { + @ru.tinkoff.kora.common.DefaultComponent + default {{#lambda.classname}}{{name}}{{/lambda.classname}}Config {{name}}Config(ru.tinkoff.kora.config.common.Config config, ConfigValueExtractor<{{#lambda.classname}}{{name}}{{/lambda.classname}}Config> extractor) { var configPath = "{{#hasSecurityConfigPrefix}}{{securityConfigPrefix}}.{{/hasSecurityConfigPrefix}}{{name}}"; var configValue = config.get(configPath); var parsed = extractor.extract(configValue); @@ -57,26 +54,45 @@ public interface ApiSecurity { return parsed; } - @Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}.class) - default BasicAuthHttpClientTokenProvider {{name}}BasicAuthHttpClientTokenProvider({{#lambda.classname}}{{name}}{{/lambda.classname}}Config config) { + @ru.tinkoff.kora.common.DefaultComponent + @ru.tinkoff.kora.common.Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}.class) + default HttpClientTokenProvider {{name}}BasicAuthHttpClientTokenProvider({{#lambda.classname}}{{name}}{{/lambda.classname}}Config config) { return new BasicAuthHttpClientTokenProvider(config.username(), config.password()); } - @DefaultComponent - @Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}.class) - default BasicAuthHttpClientInterceptor {{name}}HttpClientAuthInterceptor(@Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}.class) HttpClientTokenProvider tokenProvider) { + @ru.tinkoff.kora.common.DefaultComponent + @ru.tinkoff.kora.common.Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}.class) + default HttpClientInterceptor {{name}}HttpClientAuthInterceptor(@ru.tinkoff.kora.common.Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}.class) HttpClientTokenProvider tokenProvider) { return new BasicAuthHttpClientInterceptor(tokenProvider); } {{/isBasicBasic}}{{#isBasicBearer}} - @Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}.class) - default BearerAuthHttpClientInterceptor {{name}}HttpClientAuthInterceptor(@Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}.class) HttpClientTokenProvider tokenProvider) { + @ru.tinkoff.kora.common.DefaultComponent + @ru.tinkoff.kora.common.Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}.class) + default HttpClientInterceptor {{name}}HttpClientAuthInterceptor(@ru.tinkoff.kora.common.Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}.class) HttpClientTokenProvider tokenProvider) { return new BearerAuthHttpClientInterceptor(tokenProvider); } {{/isBasicBearer}}{{#isOAuth}} - @DefaultComponent - @Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}.class) - default BearerAuthHttpClientInterceptor {{name}}HttpClientAuthInterceptor(@Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}.class) HttpClientTokenProvider tokenProvider) { + @ru.tinkoff.kora.common.DefaultComponent + @ru.tinkoff.kora.common.Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}.class) + default HttpClientInterceptor {{name}}HttpClientAuthInterceptor(@ru.tinkoff.kora.common.Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}.class) HttpClientTokenProvider tokenProvider) { return new BearerAuthHttpClientInterceptor(tokenProvider); } -{{/isOAuth}}{{/authMethods}} +{{/isOAuth}}{{/authMethods}} {{#vendorExtensions.authMethodTags}} + + @ru.tinkoff.kora.common.annotation.Generated("openapi generator kora client") + static final class {{.}} {} +{{/vendorExtensions.authMethodTags}} +{{#vendorExtensions.authMethods}} + + @ru.tinkoff.kora.common.DefaultComponent + @ru.tinkoff.kora.common.Tag({{name}}.class) + default HttpClientInterceptor {{name}}HttpClientAuthInterceptor({{#methods}} + @ru.tinkoff.kora.common.Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}.class) HttpClientInterceptor {{#lambda.camelcase}}{{name}}{{/lambda.camelcase}}{{^vendorExtensions.isLast}},{{/vendorExtensions.isLast}}{{/methods}}) { + return (ctx, chain, request) -> {{#methods}}{{#vendorExtensions.isFirst}} + {{#lambda.camelcase}}{{name}}{{/lambda.camelcase}}.processRequest(ctx, {{/vendorExtensions.isFirst}}{{^vendorExtensions.isFirst}} + (ctx{{vendorExtensions.index}}, request{{vendorExtensions.index}}) -> {{#lambda.camelcase}}{{name}}{{/lambda.camelcase}}.processRequest(ctx{{vendorExtensions.index}}, {{/vendorExtensions.isFirst}}{{#vendorExtensions.isLast}}chain, request{{vendorExtensions.index}}), {{/vendorExtensions.isLast}} {{/methods}}{{#methods}} {{^vendorExtensions.isFirst}}{{^vendorExtensions.isLast}} + request{{vendorExtensions.revertIndex}}), {{/vendorExtensions.isLast}}{{/vendorExtensions.isFirst}} {{/methods}} + request); + } +{{/vendorExtensions.authMethods}} } diff --git a/openapi/openapi-generator/src/main/resources/openapi/templates/kora/javaServerSecuritySchema.mustache b/openapi/openapi-generator/src/main/resources/openapi/templates/kora/javaServerSecuritySchema.mustache index c96846116..629f4ae11 100644 --- a/openapi/openapi-generator/src/main/resources/openapi/templates/kora/javaServerSecuritySchema.mustache +++ b/openapi/openapi-generator/src/main/resources/openapi/templates/kora/javaServerSecuritySchema.mustache @@ -30,10 +30,11 @@ import ru.tinkoff.kora.http.server.common.auth.*; @ru.tinkoff.kora.common.annotation.Generated("openapi generator kora server") @ru.tinkoff.kora.common.Module public interface ApiSecurity { -{{#vendorExtensions.tags}} +{{#vendorExtensions.authTags}} + @ru.tinkoff.kora.common.annotation.Generated("openapi generator kora server") static final class {{.}} {} -{{/vendorExtensions.tags}} +{{/vendorExtensions.authTags}} {{#vendorExtensions.authMethods}} @ru.tinkoff.kora.common.annotation.Generated("openapi generator kora server") @@ -131,7 +132,7 @@ public interface ApiSecurity { } @ru.tinkoff.kora.common.Tag({{name}}.class) - @DefaultComponent + @ru.tinkoff.kora.common.DefaultComponent default {{name}}HttpServerInterceptor {{name}}HttpServerAuthInterceptor({{#methods}} @ru.tinkoff.kora.common.Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}.class) HttpServerPrincipalExtractor<{{#isOAuth}}PrincipalWithScopes{{/isOAuth}}{{^isOAuth}}Principal{{/isOAuth}}> {{#lambda.camelcase}}{{name}}{{/lambda.camelcase}}{{^vendorExtensions.isLast}},{{/vendorExtensions.isLast}}{{/methods}}) { return new {{name}}HttpServerInterceptor({{#methods}}{{#lambda.camelcase}}{{name}}{{/lambda.camelcase}}{{^vendorExtensions.isLast}}, {{/vendorExtensions.isLast}}{{/methods}}); diff --git a/openapi/openapi-generator/src/main/resources/openapi/templates/kora/kotlinClientSecuritySchema.mustache b/openapi/openapi-generator/src/main/resources/openapi/templates/kora/kotlinClientSecuritySchema.mustache index 40778c378..f4d9d1832 100644 --- a/openapi/openapi-generator/src/main/resources/openapi/templates/kora/kotlinClientSecuritySchema.mustache +++ b/openapi/openapi-generator/src/main/resources/openapi/templates/kora/kotlinClientSecuritySchema.mustache @@ -6,11 +6,7 @@ package {{apiPackage}} -import ru.tinkoff.kora.common.Module -import ru.tinkoff.kora.common.Tag -import ru.tinkoff.kora.common.DefaultComponent; import ru.tinkoff.kora.config.common.extractor.ConfigValueExtractor -import ru.tinkoff.kora.config.common.Config import ru.tinkoff.kora.http.client.common.interceptor.* import ru.tinkoff.kora.http.common.auth.* @@ -26,16 +22,17 @@ interface ApiSecurity { class {{#lambda.classname}}{{name}}{{/lambda.classname}} {{#isApiKey}} - @Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}::class) - fun {{name}}Config(config: Config, extractor: ConfigValueExtractor): String { + @ru.tinkoff.kora.common.DefaultComponent + @ru.tinkoff.kora.common.Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}::class) + fun {{name}}Config(config: ru.tinkoff.kora.config.common.Config, extractor: ConfigValueExtractor): String { val configPath = "{{#hasSecurityConfigPrefix}}{{securityConfigPrefix}}.{{/hasSecurityConfigPrefix}}{{name}}" val configValue = config.get(configPath) return extractor.extract(configValue) ?: throw ru.tinkoff.kora.config.common.extractor.ConfigValueExtractionException.missingValueAfterParse(configValue) } - @DefaultComponent - @Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}::class) - fun {{name}}HttpClientAuthInterceptor(@Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}::class) apiKey: String): ApiKeyHttpClientInterceptor { + @ru.tinkoff.kora.common.DefaultComponent + @ru.tinkoff.kora.common.Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}::class) + fun {{name}}HttpClientAuthInterceptor(@ru.tinkoff.kora.common.Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}::class) apiKey: String): HttpClientTokenProvider { val paramLocation = ApiKeyHttpClientInterceptor.ApiKeyLocation.{{#isKeyInQuery}}QUERY{{/isKeyInQuery}}{{#isKeyInHeader}}HEADER{{/isKeyInHeader}}{{#isKeyInCookie}}COOKIE{{/isKeyInCookie}} return ApiKeyHttpClientInterceptor(paramLocation, "{{keyParamName}}", apiKey) } @@ -43,32 +40,34 @@ interface ApiSecurity { @ru.tinkoff.kora.common.annotation.Generated("openapi generator kora client") data class {{#lambda.classname}}{{name}}{{/lambda.classname}}Config(val username: String, val password: String) - fun {{name}}Config(config: Config, extractor: ConfigValueExtractor<{{#lambda.classname}}{{name}}{{/lambda.classname}}Config>): {{#lambda.classname}}{{name}}{{/lambda.classname}}Config { + @ru.tinkoff.kora.common.DefaultComponent + fun {{name}}Config(config: ru.tinkoff.kora.config.common.Config, extractor: ConfigValueExtractor<{{#lambda.classname}}{{name}}{{/lambda.classname}}Config>): {{#lambda.classname}}{{name}}{{/lambda.classname}}Config { val configPath = "{{#hasSecurityConfigPrefix}}{{securityConfigPrefix}}.{{/hasSecurityConfigPrefix}}{{name}}" val configValue = config.get(configPath) return extractor.extract(configValue) ?: throw ru.tinkoff.kora.config.common.extractor.ConfigValueExtractionException.missingValueAfterParse(configValue) } - @Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}::class) - fun {{name}}BasicAuthHttpClientTokenProvider(config: {{#lambda.classname}}{{name}}{{/lambda.classname}}Config): BasicAuthHttpClientTokenProvider { + @ru.tinkoff.kora.common.DefaultComponent + @ru.tinkoff.kora.common.Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}::class) + fun {{name}}BasicAuthHttpClientTokenProvider(config: {{#lambda.classname}}{{name}}{{/lambda.classname}}Config): HttpClientTokenProvider { return BasicAuthHttpClientTokenProvider(config.username, config.password) } - @DefaultComponent - @Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}::class) - fun {{name}}HttpClientAuthInterceptor(@Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}::class) provider: BasicAuthHttpClientTokenProvider): BasicAuthHttpClientInterceptor { + @ru.tinkoff.kora.common.DefaultComponent + @ru.tinkoff.kora.common.Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}::class) + fun {{name}}HttpClientAuthInterceptor(@ru.tinkoff.kora.common.Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}::class) provider: HttpClientTokenProvider): HttpClientInterceptor { return BasicAuthHttpClientInterceptor(provider) } {{/isBasicBasic}}{{#isBasicBearer}} - @DefaultComponent - @Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}::class) - fun {{name}}HttpClientAuthInterceptor(@Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}::class) tokenProvider: HttpClientTokenProvider): BearerAuthHttpClientInterceptor { + @ru.tinkoff.kora.common.DefaultComponent + @ru.tinkoff.kora.common.Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}::class) + fun {{name}}HttpClientAuthInterceptor(@ru.tinkoff.kora.common.Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}::class) tokenProvider: HttpClientTokenProvider): HttpClientInterceptor { return BearerAuthHttpClientInterceptor(tokenProvider) } {{/isBasicBearer}}{{#isOAuth}} - @DefaultComponent - @Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}::class) - fun {{name}}HttpClientAuthInterceptor(@Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}::class) tokenProvider: HttpClientTokenProvider): BearerAuthHttpClientInterceptor { + @ru.tinkoff.kora.common.DefaultComponent + @ru.tinkoff.kora.common.Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}::class) + fun {{name}}HttpClientAuthInterceptor(@ru.tinkoff.kora.common.Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}::class) tokenProvider: HttpClientTokenProvider): HttpClientInterceptor { return BearerAuthHttpClientInterceptor(tokenProvider) } {{/isOAuth}}{{/authMethods}} diff --git a/openapi/openapi-generator/src/main/resources/openapi/templates/kora/kotlinServerSecuritySchema.mustache b/openapi/openapi-generator/src/main/resources/openapi/templates/kora/kotlinServerSecuritySchema.mustache index ee8ea5719..4a39d0a94 100644 --- a/openapi/openapi-generator/src/main/resources/openapi/templates/kora/kotlinServerSecuritySchema.mustache +++ b/openapi/openapi-generator/src/main/resources/openapi/templates/kora/kotlinServerSecuritySchema.mustache @@ -32,15 +32,16 @@ import ru.tinkoff.kora.http.server.common.auth.* @ru.tinkoff.kora.common.annotation.Generated("openapi generator kora server") @ru.tinkoff.kora.common.Module public interface ApiSecurity { -{{#vendorExtensions.tags}} +{{#vendorExtensions.authTags}} + @ru.tinkoff.kora.common.annotation.Generated("openapi generator kora server") class {{.}} {} -{{/vendorExtensions.tags}} +{{/vendorExtensions.authTags}} {{#vendorExtensions.authMethods}} @ru.tinkoff.kora.common.annotation.Generated("openapi generator kora server") class {{name}}HttpServerInterceptor( - {{#methods}} @Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}::class) val {{#lambda.camelcase}}{{name}}{{/lambda.camelcase}}: HttpServerPrincipalExtractor<{{#isOAuth}}PrincipalWithScopes{{/isOAuth}}{{^isOAuth}}Principal{{/isOAuth}}>{{^vendorExtensions.isLast}}, + {{#methods}} @ru.tinkoff.kora.common.Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}::class) val {{#lambda.camelcase}}{{name}}{{/lambda.camelcase}}: HttpServerPrincipalExtractor<{{#isOAuth}}PrincipalWithScopes{{/isOAuth}}{{^isOAuth}}Principal{{/isOAuth}}>{{^vendorExtensions.isLast}}, {{/vendorExtensions.isLast}}{{/methods}} ) : HttpServerInterceptor { @@ -121,10 +122,10 @@ public interface ApiSecurity { } } - @Tag({{name}}::class) - @DefaultComponent + @ru.tinkoff.kora.common.Tag({{name}}::class) + @ru.tinkoff.kora.common.DefaultComponent fun {{#lambda.camelcase}}{{name}}HttpServerAuthInterceptor{{/lambda.camelcase}}({{#methods}} - @Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}::class) {{#lambda.camelcase}}{{name}}{{/lambda.camelcase}}: HttpServerPrincipalExtractor<{{#isOAuth}}PrincipalWithScopes{{/isOAuth}}{{^isOAuth}}Principal{{/isOAuth}}>{{^vendorExtensions.isLast}}, + @ru.tinkoff.kora.common.Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}::class) {{#lambda.camelcase}}{{name}}{{/lambda.camelcase}}: HttpServerPrincipalExtractor<{{#isOAuth}}PrincipalWithScopes{{/isOAuth}}{{^isOAuth}}Principal{{/isOAuth}}>{{^vendorExtensions.isLast}}, {{/vendorExtensions.isLast}}{{/methods}}): {{name}}HttpServerInterceptor { return {{name}}HttpServerInterceptor({{#methods}}{{#lambda.camelcase}}{{name}}{{/lambda.camelcase}}{{^vendorExtensions.isLast}}, {{/vendorExtensions.isLast}}{{/methods}}) } diff --git a/openapi/openapi-generator/src/test/java/ru/tinkoff/kora/openapi/generator/KoraCodegenTest.java b/openapi/openapi-generator/src/test/java/ru/tinkoff/kora/openapi/generator/KoraCodegenTest.java index 175fcf8bd..9b6bd61ec 100644 --- a/openapi/openapi-generator/src/test/java/ru/tinkoff/kora/openapi/generator/KoraCodegenTest.java +++ b/openapi/openapi-generator/src/test/java/ru/tinkoff/kora/openapi/generator/KoraCodegenTest.java @@ -125,6 +125,7 @@ static SwaggerParams[] generateParams() { "/example/petstoreV3_validation.yaml", "/example/petstoreV3_single_response.yaml", "/example/petstoreV3_security_all.yaml", + "/example/petstoreV3_security_multi.yaml", "/example/petstoreV3_security_api_key.yaml", "/example/petstoreV3_security_basic.yaml", "/example/petstoreV3_security_bearer.yaml", diff --git a/openapi/openapi-generator/src/test/resources/example/petstoreV3_security_multi.yaml b/openapi/openapi-generator/src/test/resources/example/petstoreV3_security_multi.yaml new file mode 100644 index 000000000..3da3d737c --- /dev/null +++ b/openapi/openapi-generator/src/test/resources/example/petstoreV3_security_multi.yaml @@ -0,0 +1,204 @@ +openapi: 3.0.3 + +info: + title: Petstore with discriminator + version: 1.0.0 + +paths: + /petsOneSecurity: + get: + security: + - headerAuth: [ ] + summary: List all pets + operationId: listOneSecurity + tags: + - pets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int32 + responses: + '200': + description: A paged array of pets + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /petsTwoSecurity: + get: + security: + - headerAuth: [ ] + queryAuth: [ ] + summary: List all pets + operationId: listTwoSecurity + tags: + - pets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int32 + responses: + '200': + description: A paged array of pets + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /petsThreeSecurity: + get: + security: + - headerAuth: [ ] + cookieAuth: [ ] + queryAuth: [ ] + summary: List all pets + operationId: listThreeSecurity + tags: + - pets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int32 + responses: + '200': + description: A paged array of pets + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /petsNoSecurity: + get: + summary: List all pets + operationId: listNoSecurity + tags: + - pets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int32 + responses: + '200': + description: A paged array of pets + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" +components: + schemas: + Pet: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Pets: + type: array + items: + $ref: "#/components/schemas/Pet" + Error: + type: object + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string + securitySchemes: + headerAuth: + type: apiKey + in: header + name: X-API-KEY + queryAuth: + type: apiKey + in: query + name: X-QUERY-KEY + cookieAuth: + type: apiKey + in: cookie + name: X-COOKIE-KEY + oAuth: + type: oauth2 + description: This API uses OAuth 2 with the implicit grant flow. [More info](https://api.example.com/docs/auth) + flows: + implicit: + authorizationUrl: https://api.example.com/oauth2/authorize + scopes: + read_pets: read your pets + write_pets: modify pets in your account +#security: +# - headerAuth: [ ] +# cookieAuth: [ ] +# queryAuth: [ ] +# - oAuth: +# - write_pets +# - read_pets +# From f6120a66876a244662997977097b8a79eed51ed1 Mon Sep 17 00:00:00 2001 From: Anton Kurako Date: Mon, 10 Nov 2025 12:55:40 +0300 Subject: [PATCH 2/4] Added OpenAPI generator HTTP client multi auth support --- openapi/openapi-generator/build.gradle | 1 + .../kora/openapi/generator/KoraCodegen.java | 30 +++++++++---------- .../openapi/generator/KoraCodegenTest.java | 17 +++++++++-- 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/openapi/openapi-generator/build.gradle b/openapi/openapi-generator/build.gradle index b794cad50..799f3d169 100644 --- a/openapi/openapi-generator/build.gradle +++ b/openapi/openapi-generator/build.gradle @@ -94,6 +94,7 @@ sourceSets { addOpenapiDir("petstoreV3_security_all") addOpenapiDir("petstoreV3_security_all_auth_arg") addOpenapiDir("petstoreV3_security_multi") + addOpenapiDir("petstoreV3_security_multi_auth") addOpenapiDir("petstoreV3_security_multi_auth_arg") addOpenapiDir("petstoreV3_security_api_key") addOpenapiDir("petstoreV3_security_api_key_auth_arg") diff --git a/openapi/openapi-generator/src/main/java/ru/tinkoff/kora/openapi/generator/KoraCodegen.java b/openapi/openapi-generator/src/main/java/ru/tinkoff/kora/openapi/generator/KoraCodegen.java index c34839f0e..fa34b55e4 100644 --- a/openapi/openapi-generator/src/main/java/ru/tinkoff/kora/openapi/generator/KoraCodegen.java +++ b/openapi/openapi-generator/src/main/java/ru/tinkoff/kora/openapi/generator/KoraCodegen.java @@ -2565,7 +2565,21 @@ record AuthMethodGroup(String name, int index, List methods) {} } } - if (op.authMethods.size() == 1 || params.primaryAuth == null) { + if (params.primaryAuth != null) { + CodegenSecurity authMethod = op.authMethods.stream() + .filter(a -> a.name.equals(params.primaryAuth)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Can't find OpenAPI securitySchema named: " + params.primaryAuth)); + + if (params.authAsMethodArgument) { + CodegenParameter fakeAuthParameter = getAuthArgumentParameter(authMethod, op.allParams); + op.allParams.add(fakeAuthParameter); + } else { + var authName = camelize(toVarName(authMethod.name)); + authTags.add(upperCase(authName)); + op.vendorExtensions.put("authInterceptorTag", authName); + } + } else { if (op.authMethods.size() > 1) { Set secSchemes = op.authMethods.stream() .map(s -> s.name) @@ -2595,20 +2609,6 @@ record AuthMethodGroup(String name, int index, List methods) {} op.vendorExtensions.put("authInterceptorTag", authName); } } - } else { - CodegenSecurity authMethod = op.authMethods.stream() - .filter(a -> a.name.equals(params.primaryAuth)) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException("Can't find OpenAPI securitySchema named: " + params.primaryAuth)); - - if (params.authAsMethodArgument) { - CodegenParameter fakeAuthParameter = getAuthArgumentParameter(authMethod, op.allParams); - op.allParams.add(fakeAuthParameter); - } else { - var authName = camelize(toVarName(authMethod.name)); - authTags.add(upperCase(authName)); - op.vendorExtensions.put("authInterceptorTag", authName); - } } } } diff --git a/openapi/openapi-generator/src/test/java/ru/tinkoff/kora/openapi/generator/KoraCodegenTest.java b/openapi/openapi-generator/src/test/java/ru/tinkoff/kora/openapi/generator/KoraCodegenTest.java index 9b6bd61ec..6c9f3ff80 100644 --- a/openapi/openapi-generator/src/test/java/ru/tinkoff/kora/openapi/generator/KoraCodegenTest.java +++ b/openapi/openapi-generator/src/test/java/ru/tinkoff/kora/openapi/generator/KoraCodegenTest.java @@ -48,6 +48,7 @@ record SwaggerParams(String mode, String spec, String name, Options options) { static final class Options { private boolean authAsArg; + private boolean authAllowMultiple; private boolean jsonNullable; private boolean includeServerRequest; private boolean implicitHeaders; @@ -57,6 +58,8 @@ static final class Options { public boolean authAsArg() {return authAsArg;} + public boolean authAllowMultiple() {return authAllowMultiple;} + public boolean jsonNullable() {return jsonNullable;} public boolean includeServerRequest() {return includeServerRequest;} @@ -73,6 +76,11 @@ public Options setAuthAsArg(boolean authAsArg) { return this; } + public Options setAuthAllowMultiple(boolean authAllowMultiple) { + this.authAllowMultiple = authAllowMultiple; + return this; + } + public Options setJsonNullable(boolean jsonNullable) { this.jsonNullable = jsonNullable; return this; @@ -146,8 +154,11 @@ static SwaggerParams[] generateParams() { result.add(new SwaggerParams(mode, fileName, name, new SwaggerParams.Options())); - if (fileName.contains("security")) { - result.add(new SwaggerParams(mode, fileName, name + "_auth_arg", new SwaggerParams.Options().setIncludeServerRequest(true))); + if (fileName.contains("security_multi")) { + result.add(new SwaggerParams(mode, fileName, name + "_auth", new SwaggerParams.Options().setAuthAllowMultiple(true))); + result.add(new SwaggerParams(mode, fileName, name + "_auth_arg", new SwaggerParams.Options().setAuthAllowMultiple(true).setAuthAsArg(true))); + } else if (fileName.contains("security")) { + result.add(new SwaggerParams(mode, fileName, name + "_auth_arg", new SwaggerParams.Options().setAuthAsArg(true))); } if (name.equals("petstoreV2") || name.equals("petstoreV3")) { @@ -215,10 +226,10 @@ private void generate(String name, String mode, String spec, String dir, Swagger """) .addAdditionalProperty("enableServerValidation", name.contains("validation")) .addAdditionalProperty("authAsMethodArgument", options.authAsArg()) + .addAdditionalProperty("authAllowMultiple", options.authAllowMultiple()) .addAdditionalProperty("enableJsonNullable", options.jsonNullable()) .addAdditionalProperty("implicitHeaders", options.implicitHeaders()) .addAdditionalProperty("requestInDelegateParams", options.includeServerRequest()) - .addAdditionalProperty("requestInDelegateParams", options.includeServerRequest()) .addAdditionalProperty("clientConfigPrefix", "test"); if (options.isDefaultDelegate()) { From 47a7bb8c289d50a5bba768de7cfbffda46ab907f Mon Sep 17 00:00:00 2001 From: Anton Kurako Date: Mon, 10 Nov 2025 13:21:54 +0300 Subject: [PATCH 3/4] Fixed kotlinClientSecuritySchema.mustache --- .../kora/kotlinClientSecuritySchema.mustache | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/openapi/openapi-generator/src/main/resources/openapi/templates/kora/kotlinClientSecuritySchema.mustache b/openapi/openapi-generator/src/main/resources/openapi/templates/kora/kotlinClientSecuritySchema.mustache index f4d9d1832..f44bcf713 100644 --- a/openapi/openapi-generator/src/main/resources/openapi/templates/kora/kotlinClientSecuritySchema.mustache +++ b/openapi/openapi-generator/src/main/resources/openapi/templates/kora/kotlinClientSecuritySchema.mustache @@ -32,7 +32,7 @@ interface ApiSecurity { @ru.tinkoff.kora.common.DefaultComponent @ru.tinkoff.kora.common.Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}::class) - fun {{name}}HttpClientAuthInterceptor(@ru.tinkoff.kora.common.Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}::class) apiKey: String): HttpClientTokenProvider { + fun {{name}}HttpClientAuthInterceptor(@ru.tinkoff.kora.common.Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}::class) apiKey: String): HttpClientInterceptor { val paramLocation = ApiKeyHttpClientInterceptor.ApiKeyLocation.{{#isKeyInQuery}}QUERY{{/isKeyInQuery}}{{#isKeyInHeader}}HEADER{{/isKeyInHeader}}{{#isKeyInCookie}}COOKIE{{/isKeyInCookie}} return ApiKeyHttpClientInterceptor(paramLocation, "{{keyParamName}}", apiKey) } @@ -70,5 +70,21 @@ interface ApiSecurity { fun {{name}}HttpClientAuthInterceptor(@ru.tinkoff.kora.common.Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}::class) tokenProvider: HttpClientTokenProvider): HttpClientInterceptor { return BearerAuthHttpClientInterceptor(tokenProvider) } -{{/isOAuth}}{{/authMethods}} +{{/isOAuth}}{{/authMethods}}{{#vendorExtensions.authMethodTags}} + + @ru.tinkoff.kora.common.annotation.Generated("openapi generator kora client") + class {{.}} +{{/vendorExtensions.authMethodTags}} +{{#vendorExtensions.authMethods}} + + @ru.tinkoff.kora.common.DefaultComponent + @ru.tinkoff.kora.common.Tag({{name}}::class) + fun {{name}}HttpClientAuthInterceptor({{#methods}} + @ru.tinkoff.kora.common.Tag(ApiSecurity.{{#lambda.classname}}{{name}}{{/lambda.classname}}::class) {{#lambda.camelcase}}{{name}}{{/lambda.camelcase}}: HttpClientInterceptor{{^vendorExtensions.isLast}},{{/vendorExtensions.isLast}}{{/methods}}) : HttpClientInterceptor { + return HttpClientInterceptor { ctx, chain, request -> {{#methods}}{{#vendorExtensions.isFirst}} {{#lambda.camelcase}}{{name}}{{/lambda.camelcase}}.processRequest(ctx, {{/vendorExtensions.isFirst}}{{^vendorExtensions.isFirst}} + { ctx{{vendorExtensions.index}}, request{{vendorExtensions.index}} -> {{#lambda.camelcase}}{{name}}{{/lambda.camelcase}}.processRequest(ctx{{vendorExtensions.index}}, {{/vendorExtensions.isFirst}}{{#vendorExtensions.isLast}}chain, request{{vendorExtensions.index}}) }, {{/vendorExtensions.isLast}} {{/methods}}{{#methods}} {{^vendorExtensions.isFirst}}{{^vendorExtensions.isLast}} + request{{vendorExtensions.revertIndex}}) }, {{/vendorExtensions.isLast}}{{/vendorExtensions.isFirst}} {{/methods}} + request) } + } +{{/vendorExtensions.authMethods}} } From 23f0c687cd88077a7e0a943f723e4a825e7be84d Mon Sep 17 00:00:00 2001 From: Anton Kurako Date: Thu, 13 Nov 2025 13:21:09 +0300 Subject: [PATCH 4/4] Fixed OpenAPI auth execution order to reflect OpenAPI spec order --- .../java/ru/tinkoff/kora/openapi/generator/KoraCodegen.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openapi/openapi-generator/src/main/java/ru/tinkoff/kora/openapi/generator/KoraCodegen.java b/openapi/openapi-generator/src/main/java/ru/tinkoff/kora/openapi/generator/KoraCodegen.java index fa34b55e4..f3bcdab9e 100644 --- a/openapi/openapi-generator/src/main/java/ru/tinkoff/kora/openapi/generator/KoraCodegen.java +++ b/openapi/openapi-generator/src/main/java/ru/tinkoff/kora/openapi/generator/KoraCodegen.java @@ -2253,8 +2253,8 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List methods) {} var authMethods = (List) this.vendorExtensions.computeIfAbsent("authMethods", k -> new ArrayList()); - var authMethodTags = (Set) this.vendorExtensions.computeIfAbsent("authMethodTags", k -> new TreeSet()); - var authTags = (Set) this.vendorExtensions.computeIfAbsent("authTags", k -> new TreeSet()); + var authMethodTags = (Set) this.vendorExtensions.computeIfAbsent("authMethodTags", k -> new LinkedHashSet<>()); + var authTags = (Set) this.vendorExtensions.computeIfAbsent("authTags", k -> new LinkedHashSet()); var operations = (Map) objs.get("operations"); if (params.clientConfigPrefix != null) { httpClientAnnotationParams.put("configPath", "\"" + params.clientConfigPrefix + "." + operations.get("classname") + "\"");