Enterprise systems rarely evolve in a perfectly linear way. Over time, authentication flows, external services, legacy integrations, and business rules tend to overlap. One common scenario is when a system starts with a direct integration to one identity provider and later needs to adapt to a new institutional integration layer without breaking the existing login process.
Recently, I worked on a Java enterprise application where the OAuth2 login flow was already working, but the user information retrieval was tightly coupled to two specific IDG consumers:
ConsumerCidadaoRS
ConsumerEstabelecimentoRS
The goal was to evaluate whether these direct consumers could be replaced by a unified SOAP service exposed through an existing WSDL:
iddigitalPipelineProxyService
This service exposed two operations:
consultaCidadao
consultaEstabelecimento
The solution was not to rewrite the authentication flow, but to isolate the new SOAP integration behind a provider class and keep the rest of the login process unchanged.
The Existing Login Flow
The application used an OAuth2AuthenticationFilter to control the IDG login flow. At a high level, the flow worked like this:
User accesses protected URL
↓
OAuth2AuthenticationFilter redirects to IDG
↓
IDG authenticates the user
↓
Application receives authorization code
↓
Filter exchanges code for TokenDTO
↓
Filter reads documentNumber from token
↓
Filter queries user data
↓
UserInfoDTO is stored in session
The important part was the user data retrieval step.
After receiving the TokenDTO, the filter checked whether the authenticated document was a CPF or CNPJ. If it was CPF, the application used ConsumerCidadaoRS. If it was CNPJ, the application used ConsumerEstabelecimentoRS.
The code looked conceptually like this:
if (Util.validarCPF(responseToken.getDocumentNumber())) {
CidadaoEntity entity =
new ConsumerCidadaoRS(citizenUserInformationURI)
.obter(responseToken.getDocumentNumber());
userInfoDTO.setCPF(entity.getCpf());
userInfoDTO.setNome(entity.getNome());
userInfoDTO.setDtNascimento(entity.getDtNascimento());
userInfoDTO.setDtAtualizacao(entity.getDtAtualizacao());
userInfoDTO.setDtUltimaAtualizacaoRFB(entity.getDtUltimaAtualizacaoRFB());
userInfoDTO.setEmail(entity.getEmail());
} else if (Util.validarCNPJ(responseToken.getDocumentNumber())) {
EstabelecimentoEntity entity =
new ConsumerEstabelecimentoRS(companyUserInformationURI)
.obter(responseToken.getDocumentNumber());
userInfoDTO.setCNPJ(entity.getCnpj());
userInfoDTO.setNmEmpresarial(entity.getNmEmpresarial());
userInfoDTO.setCpfResponsavelLegal(entity.getCpfResponsavelLegal());
userInfoDTO.setDtAtualizacao(entity.getDtAtualizacao());
userInfoDTO.setEmail(entity.getEmailContato());
}
This worked, but it had a clear limitation: the authentication filter was coupled directly to the data source implementation.
The New Requirement
The new requirement was to replace those two direct consumers with a SOAP service already available in the project.
The WSDL exposed two operations:
consultaCidadao(cpf, rfb)
consultaEstabelecimento(cnpj, rfb)
For individuals, consultaCidadao returned fields such as:
cpf
nome
dataNascimento
dataAtualizacao
logradouro
bairro
municipio
uf
telefone
situacaoCadastral
For companies, consultaEstabelecimento returned fields such as:
cnpj
nomeEmpresarial
nomeFantasia
email
cpfResponsavel
nomeResponsavel
situacaoCadastral
dataSituacaoCadastral
This matched most of the information needed to populate UserInfoDTO.
There was one important detail: the consultaCidadao response did not include an email field for individuals. So, depending on how the application uses email for CPF users, this field would either remain null, be retrieved from another source, or require a fallback strategy.
The Design Decision
Instead of changing the OAuth2AuthenticationFilter to directly call the SOAP service, I created a provider abstraction:
IDDigitalInfoProvider
The idea was simple:
OAuth2AuthenticationFilter
↓
IDDigitalInfoProvider
↓
SOAP service
↓
UserInfoDTO
This keeps the filter focused on authentication and delegates user data retrieval to a dedicated class.
The interface is small:
package br.gov.pr.celepar.idig.consumer.iddigital;
import br.gov.pr.celepar.cidadao.seguranca.oauth2.dto.UserInfoDTO;
public interface IDDigitalInfoProvider {
UserInfoDTO obterUsuario(Long documento) throws Exception;
}
Implementing the SOAP Provider
The implementation uses the JAX-WS classes generated from the WSDL.
The generated service class was:
ConsultaCidadaoRequestSoap11QSService
And the generated port exposed both operations:
port.consultaCidadao(request);
port.consultaEstabelecimento(request);
A simplified provider implementation looks like this:
package br.gov.pr.celepar.idig.consumer.iddigital;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import br.gov.pr.celepar.cidadao.seguranca.oauth2.dto.UserInfoDTO;
import br.gov.pr.celepar.idig.consumer.util.Util;
import br.gov.sefa.itcmd.idg.schema.Cidadao;
import br.gov.sefa.itcmd.idg.schema.ConsultaCidadaoResponse;
import br.gov.sefa.itcmd.idg.schema.ConsultaEstabelecimentoResponse;
import br.gov.sefa.itcmd.idg.schema.Estabelecimento;
import br.gov.sefa.itcmd.idg.service.ConsultaCidadaoRequest;
import br.gov.sefa.itcmd.idg.service.ConsultaCidadaoRequestSoap11QSService;
public class IDDigitalInfoProviderImpl implements IDDigitalInfoProvider {
private static final String CONSULTA_RFB_SIM = "S";
@Override
public UserInfoDTO obterUsuario(Long documento) throws Exception {
if (documento == null) {
return new UserInfoDTO();
}
if (Util.validarCPF(documento)) {
return consultarCidadao(documento);
}
if (Util.validarCNPJ(documento)) {
return consultarEstabelecimento(documento);
}
return new UserInfoDTO();
}
private UserInfoDTO consultarCidadao(Long cpf) throws Exception {
ConsultaCidadaoRequestSoap11QSService service =
new ConsultaCidadaoRequestSoap11QSService();
ConsultaCidadaoRequest port =
service.getConsultaCidadaoRequestSoap11QSPort();
br.gov.sefa.itcmd.idg.schema.ConsultaCidadaoRequest request =
new br.gov.sefa.itcmd.idg.schema.ConsultaCidadaoRequest();
request.setCpf(formatarCpf(cpf));
request.setRfb(CONSULTA_RFB_SIM);
ConsultaCidadaoResponse response =
port.consultaCidadao(request);
UserInfoDTO dto =
new UserInfoDTO();
if (response == null || response.getCidadao() == null) {
return dto;
}
Cidadao cidadao =
response.getCidadao();
if (isNotBlank(cidadao.getErro())) {
throw new RuntimeException(
"Error returned by consultaCidadao: " +
cidadao.getErro());
}
dto.setCPF(toLong(cidadao.getCpf()));
dto.setNome(cidadao.getNome());
dto.setDtNascimento(parseDate(cidadao.getDataNascimento()));
dto.setDtAtualizacao(parseDate(cidadao.getDataAtualizacao()));
/*
* The consultaCidadao response does not expose an email field.
* For CPF users, email remains null unless another source is used.
*/
dto.setEmail(null);
* return dto;
}
private*UserInfoDTO consultarEstabelecimen*o(Long cnpj) throws Exception {
* ConsultaCidadaoRequestSoap11*SService service =
new*ConsultaCidadaoRequestSoap11QSServ*ce();
ConsultaCidadaoRequ*st port =
service.getC*nsultaCidadaoRequestSoap11QSPort()*
br.gov.sefa.itcmd.idg.sc*ema.ConsultaEstabelecimentoRequest*request =
new br.gov.s*fa.itcmd.idg.schema.ConsultaEstabe*ecimentoRequest();
reques*.setCnpj(formatarCnpj(cnpj));
* request.setRfb(CONSULTA_RFB_SIM*;
ConsultaEstabelecimento*esponse response =
por*.consultaEstabelecimento(request);*
UserInfoDTO dto =
* new UserInfoDTO();
if*(response == null || response.getE*tabelecimentoList() == null) {
* return dto;
}
* Estabelecimento estabeleciment* =
response.getEstabel*cimentoList();
if (isNotB*ank(estabelecimento.getErro())) {
* throw new RuntimeExcept*on(
"Error returne* by consultaEstabelecimento: " +
* estabelecimento.getE*ro());
}
dto.setC*PJ(toLong(estabelecimento.getCnpj(*));
dto.setNmEmpresarial(e*tabelecimento.getNomeEmpresarial()*;
dto.setCpfResponsavelLeg*l(toLong(estabelecimento.getCpfRes*onsavel()));
dto.setEmail(*stabelecimento.getEmail());
* dto.setDtAtualizacao(parseDate(es*abelecimento.getDataSituacaoCadast*al()));
return dto;
}*
private Long toLong(String va*ue) {
if (!isNotBlank(val*e)) {
return null;
* }
String onlyNumbers *
value.replaceAll("[^0-9]", "");
if (!isNotBlank*onlyNumbers)) {
return*null;
}
return Lo*g.valueOf(onlyNumbers);
}
*private String formatarCpf(Long cp*) {
if (cpf == null) {
* return null;
}
* return String.format("%011d"* cpf);
}
private String f*rmatarCnpj(Long cnpj) {
i* (cnpj == null) {
retu*n null;
}
return *tring.format("%014d", cnpj);
}*
private boolean isNotBlank(St*ing value) {
return value *= null && value.trim().length() > *;
}
private Date parseDat*(String value) {
if (!isN*tBlank(value)) {
retur* null;
}
String d*te =
value.trim();
* String[] patterns = new Strin*[] {
"yyyy-MM-dd",
* "dd/MM/yyyy",
*yyyyMMdd",
"ddMMyyyy"
* };
for (int i = 0;*i < patterns.length; i++) {
* try {
SimpleD*teFormat sdf =
*new SimpleDateFormat(patterns[i]);*
sdf.setLenient(fa*se);
return sdf.p*rse(date);
} catch (P*rseException e) {
*/ Try next pattern
}
* }
return null;
*
}
Updating the OAuth2AuthenticationFilter
After creating the provider, the change inside OAuth2AuthenticationFilter became much smaller.
The imports were changed to:
import br.gov.pr*celepar.idig.consumer.iddigital.ID*igitalInfoProvider;
import br.gov.*r.celepar.idig.consumer.iddigital.*DDigitalInfoProviderImpl;
Then the old CPF/CNPJ consumer block was replaced with:
IDDigita*InfoProvider provider =
new ID*igitalInfoProviderImpl();
UserInf*DTO dto =
provider.obterUsuari*(responseToken.getDocumentNumber()*;
if (dto != null) {
userInf*DTO.setCPF(dto.getCPF());
user*nfoDTO.setCNPJ(dto.getCNPJ());
* userInfoDTO.setNome(dto.getNome()*;
userInfoDTO.setNmEmpresarial*dto.getNmEmpresarial());
user*nfoDTO.setCpfResponsavelLegal(dto.*etCpfResponsavelLegal());
use*InfoDTO.setDtNascimento(dto.getDtN*scimento());
userInfoDTO.setDt*tualizacao(dto.getDtAtualizacao())*
userInfoDTO.setDtUltimaAtuali*acaoRFB(dto.getDtUltimaAtualizacao*FB());
userInfoDTO.setEmail(d*o.getEmail());
}
The rest of the authentication flow remained unchanged:
userInfoDTO.setOr*gemClientId(responseToken.getSourc*());
httpServletRequest.getSessio*()
.setAttribute(SESSION_ATTRI*UTE_USER_INFO, userInfoDTO);
http*ervletRequest.getSession()
.se*Attribute(SESSION_ATTRIBUTE_SERVER*AD, serverAD);
httpServletRequest*getSession()
.setAttribute(
* SESSION_ATTRIBUTE_AUTHENTICAT*ON_TIMESTAMP,
userInfoDTO.*etDtAutenticacao());
This is important because the downstream flow still works the same way:
UserInfoDTO
↓
Session THE_US*R_INFO
↓
UsuarioLoginUtil
*
UsuarioLogado
↓
Portal functi*nal area
Why This Approach Is Better
This change brings a few practical benefits.
First, the authentication filter becomes cleaner. It no longer needs to know whether CPF data comes from one consumer, CNPJ data comes from another consumer, or both come from a SOAP service. It only asks for a UserInfoDTO.
Second, the integration point is centralized. If the service endpoint changes, or if the response mapping needs adjustment, the change happens inside IDDigitalInfoProviderImpl, not inside the login filter.
Third, the solution reduces coupling. The filter remains focused on OAuth2 responsibilities: authorization code, token exchange, cookies, session attributes, and redirects. User data retrieval becomes a separate responsibility.
Finally, this approach makes rollback easier. During validation, a fallback strategy can be added inside the provider. For example, if the SOAP service fails, the provider could temporarily call the legacy consumers. That makes the migration safer in a production-like environment.
Important Considerations
The WSDL contract needs to be carefully compared with the previous consumer responses.
In this case, the SOAP service returned enough data to populate CPF and CNPJ identity fields, but the individual citizen response did not expose an email field. This means the impact of email = null for CPF users must be validated against the application behavior.
Another point is date parsing. SOAP responses often expose dates as strings, and different environments may return different formats. A defensive parser supporting multiple patterns helps reduce runtime issues.
It is also important to avoid exposing internal endpoints, credentials, tokens, or real user data in code samples, logs, or blog posts. In enterprise integration scenarios, examples should be sanitized and focused on architecture and implementation patterns.
Final Result
The final architecture became:
OAuth2AuthenticationFilter
↓
TokenDTO
↓
documentNumber
↓
IDDigitalInfoProvider
↓
consultaCidadao or consultaEstabelecimento
↓
UserInfoDTO
↓
Session
↓
UsuarioLoginUtil
↓
UsuarioLogado
The key improvement was not only replacing two consumers with one SOAP service. The real improvement was introducing a provider layer that isolated the external integration from the authentication flow.
This made the solution easier to maintain, safer to evolve, and more aligned with enterprise integration best practices.

Leave a Comment