Integrar o Contas Voneka
Login e registo únicos para as apps Voneka. OAuth 2.0 (Authorization Code). Base: https://contas.get.co.mz
Começar em 5 minutos
- Peça um cliente OAuth à equipa Voneka (recebe client_id e client_secret).
- Registe a redirect URI exacta da sua app (ex.: https://sua-app.co.mz/callback).
- Configure as variáveis de ambiente abaixo.
- Redireccione o utilizador para /oauth/authorize (login) ou /register (criar conta).
- No callback, troque o code por access_token e chame GET /api/user.
Variáveis na sua aplicação
OAUTH_BASE_URI=https://contas.get.co.mz/
OAUTH_CLIENT_ID=seu-uuid-aqui
OAUTH_CLIENT_SECRET=seu-segredo-aqui
OAUTH_REDIRECT_URI=https://sua-app.co.mz/callback
Como funciona (visão geral)
- A sua app redirecciona o browser para o Contas.
- O utilizador entra ou cria conta no Contas.
- O Contas devolve um code para a sua redirect_uri.
- O seu backend troca o code por access_token (nunca no browser se tiver secret).
- Com o token, obtém o perfil em /api/user e cria/actualiza a sessão local.
Guarde o client_secret só no servidor. Use state aleatório e valide-o no callback (protecção CSRF).
Exemplos de código
Substitua CLIENT_ID, CLIENT_SECRET, REDIRECT_URI e BASE. Os snippets cobrem o fluxo completo: autorizar → trocar code → ler /api/user.
# 1) Abrir no browser (guarde state na sessão)
https://contas.get.co.mz/oauth/authorize?response_type=code&client_id=CLIENT_ID&redirect_uri=https%3A%2F%2Fsua-app.co.mz%2Fcallback&scope=&state=RANDOM
# 2) Trocar o code (no servidor)
curl -sS -X POST https://contas.get.co.mz/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "client_id=CLIENT_ID" \
-d "client_secret=CLIENT_SECRET" \
-d "redirect_uri=https://sua-app.co.mz/callback" \
-d "code=CODE_DO_CALLBACK"
# 3) Perfil
curl -sS https://contas.get.co.mz/api/user \
-H "Authorization: Bearer ACCESS_TOKEN" \
-H "Accept: application/json"
// Laravel — redireccionar para login Contas
$state = Str::random(40);
session(['oauth_state' => $state]);
return redirect(config('services.contas.base').'oauth/authorize?'.http_build_query([
'response_type' => 'code',
'client_id' => config('services.contas.client_id'),
'redirect_uri' => config('services.contas.redirect'),
'scope' => '',
'state' => $state,
]));
// Callback — trocar code + obter user
if ($request->state !== session('oauth_state')) {
abort(403);
}
$token = Http::asForm()->post(config('services.contas.base').'oauth/token', [
'grant_type' => 'authorization_code',
'client_id' => config('services.contas.client_id'),
'client_secret' => config('services.contas.client_secret'),
'redirect_uri' => config('services.contas.redirect'),
'code' => $request->code,
])->throw()->json();
$user = Http::withToken($token['access_token'])
->acceptJson()
->get(config('services.contas.base').'api/user')
->throw()
->json();
// $user['email'], $user['name'], $user['telefone'], …
// Node.js (Express) — callback
import crypto from 'crypto';
import express from 'express';
const BASE = process.env.OAUTH_BASE_URI; // https://contas.get.co.mz/
const app = express();
app.get('/login', (req, res) => {
const state = crypto.randomBytes(20).toString('hex');
req.session.oauth_state = state;
const q = new URLSearchParams({
response_type: 'code',
client_id: process.env.OAUTH_CLIENT_ID,
redirect_uri: process.env.OAUTH_REDIRECT_URI,
scope: '',
state,
});
res.redirect(`${BASE}oauth/authorize?${q}`);
});
app.get('/callback', async (req, res) => {
if (req.query.state !== req.session.oauth_state) {
return res.status(403).send('Invalid state');
}
const body = new URLSearchParams({
grant_type: 'authorization_code',
client_id: process.env.OAUTH_CLIENT_ID,
client_secret: process.env.OAUTH_CLIENT_SECRET,
redirect_uri: process.env.OAUTH_REDIRECT_URI,
code: req.query.code,
});
const tok = await fetch(`${BASE}oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body,
}).then((r) => r.json());
const user = await fetch(`${BASE}api/user`, {
headers: {
Authorization: `Bearer ${tok.access_token}`,
Accept: 'application/json',
},
}).then((r) => r.json());
// criar sessão local com user.email / user.name
res.redirect('/');
});
# Python (Flask) — callback
import os, secrets, requests
from flask import Flask, redirect, request, session, abort
from urllib.parse import urlencode
BASE = os.environ["OAUTH_BASE_URI"] # https://contas.get.co.mz/
app = Flask(__name__)
@app.get("/login")
def login():
state = secrets.token_urlsafe(24)
session["oauth_state"] = state
q = urlencode({
"response_type": "code",
"client_id": os.environ["OAUTH_CLIENT_ID"],
"redirect_uri": os.environ["OAUTH_REDIRECT_URI"],
"scope": "",
"state": state,
})
return redirect(f"{BASE}oauth/authorize?{q}")
@app.get("/callback")
def callback():
if request.args.get("state") != session.get("oauth_state"):
abort(403)
tok = requests.post(
f"{BASE}oauth/token",
data={
"grant_type": "authorization_code",
"client_id": os.environ["OAUTH_CLIENT_ID"],
"client_secret": os.environ["OAUTH_CLIENT_SECRET"],
"redirect_uri": os.environ["OAUTH_REDIRECT_URI"],
"code": request.args["code"],
},
timeout=15,
).json()
user = requests.get(
f"{BASE}api/user",
headers={
"Authorization": f"Bearer {tok['access_token']}",
"Accept": "application/json",
},
timeout=15,
).json()
# session["user"] = user["email"]
return redirect("/")
// Browser / SPA com PKCE (sem client_secret)
async function loginWithContas() {
const verifier = base64Url(crypto.getRandomValues(new Uint8Array(32)));
const challenge = base64Url(
await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier))
);
sessionStorage.setItem('pkce_verifier', verifier);
const state = crypto.randomUUID();
sessionStorage.setItem('oauth_state', state);
const q = new URLSearchParams({
response_type: 'code',
client_id: 'CLIENT_ID_PUBLICO',
redirect_uri: 'https://sua-spa.co.mz/callback',
scope: '',
state,
code_challenge: challenge,
code_challenge_method: 'S256',
});
location.href = 'https://contas.get.co.mz/oauth/authorize?' + q;
}
// No callback (idealmente via BFF; se no browser, só com cliente público)
async function exchange(code) {
const body = new URLSearchParams({
grant_type: 'authorization_code',
client_id: 'CLIENT_ID_PUBLICO',
redirect_uri: 'https://sua-spa.co.mz/callback',
code,
code_verifier: sessionStorage.getItem('pkce_verifier'),
});
const tok = await fetch('https://contas.get.co.mz/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body,
}).then((r) => r.json());
return tok.access_token;
}
function base64Url(buf) {
const b = buf instanceof ArrayBuffer ? new Uint8Array(buf) : buf;
let s = '';
b.forEach((x) => (s += String.fromCharCode(x)));
return btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
Criar conta a partir da sua app
Em vez do formulário local, envie o utilizador ao Contas. Cada cliente define quais campos extras são obrigatórios (telefone, morada, etc.).
- Sempre: nome, email, password.
- Opcionais por app: phone, birthdate, gender, address.
- Presets: minimal · standard (+phone) · full
URL de registo
https://contas.get.co.mz/register?client_id=CLIENT_ID&return_to=https%3A%2F%2Fsua-app.co.mz%2Fredirect
Ver campos exigidos (JSON público)
GET https://contas.get.co.mz/api/registration-schema?client_id=CLIENT_ID
return_to deve ser HTTPS no seu domínio (ex.: https://sua-app.co.mz/redirect). Após o registo, o Contas devolve o utilizador a esse URL.
Perfil do utilizador
Com Authorization: Bearer {access_token}:
GET https://contas.get.co.mz/api/user
Authorization: Bearer ACCESS_TOKEN
Accept: application/json
Resposta típica (campos podem ser null se o perfil da app não os pediu):
{
"pessoa_id": 1,
"id": 1,
"name": "Agostinho Uanicela",
"email": "user@exemplo.com",
"telefone": "+25884xxxxxxx",
"bairro": null,
"distrito_id": null,
"genero": null,
"data_nascimento": null,
"email_verified_at": "2026-08-04T00:00:00.000000Z"
}
SPA e mobile (PKCE)
Apps públicas (sem secret no cliente) usam PKCE: gere code_verifier e code_challenge (S256) no authorize; no token envie code_verifier em vez de client_secret.
Nunca embuta client_secret em apps móveis ou JavaScript público.
Referência rápida
| Endpoint | Para quê |
|---|---|
GET https://contas.get.co.mz/oauth/authorize |
Iniciar login / consentimento |
POST https://contas.get.co.mz/oauth/token |
Trocar code ou refresh por tokens |
GET https://contas.get.co.mz/api/user |
Perfil do utilizador autenticado |
GET https://contas.get.co.mz/register |
Formulário de registo Contas |
GET https://contas.get.co.mz/api/registration-schema |
Schema de campos de registo |
GET https://contas.get.co.mz/up |
Health check |
Erros comuns
- redirect_uri não coincide exactamente com o registado (https, host, path, barra final).
- state do callback ≠ state da sessão → possível CSRF; rejeite o pedido.
- invalid_grant: code já usado ou expirado — reinicie o fluxo de authorize.
- invalid_client: client_id/secret errados ou cliente revogado.
- 401 em /api/user: token em falta, expirado ou header Authorization incorrecto.
Precisa de um client_id? Contacte a equipa Voneka com a URL da app e a redirect_uri pretendida.