EDIT: I'm using Swagger UI 2.5.0 and attempting to configure it to use oauth authentication. From what I understand from looking at the petstore example and other pieces I've read if I include a security schema & context in my Docket it should automatically display the button - is this the case? If so what else am I missing here?
My API's appear fine in the swagger UI - it's just that the authorize button (and therefore any means of authorization) is missing
#Configuration
#EnableSwagger2
public class SwaggerConfig {
#Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.groupName("api")
.select()
.apis(RequestHandlerSelectors.basePackage("com.example"))
.paths(PathSelectors.regex("/api.*"))
.build()
.securitySchemes(newArrayList(securitySchema()))
.securityContexts(newArrayList(securityContext()));
}
public static final String securitySchemaOAuth2 = "oauth2schema";
public static final String authorizationScopeGlobal = "global";
public static final String authorizationScopeGlobalDesc ="accessEverything";
private OAuth securitySchema() {
AuthorizationScope authorizationScope = new AuthorizationScope(authorizationScopeGlobal, authorizationScopeGlobal);
LoginEndpoint loginEndpoint = new LoginEndpoint("http://localhost:9999/sso/login");
GrantType grantType = new ImplicitGrant(loginEndpoint, "access_token");
return new OAuth(securitySchemaOAuth2, newArrayList(authorizationScope), newArrayList(grantType));
}
private SecurityContext securityContext() {
return SecurityContext.builder()
.securityReferences(defaultAuth())
.forPaths(PathSelectors.regex("/api.*"))
.build();
}
private List<SecurityReference> defaultAuth() {
AuthorizationScope authorizationScope
= new AuthorizationScope(authorizationScopeGlobal, authorizationScopeGlobalDesc);
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = authorizationScope;
return newArrayList(
new SecurityReference(securitySchemaOAuth2, authorizationScopes));
}
Related
I'm using springdoc-openapi-ui 1.6.14
I have following class
#Configuration
public class GroupsConfig {
private final PropertyResolver propertyResolver;
public GroupsConfig(PropertyResolver propertyResolver) {
this.propertyResolver = propertyResolver;
}
#Bean
public GroupedOpenApi adminApi() {
return GroupedOpenApi.builder()
.group("admin")
.pathsToMatch("/admin/**")
.build();
}
#Bean
public GroupedOpenApi externalApi() {
return GroupedOpenApi.builder()
.group("external")
.pathsToMatch("/external/**")
.build();
}
#Bean
public GroupedOpenApi clientApi() {
return GroupedOpenApi.builder()
.group("client")
.pathsToMatch("/client/**")
.build();
}
#Bean
public GroupedOpenApi externalClientApi() {
return GroupedOpenApi.builder()
.group("extclient")
.pathsToMatch("/extclient/**")
.build();
}
#Bean
public OpenAPI apiInfo() {
String title = propertyResolver.getRequiredProperty(SwaggerPropertyKey.API_TITLE);
String description = propertyResolver.getRequiredProperty(SwaggerPropertyKey.API_DESCRIPTION);
String version = propertyResolver.getRequiredProperty(SwaggerPropertyKey.API_VERSION);
String contactName = propertyResolver.getRequiredProperty(SwaggerPropertyKey.API_CONTACT_NAME);
String contactUrl = propertyResolver.getRequiredProperty(SwaggerPropertyKey.API_CONTACT_URL);
String contactEmail = propertyResolver.getRequiredProperty(SwaggerPropertyKey.API_CONTACT_EMAIL);
String termsOfServiceUrl = propertyResolver.getRequiredProperty(SwaggerPropertyKey.API_TERMS_OF_SERVICE_URL);
String licence = propertyResolver.getRequiredProperty(SwaggerPropertyKey.API_LICENCE);
String licenceUrl = propertyResolver.getRequiredProperty(SwaggerPropertyKey.API_LICENCE_URL);
Contact contact = new Contact()
.name(contactName)
.url(contactUrl)
.email(contactEmail);
return new OpenAPI()
.info(new Info().title(title)
.description(description)
.version(version)
.license(new License().name(licence).url(licenceUrl))
.contact(contact)
.termsOfService(termsOfServiceUrl))
.components(new Components());
}
}
The OpenAPI info is working correctly and displayed in the UI.
I then have follewing class to import all Springdoc configurations manually
#Configuration
#Import({org.springdoc.core.SpringDocConfigProperties.class,
org.springdoc.webmvc.core.MultipleOpenApiSupportConfiguration.class,
org.springdoc.core.SpringDocConfiguration.class, org.springdoc.webmvc.core.SpringDocWebMvcConfiguration.class,
SwaggerUiConfigParameters.class, SwaggerUiOAuthProperties.class,
org.springdoc.core.SwaggerUiConfigProperties.class, org.springdoc.core.SwaggerUiOAuthProperties.class,
org.springdoc.webmvc.ui.SwaggerConfig.class, GroupsConfig.class,
org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration.class})
public class SwaggerConfig {
}
If I go to /v3/api-docs, I get a giant JSON with all the different paths in my application. If I go to /v3/api-docs/admin, I get a 404. So the GroupedOpenApi beans are not getting picked up by Springdoc.
Anyone having the same issue or an idea how to fix this?
Thanks in advance!
Edit: I just tried it with 1.4.4 and it works. What should I do to get it working with the newest version?
I run the following configuration to enable basic authentication for my (non-spring boot) project
#Configuration
#EnableOpenApi
#EnableWebMvc
public class SpringFoxConfig implements WebMvcConfigurer {
#Bean
public Docket api() {
return new Docket(DocumentationType.OAS_30)
.select()
.apis(RequestHandlerSelectors.withClassAnnotation(PublicAPI.class))
.paths(PathSelectors.any())
.build()
.securityContexts(Arrays.asList(securityContext()))
.securitySchemes(Arrays.asList(securityScheme())));
}
private SecurityScheme securityScheme() {
return new HttpAuthenticationBuilder()
.name("basic")
.scheme("basic")
.build();
}
private SecurityContext securityContext() {
return SecurityContext
.builder()
.securityReferences(securityReferences())
.operationSelector(operationContext -> true)
.build();
}
private List<SecurityReference> securityReferences() {
return singletonList(new SecurityReference("Authorization", new AuthorizationScope[] {new AuthorizationScope("global", "global")}));
}
}
this allows me to authorize my requests
but when testing the call, the authorization header isn't built nor sent with the request:
curl -X GET "https://localhost:8443/foo/rest/ws/info/get-master/code/awd" -H "accept: application/json"
Security reference name was different from the one defined in the securityScheme
private SecurityScheme securityScheme() {
return new HttpAuthenticationBuilder()
.name("basic")
.scheme("basic")
.build();
}
private SecurityContext securityContext() {
return SecurityContext.builder()
.securityReferences(defaultAuth())
.forPaths(PathSelectors.any())
.build();
}
private List<SecurityReference> defaultAuth() {
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = new AuthorizationScope("global", "accessEverything");
return singletonList(new SecurityReference("basic", authorizationScopes));
}
I have created a Spring Boot 2 Application, integrated SpringFox Swagger 2.8.0 with Implicit Oauth2 Grant for Authentication and Authorization.
The Code is working fine but when I click Authorize button it redirects to the
http://localhost:8080/oauth/authorize?response_type=token&client_id=test-app-client-id&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fwebjars%2Fspringfox-swagger-ui%2Foauth2-redirect.html&scope=read&state=U3VuIE9jdCAxNCAyMDE4IDIwOjQyOjUwIEdNVCswNTMwIChJbmRpYSBTdGFuZGFyZCBUaW1lKQ%3D%3D
but shows Access Denied like as shown below.
My complete project is available in GitHub
MainApplication.java
#EnableSwagger2
#SpringBootApplication
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
#RestController
public class MainApplication /*extends WebMvcConfigurerAdapter*/
{
public static void main(String[] args)
{
SpringApplication.run(MainApplication.class, args);
}
#RequestMapping("/user")
public Principal user(Principal user) {
return user;
}
#Bean
SecurityConfiguration security() {
return SecurityConfigurationBuilder.builder()//<19>
.clientId("test-app-client-id")
.build();
}
#Bean
SecurityScheme oauth() {
List<GrantType> grantTypes = new ArrayList<>();
ImplicitGrant implicitGrant = new ImplicitGrant(new LoginEndpoint("http://localhost:8080/oauth/authorize"),"access_code");
grantTypes.add(implicitGrant);
List<AuthorizationScope> scopes = new ArrayList<>();
scopes.add(new AuthorizationScope("read","Read access on the API"));
return new OAuthBuilder()
.name("SECURITY_SCHEME_OAUTH2")
.grantTypes(grantTypes)
.scopes(scopes)
.build();
}
#Bean
public Docket docket()
{
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage(getClass().getPackage().getName()))
.paths(PathSelectors.any())
.build()
.securitySchemes(Collections.singletonList(oauth()))
.apiInfo(generateApiInfo());
}
private ApiInfo generateApiInfo()
{
return new ApiInfo("Sample Service", "This service is to check Sample Service.", "Version 1.0",
"Sample Service", "123#test.com", "Apache 2.0", "http://www.apache.org/licenses/LICENSE-2.0");
}
}
Update 1
I have added the security and the passwordencoder configure suggested from #AlexanderPetrov. Things are working fine, when I add #EnableResourceServer my login screen is showing Full authentication is required to access this resource like as shown below
Can anyone please help me on this
You need to do the following changes in your code
Form login configuration is necessary for implicit flow.
Also if we use implicit flow token will be generated through authorization url instead of token url. So you need to change "/oauth/token" to "oauth/authorize". configure method below
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/oauth/authorize").authenticated()
.and()
.authorizeRequests().anyRequest().permitAll()
.and()
.formLogin().permitAll()
.and()
.csrf().disable();
}
Add password encoder in SecurityConfig class and invoke it to encode user password in globalUserDetails method. Encoder is necessary because you use in memory passwords. So without password encoder application fails with an error:
Encoded password does not look like BCrypt
Code fragment below
#Autowired
public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
PasswordEncoder passwordEncoder = passwordEncoder();
auth.inMemoryAuthentication().passwordEncoder(passwordEncoder()).
withUser("bill").password(passwordEncoder.encode("abc123")).roles("ADMIN").and()
.withUser("$2a$10$TT7USzDvMxMZvf0HUVh9p.er1GGnjNQzlcGivj8CivnaZf9edaz6C")
.password("$2a$10$TT7USzDvMxMZvf0HUVh9p.er1GGnjNQzlcGivj8CivnaZf9edaz6C").roles("USER");
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
Hope it helps. I've created branch for your project but couldn't push it because of 403. So all necessary code is here in my answer.
When you enable a resource server, you need to configure the check_token URL, so that it can reach the OAuth2 authorization server and validate the given access_token.
You could do something like:
#Configuration
#EnableResourceServer
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class OAuth2ResourceServerConfig extends GlobalMethodSecurityConfiguration {
#Value("${oauth.url.internal}") // e.g. http://localhost:8082/oauth
private String oauthUrl;
#Value("${oauth.client}")
private String oauthClient;
#Value("${oauth.secret}")
private String oauthSecret;
#Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
return new OAuth2MethodSecurityExpressionHandler();
}
#Primary
#Bean
public RemoteTokenServices tokenService() {
RemoteTokenServices tokenService = new RemoteTokenServices();
tokenService.setCheckTokenEndpointUrl(oauthUrl + "/check_token");
tokenService.setClientId(oauthClient);
tokenService.setClientSecret(oauthSecret);
return tokenService;
}
}
Besides this, you may want to ignore Swagger-specific endpoints:
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/v2/api-docs", "/configuration/ui", "/swagger-resources", "/configuration/security", "/swagger-ui.html", "/webjars/**");
}
}
Just in case, this is the class I implemented for Swagger w/ OAuth2 authorization:
#EnableSwagger2
#Configuration
public class SwaggerConfig implements WebMvcConfigurer {
private static final String BASE_PACKAGE = "com.somepackage.api";
#Value("${oauth.url}") // Make sure this is an external URL, i.e. accessible from Swagger UI
private String oauthUrl;
#Value("${swagger.scopes}")
private String swaggerScopes;
#Value("${swagger.urls}")
private String swaggerUrls; // Your v2/api-docs URL accessible from the UI
#Bean
public Docket api(){
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage(BASE_PACKAGE))
.apis(RequestHandlerSelectors.any())
.paths(PathSelectors.any())
.build()
.securitySchemes(Collections.singletonList(securitySchema()))
.securityContexts(Collections.singletonList(securityContext()));
}
private OAuth securitySchema() {
List<AuthorizationScope> authorizationScopeList = new ArrayList<>();
authorizationScopeList.add(new AuthorizationScope(swaggerScopes, ""));
List<GrantType> grantTypes = new ArrayList<>();
GrantType creGrant = new ResourceOwnerPasswordCredentialsGrant(oauthUrl + "/token");
grantTypes.add(creGrant);
return new OAuth("oauth2schema", authorizationScopeList, grantTypes);
}
private SecurityContext securityContext() {
return SecurityContext.builder().securityReferences(defaultAuth()).forPaths(PathSelectors.ant(swaggerUrls)).build();
}
private List<SecurityReference> defaultAuth() {
final AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = new AuthorizationScope(swaggerScopes, "");
return Collections.singletonList(new SecurityReference("oauth2schema", authorizationScopes));
}
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}
}
Versions:
springSecurityVersion = '2.0.5.RELEASE'
swaggerVersion = '2.8.0'
springBootVersion = '2.0.5.RELEASE'
I have configured swagger to use login / password as followed:
#Configuration
#EnableSwagger2
public class SwaggerConfiguration {
#Bean
public Docket SwaggerApi() {
return new Docket(DocumentationType.SWAGGER_2)
.groupName("cms")
.select().apis(RequestHandlerSelectors.any())
.paths(PathSelectors.any())
.build()
.securitySchemes(Collections.singletonList(securitySchema()))
.securityContexts(Collections.singletonList(securityContext())).pathMapping("/")
.useDefaultResponseMessages(false)
.apiInfo(apiInfo());
}
private SecurityContext securityContext() {
return SecurityContext.builder().securityReferences(defaultAuth()).forPaths(PathSelectors.ant("/**"))
.build();
}
private List<SecurityReference> defaultAuth() {
final AuthorizationScope[] authorizationScopes = new AuthorizationScope[3];
authorizationScopes[0] = new AuthorizationScope("read", "read all");
authorizationScopes[1] = new AuthorizationScope("trust", "trust all");
authorizationScopes[2] = new AuthorizationScope("write", "write all");
return Collections.singletonList(new SecurityReference("oauth2schema", authorizationScopes));
}
#Bean
public SecurityConfiguration securityInfo() {
return new SecurityConfiguration("app", "app-secret", "", "", "", ApiKeyVehicle.HEADER, "", " ");
}
private OAuth securitySchema() {
List<AuthorizationScope> authorizationScopeList = new ArrayList<>();
authorizationScopeList.add(new AuthorizationScope("read", "read all"));
authorizationScopeList.add(new AuthorizationScope("trust", "trust all"));
authorizationScopeList.add(new AuthorizationScope("write", "access all"));
List<GrantType> grantTypes = new ArrayList<>();
GrantType creGrant = new ResourceOwnerPasswordCredentialsGrant("http://localhost/swaggerAuth");
grantTypes.add(creGrant);
return new OAuth("oauth2schema", authorizationScopeList, grantTypes);
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("Application")
.version("1.0")
.build();
}
And this is my method for autorization:
#RequestMapping(value = "/swaggerAuth", method = RequestMethod.POST,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces = {MediaType.APPLICATION_ATOM_XML_VALUE, MediaType.APPLICATION_JSON_VALUE})
public ResponseEntity<?> authenticate(#RequestBody MultiValueMap<String, String> formData) {
String username = formData.get("username").get(0);
String password = formData.get("password").get(0);
final Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(username, password)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
final UserDetails userDetails = userDetailsService.loadUserByUsername(username );
return jwtTokenUtil.generateToken(userDetails);
}
When I login via Swagger all is fine. User gets authneticated and Authentication is set to SecurityContextHolder.
But with every next swagger request the user is anonymousUser and not the one I authenticated with.
What is wrong with my configuration?
EDIT:
The token I return from authorization controller is not beeing sent in swagger request headers...
The problem was that in authenticate() method I was returning plain string. Instead I should return an object with access_token string field:
public class SwaggerAuthenticationResponse {
private final String access_token;
public SwaggerAuthenticationResponse(String access_token) {
this.access_token = access_token;
}
public String getAccess_token() {
return this.access_token;
}
}
I have the AuthorizationServer. Besides standard functions i have controller who let to create user. After successful user creates the method must to return token for this user. The problem is that the method return valid token only at first call. At next calls - following users will get the first user's token. I tried to set scope(request) for restTemplate - but obtained the error: " Scope 'request' is not active for the current thread"
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
#Override
public void configure(ClientDetailsServiceConfigurer configurer) throws Exception {
...
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
...
}
protected ResourceOwnerPasswordResourceDetails getOwnerPasswordResource(){
ResourceOwnerPasswordResourceDetails resource = new ResourceOwnerPasswordResourceDetails();
List scopes = new ArrayList<String>(3);
scopes.add(SCOPE_READ);
scopes.add(SCOPE_WRITE);
scopes.add(SCOPE_TRUST);
resource.setAccessTokenUri(tokenUrl);
resource.setClientId(CLIENT_ID);
resource.setClientSecret(CLIENT_SECRET_UNCODED);
resource.setGrantType(GRANT_TYPE_PASSWORD);
resource.setScope(scopes);
return resource;
}
}
Here the OAuth2Client:
#EnableOAuth2Client
#Configuration
public class ClientConfig {
#Autowired
AuthorizationServerConfig authorizationServerConfig;
#Bean
//#Scope("request")
public OAuth2RestOperations restTemplate() {
AccessTokenRequest atr = new DefaultAccessTokenRequest();
return new OAuth2RestTemplate(authorizationServerConfig.getOwnerPasswordResource(), new DefaultOAuth2ClientContext(atr));
}
}
And my controller:
#RestController
public class UserRestController {
#Autowired
private OAuth2RestOperations restTemplate;
#PostMapping("/user")
public OAuth2AccessToken createUserCredential(#RequestBody UserCredential user) {
user.validate();
userCredentialService.checkAndSaveUser(user, getClientIp(request));
restTemplate.getOAuth2ClientContext().getAccessTokenRequest().set("username", user.getLogin());
restTemplate.getOAuth2ClientContext().getAccessTokenRequest().set("password", user.getPassword);
return restTemplate.getAccessToken();
}
}
May be exists more correct way to obtain token inside of AuthorizationServer ?
I thought have some special way.. but not found it. And solved problem on following way
#EnableOAuth2Client
#Configuration
public class OAuthClientConfig {
#Autowired
AuthorizationServerConfig authorizationServerConfig;
public OAuth2RestOperations restTemplate() {
AccessTokenRequest atr = new DefaultAccessTokenRequest();
return new OAuth2RestTemplate(authorizationServerConfig.getOwnerPasswordResource(), new DefaultOAuth2ClientContext(atr));
}
}
And my controller:
#RestController
public class UserRestController {
#Autowired
private OAuthClientConfig oAuthClientConfig;
#PostMapping("/user")
public OAuth2AccessToken createUserCredential(#RequestBody UserCredential user) {
user.validate();
userCredentialService.checkAndSaveUser(user, getClientIp(request));
OAuth2RestOperations restTemplate = oAuthClientConfig.restTemplate();
restTemplate.getOAuth2ClientContext().getAccessTokenRequest().set("username", user.getLogin());
restTemplate.getOAuth2ClientContext().getAccessTokenRequest().set("password", user.getPassword);
return restTemplate.getAccessToken();
}
}
May be it will help to someone
I was facing the same issue I found this other way to make it work
#Bean
#Primary
#Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public OAuth2RestTemplate oauth2RestTemplate(OAuth2ClientContext context,
OAuth2ProtectedResourceDetails details) {
AccessTokenRequest atr = new DefaultAccessTokenRequest();
OAuth2RestTemplate template = new OAuth2RestTemplate(resource(), new DefaultOAuth2ClientContext(atr));
AccessTokenProvider accessTokenProvider = new AccessTokenProviderChain(Arrays.<AccessTokenProvider>asList(
new AuthorizationCodeAccessTokenProvider(), new ImplicitAccessTokenProvider(),
new ResourceOwnerPasswordAccessTokenProvider(), new ClientCredentialsAccessTokenProvider()));
template.setAccessTokenProvider(accessTokenProvider);
return template;
}
And then I just did the injection
private final OAuth2RestTemplate oauth2RestTemplate;
#GetMapping(path = "/token")
public String token(Credentials credentials) {
oauth2RestTemplate.getOAuth2ClientContext()
.getAccessTokenRequest().add("username", credentials.getEmail());
oauth2RestTemplate.getOAuth2ClientContext()
.getAccessTokenRequest().add("password", credentials.getPass());
final OAuth2AccessToken accessToken = oauth2RestTemplate.getAccessToken();
final String accessTokenAsString = accessToken.getValue();
return accessTokenAsString ;
}