En nuestro capítulo anterior vimos cómo implementar Proof Of Key en el lado del servidor usando OAUTH 2.0 con SpringBoot. Hoy corresponde enfocarnos en ese concepto del lado del cliente. En esta oportunidad vamos a desarrollar una aplicación que interactúe con el servidor o proveedor de OAUTH tras demostrar que está en posesión de la clave privada relacionada con la clave pública que se nos provee en el JWT correspondiente.
Al igual que la anterior, esta publicación va a hacer uso de Java 8, Maven, Spring Web, Spring Security y Nimbus JOSE + JWT.
Así que sin mucho preámbulo vamos manos a las sobras!!
- Primero que todo, vamos a empezar por crear la aplicación pop-client usando el inicializador de spring: Spring Initializr.
Escoja un Tipo de proyecto Maven. Java como lenguaje. Sprig boot 2.1.3.
Ingrese su Group de preferencia como por ejemplo:
org.pigbar.hal9k.oauth2.popIngrese el nombre del Artifact:
pop-client
En las dependencias agregue:
Web, Security y ThymeleafGenere el proyecto, descárguelo y proceda a importarlo en su IDE de preferencia como un proyecto Maven existente. - En su proyecto Maven verifique el contenido del archivo pom.xml, debe ser como el siguiente:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
<
project
xmlns:xsi
=
"http://www.w3.org/2001/XMLSchema-instance"
xmlns
=
"http://maven.apache.org/POM/4.0.0"
xsi:schemalocation
=
"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
>
<
modelversion
>4.0.0</
modelversion
>
<
parent
>
<
groupid
>org.springframework.boot</
groupid
>
<
artifactid
>spring-boot-starter-parent</
artifactid
>
<
version
>2.1.3.RELEASE</
version
>
<
relativepath
>
</
relativepath
></
parent
>
<
groupid
>org.pigbar.hal9k.oauth2.pop</
groupid
>
<
artifactid
>pop-client</
artifactid
>
<
version
>0.0.1-SNAPSHOT</
version
>
<
name
>pop-client</
name
>
<
description
>Demo project for Spring Boot</
description
>
<
properties
>
<
java
.version
=
""
>1.8</
java
>
</
properties
>
<
dependencies
>
<
dependency
>
<
groupid
>org.springframework.boot</
groupid
>
<
artifactid
>spring-boot-starter-security</
artifactid
>
</
dependency
>
<
dependency
>
<
groupid
>org.springframework.boot</
groupid
>
<
artifactid
>spring-boot-starter-thymeleaf</
artifactid
>
</
dependency
>
<
dependency
>
<
groupid
>org.springframework.boot</
groupid
>
<
artifactid
>spring-boot-starter-web</
artifactid
>
</
dependency
>
<
dependency
>
<
groupid
>org.springframework.boot</
groupid
>
<
artifactid
>spring-boot-starter-test</
artifactid
>
<
scope
>test</
scope
>
</
dependency
>
<
dependency
>
<
groupid
>org.springframework.security</
groupid
>
<
artifactid
>spring-security-test</
artifactid
>
<
scope
>test</
scope
>
</
dependency
>
<
dependency
>
<
groupid
>org.springframework.security.oauth</
groupid
>
<
artifactid
>spring-security-oauth2</
artifactid
>
<
version
>2.2.0.RELEASE</
version
>
</
dependency
>
<
dependency
>
<
groupid
>org.springframework.security</
groupid
>
<
artifactid
>spring-security-jwt</
artifactid
>
<
version
>1.0.10.RELEASE</
version
>
</
dependency
>
<
dependency
>
<
groupid
>com.nimbusds</
groupid
>
<
artifactid
>nimbus-jose-jwt</
artifactid
>
<
version
>4.23</
version
>
</
dependency
>
</
dependencies
>
<
build
>
<
plugins
>
<
plugin
>
<
groupid
>org.springframework.boot</
groupid
>
<
artifactid
>spring-boot-maven-plugin</
artifactid
>
</
plugin
>
</
plugins
>
</
build
>
</
project
>
- Dentro de la carpeta src/maim/resources, en el archivo application.properties ingrese los siguientes registros de key = value:
123456789spring.security.user.name=pigbar
spring.security.user.password=1234
security.oauth2.resource.jwt.key-uri=http:
//localhost:8080/oauth/token_key
security.oauth2.client.client-id=clientapp
security.oauth2.client.client-secret=123456
server.port=9000
spring.http.converters.preferred-json-mapper=jackson
spring.thymeleaf.cache=
false
- En el package org.pigbar.hal9k.oauth2.pop cree los siguientes sub-packages:
oauth
dashboard - En el sub-package ouath vamos a crear una clase denominada JwkKeyPairManager, que será la encargada de manejar el keypair para firmar el atributo nonce del header y demostrar que se posee la clave privada (POP).
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465package
org.pigbar.hal9k.oauth2.pop.oauth;
import
java.security.KeyPair;
import
java.security.KeyPairGenerator;
import
java.security.NoSuchAlgorithmException;
import
java.security.interfaces.RSAPrivateKey;
import
java.security.interfaces.RSAPublicKey;
import
org.springframework.security.oauth2.common.util.RandomValueStringGenerator;
import
org.springframework.stereotype.Component;
import
com.nimbusds.jose.JWSAlgorithm;
import
com.nimbusds.jose.JWSHeader;
import
com.nimbusds.jose.JWSObject;
import
com.nimbusds.jose.Payload;
import
com.nimbusds.jose.crypto.RSASSASigner;
import
com.nimbusds.jose.jwk.JWK;
import
com.nimbusds.jose.jwk.RSAKey;
@Component
public
class
JwkKeyPairManager {
private
final
JWK clientJwk;
public
JwkKeyPairManager() {
KeyPair keyPair = createRSA256KeyPair();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
RandomValueStringGenerator random =
new
RandomValueStringGenerator();
RSAKey.Builder builder =
new
RSAKey.Builder(publicKey);
builder.keyID(random.generate());
builder.privateKey(privateKey);
this
.clientJwk = builder.build();
}
public
JWK createJWK() {
return
clientJwk.toPublicJWK();
}
public
String getSignedContent(String content) {
Payload contentPayload =
new
Payload(content);
try
{
RSASSASigner rsa =
new
RSASSASigner(((RSAKey) clientJwk).toPrivateKey());
JWSAlgorithm alg = JWSAlgorithm.RS256;
JWSHeader header =
new
JWSHeader.Builder(alg).keyID(clientJwk.getKeyID()).build();
JWSObject jws =
new
JWSObject(header, contentPayload);
jws.sign(rsa);
return
jws.serialize();
}
catch
(Exception e) {
throw
new
RuntimeException(e);
}
}
private
KeyPair createRSA256KeyPair() {
try
{
KeyPairGenerator generator = KeyPairGenerator.getInstance(
"RSA"
);
generator.initialize(
2048
);
return
generator.generateKeyPair();
}
catch
(NoSuchAlgorithmException e) {
throw
new
RuntimeException(e);
}
}
}
- Ahora, en el mismo paquete, vamos a crear un una clase denominada PoPTokenRequestEnhancer que se va a encargar de enviar la clave pública al servidor de autorización, Authorization Server, cuando se solicite un nuevo Token de acceso.
12345678910111213141516171819202122232425package
org.pigbar.hal9k.oauth2.pop.oauth;
import
org.springframework.beans.factory.annotation.Autowired;
import
org.springframework.http.HttpHeaders;
import
org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
import
org.springframework.security.oauth2.client.token.AccessTokenRequest;
import
org.springframework.security.oauth2.client.token.RequestEnhancer;
import
org.springframework.stereotype.Component;
import
org.springframework.util.MultiValueMap;
@Component
public
class
PoPTokenRequestEnhancer
implements
RequestEnhancer {
@Autowired
private
JwkKeyPairManager keyPairManager;
@Override
public
void
enhance(AccessTokenRequest request,
OAuth2ProtectedResourceDetails resource,
MultiValueMap<String, String> form,
HttpHeaders headers) {
form.add(
"public_key"
, keyPairManager.createJWK().toJSONString());
}
}
- Como el cliente debe enviar el nonce firmado cada vez que interactúa con un recurso protegido, vamos a crear un Interceptor, HttpRequestWithPoPSignatureInterceptor, para agregar estos datos a cada endpoint externo con el que se trabaje. En el mismo paquete creamos lo siguiente:
1234567891011121314151617181920212223242526272829303132333435363738394041424344package
org.pigbar.hal9k.oauth2.pop.oauth;
import
org.springframework.beans.BeansException;
import
org.springframework.beans.factory.annotation.Autowired;
import
org.springframework.context.ApplicationContext;
import
org.springframework.context.ApplicationContextAware;
import
org.springframework.http.HttpRequest;
import
org.springframework.http.client.ClientHttpRequestExecution;
import
org.springframework.http.client.ClientHttpRequestInterceptor;
import
org.springframework.http.client.ClientHttpResponse;
import
org.springframework.security.oauth2.client.OAuth2ClientContext;
import
org.springframework.security.oauth2.common.OAuth2AccessToken;
import
org.springframework.stereotype.Component;
import
java.io.IOException;
import
java.util.UUID;
@Component
public
class
HttpRequestWithPoPSignatureInterceptor
implements
ClientHttpRequestInterceptor, ApplicationContextAware {
private
ApplicationContext applicationContext;
@Autowired
private
JwkKeyPairManager keyPairManager;
@Override
public
void
setApplicationContext(ApplicationContext applicationContext)
throws
BeansException {
this
.applicationContext = applicationContext;
}
@Override
public
ClientHttpResponse intercept(HttpRequest request,
byte
[] body,
ClientHttpRequestExecution execution)
throws
IOException {
OAuth2ClientContext clientContext = applicationContext.getBean(OAuth2ClientContext.
class
);
OAuth2AccessToken accessToken = clientContext.getAccessToken();
request.getHeaders().set(
"Authorization"
,
"Bearer "
+ accessToken.getValue());
request.getHeaders().set(
"nonce"
, keyPairManager.getSignedContent(UUID.randomUUID().toString()));
return
execution.execute(request, body);
}
}
- Como es necesario mantener o guardar los tokens, vamos a crear un servicio que se encargue de administrar los tokens del cliente o usuario. En el mismo paquete creamos el siguiente servicio:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172package
org.pigbar.hal9k.oauth2.pop.oauth;
import
java.util.Date;
import
java.util.Map;
import
java.util.concurrent.ConcurrentHashMap;
import
org.springframework.security.core.Authentication;
import
org.springframework.security.core.userdetails.User;
import
org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
import
org.springframework.security.oauth2.client.token.ClientTokenServices;
import
org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import
org.springframework.security.oauth2.common.OAuth2AccessToken;
import
org.springframework.stereotype.Service;
@Service
public
class
OAuth2ClientTokenSevices
implements
ClientTokenServices {
private
ConcurrentHashMap<String, ClientUser> users =
new
ConcurrentHashMap<>();
@Override
public
OAuth2AccessToken getAccessToken(OAuth2ProtectedResourceDetails resource, Authentication authentication) {
ClientUser clientUser = getClientUser(authentication);
if
(clientUser.accessToken ==
null
)
return
null
;
DefaultOAuth2AccessToken oAuth2AccessToken =
new
DefaultOAuth2AccessToken(clientUser.accessToken);
oAuth2AccessToken.setAdditionalInformation(clientUser.additionalInformation);
oAuth2AccessToken.setExpiration(
new
Date(clientUser.expirationTime));
return
oAuth2AccessToken;
}
@Override
public
void
saveAccessToken(OAuth2ProtectedResourceDetails resource,
Authentication authentication, OAuth2AccessToken accessToken) {
ClientUser clientUser = getClientUser(authentication);
clientUser.accessToken = accessToken.getValue();
clientUser.expirationTime = accessToken.getExpiration().getTime();
clientUser.additionalInformation = accessToken.getAdditionalInformation();
users.put(clientUser.username, clientUser);
}
@Override
public
void
removeAccessToken(OAuth2ProtectedResourceDetails resource,
Authentication authentication) {
users.remove(getClientUser(authentication).username);
}
private
ClientUser getClientUser(Authentication authentication) {
String username = ((User) authentication.getPrincipal()).getUsername();
ClientUser clientUser = users.get(username);
if
(clientUser ==
null
) {
clientUser =
new
ClientUser(username);
}
return
clientUser;
}
private
static
class
ClientUser {
private
String username;
private
String accessToken;
private
Map<String, Object> additionalInformation;
private
long
expirationTime;
public
ClientUser(String username) {
this
.username = username;
}
}
}
- Procedamos ahora a crear la clase que se encargará de la configuración del cliente. Por lo tanto, en el paquete de costumbre, procedemos a crear:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869package
org.pigbar.hal9k.oauth2.pop.oauth;
import
java.util.Arrays;
import
org.springframework.beans.factory.annotation.Autowired;
import
org.springframework.context.annotation.Bean;
import
org.springframework.context.annotation.Configuration;
import
org.springframework.security.oauth2.client.OAuth2ClientContext;
import
org.springframework.security.oauth2.client.OAuth2RestTemplate;
import
org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
import
org.springframework.security.oauth2.client.token.AccessTokenProviderChain;
import
org.springframework.security.oauth2.client.token.ClientTokenServices;
import
org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider;
import
org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails;
import
org.springframework.security.oauth2.common.AuthenticationScheme;
import
org.springframework.security.oauth2.config.annotation.web.configuration.EnableOAuth2Client;
@Configuration
@EnableOAuth2Client
public
class
ClientConfiguration {
@Autowired
private
ClientTokenServices clientTokenServices;
@Autowired
private
OAuth2ClientContext oauth2ClientContext;
@Autowired
private
HttpRequestWithPoPSignatureInterceptor interceptor;
@Autowired
private
PoPTokenRequestEnhancer requestEnhancer;
@Bean
public
AuthorizationCodeResourceDetails authorizationCode() {
AuthorizationCodeResourceDetails resourceDetails =
new
AuthorizationCodeResourceDetails();
resourceDetails.setId(
"oauth2server"
);
resourceDetails.setTokenName(
"oauth_token"
);
resourceDetails.setClientId(
"clientapp"
);
resourceDetails.setClientSecret(
"123456"
);
resourceDetails.setScope(Arrays.asList(
"read_profile"
));
resourceDetails.setUseCurrentUri(
false
);
resourceDetails.setClientAuthenticationScheme(AuthenticationScheme.header);
return
resourceDetails;
}
@Bean
public
OAuth2RestTemplate oauth2RestTemplate() {
OAuth2ProtectedResourceDetails resourceDetails = authorizationCode();
OAuth2RestTemplate template =
new
OAuth2RestTemplate(resourceDetails, oauth2ClientContext);
AuthorizationCodeAccessTokenProvider authorizationCode =
new
AuthorizationCodeAccessTokenProvider();
authorizationCode.setTokenRequestEnhancer(requestEnhancer);
AccessTokenProviderChain provider =
new
AccessTokenProviderChain(Arrays.asList(authorizationCode));
provider.setClientTokenServices(clientTokenServices);
template.setAccessTokenProvider(provider);
template.setInterceptors(Arrays.asList(interceptor));
return
template;
}
}
- Lista la configuración, procedemos entonces a definir los controles de acceso para este cliente. Nuevamente, en el mismo paquete, creamos lo siguiente:
123456789101112131415package
org.pigbar.hal9k.oauth2.pop.oauth;
import
org.springframework.context.annotation.Configuration;
import
org.springframework.security.config.annotation.web.builders.HttpSecurity;
import
org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import
org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity
public
class
WebSecurityConfig
extends
WebSecurityConfigurerAdapter {
@Override
protected
void
configure(HttpSecurity http)
throws
Exception {
http.formLogin().permitAll().and().authorizeRequests().antMatchers(
"/"
,
"/index**"
,
"/callback**"
,
"/dashboard"
)
.authenticated().anyRequest().authenticated();
}
}
- Como esta es una aplicación cliente, es lógico que creemos algunos recursos con los que el usuario pueda interactuar. Los siguientes elementos se usan para definir un Dashboard de cliente. En el paquete dashboard procedemos a crear:
Clase EntryClase UserProfile12345678910111213141516package
org.pigbar.hal9k.oauth2.pop.dashboard;
public
class
Entry {
private
String value;
public
Entry(String value) {
super
();
this
.value = value;
}
public
String getValue() {
return
value;
}
}
Clase UserDashboard1234567891011121314151617181920212223package
org.pigbar.hal9k.oauth2.pop.dashboard;
public
class
UserProfile {
private
String name;
private
String email;
public
String getName() {
return
name;
}
public
void
setName(String name) {
this
.name = name;
}
public
String getEmail() {
return
email;
}
public
void
setEmail(String email) {
this
.email = email;
}
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051package
org.pigbar.hal9k.oauth2.pop.dashboard;
import
org.springframework.beans.factory.annotation.Autowired;
import
org.springframework.security.oauth2.client.OAuth2RestTemplate;
import
org.springframework.stereotype.Controller;
import
org.springframework.web.bind.annotation.GetMapping;
import
org.springframework.web.client.HttpClientErrorException;
import
org.springframework.web.servlet.ModelAndView;
import
java.util.Arrays;
import
java.util.List;
@Controller
public
class
UserDashboard {
@Autowired
private
OAuth2RestTemplate restTemplate;
@GetMapping
(
"/"
)
public
String home() {
return
"index"
;
}
@GetMapping
(
"/callback"
)
public
ModelAndView callback(String code, String state) {
return
new
ModelAndView(
"forward:/dashboard"
);
}
@GetMapping
(
"/dashboard"
)
public
ModelAndView dashboard() {
List<Entry> entries = Arrays.asList(
new
Entry(
"entry 1"
),
new
Entry(
"entry 2"
));
ModelAndView mv =
new
ModelAndView(
"dashboard"
);
mv.addObject(
"entries"
, entries);
tryToGetUserProfile(mv);
return
mv;
}
private
void
tryToGetUserProfile(ModelAndView mv) {
try
{
UserProfile userProfile = restTemplate.getForObject(endpoint, UserProfile.
class
);
mv.addObject(
"profile"
, userProfile);
}
catch
(HttpClientErrorException e) {
throw
new
RuntimeException(
"it was not possible to retrieve user profile"
);
}
}
}
- Ahora, en la carpeta /pop-client/src/main/resources/templates creamos los siguientes archivos:
Archivo index.html:12345678<
head
>
<
title
>client app</
title
>
</
head
>
<
body
>
<
a
href
=
"/dashboard"
>Go to your dashboard</
a
>
</
body
>
</
html
>
Archivo dashboard.html:
1234567891011121314151617181920212223242526272829<!DOCTYPE html>
<
head
>
<
title
>client app</
title
>
</
head
>
<
body
>
<
h1
>that's your dashboard</
h1
>
<
table
>
<
tr
>
<
td
><
b
>That's your entries</
b
></
td
>
</
tr
>
<
tr
th:each
=
"entry : ${entries}"
>
<
td
th:text
=
"${entry.value}"
>value</
td
>
</
tr
>
</
table
>
<
h3
>your profile from [Profile Application]</
h3
>
<
table
>
<
tr
>
<
td
><
b
>name</
b
></
td
>
<
td
th:text
=
"${profile.name}"
>username</
td
>
</
tr
>
<
tr
>
<
td
><
b
>email</
b
></
td
>
<
td
th:text
=
"${profile.email}"
>email</
td
>
</
tr
>
</
table
>
</
body
>
</
html
>
- Finalmente, procedemos a crear nuestra clase de Application de springboot. Para ello nos dirigimos al paquete org.pigbar.hal9k.oauth2.pop y creamos la siguiente clase:
123456789101112131415161718192021package
org.pigbar.hal9k.oauth2.pop;
import
javax.servlet.ServletContext;
import
javax.servlet.ServletException;
import
org.springframework.boot.SpringApplication;
import
org.springframework.boot.autoconfigure.SpringBootApplication;
import
org.springframework.boot.web.servlet.ServletContextInitializer;
@SpringBootApplication
public
class
PopClientApplication
implements
ServletContextInitializer {
public
static
void
main(String[] args) {
SpringApplication.run(PopClientApplication.
class
, args);
}
@Override
public
void
onStartup(ServletContext servletContext)
throws
ServletException {
servletContext.getSessionCookieConfig().setName(
"client-session"
);
}
}
Ya hemos creado nuestro cliente de OAUTH 2.0 con el mecanismo de Proof Of Possession of Key.
Ejecute el servidor. Ejecute este cliente. Vaya en el navegador a la URL http://localhost:9000 e introduzca sus credenciales, a saber pigbar y 1234 para el usuario y la clave respectivamente. Haga click en el enlace para el Dashboard...
y veamos que sucede!!!
P.D.: Va a notar que debe hacer Login dos veces, una vez en el cliente y otra en el servidor. Como evitar ese doble Login? Esta es precisamente su tarea a resolver!!!
Ejecute el servidor. Ejecute este cliente. Vaya en el navegador a la URL http://localhost:9000 e introduzca sus credenciales, a saber pigbar y 1234 para el usuario y la clave respectivamente. Haga click en el enlace para el Dashboard...
y veamos que sucede!!!
P.D.: Va a notar que debe hacer Login dos veces, una vez en el cliente y otra en el servidor. Como evitar ese doble Login? Esta es precisamente su tarea a resolver!!!
Comentarios
Publicar un comentario