diff --git a/sso-imgs/sso-1.png b/sso-imgs/sso-1.png new file mode 100644 index 0000000..6271ad7 Binary files /dev/null and b/sso-imgs/sso-1.png differ diff --git a/sso-imgs/sso-10.png b/sso-imgs/sso-10.png new file mode 100644 index 0000000..6199df3 Binary files /dev/null and b/sso-imgs/sso-10.png differ diff --git a/sso-imgs/sso-11.png b/sso-imgs/sso-11.png new file mode 100644 index 0000000..6293084 Binary files /dev/null and b/sso-imgs/sso-11.png differ diff --git a/sso-imgs/sso-12.png b/sso-imgs/sso-12.png new file mode 100644 index 0000000..5ab8ee4 Binary files /dev/null and b/sso-imgs/sso-12.png differ diff --git a/sso-imgs/sso-13.png b/sso-imgs/sso-13.png new file mode 100644 index 0000000..52e9179 Binary files /dev/null and b/sso-imgs/sso-13.png differ diff --git a/sso-imgs/sso-14.png b/sso-imgs/sso-14.png new file mode 100644 index 0000000..dc60b6d Binary files /dev/null and b/sso-imgs/sso-14.png differ diff --git a/sso-imgs/sso-2.png b/sso-imgs/sso-2.png new file mode 100644 index 0000000..57c80fa Binary files /dev/null and b/sso-imgs/sso-2.png differ diff --git a/sso-imgs/sso-3.png b/sso-imgs/sso-3.png new file mode 100644 index 0000000..280a96b Binary files /dev/null and b/sso-imgs/sso-3.png differ diff --git a/sso-imgs/sso-4.png b/sso-imgs/sso-4.png new file mode 100644 index 0000000..43ff025 Binary files /dev/null and b/sso-imgs/sso-4.png differ diff --git a/sso-imgs/sso-5.png b/sso-imgs/sso-5.png new file mode 100644 index 0000000..daa8b75 Binary files /dev/null and b/sso-imgs/sso-5.png differ diff --git a/sso-imgs/sso-6.png b/sso-imgs/sso-6.png new file mode 100644 index 0000000..97f5b39 Binary files /dev/null and b/sso-imgs/sso-6.png differ diff --git a/sso-imgs/sso-7.png b/sso-imgs/sso-7.png new file mode 100644 index 0000000..2cae680 Binary files /dev/null and b/sso-imgs/sso-7.png differ diff --git a/sso-imgs/sso-8.png b/sso-imgs/sso-8.png new file mode 100644 index 0000000..796f704 Binary files /dev/null and b/sso-imgs/sso-8.png differ diff --git a/sso-imgs/sso-9.png b/sso-imgs/sso-9.png new file mode 100644 index 0000000..9f46a86 Binary files /dev/null and b/sso-imgs/sso-9.png differ diff --git a/sso.md b/sso.md new file mode 100644 index 0000000..c9e143d --- /dev/null +++ b/sso.md @@ -0,0 +1,421 @@ +# From Monolith to K8s - Workshop + + +## Installing and Configuring Keycloak +During this step-by-step you will be using **Kubernetes Cluster** and a Keycloak as SSO to secure our API Gateway and Microservices. + +### Creating a Kubernetes Cluster with KIND + +``` +$ kind create cluster --name keycloak +``` + +Don't forget to set current cluster/context + +``` +$ kubectl cluster-info --context kind-keycloak +``` + +In this example we'll not create a namespace (It is not a best practice ) + +### Adding Keycloak on Cluster Kubernetes + +``` +kubectl cretate -f https://raw.githubusercontent.com/keycloak/keycloak-quickstarts/latest/kubernetes-examples/keycloak.yaml +``` + +Let's see the keycloak pod + +``` +$ kubectl get pods +``` + +## OAuth2 Concepts + +Before you start, I want to speak a bit about OAuth2 concepts: + +### OAuth2 Roles +
+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** + +
+ +### OAuth2 Grant Types: + +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](https://oauth.net/2/) + +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* + + +## Configuring Keycloak +### 1 - Let's access Administration Console: + +Go to Administration Console + +### 2 - We'll access using our credentials passed through configurations + +Login + +### 3 - Let's create our realm (fmtok8s) + +
+
+ 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. +
+
+ +Creating Keycloak Realm + +
+ +### 4 - Creating a Realm's Client + +
+
+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. +
+
+ +Creating Realm's Client part 1 + +Creating Realm's Client part 2 + +### 5 - Configuring a Client + +The client configuration's page is very large, then I will divide it in two parts: + +Parte 1: +Configuring The Client Gateway part 1 + +Parte 2: +Configuring The Client Gateway part 2 + +## 6 - Creating an Realm's User + +Creating an user to Realm + +Adding an user to Realm + +After, you should set the user's password + +Setting user's password + +### 7 - Creating a Realm's Role + +Creating a Realm's Role + +Adding Realm's Role + +### 8 - Adding a role to user + +Adding Realm's Role to User + + +## Changing API Gateway to secure our hidden microservices + +[API Gateway](https://github.com/mcruzdev/fmtok8s-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](https://keycloak.discourse.group/t/webflux-support-for-spring-boot-and-spring-security-adapters/2936) + +In this workshop, you will use Spring Security OAuth2. Let's go to use it. + +### Adding Spring OAuth2 dependecies in API Gateway + +``` + + org.springframework.boot + spring-boot-starter-oauth2-client + + + org.springframework.cloud + spring-cloud-starter-security + +``` + +### Configuring + +We should change the configuration of **API Gateway** on application.yml + +``` +spring: + security: + oauth2: + client: + provider: + oidc: + issuer-uri: http://localhost:8081/auth/realms/fmtok8s + registration: + oidc: + client-name: keycloak + provider: oidc + client-id: gateway + client-secret: 7208a758-e57c-4045-8c4a-9831bb2b8144 +``` + +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:** + +Adding Realm's Role + + +And add a filter a new filter on **API Gateway** to relay de Token to hiden services + +``` + cloud: + gateway: + default-filters: + - TokenRelay= + - RemoveRequestHeader=Cookie + httpclient: +``` + +### Creating class SecurityConfig to our Gateway + +``` +package com.salaboy.conferences.site.security; + +import org.springframework.context.annotation.Bean; +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; + +@EnableWebFluxSecurity +public class SecurityConfig { + + @Bean + public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + return http.csrf().disable() + .authorizeExchange() + .pathMatchers("/backoffice/**").hasRole("approver") + .anyExchange().permitAll() + .and() + .oauth2Login() + .and() + .oauth2Client() + .and() + .build(); + } + + @Bean + public ReactiveOAuth2UserService oidcUserService() { + final OidcReactiveOAuth2UserService delegate = new OidcReactiveOAuth2UserService(); + + return (userRequest) -> { + // Delegate to the default implementation for loading a user + return delegate.loadUser(userRequest).map(user -> { + Set 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 extractAuthorityFromClaims(Map claims) { + return mapRolesToGrantedAuthorities(getRolesFromClaims(claims)); + } + + @SuppressWarnings("unchecked") + private static Collection getRolesFromClaims(Map claims) { + return (Collection) claims.getOrDefault("groups", + claims.getOrDefault("roles", new ArrayList<>())); + } + + private static List mapRolesToGrantedAuthorities(Collection roles) { + return roles.stream() + .map("ROLE_"::concat) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + } +} +``` + + +## Securing our microservices (C4P) + +### We need to add some dependecies on pom.xml + +``` + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.security + spring-security-oauth2-resource-server + + + + org.springframework.security + spring-security-oauth2-jose + +``` + +### Configuring Spring OAuth2 Resource Server through application.properties: + +``` +spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8080/auth/realms/fmtok8s +``` + +### Creat an class to configure CORS + +``` +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("*"); + } +} +``` + +### Creating our SecurityConfig + +``` +package com.salaboy.conferences.c4p.rest.configuration; + +import org.springframework.context.annotation.Bean; +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.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +@EnableWebFluxSecurity +public class SecurityConfig { + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + + http + .csrf().disable() + .authorizeExchange(exchanges -> + exchanges + .pathMatchers(HttpMethod.POST, "/**").hasAnyAuthority("approver") + .pathMatchers(HttpMethod.DELETE, "/**").hasAnyAuthority("approver") + .anyExchange().permitAll()) + .oauth2ResourceServer(oauth2 -> + oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(grantedAuthoritiesExtractor()))); + + return http.build(); + } + + Converter> grantedAuthoritiesExtractor() { + JwtAuthenticationConverter jwtAuthenticationConverter = + new JwtAuthenticationConverter(); + + jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(new GrantedAuthoritiesExtractor()); + + return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter); + } + + static class GrantedAuthoritiesExtractor implements Converter> { + + @Override + public Collection convert(Jwt jwt) { + + @SuppressWarnings("unchecked") + var roles = (List) jwt.getClaims().getOrDefault("groups", Collections.emptyList()); + + return roles.stream() + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + } + } +} + +``` \ No newline at end of file