Pular para o conteúdo principal
Versão: Versão Mercados

Autenticação e Autorização

Esta página documenta como funciona o sistema de autenticação e autorização no Sistema Divino Alimento versão Mercados, incluindo OAuth 2.0, gerenciamento de sessões e controle de acesso por perfil.

Visão Geral

O sistema utiliza uma combinação de:

  • Autenticação: OAuth 2.0 com Google (via Passport.js)
  • Sessões: Express Session com armazenamento seguro
  • Autorização: Controle de acesso baseado em perfis (RBAC - Role-Based Access Control)
  • GraphQL: Directives customizadas para proteção de resolvers

Fluxo de Autenticação

Autenticação OAuth 2.0 (Google)

O sistema utiliza Google OAuth 2.0 para autenticação de usuários.

Fluxo completo:

1. Usuário acessa /login
2. Sistema redireciona para Google OAuth
3. Usuário autoriza a aplicação no Google
4. Google redireciona para /auth/google/callback com código
5. Passport.js troca código por token de acesso
6. Sistema busca/cria usuário no banco de dados
7. Sessão é criada com dados do usuário
8. Usuário é redirecionado para área autenticada

Diagrama:

┌─────────┐          ┌──────────────┐          ┌────────────┐
│ Cliente │ │ Divino │ │ Google │
│ │ │ Alimento │ │ OAuth │
└────┬────┘ └──────┬───────┘ └─────┬──────┘
│ │ │
│ 1. GET /login │ │
├─────────────────────►│ │
│ │ │
│ 2. Redirect OAuth │ │
│◄─────────────────────┤ │
│ │ │
│ 3. Authorize │ │
├──────────────────────┼───────────────────────►│
│ │ │
│ 4. Callback + code │ │
│◄─────────────────────┼────────────────────────┤
│ │ │
│ │ 5. Exchange code │
│ ├───────────────────────►│
│ │ │
│ │ 6. Access token │
│ │◄───────────────────────┤
│ │ │
│ │ 7. Get user info │
│ ├───────────────────────►│
│ │ │
│ │ 8. User data │
│ │◄───────────────────────┤
│ │ │
│ 9. Create session │ │
│◄─────────────────────┤ │
│ │ │
│ 10. Redirect /home │ │
│◄─────────────────────┤ │
│ │ │

Configuração do Passport.js

Estratégia Google OAuth 2.0

// config/passport.js
const GoogleStrategy = require('passport-google-oauth20').Strategy;

passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: process.env.GOOGLE_CALLBACK_URL
},
async (accessToken, refreshToken, profile, done) => {
try {
// Buscar usuário pelo Google ID
let user = await User.findOne({
where: { googleId: profile.id }
});

// Se não existir, criar novo usuário
if (!user) {
user = await User.create({
googleId: profile.id,
email: profile.emails[0].value,
name: profile.displayName,
avatar: profile.photos[0]?.value
});

// Atribuir perfil padrão (Consumidor)
const consumidorProfile = await Profile.findOne({
where: { name: 'Consumidor' }
});
await user.addProfile(consumidorProfile);
}

return done(null, user);
} catch (error) {
return done(error, null);
}
}
));

// Serialização para a sessão
passport.serializeUser((user, done) => {
done(null, user.id);
});

// Desserialização da sessão
passport.deserializeUser(async (id, done) => {
try {
const user = await User.findByPk(id, {
include: [Profile]
});
done(null, user);
} catch (error) {
done(error, null);
}
});

Rotas de Autenticação

// routes/auth.js
const express = require('express');
const passport = require('passport');
const router = express.Router();

// Rota de login - redireciona para Google
router.get('/auth/google',
passport.authenticate('google', {
scope: ['profile', 'email']
})
);

// Callback do Google OAuth
router.get('/auth/google/callback',
passport.authenticate('google', {
failureRedirect: '/login?error=auth_failed'
}),
(req, res) => {
// Sucesso - redirecionar para home
res.redirect('/dashboard');
}
);

// Rota de logout
router.get('/logout', (req, res) => {
req.logout((err) => {
if (err) {
return res.status(500).json({ error: 'Erro ao fazer logout' });
}
res.redirect('/');
});
});

// Verificar se está autenticado
router.get('/auth/status', (req, res) => {
if (req.isAuthenticated()) {
res.json({
authenticated: true,
user: {
id: req.user.id,
name: req.user.name,
email: req.user.email,
profiles: req.user.Profiles.map(p => p.name)
}
});
} else {
res.json({ authenticated: false });
}
});

module.exports = router;

Gerenciamento de Sessões

Configuração Express Session

// server.js
const session = require('express-session');
const SequelizeStore = require('connect-session-sequelize')(session.Store);

app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
store: new SequelizeStore({
db: sequelize,
tableName: 'sessions',
checkExpirationInterval: 15 * 60 * 1000, // 15 minutos
expiration: 24 * 60 * 60 * 1000 // 24 horas
}),
cookie: {
secure: process.env.NODE_ENV === 'production', // HTTPS em produção
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000 // 24 horas
}
}));

app.use(passport.initialize());
app.use(passport.session());

Middleware de Autenticação

// middlewares/auth.js

// Verificar se o usuário está autenticado
function isAuthenticated(req, res, next) {
if (req.isAuthenticated()) {
return next();
}

// Se for requisição GraphQL, retornar erro
if (req.path === '/graphql') {
return res.status(401).json({
errors: [{
message: 'Não autenticado',
extensions: { code: 'UNAUTHENTICATED' }
}]
});
}

// Se for requisição normal, redirecionar para login
res.redirect('/login');
}

// Verificar se o usuário tem um perfil específico
function hasProfile(profileName) {
return (req, res, next) => {
if (!req.isAuthenticated()) {
return res.status(401).json({
error: 'Não autenticado'
});
}

const hasRequiredProfile = req.user.Profiles.some(
profile => profile.name === profileName
);

if (!hasRequiredProfile) {
return res.status(403).json({
error: 'Acesso negado',
required: profileName
});
}

next();
};
}

// Verificar se tem qualquer um dos perfis
function hasAnyProfile(profileNames) {
return (req, res, next) => {
if (!req.isAuthenticated()) {
return res.status(401).json({
error: 'Não autenticado'
});
}

const hasRequiredProfile = req.user.Profiles.some(
profile => profileNames.includes(profile.name)
);

if (!hasRequiredProfile) {
return res.status(403).json({
error: 'Acesso negado',
required: `Um dos perfis: ${profileNames.join(', ')}`
});
}

next();
};
}

module.exports = {
isAuthenticated,
hasProfile,
hasAnyProfile
};

Autorização no GraphQL

Context com Usuário Autenticado

// config/apollo.js
const { ApolloServer } = require('apollo-server-express');

const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => {
// Adicionar usuário ao context se autenticado
const user = req.isAuthenticated() ? req.user : null;

return {
user,
isAuthenticated: !!user,
hasProfile: (profileName) => {
return user?.Profiles?.some(p => p.name === profileName) || false;
},
hasAnyProfile: (profileNames) => {
return user?.Profiles?.some(p => profileNames.includes(p.name)) || false;
}
};
}
});

Directive de Autenticação

// graphql/directives/auth.js
const { mapSchema, getDirective, MapperKind } = require('@graphql-tools/utils');
const { defaultFieldResolver } = require('graphql');
const { AuthenticationError, ForbiddenError } = require('apollo-server-express');

function authDirective(directiveName = 'auth') {
return {
authDirectiveTypeDefs: `directive @${directiveName}(requires: [String!]) on FIELD_DEFINITION`,

authDirectiveTransformer: (schema) =>
mapSchema(schema, {
[MapperKind.OBJECT_FIELD]: (fieldConfig) => {
const authDirective = getDirective(schema, fieldConfig, directiveName)?.[0];

if (authDirective) {
const { requires = [] } = authDirective;
const { resolve = defaultFieldResolver } = fieldConfig;

fieldConfig.resolve = async function (source, args, context, info) {
// Verificar autenticação
if (!context.isAuthenticated) {
throw new AuthenticationError('Você precisa estar autenticado');
}

// Verificar autorização (se requer perfis específicos)
if (requires.length > 0) {
const hasPermission = context.hasAnyProfile(requires);

if (!hasPermission) {
throw new ForbiddenError(
`Acesso negado. Perfis necessários: ${requires.join(', ')}`
);
}
}

return resolve(source, args, context, info);
};
}

return fieldConfig;
}
})
};
}

module.exports = authDirective;

Uso da Directive no Schema

# graphql/schema/mercado.graphql

# Definir a directive
directive @auth(requires: [String!]) on FIELD_DEFINITION

type Query {
# Qualquer usuário autenticado pode listar mercados
mercados(ativo: Boolean): [Mercado!]! @auth

# Qualquer usuário autenticado pode ver detalhes
mercado(id: ID!): Mercado @auth
}

type Mutation {
# Apenas Administrador pode criar mercado
criarMercado(input: MercadoInput!): Mercado!
@auth(requires: ["Administrador"])

# Administrador ou AdminMercado podem atualizar
atualizarMercado(id: ID!, input: MercadoInput!): Mercado!
@auth(requires: ["Administrador", "AdminMercado"])

# Apenas Administrador pode desativar
desativarMercado(id: ID!): Mercado!
@auth(requires: ["Administrador"])
}

Verificação Manual em Resolvers

// graphql/resolvers/mercado.js
const { AuthenticationError, ForbiddenError } = require('apollo-server-express');

const mercadoResolvers = {
Query: {
mercados: async (_, { ativo }, context) => {
// Verificação manual de autenticação
if (!context.isAuthenticated) {
throw new AuthenticationError('Não autenticado');
}

// Buscar mercados
const where = ativo !== undefined ? { ativo } : {};
return await Mercado.findAll({ where });
}
},

Mutation: {
criarMercado: async (_, { input }, context) => {
// Verificar autenticação
if (!context.isAuthenticated) {
throw new AuthenticationError('Não autenticado');
}

// Verificar autorização
if (!context.hasProfile('Administrador')) {
throw new ForbiddenError('Apenas administradores podem criar mercados');
}

// Lógica de negócio
return await mercadoService.criar(input, context.user);
},

atualizarPrecoMercado: async (_, { id, input }, context) => {
// Verificar autenticação
if (!context.isAuthenticated) {
throw new AuthenticationError('Não autenticado');
}

// AdminMercado só pode atualizar seu próprio mercado
if (context.hasProfile('AdminMercado')) {
const mercado = await Mercado.findByPk(id);

if (mercado.adminId !== context.user.id) {
throw new ForbiddenError('Você só pode atualizar preços do seu mercado');
}
}
// Administrador pode atualizar qualquer mercado
else if (!context.hasProfile('Administrador')) {
throw new ForbiddenError('Acesso negado');
}

return await mercadoService.atualizarPreco(id, input);
}
}
};

module.exports = mercadoResolvers;

Perfis de Usuário

Tipos de Perfis

O sistema possui os seguintes perfis:

PerfilDescriçãoPermissões
AdministradorAdministrador geral do sistemaAcesso total a todas as funcionalidades
AdminMercadoAdministrador de um mercado específicoGerenciar seu mercado, produtos e preços
FornecedorProdutor/fornecedor de produtosCadastrar ofertas, ver seus pedidos
ConsumidorConsumidor de produtosFazer pedidos, ver ciclos disponíveis

Modelo de Perfis

// models/profile.js
module.exports = (sequelize, DataTypes) => {
const Profile = sequelize.define('Profile', {
name: {
type: DataTypes.ENUM(
'Administrador',
'AdminMercado',
'Fornecedor',
'Consumidor'
),
allowNull: false,
unique: true
},
description: {
type: DataTypes.TEXT,
allowNull: true
}
});

Profile.associate = (models) => {
Profile.belongsToMany(models.User, {
through: 'UserProfiles',
foreignKey: 'profileId'
});
};

return Profile;
};

Associação User-Profile

// models/user.js
module.exports = (sequelize, DataTypes) => {
const User = sequelize.define('User', {
googleId: {
type: DataTypes.STRING,
unique: true,
allowNull: false
},
email: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
validate: {
isEmail: true
}
},
name: {
type: DataTypes.STRING,
allowNull: false
},
avatar: {
type: DataTypes.STRING,
allowNull: true
}
});

User.associate = (models) => {
User.belongsToMany(models.Profile, {
through: 'UserProfiles',
foreignKey: 'userId'
});

User.hasMany(models.Mercado, {
foreignKey: 'adminId',
as: 'mercadosAdmin'
});
};

// Métodos auxiliares
User.prototype.hasProfile = function(profileName) {
return this.Profiles?.some(p => p.name === profileName) || false;
};

User.prototype.hasAnyProfile = function(profileNames) {
return this.Profiles?.some(p => profileNames.includes(p.name)) || false;
};

User.prototype.isAdmin = function() {
return this.hasProfile('Administrador');
};

return User;
};

Autenticação no Frontend

Apollo Client com Credentials

// lib/apollo-client.ts
import { ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client';

const httpLink = createHttpLink({
uri: process.env.NEXT_PUBLIC_GRAPHQL_URL || 'http://localhost:3000/graphql',
credentials: 'include', // Importante: envia cookies de sessão
});

export const apolloClient = new ApolloClient({
link: httpLink,
cache: new InMemoryCache(),
defaultOptions: {
watchQuery: {
fetchPolicy: 'cache-and-network',
},
},
});

Hook de Autenticação

// hooks/use-auth.ts
import { useQuery, useMutation } from '@apollo/client';
import { useRouter } from 'next/navigation';

const GET_AUTH_STATUS = gql`
query GetAuthStatus {
me {
id
name
email
profiles
}
}
`;

export function useAuth() {
const router = useRouter();
const { data, loading, error } = useQuery(GET_AUTH_STATUS);

const logout = async () => {
// Chamar endpoint de logout
await fetch('/auth/logout', {
credentials: 'include'
});

// Redirecionar para home
router.push('/');
};

const hasProfile = (profileName: string) => {
return data?.me?.profiles?.includes(profileName) || false;
};

return {
user: data?.me,
isAuthenticated: !!data?.me,
loading,
error,
logout,
hasProfile,
isAdmin: hasProfile('Administrador')
};
}

Componente de Proteção de Rotas

// components/auth/protected-route.tsx
'use client';

import { useAuth } from '@/hooks/use-auth';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { Loading } from '@/components/shared/loading';

interface ProtectedRouteProps {
children: React.ReactNode;
requiredProfile?: string | string[];
}

export function ProtectedRoute({
children,
requiredProfile
}: ProtectedRouteProps) {
const { isAuthenticated, hasProfile, loading } = useAuth();
const router = useRouter();

useEffect(() => {
if (!loading && !isAuthenticated) {
router.push('/login');
}

if (!loading && requiredProfile) {
const profiles = Array.isArray(requiredProfile)
? requiredProfile
: [requiredProfile];

const hasPermission = profiles.some(p => hasProfile(p));

if (!hasPermission) {
router.push('/unauthorized');
}
}
}, [isAuthenticated, loading, requiredProfile]);

if (loading) {
return <Loading />;
}

if (!isAuthenticated) {
return null;
}

return <>{children}</>;
}

Uso em Páginas Next.js

// app/(dashboard)/mercados/page.tsx
import { ProtectedRoute } from '@/components/auth/protected-route';
import { MercadosList } from '@/components/mercados/mercados-list';

export default function MercadosPage() {
return (
<ProtectedRoute>
<MercadosList />
</ProtectedRoute>
);
}

// Página restrita a administradores
// app/(dashboard)/mercados/novo/page.tsx
export default function NovoMercadoPage() {
return (
<ProtectedRoute requiredProfile="Administrador">
<MercadoForm />
</ProtectedRoute>
);
}

Boas Práticas de Segurança

Variáveis de Ambiente

# .env
# Google OAuth
GOOGLE_CLIENT_ID=seu_client_id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=seu_client_secret
GOOGLE_CALLBACK_URL=http://localhost:3000/auth/google/callback

# Sessão
SESSION_SECRET=chave_secreta_longa_e_aleatoria_minimo_32_caracteres

# Ambiente
NODE_ENV=development

Proteção CSRF

// Usar csurf para proteção contra CSRF
const csrf = require('csurf');

app.use(csrf({ cookie: true }));

app.use((req, res, next) => {
res.locals.csrfToken = req.csrfToken();
next();
});

Rate Limiting

// Limitar tentativas de login
const rateLimit = require('express-rate-limit');

const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutos
max: 5, // 5 tentativas
message: 'Muitas tentativas de login. Tente novamente em 15 minutos.'
});

app.use('/auth/google', authLimiter);

Próximos Passos