Documenting Spring's login/logout API in Swagger
Solution 1
A bit late for the party, but since SpringFox relies on Spring beans for building the documentation, we can easily manipulate it. Hope this can help someone!
Register it as a bean
@Primary
@Bean
public ApiListingScanner addExtraOperations(ApiDescriptionReader apiDescriptionReader, ApiModelReader apiModelReader, DocumentationPluginsManager pluginsManager)
{
return new FormLoginOperations(apiDescriptionReader, apiModelReader, pluginsManager);
}
The class used to add any operation manually:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import com.fasterxml.classmate.TypeResolver;
import com.google.common.collect.Multimap;
import springfox.documentation.builders.ApiListingBuilder;
import springfox.documentation.builders.OperationBuilder;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.ApiDescription;
import springfox.documentation.service.ApiListing;
import springfox.documentation.service.Operation;
import springfox.documentation.spring.web.plugins.DocumentationPluginsManager;
import springfox.documentation.spring.web.readers.operation.CachingOperationNameGenerator;
import springfox.documentation.spring.web.scanners.ApiDescriptionReader;
import springfox.documentation.spring.web.scanners.ApiListingScanner;
import springfox.documentation.spring.web.scanners.ApiListingScanningContext;
import springfox.documentation.spring.web.scanners.ApiModelReader;
public class FormLoginOperations extends ApiListingScanner
{
@Autowired
private TypeResolver typeResolver;
@Autowired
public FormLoginOperations(ApiDescriptionReader apiDescriptionReader, ApiModelReader apiModelReader, DocumentationPluginsManager pluginsManager)
{
super(apiDescriptionReader, apiModelReader, pluginsManager);
}
@Override
public Multimap<String, ApiListing> scan(ApiListingScanningContext context)
{
final Multimap<String, ApiListing> def = super.scan(context);
final List<ApiDescription> apis = new LinkedList<>();
final List<Operation> operations = new ArrayList<>();
operations.add(new OperationBuilder(new CachingOperationNameGenerator())
.method(HttpMethod.POST)
.uniqueId("login")
.parameters(Arrays.asList(new ParameterBuilder()
.name("username")
.description("The username")
.parameterType("query")
.type(typeResolver.resolve(String.class))
.modelRef(new ModelRef("string"))
.build(),
new ParameterBuilder()
.name("password")
.description("The password")
.parameterType("query")
.type(typeResolver.resolve(String.class))
.modelRef(new ModelRef("string"))
.build()))
.summary("Log in") //
.notes("Here you can log in")
.build());
apis.add(new ApiDescription("/api/login/", "Authentication documentation", operations, false));
def.put("authentication", new ApiListingBuilder(context.getDocumentationContext().getApiDescriptionOrdering())
.apis(apis)
.description("Custom authentication")
.build());
return def;
}
}
Rendering Swagger json:
"/api/login/" : {
"post" : {
"summary" : "Log in",
"description" : "Here you can log in",
"operationId" : "loginUsingPOST",
"parameters" : [ {
"name" : "username",
"in" : "query",
"description" : "The username",
"required" : false,
"type" : "string"
}, {
"name" : "password",
"in" : "query",
"description" : "The password",
"required" : false,
"type" : "string"
} ]
}
}
Solution 2
You can add a fake login and logout method in your API just to generate the Swagger documentation, it'll be automatically overriden by Spring Security filters.
@ApiOperation("Login.")
@PostMapping("/login")
public void fakeLogin(@ApiParam("User") @RequestParam String email, @ApiParam("Password") @RequestParam String password) {
throw new IllegalStateException("This method shouldn't be called. It's implemented by Spring Security filters.");
}
@ApiOperation("Logout.")
@PostMapping("/logout")
public void fakeLogout() {
throw new IllegalStateException("This method shouldn't be called. It's implemented by Spring Security filters.");
}
Solution 3
Just adding a little correction. If you want to make real POST-request (through HTML page of swagger-ui for example), you need to make little changes to Morten's answer.
Morten's code makes POST request to /login like this:
http://<hostname>/api/login?username=<user>&password=<password>
But if you want to make a POST request you need to pass a body with it, not just query parameters.
To make that happen, you need to add parameter with name body
and parameter type body
like this:
@Override
public Multimap<String, ApiListing> scan(ApiListingScanningContext context)
{
final Multimap<String, ApiListing> def = super.scan(context);
final List<ApiDescription> apis = new LinkedList<>();
final List<Operation> operations = new ArrayList<>();
operations.add(new OperationBuilder(new CachingOperationNameGenerator())
.method(HttpMethod.POST)
.uniqueId("login")
.parameters(Arrays.asList(new ParameterBuilder()
.name("body")
.required(true)
.description("The body of request")
.parameterType("body")
.type(typeResolver.resolve(String.class))
.modelRef(new ModelRef("string"))
.build()))
.summary("Log in") //
.notes("Here you can log in")
.build());
apis.add(new ApiDescription("/api/login/", "Authentication documentation", operations, false));
def.put("authentication", new ApiListingBuilder(context.getDocumentationContext().getApiDescriptionOrdering())
.apis(apis)
.description("Custom authentication")
.build());
return def;
}
Now we can pass a body with our POST request. A body could be JSON, for example:
{"username":"admin","password":"admin"}
Solution 4
You can use an interface describing the authentication API. The acutal implementation is provided by Spring Security. (This a variation of Italo's answer, where an interface is used instead of a fake implementation.)
/**
* Authentication API specification for Swagger documentation and Code Generation.
* Implemented by Spring Security.
*/
@Api("Authentication")
@RequestMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE)
public interface AuthApi {
/**
* Implemented by Spring Security
*/
@ApiOperation(value = "Login", notes = "Login with the given credentials.")
@ApiResponses({@ApiResponse(code = 200, message = "", response = Authentication.class)})
@RequestMapping(value = "/login", method = RequestMethod.POST)
default void login(
@RequestParam("username") String username,
@RequestParam("password") String password
) {
throw new IllegalStateException("Add Spring Security to handle authentication");
}
/**
* Implemented by Spring Security
*/
@ApiOperation(value = "Logout", notes = "Logout the current user.")
@ApiResponses({@ApiResponse(code = 200, message = "")})
@RequestMapping(value = "/logout", method = RequestMethod.POST)
default void logout() {
throw new IllegalStateException("Add Spring Security to handle authentication");
}
}
Comments
-
Maciej Dobrowolski about 4 years
I am developing demo REST service using
Spring Boot
where user has to login in order to to perform certain subset of operations. After addingSwagger UI
(usingspringfox
library) with that simple configuration:@Bean public Docket docApi() { return new Docket(DocumentationType.SWAGGER_2) .select() .apis(any()) .paths(PathSelectors.ant("/api/**")) .build() .pathMapping("/") .apiInfo(apiInfo()) .directModelSubstitute(LocalDate.class, String.class) .useDefaultResponseMessages(true) .enableUrlTemplating(true); }
I end up with all apis with all operations listed on
Swagger UI
page. Unfortunately I don't have login/logout endpoints listed among them.The problem is that part of that operations cannot be performed via
Swagger UI
built-in form (I find it really nice feature and would like make it work), because user is not logged in. Is there any solution to that problem? Can I define manually some endpoints inSwagger
?If there was a form to submit credentials (i.e. login/logout endpoints) I could perform authorization before using that secured endpoints. Then,
Swagger
user could extracttoken/sessionid
from response and paste it to custom query parameter defined via@ApiImplicitParams
.Below you can find my security configuration:
@Override protected void configure(HttpSecurity http) throws Exception { http .formLogin() .loginProcessingUrl("/api/login") .usernameParameter("username") .passwordParameter("password") .successHandler(new CustomAuthenticationSuccessHandler()) .failureHandler(new CustomAuthenticationFailureHandler()) .permitAll() .and() .logout() .logoutUrl("/api/logout") .logoutSuccessHandler(new CustomLogoutSuccessHandler()) .deleteCookies("JSESSIONID") .permitAll() .and() .csrf() .disable() .exceptionHandling() .authenticationEntryPoint(new CustomAuthenticationEntryPoint()) .and() .authorizeRequests() .and() .headers() .frameOptions() .disable(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .userDetailsService(userDetailsService) .passwordEncoder(passwordEncoder()); }
-
Dimon almost 6 yearsThank You very match! Bless You!
-
HungUnicorn almost 3 yearsThis will be my choice. But I don't see anything unless putting @Controller and also change the interface to public class AuthApi. Any idea?
-
Rajeev over 2 yearsi am also getting the same issue. any resolution?
-
testing_22 almost 2 yearsWhere should I put this code?