Quando uma infraestrutura geoespacial começa a ser utilizada por diferentes equipes de uma organização, chega um momento em que manter usuários e senhas diretamente no GeoNode deixa de ser uma boa estratégia.
O usuário já possui uma identidade corporativa. Ele já entra em outros sistemas com a mesma conta. Em muitos casos, seus grupos e perfis também já são gerenciados centralmente.
Nesse cenário, uma alternativa interessante é integrar o GeoNode a um provedor de identidade utilizando OpenID Connect (OIDC).
O objetivo deste artigo é mostrar um exemplo genérico de integração de um GeoNode 5 com um provedor OIDC, incluindo não apenas o login, mas também a utilização das claims retornadas pelo provedor para associar automaticamente o usuário aos grupos do GeoNode.
A arquitetura é aproximadamente esta:

O detalhe interessante é que o GeoNode não precisa saber como o Identity Provider (IdP) autenticou o usuário. Por trás do IdP pode existir LDAP, Active Directory, outro diretório corporativo ou até outro serviço de identidade. Para o GeoNode, isso é transparente.
OIDC, OAuth 2.0 e autenticação
É comum OAuth 2.0 e OpenID Connect aparecerem juntos, mas eles não representam exatamente a mesma coisa.
- OAuth 2.0 foi criado principalmente para autorização.
- OpenID Connect acrescenta uma camada de identidade e autenticação sobre o OAuth 2.0.
É por isso que em uma integração OIDC normalmente aparecem informações como: authorization_endpoint, token_endpoint, userinfo_endpoint, jwks_uri, issuer. Além de scopes como: openid, profile e email.
Para aplicações corporativas, frequentemente aparecem também scopes ou claims relacionadas a grupos e roles.
Ambiente utilizado neste exemplo
Este artigo considera uma instalação do GeoNode 5 baseada no geonode-project e executada com Docker Compose.
Os comandos docker compose devem ser executados no diretório raiz do projeto, onde está o arquivo docker-compose.yml.
Nos exemplos abaixo, utilizarei geonode_project como nome do pacote Django. Caso seu projeto utilize outro nome, substitua geonode_project pelo nome correspondente.
1. O Discovery Document
Uma das primeiras informações que devemos solicitar à equipe responsável pelo Identity Provider é a URL de discovery do OIDC. Normalmente ela segue este padrão:
https://idp.exemplo.org/.well-known/openid-configuration ou https://idp.exemplo.org/security/.well-known/openid-configuration
Ao acessar essa URL, recebemos um JSON semelhante a:
{
"issuer": "https://idp.exemplo.org/",
"authorization_endpoint": "https://idp.exemplo.org/connect/authorize",
"token_endpoint": "https://idp.exemplo.org/connect/token",
"userinfo_endpoint": "https://idp.exemplo.org/connect/userinfo",
"jwks_uri": "https://idp.exemplo.org/.well-known/jwks",
"scopes_supported": [
"openid",
"profile",
"email",
"roles"
]
}
Esse documento funciona praticamente como um mapa da integração.
2. Registrar o GeoNode como client
No lado do Identity Provider, o GeoNode precisa ser registrado como uma aplicação/client. De maneira simplificada, teremos algo como:
Client ID: geonode Client Secret: ************** Redirect URI: https://geo.exemplo.org/account/oidc/corp-idp/login/callback/
A URL de callback deve coincidir exatamente com a rota exposta pela versão do GeoNode/django-allauth utilizada. Confirme a URL do seu ambiente antes de registrá-la no Identity Provider.
O nome corp-idp neste artigo é apenas um exemplo. Esse identificador será utilizado posteriormente pelo django-allauth para identificar o provedor. Um cuidado importante: o Client Secret não deve ficar diretamente no código-fonte nem ser enviado ao Git. O ideal é mantê-lo em variável de ambiente:
OIDC_CLIENT_ID=geonode OIDC_CLIENT_SECRET=xxxxxxxxxxxxxxxx OIDC_DISCOVERY_URL=https://idp.exemplo.org/.well-known/openid-configuration
3. Criando settings específicos
Uma estratégia que considero interessante é não alterar diretamente o settings.py original do GeoNode. Podemos criar, por exemplo: src/geonode_project/corporate_settings.py com o seguinte conteúdo:
import os
from .settings import *
OIDC_CLIENT_ID = os.getenv(
"OIDC_CLIENT_ID",
"geonode",
)
OIDC_CLIENT_SECRET = os.getenv(
"OIDC_CLIENT_SECRET",
"",
)
OIDC_DISCOVERY_URL = os.getenv(
"OIDC_DISCOVERY_URL",
"",
)
E então definir no ambiente:
DJANGO_SETTINGS_MODULE=geonode_project.corporate_settings
Isso facilita bastante manutenção, deploy e rollback.
4. Habilitando o provider OpenID Connect
O GeoNode utiliza django-allauth para parte da integração com contas sociais/federadas. O código atual do projeto continua trazendo um GenericOpenIDConnectAdaptere mecanismos próprios para extrair atributos e sincronizar grupos. Podemos registrar o provider desta forma:
OIDC_PROVIDER_APP = (
"allauth.socialaccount.providers.openid_connect"
)
if OIDC_PROVIDER_APP not in INSTALLED_APPS:
INSTALLED_APPS += (OIDC_PROVIDER_APP,)
SOCIALACCOUNT_PROVIDERS = dict(
SOCIALACCOUNT_PROVIDERS
)
SOCIALACCOUNT_PROVIDERS["openid_connect"] = {
"OAUTH_PKCE_ENABLED": True,
"SCOPE": [
"openid",
"profile",
"email",
"roles",
],
"APPS": [
{
"provider_id": "corp-idp",
"name": "Login Corporativo",
"client_id": OIDC_CLIENT_ID,
"secret": OIDC_CLIENT_SECRET,
"settings": {
"server_url": OIDC_DISCOVERY_URL,
},
}
],
}
O scope roles utilizado neste exemplo não é obrigatório no padrão OIDC. Os scopes e claims disponíveis dependem do Identity Provider. Alguns provedores retornam grupos/roles com outros scopes ou apenas pelo endpoint userinfo.
Também podemos permitir criação automática da conta local:
SOCIALACCOUNT_AUTO_SIGNUP = True
Durante homologação, particularmente prefiro manter o login local disponível:
SOCIALACCOUNT_ONLY = False
Assim, caso algo dê errado com o IdP, ainda existe uma forma administrativa de entrar no GeoNode.
5. Primeiro teste: autenticação apenas
Neste momento eu ainda não me preocuparia com grupos. O primeiro objetivo é provar apenas este fluxo:

Depois de reiniciar a aplicação:
docker compose up -d --build django celery
Valide:
docker compose exec django python manage.py check
Podemos também acompanhar o log:
docker compose logs -f --tail=200 django
Ao clicar em Login Corporativo, devemos ser redirecionados ao IdP e, após a autenticação, voltar para algo semelhante a: /account/oidc/corp-idp/login/callback/
Se o usuário for criado e entrar no GeoNode, a primeira etapa está concluída.
6. O que o Identity Provider enviou?
Depois da autenticação, uma das etapas mais importantes é verificar o conteúdo recebido. Um conjunto de claims pode ser parecido com:
{
"sub": "5d823759-3dc0-4414-a936-2ef659829321",
"name": "Fernando Quadro",
"preferred_username": "fernando.quadro",
"email": "fernando.quadro@exemplo.org",
"given_name": "Fernando",
"family_name": "Quadro",
"role": [
"gestores"
]
}
O campo mais importante do ponto de vista de identidade é normalmente o sub, ou Subject Identifier, que é o identificador estável daquele usuário dentro do provedor. Nome, username e até e-mail podem mudar. O sub não deveria.
7. Consultando as claims armazenadas pelo GeoNode
Podemos verificar exatamente o que ficou armazenado no SocialAccount. Por exemplo:
docker compose exec django python manage.py shell -c "
import json
from allauth.socialaccount.models import SocialAccount
for account in (
SocialAccount.objects
.filter(provider='corp-idp')
.select_related('user')
):
print('GeoNode:', account.user.username)
print('UID/sub:', account.uid)
print(
json.dumps(
account.extra_data,
indent=2,
ensure_ascii=False
)
)
print()
"
Esse comando é extremamente útil durante uma homologação. Antes de escrever qualquer lógica de grupos, vale confirmar:
- sub
- preferred_username
- given_name
- family_name
- role / roles
- groups
E principalmente os tipos desses atributos. Uma role pode chegar como: “role”: “gestores” ou “role”: [“gestores”]
Essas diferenças parecem pequenas, mas fazem bastante diferença no código.
8. GeoNode já possui mecanismo para sincronizar grupos
Aqui existe uma parte interessante do GeoNode que nem sempre é muito conhecida. O projeto possui um mecanismo de Profile Extractors. A própria classe base explica que extractors customizados podem ser registrados através de: SOCIALACCOUNT_PROFILE_EXTRACTORS
Existe também um OpenIDExtractor. Entre outras coisas, ele possui:
def extract_groups(self, data):
return data.get("groups", "")
def extract_roles(self, data):
return data.get("roles", "")
Perceba um detalhe. Ele procura roles no plural. Mas nosso Identity Provider fictício retorna role no singular. É exatamente aí que um pequeno extractor customizado pode ser útil.
9. Criando um extractor específico
Vamos criar esse extractor na pasta src/geonode_project/corporate_auth.

Primeiro o arquivo profileextractors.py:
import logging
from geonode.people.profileextractors import (
OpenIDExtractor,
)
logger = logging.getLogger(__name__)
class CorporateOpenIDExtractor(OpenIDExtractor):
ROLE_MAPPING = {
"publico": "publico",
"analistas": "analistas",
"gestores": "gestores",
# Compatibilidade com outra nomenclatura
"role_publico": "publico",
"role_analistas": "analistas",
"role_gestores": "gestores",
}
def extract_roles(self, data):
roles = data.get("role", [])
if isinstance(roles, str):
roles = [roles]
mapped_roles = [
self.ROLE_MAPPING[role]
for role in roles
if role in self.ROLE_MAPPING
]
unknown_roles = [
role
for role in roles
if role not in self.ROLE_MAPPING
]
if unknown_roles:
logger.warning(
"OIDC retornou roles não mapeadas: %s",
unknown_roles,
)
return mapped_roles
Perceba que esse código não implementa autenticação. Ele também não implementa a associação ao grupo. Sua única responsabilidade é:
claim do IdP
↓
normalização
↓
slug conhecido pelo GeoNode
10. Registrando o extractor
Vamos registrar o extractor no arquivo src/geonode_project/corporate_settings.py:
SOCIALACCOUNT_PROFILE_EXTRACTORS = dict(
SOCIALACCOUNT_PROFILE_EXTRACTORS
)
SOCIALACCOUNT_PROFILE_EXTRACTORS[
"corp-idp"
] = (
"geonode_project."
"geonode.corporate_auth."
"profileextractors."
"CorporateOpenIDExtractor"
)
Para testar se funcionou, você pode usar o comando abaixo:
docker compose exec django python manage.py shell -c "
from geonode.people.adapters import get_data_extractor
extractor = get_data_extractor('corp-idp')
print(
extractor.__class__.__module__
+ '.'
+ extractor.__class__.__name__
)
"
O resultado esperado é: geonode_project.corporate_auth.profileextractors.CorporateOpenIDExtractor
Por último, vamos validar o adapter:
docker compose exec django python manage.py shell -c "
from django.conf import settings
from geonode.people.adapters import get_data_extractor
print(
'SETTINGS:',
settings.SETTINGS_MODULE
)
print(
'ADAPTER:',
settings.SOCIALACCOUNT_ADAPTER
)
print(
'EXTRACTOR:',
get_data_extractor('corp-idp')
)
"
O resultado esperado é:
SETTINGS: geonode_project.corporate_settings ADAPTER: geonode.people.adapters.GenericOpenIDConnectAdapter EXTRACTOR: <...CorporateOpenIDExtractor object...>
11. Criando os grupos no GeoNode
No GeoNode, acesse Grupos → Criar um Novo Grupo e crie Público, Analistas e Gestores, garantindo que os slugs sejam respectivamente publico, analistas e gestores.
Agora criamos os GroupProfile: Público, Analistas e Gestores com os seguintes slugs: publico, analistas e gestores. Esse detalhe é importante. O mecanismo de associação procura o grupo pelo slug, não pelo título visual.
Então: role = gestores
Precisa resular em: slug = gestores
12. O que acontece no login?
O GeoNode possui uma rotina interna para sincronizar grupos provenientes da conta social. O código atual busca o extractor correspondente ao provider, extrai groups ou roles, localiza o GroupProfile pelo slug e executa join(user).
O GenericOpenIDConnectAdapter chama essa sincronização tanto na criação da conta quanto em logins posteriores. GitHub O fluxo fica:

É uma solução interessante porque aproveita a infraestrutura que já existe no GeoNode em vez de criar um segundo sistema paralelo de autorização.
13. Sincronização em logins seguintes
Essa parte é especialmente importante em ambientes corporativos. Imagine:
Na segunda-feira o usuário “fernando.quadro” foi colocado na role analistas. O usuário acessa o GeoNode e é inserido no grupo Analistas. Posterior a essa data o administrador alterar o perfil desse usuário para a role gestores.
No próximo login, a associação pode ser sincronizada novamente. Na versão atual do código do GeoNode, inclusive, existem estratégias explícitas para sincronização dos grupos sociais, e o adapter continua executando essa lógica no pre_social_login.
Isso permite tratar o provedor corporativo como fonte da verdade da associação de grupos.
Na estratégia nativa apresentada, durante o login social o GeoNode pode remover os
GroupProfile atualmente associados ao usuário e recriar as associações com base nos grupos ou roles recebidos do Identity Provider.
Portanto, esta abordagem é apropriada quando o Identity Provider é realmente
a fonte de verdade dos grupos corporativos. Se a mesma conta também precisar
pertencer a grupos do GeoNode administrados localmente e que não são enviados pelo IdP, a estratégia de sincronização deverá ser customizada.
14. E administradores?
Eu separaria duas coisas: grupo funcional e privilégio administrativo do GeoNode. Um grupo chamado Gestores não significa necessariamente que esse usuário tem privilégios de Staff e/ou Superusuário.
Você pode criar uma role especial chamada admin, com isso é possível implementar uma pequena lógica adicional, por exemplo usando um signal do django-allauth, para controlar explicitamente:
user.is_staff = True user.is_superuser = True
Essa separação evita transformar um grupo corporativo em sinônimo de superusuário da aplicação.
O tratamento de administradores é uma política adicional e não faz parte da configuração mínima necessária para autenticação OIDC e sincronização de grupos deste artigo.
15. Verificando usuários e grupos
Depois da implementação acima, é interessante realizar alguns testes de auditoria rápida:
docker compose exec django python manage.py shell -c "
from allauth.socialaccount.models import (
SocialAccount,
)
for account in (
SocialAccount.objects
.filter(provider='corp-idp')
.select_related('user')
):
user = account.user
data = account.extra_data or {}
print(
user.username,
'| role(s):', data.get('role') or data.get('roles'),
'| grupos:',
[
(g.title, g.slug)
for g in user.group_list_all()
],
'| staff:', user.is_staff,
'| superuser:', user.is_superuser,
)
"
A saída será algo semelhante a:
fernando.quadro
| role(s): ['gestores']
| grupos: [('Gestores', 'gestores')]
| staff: False
| superuser: False
Isso permite verificar em uma única consulta claim recebida, role, grupo no GeoNode e status administrativo.
16. Conclusão
Integrar o GeoNode a um Identity Provider via OpenID Connect não precisa significar criar uma grande camada customizada de autenticação. Boa parte da infraestrutura já existe no próprio GeoNode e no django-allauth. O ponto mais importante é entender o contrato entre os dois sistemas:
- Qual provider?
- Qual callback?
- Quais scopes?
- Quais claims?
- Qual identificador do usuário?
- Como chegam grupos e roles?
- Qual slug existe no GeoNode?
A partir daí, um pequeno extractor pode ser suficiente para adaptar as claims corporativas à estrutura de grupos utilizada pelo GeoNode.
O resultado é uma arquitetura em que a identidade fica centralizada no serviço corporativo e o GeoNode continua responsável pelo que faz melhor: controlar o acesso aos recursos geoespaciais.