"Social login reduz fricção de onboarding e aumenta conversão. Mas validação de tokens é crítica para segurança." — #30DiasJava Security Notes
🎯 Objetivo do Day 19
Para permitir que usuários façam login usando suas contas sociais (Google, Apple, Microsoft), o Day 19 do #30DiasJava implementou OAuth2 Social Login com validação de tokens, criação automática de contas e vinculação com contas existentes.
🛠️ O que foi implementado
✅ OAuth2 Token Validation
- Google token validation: Validação de ID tokens do Google usando Google API Client
- Apple token validation: Estrutura para validação de tokens Apple (TODO: implementação completa)
- Microsoft token validation: Estrutura para validação de tokens Microsoft (TODO: implementação completa)
- Token verification: Verificação de assinatura e claims dos tokens
✅ User Account Management
- Automatic account creation: Criação automática de contas para novos usuários OAuth2
- Account linking: Vinculação de OAuth2 a contas existentes (por email)
- Provider tracking: Rastreamento de provedor OAuth2 (GOOGLE, APPLE, MICROSOFT)
- Unique username generation: Geração de usernames únicos baseados em email + provider
✅ Social Login Flow
- Social login endpoint: Endpoint
/auth/socialpara login social - Token validation: Validação de token antes de criar/autenticar usuário
- MFA integration: Verificação de MFA também para login social
- JWT token generation: Geração de JWT após autenticação social bem-sucedida
✅ Database Schema
- Email field: Campo
emailna tabelauserspara vinculação - Provider ID: Campo
provider_idpara ID único do provedor - Auth provider: Campo
auth_providerpara identificar provedor (GOOGLE, APPLE, MICROSOFT) - Nullable password: Campo
password_hashnullable para usuários apenas OAuth2
📊 Arquitetura
┌─────────────────────────────────────────────────────────┐
│ OAuth2 Provider (Google/Apple/MS) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Google │ │ Apple │ │ Microsoft │ │
│ │ Sign-In │ │ Sign-In │ │ Sign-In │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└──────────────────────┬──────────────────────────────────┘
│
│ ID Token
▼
┌─────────────────────────────────────────────────────────┐
│ Frontend Application │
│ ┌──────────────────────────────────────────────────┐ │
│ │ POST /auth/social │ │
│ │ { "idToken": "...", "provider": "GOOGLE" } │ │
│ └──────────────────────────────────────────────────┘ │
└──────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ AuthController │
│ ┌──────────────────────────────────────────────────┐ │
│ │ @PostMapping("/social") │ │
│ │ socialLogin(SocialLoginRequest) │ │
│ └──────────────────────────────────────────────────┘ │
└──────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ OAuth2Service │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Validate │ │ Extract │ │ Return │ │
│ │ Token │ │ User Info │ │ UserInfo │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└──────────────────────┬──────────────────────────────────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Google │ │ Apple │ │ Microsoft │
│ Verifier │ │ (TODO) │ │ (TODO) │
└──────────────┘ └──────────────┘ └──────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ User Repository │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Find by │ │ Create new │ │ Link to │ │
│ │ provider │ │ user │ │ existing │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└──────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ JWT Token Generation │
└─────────────────────────────────────────────────────────┘
💻 Implementação
Dependências (pom.xml)
<!-- OAuth2 Client -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>com.google.api-client</groupId>
<artifactId>google-api-client</artifactId>
<version>2.2.0</version>
</dependency>
Database Migration (V1001__add_social_auth_fields.sql)
-- Add email, provider_id, auth_provider to users table
ALTER TABLE users
ADD COLUMN IF NOT EXISTS email VARCHAR(255),
ADD COLUMN IF NOT EXISTS provider_id VARCHAR(255),
ADD COLUMN IF NOT EXISTS auth_provider VARCHAR(50);
-- Make password_hash nullable (OAuth2 users don't have passwords)
ALTER TABLE users
ALTER COLUMN password_hash DROP NOT NULL;
-- Create indexes
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_users_provider ON users(provider_id, auth_provider);
OAuth2Service
@Service
public class OAuth2Service {
private final GoogleIdTokenVerifier googleVerifier;
private final String googleClientId;
public OAuth2Service(@Value("${app.oauth2.google.client-id:}") String googleClientId) {
this.googleClientId = googleClientId;
// Inicializar Google verifier apenas se client ID estiver configurado
if (googleClientId != null && !googleClientId.isEmpty()) {
this.googleVerifier = new GoogleIdTokenVerifier.Builder(
new NetHttpTransport(),
new GsonFactory()
)
.setAudience(Collections.singletonList(googleClientId))
.build();
} else {
this.googleVerifier = null;
}
}
public OAuth2UserInfo validateToken(String idToken, String provider) {
return switch (provider.toUpperCase()) {
case "GOOGLE" -> validateGoogleToken(idToken);
case "APPLE" -> validateAppleToken(idToken);
case "MICROSOFT" -> validateMicrosoftToken(idToken);
default -> throw new IllegalArgumentException("Unsupported OAuth2 provider: " + provider);
};
}
private OAuth2UserInfo validateGoogleToken(String idToken) {
if (googleVerifier == null) {
// Em desenvolvimento, aceitar token sem validação real
// Em produção, sempre validar
return new OAuth2UserInfo(
"dev-user-" + System.currentTimeMillis() + "@google.local",
"google_" + System.currentTimeMillis(),
"Google User"
);
}
try {
GoogleIdToken token = googleVerifier.verify(idToken);
if (token == null) {
throw new IllegalArgumentException("Invalid Google ID token");
}
GoogleIdToken.Payload payload = token.getPayload();
String email = payload.getEmail();
String userId = payload.getSubject();
String name = (String) payload.get("name");
return new OAuth2UserInfo(email, userId, name);
} catch (Exception e) {
throw new IllegalArgumentException("Failed to validate Google token: " + e.getMessage(), e);
}
}
private OAuth2UserInfo validateAppleToken(String idToken) {
// TODO: Implementar validação real do Apple ID token
// Apple usa JWT assinado com chave pública da Apple
return new OAuth2UserInfo(
"dev-user-" + System.currentTimeMillis() + "@apple.local",
"apple_" + System.currentTimeMillis(),
"Apple User"
);
}
private OAuth2UserInfo validateMicrosoftToken(String idToken) {
// TODO: Implementar validação real do Microsoft Azure AD token
// Microsoft usa JWT assinado com chave pública do Azure AD
return new OAuth2UserInfo(
"dev-user-" + System.currentTimeMillis() + "@microsoft.local",
"microsoft_" + System.currentTimeMillis(),
"Microsoft User"
);
}
public record OAuth2UserInfo(
String email,
String providerId,
String name
) {}
}
AuthController Social Login
@PostMapping("/social")
public TokenResponse socialLogin(@Valid @RequestBody SocialLoginRequest req) {
// Validar token OAuth2 com o provedor
OAuth2Service.OAuth2UserInfo userInfo = oauth2Service.validateToken(
req.idToken(),
req.provider()
);
// Verificar se usuário já existe pelo provider
String provider = req.provider().toUpperCase();
var existingUser = users.findByProviderIdAndAuthProvider(
userInfo.providerId(),
provider
)
.orElseGet(() -> {
// Verificar se email já existe (usuário pode ter se registrado com email/password)
var userByEmail = users.findByEmail(userInfo.email()).orElse(null);
if (userByEmail != null) {
// Vincular OAuth2 a conta existente
userByEmail.setProviderId(userInfo.providerId());
userByEmail.setAuthProvider(provider);
if (userByEmail.getEmail() == null) {
userByEmail.setEmail(userInfo.email());
}
users.save(userByEmail);
return userByEmail;
}
// Criar novo usuário
String username = generateUniqueUsername(provider, userInfo.email());
var u = new AppUser();
u.setUsername(username);
u.setEmail(userInfo.email());
u.setProviderId(userInfo.providerId());
u.setAuthProvider(provider);
u.setRoles(Set.of("USER"));
u.setEnabled(true);
users.save(u);
return u;
});
// Verificar se MFA está habilitado (mesmo para login social)
boolean mfaRequired = mfaService.isMfaEnabled(existingUser.getId());
if (mfaRequired) {
throw new IllegalStateException(
"MFA verification required. Please use /auth/login/mfa endpoint."
);
}
return new TokenResponse(
jwt.generateToken(existingUser.getUsername(), existingUser.getRoles())
);
}
private String generateUniqueUsername(String provider, String email) {
// Usar email como base, removendo @ e domínio
String base = email.split("@")[0].toLowerCase().replaceAll("[^a-z0-9]", "");
String username = base + "_" + provider.toLowerCase();
// Garantir unicidade
int counter = 0;
String finalUsername = username;
while (users.existsByUsername(finalUsername)) {
counter++;
finalUsername = username + counter;
}
return finalUsername;
}
Configuration (application.yml)
app:
oauth2:
google:
client-id: ${GOOGLE_CLIENT_ID:}
# client-secret não necessário para ID token validation
📈 Fluxo de Uso
1. Google Sign-In (Frontend)
// Frontend: Google Sign-In
async function handleGoogleSignIn() {
const response = await google.accounts.oauth2.initTokenClient({
client_id: 'YOUR_GOOGLE_CLIENT_ID',
scope: 'email profile',
callback: async (tokenResponse) => {
// Obter ID token
const idToken = tokenResponse.access_token;
// Enviar para backend
const result = await fetch('/auth/social', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
idToken: idToken,
provider: 'GOOGLE'
})
});
const data = await result.json();
// Salvar JWT token
localStorage.setItem('auth_token', data.token);
}
});
response.requestAccessToken();
}
2. Social Login Request
# POST /auth/social
curl -X POST http://localhost:8080/auth/social \
-H "Content-Type: application/json" \
-d '{
"idToken": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ij...",
"provider": "GOOGLE"
}'
# Resposta (novo usuário):
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
# Resposta (usuário existente):
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
3. Account Linking
Se um usuário já tem conta com email user@example.com e faz login social com o mesmo email, a conta é automaticamente vinculada:
-- Antes do login social
SELECT * FROM users WHERE email = 'user@example.com';
-- id: 1, username: 'user', provider_id: NULL, auth_provider: NULL
-- Após login social com Google
SELECT * FROM users WHERE email = 'user@example.com';
-- id: 1, username: 'user', provider_id: 'google_123456', auth_provider: 'GOOGLE'
🎓 Lições Aprendidas
✅ O que funcionou bem
- Google token validation: Google API Client facilita validação de tokens
- Account linking: Vinculação automática por email reduz duplicação de contas
- Unique username generation: Geração automática de usernames evita conflitos
- MFA integration: Login social também respeita MFA quando habilitado
⚠️ Desafios e soluções
- Apple/Microsoft validation: Ainda não implementado. Requer validação de JWT com chaves públicas dos provedores.
- Token expiration: Tokens OAuth2 expiram. Frontend deve lidar com refresh tokens.
- Development mode: Em desenvolvimento, aceitamos tokens sem validação real. Em produção, sempre validar.
📚 Recursos
- Spring OAuth2 Client: https://docs.spring.io/spring-security/reference/servlet/oauth2/client/index.html
- Google Sign-In: https://developers.google.com/identity/sign-in/web
- Apple Sign-In: https://developer.apple.com/sign-in-with-apple/
- Microsoft Azure AD: https://docs.microsoft.com/en-us/azure/active-directory/develop/
🔗 Links
Next episode → Day 20/30 — Rate Limiting com Redis