Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
import org.springdoc.core.configuration.hints.SpringDocSecurityHints;
import org.springdoc.core.customizers.GlobalOpenApiCustomizer;
import org.springdoc.core.customizers.OpenApiCustomizer;
import org.springdoc.core.properties.SpringDocConfigProperties;
import org.springdoc.core.properties.SpringDocConfigProperties.LoginEndpoint;

import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
Expand Down Expand Up @@ -110,14 +112,18 @@ class SpringSecurityLoginEndpointConfiguration {
/**
* Spring security login endpoint customiser open api customiser.
*
* @param applicationContext the application context
* @param applicationContext the application context
* @param springDocConfigProperties the springdoc configuration properties
* @return the open api customiser
*/
@Bean
@ConditionalOnProperty(SPRINGDOC_SHOW_LOGIN_ENDPOINT)
@Lazy(false)
OpenApiCustomizer springSecurityLoginEndpointCustomizer(ApplicationContext applicationContext) {
OpenApiCustomizer springSecurityLoginEndpointCustomizer(ApplicationContext applicationContext, SpringDocConfigProperties springDocConfigProperties) {
FilterChainProxy filterChainProxy = applicationContext.getBean(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME, FilterChainProxy.class);
LoginEndpoint loginEndpoint = springDocConfigProperties.getLoginEndpoint();
String usernameExample = loginEndpoint.getUsernameExample();
String passwordExample = loginEndpoint.getPasswordExample();
return openAPI -> {
for (SecurityFilterChain filterChain : filterChainProxy.getFilterChains()) {
Optional<UsernamePasswordAuthenticationFilter> optionalFilter =
Expand All @@ -132,29 +138,8 @@ OpenApiCustomizer springSecurityLoginEndpointCustomizer(ApplicationContext appli
.findAny();
if (optionalFilter.isPresent()) {
UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter = optionalFilter.get();
Operation operation = new Operation();
Schema<?> schema = new ObjectSchema()
.addProperty(usernamePasswordAuthenticationFilter.getUsernameParameter(), new StringSchema())
.addProperty(usernamePasswordAuthenticationFilter.getPasswordParameter(), new StringSchema());
String mediaType = org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
if (optionalDefaultLoginPageGeneratingFilter.isPresent()) {
DefaultLoginPageGeneratingFilter defaultLoginPageGeneratingFilter = optionalDefaultLoginPageGeneratingFilter.get();
try {
boolean formLoginEnabled = (boolean) FieldUtils.readDeclaredField(defaultLoginPageGeneratingFilter, "formLoginEnabled", true);
if (formLoginEnabled)
mediaType = org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VALUE;
}
catch (IllegalAccessException e) {
LOGGER.warn(e.getMessage());
}
}
RequestBody requestBody = new RequestBody().content(new Content().addMediaType(mediaType, new MediaType().schema(schema)));
operation.requestBody(requestBody);
ApiResponses apiResponses = new ApiResponses();
apiResponses.addApiResponse(String.valueOf(HttpStatus.OK.value()), new ApiResponse().description(HttpStatus.OK.getReasonPhrase()));
apiResponses.addApiResponse(String.valueOf(HttpStatus.UNAUTHORIZED.value()), new ApiResponse().description(HttpStatus.UNAUTHORIZED.getReasonPhrase()));
operation.responses(apiResponses);
operation.addTagsItem("login-endpoint");
String mediaType = resolveMediaType(optionalDefaultLoginPageGeneratingFilter);
Operation operation = buildOperation(usernamePasswordAuthenticationFilter, mediaType, usernameExample, passwordExample);
PathItem pathItem = new PathItem().post(operation);
try {
RequestMatcher requestMatcher = (RequestMatcher) FieldUtils.readField(
Expand All @@ -176,6 +161,81 @@ OpenApiCustomizer springSecurityLoginEndpointCustomizer(ApplicationContext appli
}
};
}

/**
* Resolves the request body media type based on the presence of a form login configuration.
*
* @param optionalDefaultLoginPageGeneratingFilter the optional default login page generating filter
* @return the resolved media type
*/
private String resolveMediaType(Optional<DefaultLoginPageGeneratingFilter> optionalDefaultLoginPageGeneratingFilter) {
String mediaType = org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
if (optionalDefaultLoginPageGeneratingFilter.isPresent()) {
DefaultLoginPageGeneratingFilter defaultLoginPageGeneratingFilter = optionalDefaultLoginPageGeneratingFilter.get();
try {
boolean formLoginEnabled = (boolean) FieldUtils.readDeclaredField(defaultLoginPageGeneratingFilter, "formLoginEnabled", true);
if (formLoginEnabled)
mediaType = org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VALUE;
}
catch (IllegalAccessException e) {
LOGGER.warn(e.getMessage());
}
}
return mediaType;
}

/**
* Builds the login endpoint operation.
*
* @param usernamePasswordAuthenticationFilter the username password authentication filter
* @param mediaType the request body media type
* @param usernameExample the username example value
* @param passwordExample the password example value
* @return the operation
*/
private Operation buildOperation(UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter,
String mediaType, String usernameExample, String passwordExample) {
Operation operation = new Operation();
operation.requestBody(buildRequestBody(usernamePasswordAuthenticationFilter, mediaType, usernameExample, passwordExample));
operation.responses(buildApiResponses());
operation.addTagsItem("login-endpoint");
return operation;
}

/**
* Builds the request body for the login endpoint operation.
*
* @param usernamePasswordAuthenticationFilter the username password authentication filter
* @param mediaType the request body media type
* @param usernameExample the username example value
* @param passwordExample the password example value
* @return the request body
*/
private RequestBody buildRequestBody(UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter,
String mediaType, String usernameExample, String passwordExample) {
StringSchema usernameSchema = new StringSchema();
if (usernameExample != null)
usernameSchema.example(usernameExample);
StringSchema passwordSchema = new StringSchema();
if (passwordExample != null)
passwordSchema.example(passwordExample);
Schema<?> schema = new ObjectSchema()
.addProperty(usernamePasswordAuthenticationFilter.getUsernameParameter(), usernameSchema)
.addProperty(usernamePasswordAuthenticationFilter.getPasswordParameter(), passwordSchema);
return new RequestBody().content(new Content().addMediaType(mediaType, new MediaType().schema(schema)));
}

/**
* Builds the API responses for the login endpoint operation.
*
* @return the api responses
*/
private ApiResponses buildApiResponses() {
ApiResponses apiResponses = new ApiResponses();
apiResponses.addApiResponse(String.valueOf(HttpStatus.OK.value()), new ApiResponse().description(HttpStatus.OK.getReasonPhrase()));
apiResponses.addApiResponse(String.valueOf(HttpStatus.UNAUTHORIZED.value()), new ApiResponse().description(HttpStatus.UNAUTHORIZED.getReasonPhrase()));
return apiResponses;
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,11 @@ public class SpringDocConfigProperties {
*/
private boolean showLoginEndpoint;

/**
* The login endpoint configuration.
*/
private LoginEndpoint loginEndpoint = new LoginEndpoint();

/**
* Allow for pre-loading OpenAPI
*/
Expand Down Expand Up @@ -747,6 +752,24 @@ public void setShowLoginEndpoint(boolean showLoginEndpoint) {
this.showLoginEndpoint = showLoginEndpoint;
}

/**
* Gets login endpoint.
*
* @return the login endpoint
*/
public LoginEndpoint getLoginEndpoint() {
return loginEndpoint;
}

/**
* Sets login endpoint.
*
* @param loginEndpoint the login endpoint
*/
public void setLoginEndpoint(LoginEndpoint loginEndpoint) {
this.loginEndpoint = loginEndpoint;
}

/**
* Gets packages to scan.
*
Expand Down Expand Up @@ -1896,4 +1919,61 @@ public int hashCode() {
return Objects.hash(group);
}
}

/**
* The type Login endpoint.
* <p>
* These settings only take effect when the login endpoint is exposed, i.e. when
* {@code springdoc.show-login-endpoint=true}. Otherwise, they are ignored.
*/
public static class LoginEndpoint {

/**
* The example value for the username field of the login request body.
* Only applied when {@code springdoc.show-login-endpoint=true}.
*/
private String usernameExample;

/**
* The example value for the password field of the login request body.
* Only applied when {@code springdoc.show-login-endpoint=true}.
*/
private String passwordExample;

/**
* Gets username example.
*
* @return the username example
*/
public String getUsernameExample() {
return usernameExample;
}

/**
* Sets username example.
*
* @param usernameExample the username example
*/
public void setUsernameExample(String usernameExample) {
this.usernameExample = usernameExample;
}

/**
* Gets password example.
*
* @return the password example
*/
public String getPasswordExample() {
return passwordExample;
}

/**
* Sets password example.
*
* @param passwordExample the password example
*/
public void setPasswordExample(String passwordExample) {
this.passwordExample = passwordExample;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
*
* *
* * *
* * * *
* * * * *
* * * * * * Copyright 2019-2026 the original author or authors.
* * * * * *
* * * * * * Licensed under the Apache License, Version 2.0 (the "License");
* * * * * * you may not use this file except in compliance with the License.
* * * * * * You may obtain a copy of the License at
* * * * * *
* * * * * * https://www.apache.org/licenses/LICENSE-2.0
* * * * * *
* * * * * * Unless required by applicable law or agreed to in writing, software
* * * * * * distributed under the License is distributed on an "AS IS" BASIS,
* * * * * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* * * * * * See the License for the specific language governing permissions and
* * * * * * limitations under the License.
* * * *
* * *
* *
*
*/

package test.org.springdoc.api.v30.app13;

import test.org.springdoc.api.v30.AbstractSpringDocTest;
import test.org.springdoc.api.v30.app13.security.MyUserDetailsService;

import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.test.context.TestPropertySource;

@TestPropertySource(properties = {
"springdoc.show-login-endpoint=true",
"springdoc.login-endpoint.username-example=demouser",
"springdoc.login-endpoint.password-example=secret"
})
public class SpringDocApp13Test extends AbstractSpringDocTest {

@SpringBootApplication(scanBasePackages = { "test.org.springdoc.api.v30.configuration", "test.org.springdoc.api.v30.app13" })
static class SpringDocTestApp {
@Bean
MyUserDetailsService userDetailsService() {
return new MyUserDetailsService();
}
}

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
*
* *
* * *
* * * *
* * * * *
* * * * * * Copyright 2019-2026 the original author or authors.
* * * * * *
* * * * * * Licensed under the Apache License, Version 2.0 (the "License");
* * * * * * you may not use this file except in compliance with the License.
* * * * * * You may obtain a copy of the License at
* * * * * *
* * * * * * https://www.apache.org/licenses/LICENSE-2.0
* * * * * *
* * * * * * Unless required by applicable law or agreed to in writing, software
* * * * * * distributed under the License is distributed on an "AS IS" BASIS,
* * * * * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* * * * * * See the License for the specific language governing permissions and
* * * * * * limitations under the License.
* * * *
* * *
* *
*
*/

package test.org.springdoc.api.v30.app13;

import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


@Configuration
public class SpringDocConfig {


@Bean
public OpenAPI myOpenAPI() {
final String securitySchemeName = "bearerAuth";
return new OpenAPI().info(new Info().title("My MWE API")
.description("This document specifies the API")
.version("v23"))
.addSecurityItem(new SecurityRequirement().addList(securitySchemeName))
.components(new Components().addSecuritySchemes(securitySchemeName,
new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")));
}

}

Loading
Loading