SecurityContextHolder.getContext().getAuthentication() not anonymous, but request.getHeader("Authorization") missing - java

We are implementing a role-based security API (bearer-token only) with spring-boot and Keycloak.
The security config looks as follows:
#KeycloakConfiguration
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
/**
* Registers the KeycloakAuthenticationProvider with the authentication manager.
*/
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(keycloakAuthenticationProvider());
}
#Bean
public KeycloakConfigResolver KeycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
/**
* Defines the session authentication strategy.
* For bearer-only applications there is no session needed and therefor
* we use the NullAuthenticatedSessionStrategy.
*/
#Bean
#Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new NullAuthenticatedSessionStrategy();
}
#Override
protected void configure(HttpSecurity http) throws Exception
{
super.configure(http);
http.authorizeRequests()
.anyRequest()
.permitAll();
}
}
I retrieve roles with
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth instanceof KeycloakAuthenticationToken) {
SimpleKeycloakAccount user = (SimpleKeycloakAccount) auth.getDetails();
for (String role : user.getRoles()) {
// Clean and collect roles...
}
}
This works and we get the roles. The problem is, that we even get roles when no request header "Authorization: Bearer [token]" has been send. This happens, when we have two consecutive calls, the first with valid token, then the second call (without Authorization header) has the same KeycloakAuthenticationToken with the same roles.
My questions are now:
How can it be that the Authorization header is null, but the security context still returns a KeycloakAuthenticationToken?
Shouldn't the security context be per thread, and each thread exists during a single http request only?
Update
I found a workaround in the meantime, that is ugly but solves the problem for the moment. I've written a custom HandlerInterceptor, that cleans the security context, if no Authorization header can be found:
if (request.getHeader("Authorization") == null)
SecurityContextHolder.clearContext();
Although this seems to solve the issue, it shouldn't be necessary. So something is still strange, I guess.

Related

Spring Boot with read-only session for single sign on

We have a legacy Spring application (A) (that is not using spring-boot) that handles authentication and writes the session to Redis using spring-session (the data in Redis is stored as XML).
We now want to introduce a new application (B), using spring-boot 2.2.6.RELEASE and spring-session Corn-RC1, that should be useable if a user has signed into (A) with ROLE_ADMIN. I.e. this can be regarded as a very crude way of doing single sign on. A user should never be able to authenticate in B (it'd like to disable authentication if possible), it should only check that an existing user is authenticated in the session repository (redis) and has ROLE_ADMIN. Both A and B will be located under the same domain so cookies will be propagated by the browser. I've tried various different ways of getting this to work, for example:
#Configuration
#EnableWebSecurity
class ServiceBSpringSecurityConfig : WebSecurityConfigurerAdapter() {
#Autowired
fun configureGlobal(auth: AuthenticationManagerBuilder) {
auth.inMemoryAuthentication()
}
override fun configure(http: HttpSecurity) {
http
.authorizeRequests()
.anyRequest().hasRole("ADMIN")
.and()
.formLogin()
.and()
.httpBasic().disable()
}
}
but this will show the default login screen:
I've also tried removing this part entirely:
#Autowired
fun configureGlobal(auth: AuthenticationManagerBuilder) {
auth.inMemoryAuthentication()
}
but then it'll generate a default user and password and it does not seem to call the configure method (or the configuration doesn't work regardless).
How can I solve this?
What you need is to disable formLogin and httBasic on Application B and add a filter before spring's authentication filter AnonymousAuthenticationFilter or UsernamePasswordAuthenticationFilter. In the custom filter you will extract the cookie/header/token from the request object and based on that reach out to the redis cache for session details. This filter would then validate the session and create object of type org.springframework.security.core.Authentication and set that in the current SpringSecurityContext.
Below is the sudo code for this;
ServiceBSpringSecurityConfig
#Configuration
#EnableWebSecurity
public class ServiceBSpringSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.exceptionHandling().authenticationEntryPoint(authEntryPoint()).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.httpBasic().disabled().and()
.formLogin().disabled().and()
.authorizeRequests().anyRequest().hasRole("ADMIN")
http.addFilterBefore(authTokenFilter(), UsernamePasswordAuthenticationFilter.class);
}
#Bean
public AuthTokenFilter authTokenFilter() {
return new AuthTokenFilter();
}
#Bean
public AuthEntryPoint authEntryPoint() {
return new AuthEntryPoint()
}
}
AuthEntryPoint
public class AuthEntryPoint implements AuthenticationEntryPoint {
private static final Logger logger = LoggerFactory.getLogger(AuthEntryPoint.class);
#Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
// Very generic authEntryPoint which simply returns unauthorized
// Could implement additional functionality of forwarding the Application A login-page
logger.error("Unauthorized error: {}", authException.getMessage());
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Error: Unauthorized");
}
}
AuthTokenFilter
public class AuthTokenFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// extract some sort of token or cookie value from request
token = request.getHeader("Token");
if (token != null) {
// Validate the token by retrieving session from redis cache
// Create org.springframework.security.core.Authentication from the token
Authentication auth = authFactory.getAuthentication(token);
// Set the spring security context with the auth
SecurityContextHolder.getContext().setAuthentication(auth);
} else {
// Do something if token not present at all
}
// Continue to to filter chain
filterChain.doFilter(request, response);
}
}
As mentioned this is sudo code so some adjustment might be required. However the general gist of token based auth remains the same.

How to invalidate remember-me on logout?

Well, I do not implement PersistentTokenBasedRememberMeServices therefore I cannot use .logout(request, response, auth). But I use JdbcTokenRepositoryImpl in order to use PersistentTokenRepository for remember-me feature.
LogoutController:
#Controller
public class LogoutController {
#RequestMapping(value = {"/logout"}, method = RequestMethod.GET)
public String logout() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if(auth != null) {
SecurityContextHolder.getContext().setAuthentication(null);
}
return "redirect:/login?logout";
}
}
Security config:
#Configuration
#EnableWebSecurity
public class AppSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private DataSource dataSource;
#Autowired
private UserDetailsService userDetailsService;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authProvider());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/", "/playground").hasAnyRole("ROOT", "MODER", "USER")
.antMatchers("/users/**").hasAnyRole("ROOT", "MODER")
.and()
.formLogin().loginPage("/login").loginProcessingUrl("/login").failureHandler(customAuthenticationFailureHandler())
.and()
.rememberMe().rememberMeParameter("remember-me").tokenRepository(persistentTokenRepository()).userDetailsService(userDetailsService)
.and()
.logout().logoutUrl("/logout");
}
#Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
public DaoAuthenticationProvider authProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setPasswordEncoder(passwordEncoder());
authProvider.setUserDetailsService(userDetailsService);
return authProvider;
}
#Bean
public AuthenticationFailureHandler customAuthenticationFailureHandler() {
return new CustomAuthenticationFailureHandler();
}
#Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
return jdbcTokenRepository;
}
When I log in with remember-me, I cannot log out then. I guess because of remember-me feature. What should I add to LogoutController to make a proper logout proccess?
Note: the thing is, that if I just use POST method on logout, then it perfectly works, but I'd like to use GET method and thus I have to create a logout controller to perform get method.
Try to disable crsf (http.csrf().disable()).
The default implementation in spring's security Logout filter is:
if (http.getConfigurer(CsrfConfigurer.class) != null) {
this.logoutRequestMatcher = new AntPathRequestMatcher(this.logoutUrl, "POST");
}
else {
this.logoutRequestMatcher = new OrRequestMatcher(
new AntPathRequestMatcher(this.logoutUrl, "GET"),
new AntPathRequestMatcher(this.logoutUrl, "POST"),
new AntPathRequestMatcher(this.logoutUrl, "PUT"),
new AntPathRequestMatcher(this.logoutUrl, "DELETE")
);
}
as you can see if your Csrf is enabled (by default it's enabled even if you overwride protected void configure(HttpSecurity http)) then only POST method will be work, if not all are working.
BTW: Are you sure your request reaching the LogoutController, because I thing it's uses standard spring security logout mechanism? (To disable it do http.logout().disable(), the same as csrf it's enabled by default)
To sum it up.
I've managed to test several ways and here what I have got:
As M. Deinum suggested in comments, it's possible not to use a controller and nevertheless have a logout with a GET request. Here it is.
.logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
As Andrew Sasha suggested in the answer section, you can disable csrf as it intentionally prevents using GET request. And now you can use a GET request to log out without even using any controller.
http.csrf().disable()
If you still wish to use the controller, none of the following things will help you
.deleteCookies("remember-me", "JSESSIONID")
.invalidateHttpSession(true)
.clearAuthentication(true)
.logoutSuccessUrl("/")
(I'm not sure, but I feel like it doesn't work because you perform a GET request and use your controller to control logout)
So then you can do it programmatically
First, you add a name for remember me cookie in Spring security config:
rememberMe().rememberMeCookieName("remember-me")
And then in logout controller add this:
String cookieName = "remember-me";
Cookie cookie = new Cookie(cookieName, null);
cookie.setMaxAge(0);
cookie.setPath(StringUtils.hasLength(request.getContextPath()) ? request.getContextPath() : "/");
response.addCookie(cookie);
The only problem here is that you have to delete a record from persistent_logins table manually
(In order to get a request and a response you just pass them into a method public void logout(HttpServletRequest request, HttpServletResponse response)
It is possible to use a POST request but use it as a link with the help of JavaScript or even plain HTML and CSS.
The solutions for that you can find on this topic.
So what do we have here?
Summarizing everything above I can say that if you want a controller, you have to programmatically write everything yourself (someone would tell that it's a reinventing of a wheel).
Still, it is possible to use a GET request but without controller which is described in the 1st and 2nd positions of the list.
(The consequences of using a GET request is written within CSRF Documentation and it does not recommend to use a GET request because of its invulnerability.)
So the last thing that I decided to be my favorite is to make a POST request look like a GET request (use it as a link) with the help of JS or HTML and CSS. And as you use a POST request you kind of have a CSRF protection.
I hope this will help someone.

can not call auth provider for second time hit

I am using spring security for authentication
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authProvider).authenticationProvider(secondaryAuthProvider) ;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests()
.antMatchers("/login").hasAnyRole("ADMIN","VISITOR").and().
formLogin().defaultSuccessUrl("/login").failureUrl("/")
.loginPage("/login").usernameParameter("username").passwordParameter("password").failureUrl("/").
and().logout().permitAll().and().exceptionHandling().accessDeniedPage("/403").and()
.authorizeRequests().antMatchers("/resources/**").permitAll().and().authorizeRequests().
antMatchers("/api/**").authenticated().and().httpBasic().realmName("MY_TEST_REALM").
authenticationEntryPoint(getBasicAuthEntryPoint());
}
#Bean
public CustomBasicAuthenticationEntryPoint getBasicAuthEntryPoint(){
return new CustomBasicAuthenticationEntryPoint();
}
This is working fine. When i hit /api/login i am able to use basic authentication
But after first successful authentication I am able to use /api/login without authentication.
It is not taking me to auth provider at second time. First time control is going there but not second time.
Register two WebSecurity configurations:
#Configuration
#EnableWebSecurity
#Order(1)
public class StatefulConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authProvider).authenticationProvider(secondaryAuthProvider) ;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED).and()
.antMatcher("/web/*").authorizeRequests()
.antMatchers("/*").hasAnyRole("ADMIN","VISITOR").and().
formLogin().defaultSuccessUrl("/web/login").failureUrl("/web/error").loginPage("/web/login").usernameParameter("username").passwordParameter("password").failureUrl("/").
and().logout().logoutUrl("/web/logout").permitAll().and().exceptionHandling().accessDeniedPage("/403").and()
.authorizeRequests().antMatchers("/resources/**").permitAll();
}
}
And for rest:
#Configuration
#Order(2)
public class StatelessConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authProvider).authenticationProvider(secondaryAuthProvider) ;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.antMatcher("/api/*").authorizeRequests()
.antMatchers("/api/**").authenticated().and().httpBasic().realmName("MY_TEST_REALM").
authenticationEntryPoint(getBasicAuthEntryPoint());
}
#Bean
public CustomBasicAuthenticationEntryPoint getBasicAuthEntryPoint(){
return new CustomBasicAuthenticationEntryPoint();
}
}
Be careful: there are antMatcher(...) and antMatchers(...) methods.
UPDATE: similar problem & solution here
Session is created when you log in. Session will be active until you logout (destroy session), or when time expire.
See example
EDIT:
Spring application have a few important settings associated with session.
The first one is session creation policy (by default IF_REQUIRED - if session linked with request already exists it will be not destroyed and created again).
Session is saved in cookie - you can check it hitting f12.
Application "check" does cookie exist in request. When you go to login page there are two cases:
you don't have session -> login popup appears, you can log in,
you have session because SecurityContextHolder contain information about current session.
How does it work?
When you use .httpBasic(), Spring Security registers BasicAuthenticationFilter. In method doFilterInternal you can see:
if (authenticationIsRequired(username)) {
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, tokens[1]);
authRequest.setDetails(
this.authenticationDetailsSource.buildDetails(request));
Authentication authResult = this.authenticationManager
.authenticate(authRequest);
if (debug) {
this.logger.debug("Authentication success: " + authResult);
}
SecurityContextHolder.getContext().setAuthentication(authResult);
this.rememberMeServices.loginSuccess(request, response, authResult);
onSuccessfulAuthentication(request, response, authResult);
}
After success first login, authentication is set.
When you try to log in again authenticationIsRequired method returns false. Why?
Look at the source:
private boolean authenticationIsRequired(String username) {
// Only reauthenticate if username doesn't match SecurityContextHolder and user
// isn't authenticated
// (see SEC-53)
Authentication existingAuth = SecurityContextHolder.getContext()
.getAuthentication();
if (existingAuth == null || !existingAuth.isAuthenticated()) {
return true;
}
// Limit username comparison to providers which use usernames (ie
// UsernamePasswordAuthenticationToken)
// (see SEC-348)
if (existingAuth instanceof UsernamePasswordAuthenticationToken
&& !existingAuth.getName().equals(username)) {
return true;
}
// Handle unusual condition where an AnonymousAuthenticationToken is already
// present
// This shouldn't happen very often, as BasicProcessingFitler is meant to be
// earlier in the filter
// chain than AnonymousAuthenticationFilter. Nevertheless, presence of both an
// AnonymousAuthenticationToken
// together with a BASIC authentication request header should indicate
// reauthentication using the
// BASIC protocol is desirable. This behaviour is also consistent with that
// provided by form and digest,
// both of which force re-authentication if the respective header is detected (and
// in doing so replace
// any existing AnonymousAuthenticationToken). See SEC-610.
if (existingAuth instanceof AnonymousAuthenticationToken) {
return true;
}
return false;
}
As you can see getAuthhentication invoked on SecurityContextHolder return object set in previous request.
Sorry for my bad English.
UPDATE: you can invalidate session using "/logout" url.

Spring Data Rest with JWT

I'm trying to secure a simple Spring-Data-Rest app using jwt.
Taking the seed from https://github.com/spring-projects/spring-data-examples/tree/master/rest/security
The SecurityConfig is below (using normal username, password authentication)
How can I change this to JWT Authentication?
(Authorization is already done using #PreAuthorize("hasRole('ROLE_USER')") in repositories)
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
/**
* This section defines the user accounts which can be used for
* authentication as well as the roles each user has.
*/
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("greg").password("turnquist").roles("USER").and()
.withUser("ollie").password("gierke").roles("USER", "ADMIN");
}
/**
* This section defines the security policy for the app.
* - BASIC authentication is supported (enough for this REST-based demo)
* - /employees is secured using URL security shown below
* - CSRF headers are disabled since we are only testing the REST interface,
* not a web one.
*
* NOTE: GET is not shown which defaults to permitted.
*/
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic().and()
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/employees").hasRole("ADMIN")
.antMatchers(HttpMethod.PUT, "/employees/**").hasRole("ADMIN")
.antMatchers(HttpMethod.PATCH, "/employees/**").hasRole("ADMIN").and()
.csrf().disable();
}
}
here is a good tutorial for JWT Authentication in spring boot, but in can applied for spring applications as well: https://auth0.com/blog/implementing-jwt-authentication-on-spring-boot/
According to the tutorial in your SecurityConfiguration.configure you need
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
.addFilter(new JWTAuthorizationFilter(authenticationManager()))
// this disables session creation on Spring Security
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter, is applied to /login URL and generates JWT token based on your login/password if such a user exists in the system.
JWTAuthorizationFilter verifies JWT token coming in http header
Of course you need to add more moving parts in order to enable JWT auth by this tutorial.
I followed the tutorial for Spring Security OAuth:
https://projects.spring.io/spring-security-oauth/docs/oauth2.html
In particular you have to enable the resource server. This is my (modified) configuration):
#EnableResourceServer
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {
#Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.tokenServices(tokenServices());
}
#Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
#Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
try {
// Load the public key of the authorization server.
String key = IOUtils.toString(getClass().getResource("/reng0-public.key"), Charset.forName("US-ASCII"));
converter.setVerifierKey(key);
} catch (IOException e) {
throw new RuntimeException(e);
}
return converter;
}
#Bean
#Primary
public ResourceServerTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
return defaultTokenServices;
}
}
The client has to add the Authorization: Bearer header to make it work.

LDAP authentication in spring boot app

I know almost nothing about LDAP and even less about spring security but I am trying to configure a spring boot app to authenticate against an ldap instance and am stuck.
I was given the ldap server name at adldap.company.com and base dn of dc=ad,dc=company,dc=com
I have some python code that does a simple bind and works.
LDAP_USERNAME = 'username#ad.company.com'
LDAP_PASSWORD = 'password'
base_dn = 'dc=ad,dc=company,dc=com' # not used for bind I guess, only search
try:
ldap_client = ldap.initialize('ldap://adldap.company.com')
ldap_client.set_option(ldap.OPT_REFERRALS,0)
ldap_client.simple_bind_s(LDAP_USERNAME, LDAP_PASSWORD)
except ldap.INVALID_CREDENTIALS as e:
ldap_client.unbind()
return 'Wrong username and password: %s' % e
except ldap.SERVER_DOWN:
return 'AD server not available'
If I run this code, it seems to successfully bind as "username#ad.company.com" with password "password".
I also have a WebSecurityConfig class that I think should be handling auth:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/secure")
.authorizeRequests()
.anyRequest().fullyAuthenticated()
.and()
.httpBasic();
}
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.ldapAuthentication()
.userDnPatterns("uid={0}")
.contextSource()
.url("ldap://adldap.company.com");
//.url("ldap://adldap.company.com/dc=ad,dc=company,dc=com");
}
}
When I go to /secure in the app, I get a basic auth pop up but then anything I try entering gets me a 401 Unauthorized. I have tried "username#ad.company.com", without the domain, putting that stuff in the userDnPatterns like {0}#adldap.company.com and a bunch of other things. I have tried using different URLs with the base dn in it or not. Nothing seems to work. What am I missing?
Also, is this the right way to auth users? I've read about both bind authentication and something about binding and searching but the server doesn't allow anonyous binds so I guess I would need some kind of "app user" that could bind and do the searches, right? Is that "better"?
Active Directory has its own non-standard syntax for user authentication, different from the usual LDAP DN binding.
Spring Security provides a specialized AuthenticationProvider for Active Directory.
Try this :
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/secure")
.authorizeRequests()
.anyRequest().fullyAuthenticated()
.and()
.httpBasic();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(activeDirectoryLdapAuthenticationProvider());
}
#Bean
public AuthenticationManager authenticationManager() {
return new ProviderManager(Arrays.asList(activeDirectoryLdapAuthenticationProvider()));
}
#Bean
public AuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
ActiveDirectoryLdapAuthenticationProvider provider = new ActiveDirectoryLdapAuthenticationProvider("adldap.company.com", "ldap://adldap.company.com");
provider.setConvertSubErrorCodesToExceptions(true);
provider.setUseAuthenticationRequestCredentials(true);
return provider;
}
}
Long story short, the problem is that Microsoft Active Directory LDAP is not "Vanilla" LDAP and thus you need to connect to it differently.
The working solution is here: https://medium.com/#dmarko484/spring-boot-active-directory-authentication-5ea04969f220

Categories