During this step-by-step you will be using Kubernetes Cluster and a Keycloak as SSO to secure our API Gateway and Microservices.
$ cat <<EOF | kind create cluster --name dev --config=-
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
kubeadmConfigPatches:
- |
kind: InitConfiguration
nodeRegistration:
kubeletExtraArgs:
node-labels: "ingress-ready=true"
extraPortMappings:
- containerPort: 80
hostPort: 80
protocol: TCP
- containerPort: 443
hostPort: 443
protocol: TCP
- role: worker
- role: worker
- role: worker
EOF
Don't forget to set current cluster/context
$ kubectl cluster-info --context kind-dev
Install Ingress Controller
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/static/provider/kind/deploy.yaml
helm repo add dev http://chartmuseum-jx.34.67.22.199.nip.io
helm repo update
helm install app-sso dev/fmtok8s-app-sso
$ kubectl create -f https://raw.githubusercontent.com/keycloak/keycloak-quickstarts/latest/kubernetes-examples/keycloak.yaml
Let's see if the keycloak pod is up and running with:
$ kubectl get pods
After installing keycloak you need to modify the base installation to set the keycloak service to use the port 80 instead of 8080, you can do this by editting the keyclaok service with:
$ kubectl edit svc keycloak
Modify the port from:
- name: http
nodePort: 32595
**port: 8080**
protocol: TCP
targetPort: 8080
to
- name: http
nodePort: 32595
**port: 80**
protocol: TCP
targetPort: 8080
Once this is done, you need to edit your /etc/hosts
files in your host machine to include the keycloak
host for the ingress definition to work:
$ sudo vi /etc/hosts
127.0.0.1 localhost keycloak
If you are using Windows
Go to file C:\Windows\System32\drivers\etc\hosts and add:
127.0.0.1 keycloak
If you already isntalled the application, an Ingress was created to route traffic to Keycloak under the keycloak
host. Point your browser to http://keycloak
to access the adminstration console.
User: admin Password: admin
What is Keycloak's Realm?
A realm manages a set of users, credentials, roles, and groups. In our example, we'll create a Realm for fmtok8s application. A user belongs nd accesses one realm, a realm are isolated from one another, then if you create an user in Realm A the another Realm (B) cannot see, and if you create an user on Realm A this user cannot access Realm B.What is Client Scopes?
"Client scopes allow you to define a common set of protocol mappers and roles. Wich are shared between multiple clients".It will help us to mapper roles in token.
In configure section, click on Client Scopes, after click on create button, to create a Client Scope.
You will se it:
Return to Client Scopes page and click on fmtok8s
Go to Mappers tab and click on create button.
Complete Protocol Mapper form, and click on save:
What is Keycloak's Client?
"Clients are applications and services that want to use Keycloak to secure themselves and provide a single sign-on solution". In our solution, we'll just one client API Gateway.The client configuration's page is very large, then I will divide it in two parts:
OAuth2 configuration:
Client Scope configuration:
Click on Client Scopes tab, and add fmtok8s
Client Scope
After, you should set the user's password
Changing API Gateway to secure our hidden microservices
API Gateway was created with Spring Cloud Gateway. The Spring Cloud Gateway uses Spring Webflux working with reactive stack.
There is a great lib called
org.keycloak:keycloak-spring-boot-starter
that help us to configure our application using keycloak and it runs better with Servlet applications. See
In this workshop, you will use Spring Security OAuth2. Let's go to use it.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
It is important to notice that this Spring Boot dependency makes the API Gateway component an OAuth2 Client but not a Server hosting restricted resources.
We should change the configuration of API Gateway on application.yml
spring:
security:
oauth2:
client:
provider:
oidc:
issuer-uri: http://keycloak/auth/realms/fmtok8s
registration:
oidc:
client-name: keycloak
provider: oidc
client-id: gateway
client-secret: <CLIENT SECRET>
scope:
- openid
Edit the API-Gateway deployment and add the following Environment Vairables:
$ kubectl edit deploy app-sso-fmtok8s-api-gateway
Look for the env section and add the following variables:
- name: SPRING_PROFILES_ACTIVE
value: sso
- name: SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_OIDC_ISSUER_URI
value: http://keycloak/auth/realms/fmtok8s
- name: SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_CLIENT_ID
value: gateway
- name: SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_CLIENT_SECRET
value: <CLIENT SECRET>
- name: SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_SCOPE_0
value: openid
The property client-secret should be filled with client-secret from keycloak, let's get our client-secret from gateway keycloak client:
Copy gateway's secret:
And add a filter a new filter on API Gateway to relay de Token to hiden services
spring:
cloud:
gateway:
default-filters:
- TokenRelay=
- RemoveRequestHeader=Cookie
package com.salaboy.conferences.site.security;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Profile;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcReactiveOAuth2UserService;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
import org.springframework.security.web.server.SecurityWebFilterChain;
import java.util.*;
import java.util.stream.Collectors;
@Profile("sso")
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
return http.csrf().disable()
.authorizeExchange()
.pathMatchers("/backoffice/**").hasAuthority("organizer")
.anyExchange().permitAll()
.and()
.oauth2Login()
.and()
.oauth2Client()
.and()
.build();
}
@Bean
public ReactiveOAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
final OidcReactiveOAuth2UserService delegate = new OidcReactiveOAuth2UserService();
return (userRequest) -> {
// Delegate to the default implementation for loading a user
return delegate.loadUser(userRequest).map(user -> {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
user.getAuthorities().forEach(authority -> {
if (authority instanceof OidcUserAuthority) {
OidcUserAuthority oidcUserAuthority = (OidcUserAuthority) authority;
mappedAuthorities.addAll(extractAuthorityFromClaims(oidcUserAuthority.getUserInfo().getClaims()));
}
});
return new DefaultOidcUser(mappedAuthorities, user.getIdToken(), user.getUserInfo());
});
};
}
public static List<GrantedAuthority> extractAuthorityFromClaims(Map<String, Object> claims) {
return mapRolesToGrantedAuthorities(getRolesFromClaims(claims));
}
private static Collection<String> getRolesFromClaims(Map<String, Object> claims) {
return (Collection<String>) claims.getOrDefault("roles", new ArrayList<>());
}
private static List<GrantedAuthority> mapRolesToGrantedAuthorities(Collection<String> roles) {
return roles.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
}
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
Notice that here we need spring-security-oauth2-resource-server
and spring-security-oauth2-jose
to secure REST Endpoints hosted by this service.
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://keycloak/auth/realms/fmtok8s
Edit the other services Deployments with the following Environment Variables:
$ kubectl edit deploy app-sso-fmtok8s-c4p-rest
Look for the env section and add the following variables:
- name: SPRING_PROFILES_ACTIVE
value: sso
- name: SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI
value: http://keycloak/auth/realms/fmtok8s
package com.salaboy.conferences.c4p.rest.configuration;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.config.CorsRegistry;
import org.springframework.web.reactive.config.WebFluxConfigurer;
@Configuration
public class CORSConfig implements WebFluxConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowCredentials(true).allowedMethods("*");
}
}
package com.salaboy.conferences.c4p.rest.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Profile;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter;
import org.springframework.security.web.server.SecurityWebFilterChain;
import reactor.core.publisher.Mono;
import java.util.*;
import java.util.stream.Collectors;
@Profile("sso")
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http
.csrf().disable()
.authorizeExchange(exchanges ->
exchanges
.pathMatchers(HttpMethod.POST, "/decide**").hasAuthority("organizer")
.pathMatchers(HttpMethod.POST, "/").permitAll()
.pathMatchers(HttpMethod.GET, "/actuator/health").permitAll()
.pathMatchers(HttpMethod.GET, "/actuator/info").permitAll()
.pathMatchers(HttpMethod.GET, "/prometheus").permitAll()
.anyExchange().permitAll()
)
.oauth2ResourceServer(oauth2 ->
oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(grantedAuthoritiesExtractor())));
return http.build();
}
Converter<Jwt, Mono<AbstractAuthenticationToken>> grantedAuthoritiesExtractor() {
JwtAuthenticationConverter jwtAuthenticationConverter =
new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(new GrantedAuthoritiesExtractor());
return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);
}
static class GrantedAuthoritiesExtractor implements Converter<Jwt, Collection<GrantedAuthority>> {
@Override
public Collection<GrantedAuthority> convert(Jwt jwt) {
var roles = (List<String>) jwt.getClaims().getOrDefault("roles", Collections.emptyList());
return roles.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
}
}
Before you start, I want to speak a bit about OAuth2 concepts:
OAuth2 defines four roles:
-
Resource Owner
It is an user that have some resource protected, like your facebook account, Github repositories, Instagram images etc.
In our case a Resource Owner is the admin user that approves or rejects a proposal
-
Resource Server
Provides a protected resource and permits the access through valid access tokens. Examples of Resource Server: Facebook, Github, Instagram etc.
In our case a Resource Server will be C4P Service, Email Service and Agenda Service
-
Authorization Server
Authorization Server provides an API used to autenticates an user and generates an access token. Examples of Authorizaton Server: Spring Authorization Server, Okta, Google Identity etc.
In our case, Keycloak is our Authorization Server
-
Client
It is an application, web, SPA, Desktop or mobile, that want to access the resources from Resource Owner. A Client needs to be registred on Authorization Server, being identified by a client id and a client secret.
In our case a Client is the API Gateway
A Grant Type is a way that a Client use to give an access token, OAuth2 is very flexible and provides four grant types for us:
-
Password
Used when there is a strong relationship from Client and Authorization Server. The user provides your credentials (username and password) directly to Client, that forward (with his client id and client secret) the user's credentials to Authorization Server.
-
Client Credentials
Used when there is not an user involved, it is used when there is a sytem called a protected system, just client's credentials are provides to Authorization Server.
-
Authorization Code
Used when third applications want to access data from a protected resource without the Client see the user's credentials. By example: When an user (Resource Owner) permits that the Travis CI (Client) access your repositories from Github (Authorization Server and Resource Owner).
-
Implicit
Is mostly used on Single Page Applications and Mobile Apps. The user is redirected to Authorization Server's login page, but the redirect is made directly to user-agent (A browser, by example) with access token. With this way, the SPA or Mobile application knows directly the access token.
It is a brief explanation about OAuth2, to more details look here
In this workshop we'll use Authorization Code Grant Types
Authorization Server: Keycloak
Client: API Gateway
Resourse Owner: Admin
Resource Server: C4P Service, Agenda Service, Email Service