mirror of
				https://github.com/spring-projects/spring-security.git
				synced 2025-10-30 22:28:46 +00:00 
			
		
		
		
	issue/6506: AuthenticationConverter implementation
This commit is contained in:
		
							parent
							
								
									e584207a85
								
							
						
					
					
						commit
						f1187bdfc2
					
				| @ -0,0 +1,40 @@ | ||||
| /* | ||||
|  * Copyright 2002-2019 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 org.springframework.security.web.authentication; | ||||
| 
 | ||||
| import javax.servlet.http.HttpServletRequest; | ||||
| 
 | ||||
| import org.springframework.security.authentication.AuthenticationManager; | ||||
| import org.springframework.security.core.Authentication; | ||||
| import org.springframework.security.core.AuthenticationException; | ||||
| 
 | ||||
| /** | ||||
|  * A strategy used for converting from a {@link HttpServletRequest} to an | ||||
|  * {@link Authentication} of particular type. Used to authenticate with | ||||
|  * appropriate {@link AuthenticationManager}. If the result is null, then it | ||||
|  * signals that no authentication attempt should be made. It is also possible to | ||||
|  * throw {@link AuthenticationException} within the | ||||
|  * {@link #convert(HttpServletRequest)} if there was invalid Authentication | ||||
|  * scheme value. | ||||
|  * | ||||
|  * @author Sergey Bespalov | ||||
|  * @since 5.2.0 | ||||
|  */ | ||||
| public interface AuthenticationConverter { | ||||
| 
 | ||||
| 	Authentication convert(HttpServletRequest request); | ||||
| 
 | ||||
| } | ||||
| @ -0,0 +1,48 @@ | ||||
| /* | ||||
|  * Copyright 2002-2019 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 org.springframework.security.web.authentication; | ||||
| 
 | ||||
| import java.io.IOException; | ||||
| 
 | ||||
| import javax.servlet.ServletException; | ||||
| import javax.servlet.http.HttpServletRequest; | ||||
| import javax.servlet.http.HttpServletResponse; | ||||
| 
 | ||||
| import org.springframework.security.core.AuthenticationException; | ||||
| import org.springframework.security.web.AuthenticationEntryPoint; | ||||
| import org.springframework.util.Assert; | ||||
| 
 | ||||
| /** | ||||
|  * Adapts a {@link AuthenticationEntryPoint} into a {@link AuthenticationFailureHandler} | ||||
|  * | ||||
|  * @author sbespalov | ||||
|  * @since 5.2.0 | ||||
|  */ | ||||
| public class AuthenticationEntryPointFailureHandler implements AuthenticationFailureHandler { | ||||
| 
 | ||||
| 	private final AuthenticationEntryPoint authenticationEntryPoint; | ||||
| 
 | ||||
| 	public AuthenticationEntryPointFailureHandler(AuthenticationEntryPoint authenticationEntryPoint) { | ||||
| 		Assert.notNull(authenticationEntryPoint, "authenticationEntryPoint cannot be null"); | ||||
| 		this.authenticationEntryPoint = authenticationEntryPoint; | ||||
| 	} | ||||
| 
 | ||||
| 	@Override | ||||
| 	public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, | ||||
| 			AuthenticationException exception) throws IOException, ServletException { | ||||
| 		this.authenticationEntryPoint.commence(request, response, exception); | ||||
| 	} | ||||
| } | ||||
| @ -0,0 +1,186 @@ | ||||
| /* | ||||
|  * Copyright 2002-2019 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 org.springframework.security.web.authentication; | ||||
| 
 | ||||
| import java.io.IOException; | ||||
| 
 | ||||
| import javax.servlet.Filter; | ||||
| import javax.servlet.FilterChain; | ||||
| import javax.servlet.ServletException; | ||||
| import javax.servlet.http.HttpServletRequest; | ||||
| import javax.servlet.http.HttpServletResponse; | ||||
| 
 | ||||
| import org.springframework.http.HttpStatus; | ||||
| import org.springframework.security.authentication.AuthenticationManager; | ||||
| import org.springframework.security.authentication.AuthenticationManagerResolver; | ||||
| import org.springframework.security.core.Authentication; | ||||
| import org.springframework.security.core.AuthenticationException; | ||||
| import org.springframework.security.core.context.SecurityContext; | ||||
| import org.springframework.security.core.context.SecurityContextHolder; | ||||
| import org.springframework.security.web.util.matcher.AnyRequestMatcher; | ||||
| import org.springframework.security.web.util.matcher.RequestMatcher; | ||||
| import org.springframework.util.Assert; | ||||
| import org.springframework.web.filter.OncePerRequestFilter; | ||||
| 
 | ||||
| /** | ||||
|  * A {@link Filter} that performs authentication of a particular request. An | ||||
|  * outline of the logic: | ||||
|  * | ||||
|  * <ul> | ||||
|  * <li>A request comes in and if it does not match | ||||
|  * {@link #setRequestMatcher(RequestMatcher)}, then this filter does nothing and | ||||
|  * the {@link FilterChain} is continued. If it does match then...</li> | ||||
|  * <li>An attempt to convert the {@link HttpServletRequest} into an | ||||
|  * {@link Authentication} is made. If the result is empty, then the filter does | ||||
|  * nothing more and the {@link FilterChain} is continued. If it does create an | ||||
|  * {@link Authentication}...</li> | ||||
|  * <li>The {@link AuthenticationManager} specified in | ||||
|  * {@link #GenericAuthenticationFilter(AuthenticationManager)} is used to | ||||
|  * perform authentication.</li> | ||||
|  * <li>The {@link AuthenticationManagerResolver} specified in | ||||
|  * {@link #GenericAuthenticationFilter(AuthenticationManagerResolver)} is used | ||||
|  * to resolve the appropriate authentication manager from context to perform | ||||
|  * authentication.</li> | ||||
|  * <li>If authentication is successful, {@link AuthenticationSuccessHandler} is | ||||
|  * invoked and the authentication is set on {@link SecurityContextHolder}, else | ||||
|  * {@link AuthenticationFailureHandler} is invoked</li> | ||||
|  * </ul> | ||||
|  * | ||||
|  * @author Sergey Bespalov | ||||
|  * @since 5.2.0 | ||||
|  */ | ||||
| public class AuthenticationFilter extends OncePerRequestFilter { | ||||
| 
 | ||||
| 	private RequestMatcher requestMatcher = AnyRequestMatcher.INSTANCE; | ||||
| 	private AuthenticationConverter authenticationConverter; | ||||
| 	private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler(); | ||||
| 	private AuthenticationFailureHandler failureHandler = new AuthenticationEntryPointFailureHandler( | ||||
| 			new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)); | ||||
| 	private AuthenticationManagerResolver<HttpServletRequest> authenticationManagerResolver; | ||||
| 
 | ||||
| 	public AuthenticationFilter(AuthenticationManager authenticationManager, | ||||
| 			AuthenticationConverter authenticationConverter) { | ||||
| 		this((AuthenticationManagerResolver<HttpServletRequest>) r -> authenticationManager, authenticationConverter); | ||||
| 	} | ||||
| 
 | ||||
| 	public AuthenticationFilter(AuthenticationManagerResolver<HttpServletRequest> authenticationManagerResolver, | ||||
| 			AuthenticationConverter authenticationConverter) { | ||||
| 		Assert.notNull(authenticationManagerResolver, "authenticationResolverManager cannot be null"); | ||||
| 		Assert.notNull(authenticationConverter, "authenticationConverter cannot be null"); | ||||
| 
 | ||||
| 		this.authenticationManagerResolver = authenticationManagerResolver; | ||||
| 		this.authenticationConverter = authenticationConverter; | ||||
| 	} | ||||
| 
 | ||||
| 	public RequestMatcher getRequestMatcher() { | ||||
| 		return requestMatcher; | ||||
| 	} | ||||
| 
 | ||||
| 	public void setRequestMatcher(RequestMatcher requestMatcher) { | ||||
| 		Assert.notNull(requestMatcher, "requestMatcher cannot be null"); | ||||
| 		this.requestMatcher = requestMatcher; | ||||
| 	} | ||||
| 
 | ||||
| 	public AuthenticationConverter getAuthenticationConverter() { | ||||
| 		return authenticationConverter; | ||||
| 	} | ||||
| 
 | ||||
| 	public void setAuthenticationConverter(AuthenticationConverter authenticationConverter) { | ||||
| 		Assert.notNull(authenticationConverter, "authenticationConverter cannot be null"); | ||||
| 		this.authenticationConverter = authenticationConverter; | ||||
| 	} | ||||
| 
 | ||||
| 	public AuthenticationSuccessHandler getSuccessHandler() { | ||||
| 		return successHandler; | ||||
| 	} | ||||
| 
 | ||||
| 	public void setSuccessHandler(AuthenticationSuccessHandler successHandler) { | ||||
| 		Assert.notNull(successHandler, "successHandler cannot be null"); | ||||
| 		this.successHandler = successHandler; | ||||
| 	} | ||||
| 
 | ||||
| 	public AuthenticationFailureHandler getFailureHandler() { | ||||
| 		return failureHandler; | ||||
| 	} | ||||
| 
 | ||||
| 	public void setFailureHandler(AuthenticationFailureHandler failureHandler) { | ||||
| 		Assert.notNull(failureHandler, "failureHandler cannot be null"); | ||||
| 		this.failureHandler = failureHandler; | ||||
| 	} | ||||
| 
 | ||||
| 	public AuthenticationManagerResolver<HttpServletRequest> getAuthenticationManagerResolver() { | ||||
| 		return authenticationManagerResolver; | ||||
| 	} | ||||
| 
 | ||||
| 	public void setAuthenticationManagerResolver( | ||||
| 			AuthenticationManagerResolver<HttpServletRequest> authenticationManagerResolver) { | ||||
| 		Assert.notNull(authenticationManagerResolver, "authenticationManagerResolver cannot be null"); | ||||
| 		this.authenticationManagerResolver = authenticationManagerResolver; | ||||
| 	} | ||||
| 
 | ||||
| 	@Override | ||||
| 	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) | ||||
| 			throws ServletException, IOException { | ||||
| 		if (!requestMatcher.matches(request)) { | ||||
| 			filterChain.doFilter(request, response); | ||||
| 			return; | ||||
| 		} | ||||
| 
 | ||||
| 		try { | ||||
| 			Authentication authenticationResult = attemptAuthentication(request, response); | ||||
| 			if (authenticationResult == null) { | ||||
| 				filterChain.doFilter(request, response); | ||||
| 				return; | ||||
| 			} | ||||
| 
 | ||||
| 			successfulAuthentication(request, response, filterChain, authenticationResult); | ||||
| 		} catch (AuthenticationException e) { | ||||
| 			unsuccessfulAuthentication(request, response, e); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	private void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, | ||||
| 			AuthenticationException failed) throws IOException, ServletException { | ||||
| 		SecurityContextHolder.clearContext(); | ||||
| 		failureHandler.onAuthenticationFailure(request, response, failed); | ||||
| 	} | ||||
| 
 | ||||
| 	protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, | ||||
| 			Authentication authentication) throws IOException, ServletException { | ||||
| 		SecurityContext context = SecurityContextHolder.createEmptyContext(); | ||||
| 		context.setAuthentication(authentication); | ||||
| 		SecurityContextHolder.setContext(context); | ||||
| 
 | ||||
| 		successHandler.onAuthenticationSuccess(request, response, chain, authentication); | ||||
| 	} | ||||
| 
 | ||||
| 	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) | ||||
| 			throws AuthenticationException, IOException, ServletException { | ||||
| 		Authentication authentication = authenticationConverter.convert(request); | ||||
| 		if (authentication == null) { | ||||
| 			return null; | ||||
| 		} | ||||
| 
 | ||||
| 		AuthenticationManager authenticationManager = authenticationManagerResolver.resolve(request); | ||||
| 		Authentication authenticationResult = authenticationManager.authenticate(authentication); | ||||
| 		if (authenticationResult == null) { | ||||
| 			throw new ServletException("AuthenticationManager should not return null Authentication object."); | ||||
| 		} | ||||
| 
 | ||||
| 		return authenticationResult; | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
| @ -17,6 +17,7 @@ package org.springframework.security.web.authentication; | ||||
| 
 | ||||
| import java.io.IOException; | ||||
| 
 | ||||
| import javax.servlet.FilterChain; | ||||
| import javax.servlet.ServletException; | ||||
| import javax.servlet.http.HttpServletRequest; | ||||
| import javax.servlet.http.HttpServletResponse; | ||||
| @ -38,6 +39,23 @@ import org.springframework.security.core.Authentication; | ||||
|  */ | ||||
| public interface AuthenticationSuccessHandler { | ||||
| 
 | ||||
| 	/** | ||||
| 	 * Called when a user has been successfully authenticated. | ||||
| 	 * | ||||
| 	 * @param request the request which caused the successful authentication | ||||
| 	 * @param response the response | ||||
| 	 * @param chain the {@link FilterChain} which can be used to proceed other filters in the chain | ||||
| 	 * @param authentication the <tt>Authentication</tt> object which was created during | ||||
| 	 * the authentication process. | ||||
| 	 * @since 5.2.0 | ||||
| 	 */ | ||||
| 	default void onAuthenticationSuccess(HttpServletRequest request, | ||||
| 			HttpServletResponse response, FilterChain chain, Authentication authentication) | ||||
| 			throws IOException, ServletException{ | ||||
| 		onAuthenticationSuccess(request, response, authentication); | ||||
| 		chain.doFilter(request, response); | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
| 	 * Called when a user has been successfully authenticated. | ||||
| 	 * | ||||
|  | ||||
| @ -0,0 +1,121 @@ | ||||
| /* | ||||
|  * Copyright 2002-2019 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 org.springframework.security.web.authentication.www; | ||||
| 
 | ||||
| import static org.springframework.http.HttpHeaders.AUTHORIZATION; | ||||
| 
 | ||||
| import java.io.UnsupportedEncodingException; | ||||
| import java.nio.charset.StandardCharsets; | ||||
| import java.util.Base64; | ||||
| 
 | ||||
| import javax.servlet.http.HttpServletRequest; | ||||
| 
 | ||||
| import org.springframework.security.authentication.AuthenticationDetailsSource; | ||||
| import org.springframework.security.authentication.BadCredentialsException; | ||||
| import org.springframework.security.authentication.InternalAuthenticationServiceException; | ||||
| import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | ||||
| import org.springframework.security.web.authentication.AuthenticationConverter; | ||||
| import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; | ||||
| import org.springframework.util.Assert; | ||||
| 
 | ||||
| /** | ||||
|  * Converts from a HttpServletRequest to | ||||
|  * {@link UsernamePasswordAuthenticationToken} that can be authenticated. Null | ||||
|  * authentication possible if there was no Authorization header with Basic | ||||
|  * authentication scheme. | ||||
|  * | ||||
|  * @author Sergey Bespalov | ||||
|  * @since 5.2.0 | ||||
|  */ | ||||
| public class BasicAuthenticationConverter implements AuthenticationConverter { | ||||
| 
 | ||||
| 	public static final String AUTHENTICATION_SCHEME_BASIC = "Basic"; | ||||
| 
 | ||||
| 	private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource; | ||||
| 
 | ||||
| 	private String credentialsCharset = StandardCharsets.UTF_8.name(); | ||||
| 
 | ||||
| 	public BasicAuthenticationConverter() { | ||||
| 		this(new WebAuthenticationDetailsSource()); | ||||
| 	} | ||||
| 
 | ||||
| 	public BasicAuthenticationConverter( | ||||
| 			AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) { | ||||
| 		this.authenticationDetailsSource = authenticationDetailsSource; | ||||
| 	} | ||||
| 
 | ||||
| 	public String getCredentialsCharset() { | ||||
| 		return credentialsCharset; | ||||
| 	} | ||||
| 
 | ||||
| 	public void setCredentialsCharset(String credentialsCharset) { | ||||
| 		this.credentialsCharset = credentialsCharset; | ||||
| 	} | ||||
| 
 | ||||
| 	public AuthenticationDetailsSource<HttpServletRequest, ?> getAuthenticationDetailsSource() { | ||||
| 		return authenticationDetailsSource; | ||||
| 	} | ||||
| 
 | ||||
| 	public void setAuthenticationDetailsSource( | ||||
| 			AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) { | ||||
| 		Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource required"); | ||||
| 		this.authenticationDetailsSource = authenticationDetailsSource; | ||||
| 	} | ||||
| 
 | ||||
| 	@Override | ||||
| 	public UsernamePasswordAuthenticationToken convert(HttpServletRequest request) { | ||||
| 		String header = request.getHeader(AUTHORIZATION); | ||||
| 		if (header == null) { | ||||
| 			return null; | ||||
| 		} | ||||
| 
 | ||||
| 		header = header.trim(); | ||||
| 		if (!header.startsWith(AUTHENTICATION_SCHEME_BASIC) && !header.startsWith(AUTHENTICATION_SCHEME_BASIC.toLowerCase())) { | ||||
| 			return null; | ||||
| 		} | ||||
| 
 | ||||
| 		byte[] base64Token = header.substring(6).getBytes(); | ||||
| 		byte[] decoded; | ||||
| 		try { | ||||
| 			decoded = Base64.getDecoder().decode(base64Token); | ||||
| 		} catch (IllegalArgumentException e) { | ||||
| 			throw new BadCredentialsException("Failed to decode basic authentication token"); | ||||
| 		} | ||||
| 
 | ||||
| 		String token; | ||||
| 		try { | ||||
| 			token = new String(decoded, getCredentialsCharset(request)); | ||||
| 		} catch (UnsupportedEncodingException e) { | ||||
| 			throw new InternalAuthenticationServiceException(e.getMessage(), e); | ||||
| 		} | ||||
| 
 | ||||
| 		String[] tokens = token.split(":"); | ||||
| 		if (tokens.length != 2) { | ||||
| 			throw new BadCredentialsException("Invalid basic authentication token"); | ||||
| 		} | ||||
| 
 | ||||
| 		UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(tokens[0], | ||||
| 				tokens[1]); | ||||
| 		authentication.setDetails(authenticationDetailsSource.buildDetails(request)); | ||||
| 
 | ||||
| 		return authentication; | ||||
| 	} | ||||
| 
 | ||||
| 	protected String getCredentialsCharset(HttpServletRequest request) { | ||||
| 		return getCredentialsCharset(); | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
| @ -17,7 +17,6 @@ | ||||
| package org.springframework.security.web.authentication.www; | ||||
| 
 | ||||
| import java.io.IOException; | ||||
| import java.util.Base64; | ||||
| 
 | ||||
| import javax.servlet.FilterChain; | ||||
| import javax.servlet.ServletException; | ||||
| @ -27,7 +26,6 @@ import javax.servlet.http.HttpServletResponse; | ||||
| import org.springframework.security.authentication.AnonymousAuthenticationToken; | ||||
| import org.springframework.security.authentication.AuthenticationDetailsSource; | ||||
| import org.springframework.security.authentication.AuthenticationManager; | ||||
| import org.springframework.security.authentication.BadCredentialsException; | ||||
| import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | ||||
| import org.springframework.security.core.Authentication; | ||||
| import org.springframework.security.core.AuthenticationException; | ||||
| @ -35,9 +33,7 @@ import org.springframework.security.core.context.SecurityContextHolder; | ||||
| import org.springframework.security.web.AuthenticationEntryPoint; | ||||
| import org.springframework.security.web.authentication.NullRememberMeServices; | ||||
| import org.springframework.security.web.authentication.RememberMeServices; | ||||
| import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; | ||||
| import org.springframework.util.Assert; | ||||
| import org.springframework.util.StringUtils; | ||||
| import org.springframework.web.filter.OncePerRequestFilter; | ||||
| 
 | ||||
| /** | ||||
| @ -95,12 +91,12 @@ public class BasicAuthenticationFilter extends OncePerRequestFilter { | ||||
| 	// ~ Instance fields | ||||
| 	// ================================================================================================ | ||||
| 
 | ||||
| 	private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource(); | ||||
| 	private AuthenticationEntryPoint authenticationEntryPoint; | ||||
| 	private AuthenticationManager authenticationManager; | ||||
| 	private RememberMeServices rememberMeServices = new NullRememberMeServices(); | ||||
| 	private boolean ignoreFailure = false; | ||||
| 	private String credentialsCharset = "UTF-8"; | ||||
| 	private BasicAuthenticationConverter authenticationConverter = new BasicAuthenticationConverter(); | ||||
| 
 | ||||
| 	/** | ||||
| 	 * Creates an instance which will authenticate against the supplied | ||||
| @ -152,19 +148,14 @@ public class BasicAuthenticationFilter extends OncePerRequestFilter { | ||||
| 			HttpServletResponse response, FilterChain chain) | ||||
| 					throws IOException, ServletException { | ||||
| 		final boolean debug = this.logger.isDebugEnabled(); | ||||
| 
 | ||||
| 		String header = request.getHeader("Authorization"); | ||||
| 
 | ||||
| 		if (!StringUtils.startsWithIgnoreCase(header, "basic ")) { | ||||
| 		try { | ||||
| 			UsernamePasswordAuthenticationToken authRequest = authenticationConverter.convert(request); | ||||
| 			if (authRequest == null) { | ||||
| 				chain.doFilter(request, response); | ||||
| 				return; | ||||
| 			} | ||||
| 
 | ||||
| 		try { | ||||
| 			String[] tokens = extractAndDecodeHeader(header, request); | ||||
| 			assert tokens.length == 2; | ||||
| 
 | ||||
| 			String username = tokens[0]; | ||||
| 			String username = authRequest.getName(); | ||||
| 
 | ||||
| 			if (debug) { | ||||
| 				this.logger | ||||
| @ -173,10 +164,6 @@ public class BasicAuthenticationFilter extends OncePerRequestFilter { | ||||
| 			} | ||||
| 
 | ||||
| 			if (authenticationIsRequired(username)) { | ||||
| 				UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( | ||||
| 						username, tokens[1]); | ||||
| 				authRequest.setDetails( | ||||
| 						this.authenticationDetailsSource.buildDetails(request)); | ||||
| 				Authentication authResult = this.authenticationManager | ||||
| 						.authenticate(authRequest); | ||||
| 
 | ||||
| @ -216,35 +203,6 @@ public class BasicAuthenticationFilter extends OncePerRequestFilter { | ||||
| 		chain.doFilter(request, response); | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
| 	 * Decodes the header into a username and password. | ||||
| 	 * | ||||
| 	 * @throws BadCredentialsException if the Basic header is not present or is not valid | ||||
| 	 * Base64 | ||||
| 	 */ | ||||
| 	private String[] extractAndDecodeHeader(String header, HttpServletRequest request) | ||||
| 			throws IOException { | ||||
| 
 | ||||
| 		byte[] base64Token = header.substring(6).getBytes("UTF-8"); | ||||
| 		byte[] decoded; | ||||
| 		try { | ||||
| 			decoded = Base64.getDecoder().decode(base64Token); | ||||
| 		} | ||||
| 		catch (IllegalArgumentException e) { | ||||
| 			throw new BadCredentialsException( | ||||
| 					"Failed to decode basic authentication token"); | ||||
| 		} | ||||
| 
 | ||||
| 		String token = new String(decoded, getCredentialsCharset(request)); | ||||
| 
 | ||||
| 		int delim = token.indexOf(":"); | ||||
| 
 | ||||
| 		if (delim == -1) { | ||||
| 			throw new BadCredentialsException("Invalid basic authentication token"); | ||||
| 		} | ||||
| 		return new String[] { token.substring(0, delim), token.substring(delim + 1) }; | ||||
| 	} | ||||
| 
 | ||||
| 	private boolean authenticationIsRequired(String username) { | ||||
| 		// Only reauthenticate if username doesn't match SecurityContextHolder and user | ||||
| 		// isn't authenticated | ||||
| @ -308,9 +266,7 @@ public class BasicAuthenticationFilter extends OncePerRequestFilter { | ||||
| 
 | ||||
| 	public void setAuthenticationDetailsSource( | ||||
| 			AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) { | ||||
| 		Assert.notNull(authenticationDetailsSource, | ||||
| 				"AuthenticationDetailsSource required"); | ||||
| 		this.authenticationDetailsSource = authenticationDetailsSource; | ||||
| 		authenticationConverter.setAuthenticationDetailsSource(authenticationDetailsSource); | ||||
| 	} | ||||
| 
 | ||||
| 	public void setRememberMeServices(RememberMeServices rememberMeServices) { | ||||
|  | ||||
| @ -99,7 +99,7 @@ public class AbstractAuthenticationProcessingFilterTests { | ||||
| 	} | ||||
| 
 | ||||
| 	@Test | ||||
| 	public void testDefaultProcessesFilterUrlMatchesWithPathParameter() { | ||||
| 	public void testDefaultProcessesFilterUrlMatchesWithPathParameter() throws Exception { | ||||
| 		MockHttpServletRequest request = createMockAuthenticationRequest(); | ||||
| 		MockHttpServletResponse response = new MockHttpServletResponse(); | ||||
| 		MockAuthenticationFilter filter = new MockAuthenticationFilter(); | ||||
|  | ||||
| @ -0,0 +1,249 @@ | ||||
| /* | ||||
|  * Copyright 2002-2019 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 org.springframework.security.web.authentication; | ||||
| 
 | ||||
| import static org.assertj.core.api.Assertions.assertThat; | ||||
| import static org.mockito.ArgumentMatchers.any; | ||||
| import static org.mockito.ArgumentMatchers.eq; | ||||
| import static org.mockito.Mockito.mock; | ||||
| import static org.mockito.Mockito.verify; | ||||
| import static org.mockito.Mockito.verifyZeroInteractions; | ||||
| import static org.mockito.Mockito.when; | ||||
| 
 | ||||
| import javax.servlet.FilterChain; | ||||
| import javax.servlet.ServletException; | ||||
| import javax.servlet.ServletRequest; | ||||
| import javax.servlet.ServletResponse; | ||||
| import javax.servlet.http.HttpServletRequest; | ||||
| 
 | ||||
| import org.junit.After; | ||||
| import org.junit.Before; | ||||
| import org.junit.Test; | ||||
| import org.junit.runner.RunWith; | ||||
| import org.mockito.Mock; | ||||
| import org.mockito.junit.MockitoJUnitRunner; | ||||
| import org.springframework.http.HttpStatus; | ||||
| import org.springframework.mock.web.MockHttpServletRequest; | ||||
| import org.springframework.mock.web.MockHttpServletResponse; | ||||
| import org.springframework.security.authentication.AuthenticationManager; | ||||
| import org.springframework.security.authentication.AuthenticationManagerResolver; | ||||
| import org.springframework.security.authentication.BadCredentialsException; | ||||
| import org.springframework.security.authentication.TestingAuthenticationToken; | ||||
| import org.springframework.security.core.Authentication; | ||||
| import org.springframework.security.core.context.SecurityContextHolder; | ||||
| import org.springframework.security.web.util.matcher.RequestMatcher; | ||||
| 
 | ||||
| /** | ||||
|  * @author Sergey Bespalov | ||||
|  * @since 5.2.0 | ||||
|  */ | ||||
| @RunWith(MockitoJUnitRunner.class) | ||||
| public class AuthenticationFilterTests { | ||||
| 
 | ||||
| 	@Mock | ||||
| 	private AuthenticationSuccessHandler successHandler; | ||||
| 	@Mock | ||||
| 	private AuthenticationConverter authenticationConverter; | ||||
| 	@Mock | ||||
| 	private AuthenticationManager authenticationManager; | ||||
| 	@Mock | ||||
| 	private AuthenticationFailureHandler failureHandler; | ||||
| 	@Mock | ||||
| 	private AuthenticationManagerResolver<HttpServletRequest> authenticationManagerResolver; | ||||
| 	@Mock | ||||
| 	private RequestMatcher requestMatcher; | ||||
| 
 | ||||
| 	@Before | ||||
| 	public void setup() { | ||||
| 		when(this.authenticationManagerResolver.resolve(any())).thenReturn(this.authenticationManager); | ||||
| 	} | ||||
| 
 | ||||
| 	@After | ||||
| 	public void clearContext() throws Exception { | ||||
| 		SecurityContextHolder.clearContext(); | ||||
| 	} | ||||
| 
 | ||||
| 	@Test | ||||
| 	public void filterWhenDefaultsAndNoAuthenticationThenContinues() throws Exception { | ||||
| 		AuthenticationFilter filter = new AuthenticationFilter(this.authenticationManager, this.authenticationConverter); | ||||
| 
 | ||||
| 		MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); | ||||
| 		MockHttpServletResponse response = new MockHttpServletResponse(); | ||||
| 		FilterChain chain = mock(FilterChain.class); | ||||
| 		filter.doFilter(request, response, chain); | ||||
| 
 | ||||
| 		verifyZeroInteractions(this.authenticationManager); | ||||
| 		verify(chain).doFilter(any(ServletRequest.class), any(ServletResponse.class)); | ||||
| 		assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); | ||||
| 	} | ||||
| 
 | ||||
| 	@Test | ||||
| 	public void filterWhenAuthenticationManagerResolverDefaultsAndNoAuthenticationThenContinues() throws Exception { | ||||
| 		AuthenticationFilter filter = new AuthenticationFilter(this.authenticationManagerResolver, this.authenticationConverter); | ||||
| 
 | ||||
| 		MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); | ||||
| 		MockHttpServletResponse response = new MockHttpServletResponse(); | ||||
| 		FilterChain chain = mock(FilterChain.class); | ||||
| 		filter.doFilter(request, response, chain); | ||||
| 
 | ||||
| 		verifyZeroInteractions(this.authenticationManagerResolver); | ||||
| 		verify(chain).doFilter(any(ServletRequest.class), any(ServletResponse.class)); | ||||
| 		assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); | ||||
| 	} | ||||
| 
 | ||||
| 	@Test | ||||
| 	public void filterWhenDefaultsAndAuthenticationSuccessThenContinues() throws Exception { | ||||
| 		Authentication authentication = new TestingAuthenticationToken("test", "this", "ROLE"); | ||||
| 		when(this.authenticationConverter.convert(any())).thenReturn(authentication); | ||||
| 		when(this.authenticationManager.authenticate(any())).thenReturn(authentication); | ||||
| 		AuthenticationFilter filter = new AuthenticationFilter(this.authenticationManager, this.authenticationConverter); | ||||
| 
 | ||||
| 		MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); | ||||
| 		MockHttpServletResponse response = new MockHttpServletResponse(); | ||||
| 		FilterChain chain = mock(FilterChain.class); | ||||
| 		filter.doFilter(request, response, chain); | ||||
| 
 | ||||
| 		verify(this.authenticationManager).authenticate(any(Authentication.class)); | ||||
| 		verify(chain).doFilter(any(ServletRequest.class), any(ServletResponse.class)); | ||||
| 		assertThat(SecurityContextHolder.getContext().getAuthentication()).isNotNull(); | ||||
| 	} | ||||
| 
 | ||||
| 	@Test | ||||
| 	public void filterWhenAuthenticationManagerResolverDefaultsAndAuthenticationSuccessThenContinues() | ||||
| 			throws Exception { | ||||
| 		Authentication authentication = new TestingAuthenticationToken("test", "this", "ROLE"); | ||||
| 		when(this.authenticationConverter.convert(any())).thenReturn(authentication); | ||||
| 		when(this.authenticationManager.authenticate(any())).thenReturn(authentication); | ||||
| 
 | ||||
| 		AuthenticationFilter filter = new AuthenticationFilter(this.authenticationManagerResolver, this.authenticationConverter); | ||||
| 
 | ||||
| 		MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); | ||||
| 		MockHttpServletResponse response = new MockHttpServletResponse(); | ||||
| 		FilterChain chain = mock(FilterChain.class); | ||||
| 		filter.doFilter(request, response, chain); | ||||
| 
 | ||||
| 		verify(this.authenticationManager).authenticate(any(Authentication.class)); | ||||
| 		verify(chain).doFilter(any(ServletRequest.class), any(ServletResponse.class)); | ||||
| 		assertThat(SecurityContextHolder.getContext().getAuthentication()).isNotNull(); | ||||
| 	} | ||||
| 
 | ||||
| 	@Test | ||||
| 	public void filterWhenDefaultsAndAuthenticationFailThenUnauthorized() throws Exception { | ||||
| 		Authentication authentication = new TestingAuthenticationToken("test", "this", "ROLE"); | ||||
| 		when(this.authenticationConverter.convert(any())).thenReturn(authentication); | ||||
| 		when(this.authenticationManager.authenticate(any())).thenThrow(new BadCredentialsException("failed")); | ||||
| 
 | ||||
| 		AuthenticationFilter filter = new AuthenticationFilter(this.authenticationManager, this.authenticationConverter); | ||||
| 
 | ||||
| 		MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); | ||||
| 		MockHttpServletResponse response = new MockHttpServletResponse(); | ||||
| 		FilterChain chain = mock(FilterChain.class); | ||||
| 		filter.doFilter(request, response, chain); | ||||
| 
 | ||||
| 		assertThat(response.getStatus()).isEqualTo(HttpStatus.UNAUTHORIZED.value()); | ||||
| 		assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); | ||||
| 	} | ||||
| 
 | ||||
| 	@Test | ||||
| 	public void filterWhenAuthenticationManagerResolverDefaultsAndAuthenticationFailThenUnauthorized() | ||||
| 			throws Exception { | ||||
| 		Authentication authentication = new TestingAuthenticationToken("test", "this", "ROLE"); | ||||
| 		when(this.authenticationConverter.convert(any())).thenReturn(authentication); | ||||
| 		when(this.authenticationManager.authenticate(any())).thenThrow(new BadCredentialsException("failed")); | ||||
| 
 | ||||
| 		AuthenticationFilter filter = new AuthenticationFilter(this.authenticationManagerResolver, this.authenticationConverter); | ||||
| 
 | ||||
| 		MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); | ||||
| 		MockHttpServletResponse response = new MockHttpServletResponse(); | ||||
| 		FilterChain chain = mock(FilterChain.class); | ||||
| 		filter.doFilter(request, response, chain); | ||||
| 
 | ||||
| 		assertThat(response.getStatus()).isEqualTo(HttpStatus.UNAUTHORIZED.value()); | ||||
| 		assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); | ||||
| 	} | ||||
| 
 | ||||
| 	@Test | ||||
| 	public void filterWhenConvertEmptyThenOk() throws Exception { | ||||
| 		when(this.authenticationConverter.convert(any())).thenReturn(null); | ||||
| 		AuthenticationFilter filter = new AuthenticationFilter(this.authenticationManagerResolver, this.authenticationConverter); | ||||
| 
 | ||||
| 		MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); | ||||
| 		FilterChain chain = mock(FilterChain.class); | ||||
| 		filter.doFilter(request, new MockHttpServletResponse(), chain); | ||||
| 
 | ||||
| 		verifyZeroInteractions(this.authenticationManagerResolver); | ||||
| 		verify(chain).doFilter(any(ServletRequest.class), any(ServletResponse.class)); | ||||
| 		assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); | ||||
| 	} | ||||
| 
 | ||||
| 	@Test | ||||
| 	public void filterWhenConvertAndAuthenticationSuccessThenSuccess() throws Exception { | ||||
| 		Authentication authentication = new TestingAuthenticationToken("test", "this", "ROLE_USER"); | ||||
| 		when(this.authenticationConverter.convert(any())).thenReturn(authentication); | ||||
| 		when(this.authenticationManager.authenticate(any())).thenReturn(authentication); | ||||
| 
 | ||||
| 		AuthenticationFilter filter = new AuthenticationFilter(this.authenticationManagerResolver, this.authenticationConverter); | ||||
| 		filter.setSuccessHandler(successHandler); | ||||
| 
 | ||||
| 		MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); | ||||
| 		MockHttpServletResponse response = new MockHttpServletResponse(); | ||||
| 		FilterChain chain = mock(FilterChain.class); | ||||
| 		filter.doFilter(request, response, chain); | ||||
| 
 | ||||
| 		verify(this.successHandler).onAuthenticationSuccess(any(), any(), any(), eq(authentication)); | ||||
| 		verifyZeroInteractions(this.failureHandler); | ||||
| 		assertThat(SecurityContextHolder.getContext().getAuthentication()).isNotNull(); | ||||
| 	} | ||||
| 
 | ||||
| 	@Test(expected = ServletException.class) | ||||
| 	public void filterWhenConvertAndAuthenticationEmptyThenServerError() throws Exception { | ||||
| 		Authentication authentication = new TestingAuthenticationToken("test", "this", "ROLE_USER"); | ||||
| 		when(this.authenticationConverter.convert(any())).thenReturn(authentication); | ||||
| 		when(this.authenticationManager.authenticate(any())).thenReturn(null); | ||||
| 
 | ||||
| 		AuthenticationFilter filter = new AuthenticationFilter(this.authenticationManagerResolver, this.authenticationConverter); | ||||
| 		filter.setSuccessHandler(successHandler); | ||||
| 
 | ||||
| 		MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); | ||||
| 		MockHttpServletResponse response = new MockHttpServletResponse(); | ||||
| 		FilterChain chain = mock(FilterChain.class); | ||||
| 		try { | ||||
| 			filter.doFilter(request, response, chain); | ||||
| 		} catch (ServletException e) { | ||||
| 			verifyZeroInteractions(this.successHandler); | ||||
| 			assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); | ||||
| 
 | ||||
| 			throw e; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	@Test | ||||
| 	public void filterWhenNotMatchAndConvertAndAuthenticationSuccessThenContinues() throws Exception { | ||||
| 		when(this.requestMatcher.matches(any())).thenReturn(false); | ||||
| 
 | ||||
| 		AuthenticationFilter filter = new AuthenticationFilter(this.authenticationManagerResolver, this.authenticationConverter); | ||||
| 		filter.setRequestMatcher(this.requestMatcher); | ||||
| 
 | ||||
| 		MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); | ||||
| 		MockHttpServletResponse response = new MockHttpServletResponse(); | ||||
| 		FilterChain chain = mock(FilterChain.class); | ||||
| 		filter.doFilter(request, response, chain); | ||||
| 
 | ||||
| 		verifyZeroInteractions(this.authenticationConverter, this.authenticationManagerResolver, this.successHandler); | ||||
| 		assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
| @ -0,0 +1,90 @@ | ||||
| /* | ||||
|  * Copyright 2002-2019 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 org.springframework.security.web.authentication.www; | ||||
| 
 | ||||
| import static org.assertj.core.api.Assertions.assertThat; | ||||
| import static org.mockito.ArgumentMatchers.any; | ||||
| import static org.mockito.Mockito.verify; | ||||
| import static org.mockito.Mockito.verifyZeroInteractions; | ||||
| 
 | ||||
| import javax.servlet.http.HttpServletRequest; | ||||
| 
 | ||||
| import org.apache.commons.codec.binary.Base64; | ||||
| import org.junit.Before; | ||||
| import org.junit.Test; | ||||
| import org.junit.runner.RunWith; | ||||
| import org.mockito.Mock; | ||||
| import org.mockito.junit.MockitoJUnitRunner; | ||||
| import org.springframework.mock.web.MockHttpServletRequest; | ||||
| import org.springframework.security.authentication.AuthenticationDetailsSource; | ||||
| import org.springframework.security.authentication.BadCredentialsException; | ||||
| import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | ||||
| 
 | ||||
| /** | ||||
|  * @author Sergey Bespalov | ||||
|  * @since 5.2.0 | ||||
|  */ | ||||
| @RunWith(MockitoJUnitRunner.class) | ||||
| public class BasicAuthenticationConverterTests { | ||||
| 
 | ||||
| 	@Mock | ||||
| 	private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource; | ||||
| 	private BasicAuthenticationConverter converter; | ||||
| 
 | ||||
| 	@Before | ||||
| 	public void setup() { | ||||
| 		converter = new BasicAuthenticationConverter(authenticationDetailsSource); | ||||
| 	} | ||||
| 
 | ||||
| 	@Test | ||||
| 	public void testNormalOperation() throws Exception { | ||||
| 		String token = "rod:koala"; | ||||
| 		MockHttpServletRequest request = new MockHttpServletRequest(); | ||||
| 		request.addHeader("Authorization", "Basic " + new String(Base64.encodeBase64(token.getBytes()))); | ||||
| 		UsernamePasswordAuthenticationToken authentication = converter.convert(request); | ||||
| 
 | ||||
| 		verify(authenticationDetailsSource).buildDetails(any()); | ||||
| 		assertThat(authentication).isNotNull(); | ||||
| 		assertThat(authentication.getName()).isEqualTo("rod"); | ||||
| 	} | ||||
| 
 | ||||
| 	@Test | ||||
| 	public void testWhenUnsupportedAuthorizationHeaderThenIgnored() throws Exception { | ||||
| 		MockHttpServletRequest request = new MockHttpServletRequest(); | ||||
| 		request.addHeader("Authorization", "Bearer someOtherToken"); | ||||
| 		UsernamePasswordAuthenticationToken authentication = converter.convert(request); | ||||
| 
 | ||||
| 		verifyZeroInteractions(authenticationDetailsSource); | ||||
| 		assertThat(authentication).isNull(); | ||||
| 	} | ||||
| 
 | ||||
| 	@Test(expected = BadCredentialsException.class) | ||||
| 	public void testWhenInvalidBasicAuthorizationTokenThenError() throws Exception { | ||||
| 		String token = "NOT_A_VALID_TOKEN_AS_MISSING_COLON"; | ||||
| 		MockHttpServletRequest request = new MockHttpServletRequest(); | ||||
| 		request.addHeader("Authorization", "Basic " + new String(Base64.encodeBase64(token.getBytes()))); | ||||
| 		converter.convert(request); | ||||
| 	} | ||||
| 
 | ||||
| 	@Test(expected = BadCredentialsException.class) | ||||
| 	public void testWhenInvalidBase64ThenError() throws Exception { | ||||
| 		MockHttpServletRequest request = new MockHttpServletRequest(); | ||||
| 		request.addHeader("Authorization", "Basic NOT_VALID_BASE64"); | ||||
| 
 | ||||
| 		converter.convert(request); | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user