Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make Basic Authentication work as an alternative for keycloak in a Angular JS/Spring boot app

We have migrated from Basic Authentication to Keycloak method in our project in the production environment. However we would like continue using Basic Authentication, for local development, standalone and demo instalations, which could be triggered by a profile or something like this.

In this project we have REST APIs developed with Java/Spring boot and an AngularJS application which consumes these APIs. We are using Keycloak to protect both AngularJS app and the APIs.

The problem is how to make Spring Security and Keycloak to work "together" in the same application with different profiles. The solution I found so far, was to configure both Spring Security and Keycloak, and made a workaround with properties files, as described below:

application-keycloak.properties

#Unactivate Basic Authentication
security.ignored=/**

application-local-auth.properties

#Unactivate Keycloak
spring.autoconfigure.exclude=org.keycloak.adapters.springboot.KeycloakSpringBootConfiguration

When I wanto to use keycloak, I have to ignore security in order to not have problems and when I want to use basic authentication I have to exclude Keycloak configuration in order to also prevent conflicts.

This is my Security Configuration class:

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.httpBasic().and()
            .authorizeRequests()
            .antMatchers("/","/scripts/**","/keycloak/isActive","/keycloak/config","/bower_components/**","/views/**","/fonts/**",
                    "/views/inventory/dialogs/**", "/services/**","/resources/**","/styles/**", "/info")

            .permitAll()
            .anyRequest()
            .authenticated()
            .and()
            .csrf().disable();
}


@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication().withUser("admin").password("admin").roles("ADMIN");
}

And this is my Keycloak Spring Boot configuration:

# Keycloak
keycloak.realm=local
keycloak.realmKey=MIIBIjANBgkqhkiG9wsIIBCgKCAQEAuJYmaWvF3YhifflJhspXOs8RJn74w+eVD8PtpVbu2cYG9OIa49P8SwqVn/kyJQr7kT3OlCq3XMZWBHe+JSzSz7KttKkhfFSfzISdKDKlkPena2H/i3FKlRZIldbeeuQNYdD6nMpzU6QWLwGF1cUAo1M11f2p99QI1FOhVPJSErWsjDsKpWqG+rMMjT1eos0QCNP7krx/yfMdlUyaJCYiDvpOAoec3OWXvDJovEajBNAZMWVXgJF90wAVPRF6szraA2m7K2gG9ozaCNWB0v4Sy6czekbKjqEBPJo45uEmGHd92V//uf/WQG4HSiuv8CTV+b6TQxKtZCpQpqp2DyCLewIDAQAB
keycloak.auth-server-url=http://localhost:8080/auth
keycloak.ssl-required=none
keycloak.resource=App-backend
keycloak.bearer-only=true
keycloak.credentials.secret=a714aede-5af9-4560-8c9d-d655c831772f
keycloak.securityConstraints[0].securityCollections[0].name=Secured API
keycloak.securityConstraints[0].securityCollections[0].authRoles[0]=ROLE_USER
keycloak.securityConstraints[0].securityCollections[0].patterns[0]=/api/*

It is working, however I think it is not an elegant solution. I have tried to implement this using the Keycloak property enable-basic-auth, but I could not understand how it works but it seems that it is just to protect Rest APIs, it does not allow the browser to create a session and use it for all the other requests.

Have someone ever had to implement something like this and can give me some better idea?

like image 272
sandro augusto Avatar asked Jun 06 '17 20:06

sandro augusto


2 Answers

I managed to solve this. However, how beautiful my solution is is up for debate.

My use case is that I need to secure most of my endpoints using Keycloak but some (for batch processing) should just use Basic Auth. Configuring both has the downside that Keycloak tries to validate the Authorization Header even if it is Basic Auth so I needed to do three things.

  1. Deactivate all automatic security for my batch route.
  2. Write a custom request filter which secures the batch route.
  3. Manipulate the servlet request object such that the zealous keycloak filter doesn't trip on it.

My security configuration.

@EnableWebSecurity
@EnableResourceServer
public class SecurityConfiguration extends KeycloakWebSecurityConfigureAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        super.configure(http);
        http.authorizeRequests()
             // usual configuration ...
             .antMatchers("/api/v1/batch/**").permitAll() // decouple security for this route
             .anyRequest().denyAll();
    }
}

My custom request filter (needs to run before the spring security filter, thus the ordering annotation):

@Component
@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE + 2)
public class BasicAuthRequestFilter extends OncePerRequestFilter {

    @Value("${batch.user}")
    private String user;

    @Value("${batch.password}")
    private String password;

    @Override
    protected void doFilterInternal(
        HttpServletRequest request, 
        HttpServletResponse response, 
        FilterChain filterChain
    ) throws ServletException, IOException {
        if (isBatchRequest(request)) {
            SimpleHttpFacade facade = new SimpleHttpFacade(request, response);
            if (AuthOutcome.AUTHENTICATED.equals(auth(facade))) {
                filterChain.doFilter(new AuthentifiedHttpServletRequest(request), response);
            }
            log.debug("Basic auth failed");
            SecurityContextHolder.clearContext();
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unable to authenticate with basic authentication");
            return;
        }
        filterChain.doFilter(request, response);
    }

    private boolean isBatchRequest(HttpServletRequest request) {
        return request.getRequestURI().startsWith("/api/v1/batch/");
    }

    private AuthOutcome auth(HttpFacade exchange)  {
        return extractToken(exchange.getRequest().getHeaders(HttpHeaders.AUTHORIZATION))
            .map(token -> extractUserPw(token)
            .filter(userpw -> verify(userpw.getFirst(), userpw.getSecond()))
            .map(userpw -> AuthOutcome.AUTHENTICATED)
            .orElse(AuthOutcome.FAILED))
        .orElse(AuthOutcome.NOT_ATTEMPTED);
    }

    private Optional<String> extractToken(List<String> authHeaders) {
        return authHeaders == null ? Optional.empty() : authHeaders.stream().map(authHeader -> authHeader.trim().split("\\s+"))
            .filter(split -> split.length == 2)
            .filter(split -> split[0].equalsIgnoreCase("Basic"))
            .map(split -> split[1])
            .findFirst();
    }

    private Optional<Pair<String, String>> extractUserPw(String token) {
        try {
            String userpw = new String(Base64.decode(token));
            String[] parts = userpw.split(":");
            if (parts.length == 2) {
                return Optional.of(Pair.of(parts[0], parts[1]));
            }
        } catch (Exception e) {
            log.debug("Basic Auth Token formatting error", e);
        }
        return Optional.empty();
    }

    private boolean verify(String user, String password) {
        return (this.user.equals(user) && this.password.equals(password));
    }

}

And finally the wrapped ServletRequest (as you cannot remove Headers from the request):

public class AuthentifiedHttpServletRequest extends HttpServletRequestWrapper {

    public AuthentifiedHttpServletRequest(HttpServletRequest request) {
        super(request);
    }

    @Override
    public boolean authenticate(HttpServletResponse response) throws IOException, ServletException {
        return true;
    }

    @Override
    public String getAuthType() {
        return "Basic";
    }

    @Override
    public String getHeader(String name) {
        if (!HttpHeaders.AUTHORIZATION.equalsIgnoreCase(name)) {
            return super.getHeader(name);
        }
        return null;
    }

    @Override
    public Enumeration<String> getHeaders(String name) {
        if (!HttpHeaders.AUTHORIZATION.equalsIgnoreCase(name)) {
            return super.getHeaders(name);
        }
        return Collections.enumeration(Collections.emptyList());
    }

    @Override
    public Enumeration<String> getHeaderNames() {
        return Collections.enumeration(EnumerationUtils.toList(super.getHeaderNames())
            .stream()
            .filter(s -> !HttpHeaders.AUTHORIZATION.equalsIgnoreCase(s))
            .collect(Collectors.toList()));
    }

    @Override
    public int getIntHeader(String name) {
        if (!HttpHeaders.AUTHORIZATION.equalsIgnoreCase(name)) {
            return super.getIntHeader(name);
        }
        return -1;
    }

}
like image 64
Yasammez Avatar answered Oct 13 '22 01:10

Yasammez


Not quite sure whether this is still relevant or not, but maybe someone will find it helpful.

By default, Keycloak is overwriting plenty of configurations. It's intercepting all Auth request (OAuth2, BasicAuth etc.)

Fortunately, with Keycloak, it's possible to enable authentication both with OAuth2 and BasicAuth in parallel, which I assume is what you want to enable in your dev/localhost environments.

In order to do that, you first need to add the following property to your application-local-auth.properties:

keycloak.enable-basic-auth=true

This property will enable Basic Auth in your dev environment. However, you also need to enable Basic Auth at your client in Keycloak.

You can accomplish that by connecting to the Keycloak Admin Console on your local Keycloak server and enabling the Direct Access Grant for your client:

Enabling Basic Auth in Keycloak

After that you can authenticate both with Bearer Token and Basic Auth.

like image 35
Konstantinos Giouzakov Avatar answered Oct 13 '22 00:10

Konstantinos Giouzakov