Steam OAuth
O CSGOFlip utiliza Steam OpenID 2.0 para autenticação. Usuários fazem login com sua conta Steam, sem precisar criar senha no site.
Por que Steam OAuth?
| Benefício | Descrição |
|---|---|
| Sem senhas | Não armazenamos credenciais sensíveis |
| Confiança | Usuários confiam no Steam |
| Dados verificados | Steam ID é único e verificado |
| Avatar e nome | Obtemos automaticamente |
| Segurança | Steam cuida da autenticação |
Fluxo Completo
Implementação
1. Iniciar Login
typescript
// src/presentation/controllers/auth.controller.ts
@Get('steam/login')
@Public()
async steamLogin(
@Query('redirect') redirect: string,
@Res() response: FastifyReply,
) {
// URL de retorno após Steam
const returnUrl = `${this.configService.get('API_URL')}/auth/steam/callback`;
// Salva redirect desejado em cookie temporário
if (redirect) {
response.setCookie('auth_redirect', redirect, {
httpOnly: true,
maxAge: 60 * 5, // 5 minutos
});
}
// Monta URL do Steam OpenID
const steamLoginUrl = this.steamOAuthService.getLoginUrl(returnUrl);
// Redirect para Steam
return response.redirect(steamLoginUrl);
}2. Steam OAuth Service
typescript
// src/infrastructure/external-apis/steam-oauth.service.ts
@Injectable()
export class SteamOAuthService {
private readonly STEAM_OPENID_URL = 'https://steamcommunity.com/openid/login';
getLoginUrl(returnUrl: string): string {
const params = new URLSearchParams({
'openid.ns': 'http://specs.openid.net/auth/2.0',
'openid.mode': 'checkid_setup',
'openid.return_to': returnUrl,
'openid.realm': returnUrl,
'openid.identity': 'http://specs.openid.net/auth/2.0/identifier_select',
'openid.claimed_id': 'http://specs.openid.net/auth/2.0/identifier_select',
});
return `${this.STEAM_OPENID_URL}?${params.toString()}`;
}
async verifyCallback(query: Record<string, string>): Promise<string | null> {
// Verifica que é uma resposta válida
if (query['openid.mode'] !== 'id_res') {
return null;
}
// Prepara verificação
const verifyParams = new URLSearchParams();
for (const [key, value] of Object.entries(query)) {
verifyParams.append(key, value);
}
verifyParams.set('openid.mode', 'check_authentication');
// Envia para Steam verificar
const response = await fetch(this.STEAM_OPENID_URL, {
method: 'POST',
body: verifyParams,
});
const text = await response.text();
// Steam responde com "is_valid:true" se válido
if (!text.includes('is_valid:true')) {
return null;
}
// Extrai Steam ID da claimed_id
// Formato: https://steamcommunity.com/openid/id/76561198012345678
const claimedId = query['openid.claimed_id'];
const steamId = claimedId.split('/').pop();
return steamId;
}
async getUserProfile(steamId: string): Promise<SteamProfile> {
const apiKey = this.configService.get('STEAM_API_KEY');
const url = `https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=${apiKey}&steamids=${steamId}`;
const response = await fetch(url);
const data = await response.json();
const player = data.response.players[0];
return {
steamId: player.steamid,
username: player.personaname,
avatarUrl: player.avatarfull,
profileUrl: player.profileurl,
};
}
}3. Callback Handler
typescript
// src/presentation/controllers/auth.controller.ts
@Get('steam/callback')
@Public()
async steamCallback(
@Query() query: Record<string, string>,
@Req() request: FastifyRequest,
@Res() response: FastifyReply,
) {
// 1. Verifica resposta do Steam
const steamId = await this.steamOAuthService.verifyCallback(query);
if (!steamId) {
return response.redirect('/auth/error?reason=invalid_steam_response');
}
// 2. Busca perfil completo
const profile = await this.steamOAuthService.getUserProfile(steamId);
// 3. Cria ou atualiza usuário
let user = await this.userRepository.findBySteamId(steamId);
if (user) {
// Atualiza dados do Steam (pode ter mudado avatar/nome)
user = await this.userRepository.update(user.id, {
username: profile.username,
avatarUrl: profile.avatarUrl,
lastLoginAt: new Date(),
});
} else {
// Cria novo usuário
user = await this.userRepository.create({
steamId: profile.steamId,
username: profile.username,
avatarUrl: profile.avatarUrl,
role: 'USER',
status: 'ACTIVE',
});
// Loga criação de conta
await this.auditService.log({
action: 'USER_CREATED',
userId: user.id,
metadata: { steamId, username: profile.username },
});
}
// 4. Cria sessão
const session = await this.sessionService.create({
userId: user.id,
steamId: user.steamId,
ip: request.ip,
userAgent: request.headers['user-agent'],
});
// 5. Seta cookie de sessão
response.setCookie('sessionId', session.id, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7, // 7 dias
path: '/',
});
// 6. Redirect para frontend
const redirectUrl = request.cookies['auth_redirect'] || '/';
response.clearCookie('auth_redirect');
return response.redirect(`${this.configService.get('FRONTEND_URL')}${redirectUrl}`);
}4. Frontend Integration
typescript
// client/app/lib/auth-context.tsx
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
// Busca usuário logado ao carregar
useEffect(() => {
api.auth.getMe()
.then(setUser)
.catch(() => setUser(null))
.finally(() => setIsLoading(false));
}, []);
const login = () => {
// Redirect para Steam OAuth
const currentPath = window.location.pathname;
window.location.href = `${API_BASE_URL}/auth/steam/login?redirect=${currentPath}`;
};
const logout = async () => {
await api.auth.logout();
setUser(null);
window.location.href = '/';
};
return (
<AuthContext.Provider value={{ user, isLoading, login, logout }}>
{children}
</AuthContext.Provider>
);
}Dados Obtidos do Steam
| Campo | Descrição | Exemplo |
|---|---|---|
steamId | ID único de 17 dígitos | 76561198012345678 |
username | Nome de exibição | PlayerOne |
avatarUrl | URL do avatar (184x184) | https://steamcdn.../abc.jpg |
profileUrl | URL do perfil | https://steamcommunity.com/id/playerone |
Tratamento de Erros
typescript
// Possíveis erros no callback
enum SteamAuthError {
INVALID_RESPONSE = 'invalid_steam_response',
USER_BANNED = 'user_banned',
STEAM_API_ERROR = 'steam_api_error',
SESSION_CREATE_ERROR = 'session_create_error',
}
// Controller com tratamento
@Get('steam/callback')
async steamCallback(@Query() query, @Res() response) {
try {
const steamId = await this.steamOAuthService.verifyCallback(query);
if (!steamId) {
return this.handleAuthError(response, SteamAuthError.INVALID_RESPONSE);
}
const user = await this.getOrCreateUser(steamId);
if (user.status === 'BANNED') {
return this.handleAuthError(response, SteamAuthError.USER_BANNED);
}
// ... resto do fluxo
} catch (error) {
this.logger.error('Steam callback error', error);
return this.handleAuthError(response, SteamAuthError.STEAM_API_ERROR);
}
}
private handleAuthError(response: FastifyReply, error: SteamAuthError) {
return response.redirect(
`${this.frontendUrl}/auth/error?reason=${error}`
);
}Segurança
Validação da Resposta Steam
typescript
// SEMPRE valide a resposta do Steam!
// Não confie apenas nos parâmetros do callback.
async verifyCallback(query: Record<string, string>): Promise<string | null> {
// 1. Verifica modo de resposta
if (query['openid.mode'] !== 'id_res') {
return null;
}
// 2. OBRIGATÓRIO: Envia para Steam verificar
// Sem isso, atacante poderia forjar resposta!
const verifyParams = new URLSearchParams();
verifyParams.set('openid.mode', 'check_authentication');
// ... copia todos os parâmetros
const response = await fetch(STEAM_OPENID_URL, {
method: 'POST',
body: verifyParams,
});
const text = await response.text();
// 3. Steam confirma se é válido
if (!text.includes('is_valid:true')) {
return null; // Resposta forjada!
}
return steamId;
}Rate Limiting
typescript
// Limite tentativas de login
@UseGuards(ThrottlerGuard)
@Throttle({ default: { limit: 5, ttl: 900000 } }) // 5 tentativas / 15 min
@Get('steam/callback')
async steamCallback() {
// ...
}Troubleshooting
Steam não retorna para o callback
Causa: URL de retorno não está autorizada no Steam.
Solução: Verifique que o domínio está configurado corretamente nas configurações de desenvolvedor do Steam.
Avatar não carrega
Causa: Steam CDN pode ter URLs temporárias.
Solução: Salve o avatar localmente ou use proxy.
"Invalid Steam response" frequente
Causa: Timeout na verificação com Steam API.
Solução: Aumente timeout e adicione retry:
typescript
const response = await fetch(url, {
timeout: 10000, // 10 segundos
});
// Com retry
const response = await retryFetch(url, { retries: 3 });Arquivos Fonte Relacionados
Principais Arquivos
src/presentation/controllers/auth.controller.ts- Endpoints de authsrc/infrastructure/external-apis/steam-oauth.service.ts- Integração Steamclient/app/lib/auth-context.tsx- Context de auth no frontendclient/app/auth/callback/page.tsx- Página de callback
