Guía para desarrolladores

Integrar tu app con este servidor de identidad

Cómo conectar Angular, NestJS, Flutter (o cualquier otro stack) a este servidor OAuth2 para usarlo como IAM centralizado: login, usuarios, roles globales y roles por-app.

Laravel 12 · Passport 13 (OAuth2, tokens JWT firmados RS256)
Generado el 25 de julio de 2026

Índice

1. Qué te da este servidor

Es un servidor de identidad OAuth2 (Laravel Passport). En vez de que cada app tuya tenga su propia tabla de usuarios y su propio login, todas hablan con este servidor: acá vive el usuario, la contraseña, y quién es qué.

Dos capas de permisos, no confundirlas: los scopes del token OAuth ( users:read, users:write...) definen qué puede hacer ese token contra la API de este servidor. Los roles por-app definen qué puede hacer ese usuario dentro de tu app — son datos que vos leés e interpretás como quieras, este servidor no te impone nada sobre cómo usarlos.

2. Paso 1 — Registrar tu app como cliente OAuth

Cada aplicación que se conecte necesita ser un cliente registrado. Se hace una sola vez, desde el panel (/clients, con cualquier cuenta) o por API:

curl -X POST http://localhost:8000/api/clients \
  -H "Authorization: Bearer {tu_access_token}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Mi App Angular",
    "grant_type": "authorization_code",
    "redirect_uris": ["https://mi-app.com/callback"],
    "confidential": false
  }'

La respuesta trae client.id (el client_id) y, si es confidencial, plain_secret — se muestra una sola vez, guardalo ya.

CampoQué significa
grant_typeauthorization_code, client_credentials o password — ver sección 3 para elegir.
redirect_urisA dónde te devuelve el navegador después del login. Tiene que matchear exacto con lo que mandes en /oauth/authorize.
confidentialfalse = cliente público, sin secret (SPA, apps móviles — usan PKCE en su lugar). true = confidencial, con secret (backends que pueden guardar un secreto de forma segura).

3. Elegí el flujo según tu arquitectura

SPA pura (Angular, React, Vue)

Sin backend propio, corre 100% en el navegador. Authorization Code + PKCE, cliente público. El token queda en el navegador.

App con backend (NestJS, Express)

El backend maneja el login por vos (patrón BFF). Authorization Code, cliente confidencial — el secret vive en el backend, nunca llega al navegador.

App móvil (Flutter, RN)

Authorization Code + PKCE, cliente público, con un redirect por deep link / custom scheme.

Servicio sin usuario final

Un cron, un worker, otro backend que llama a la API por su cuenta. Client Credentials.

Password Grant existe pero evitalo salvo que sea estrictamente first-party (una app que vos mismo controlás de punta a punta): tu app le pide el usuario y contraseña directamente al usuario y se los manda a este servidor. Ningún cliente de terceros debería tocar la contraseña del usuario.

4. Angular (SPA) — Authorization Code + PKCE

Ejemplo con angular-oauth2-oidc (también podés implementarlo a mano, son cuatro pasos: generar verifier/challenge, redirigir, recibir el code, canjearlo).

app.config.ts
import { provideOAuthClient, OAuthModule } from 'angular-oauth2-oidc';

export const authConfig = {
  issuer: 'http://localhost:8000',
  loginUrl: 'http://localhost:8000/oauth/authorize',
  tokenEndpoint: 'http://localhost:8000/oauth/token',
  redirectUri: window.location.origin + '/callback',
  clientId: '{tu_client_id}',
  responseType: 'code',
  scope: 'users:read account:read',
  useHttpBasicAuth: false,
  disablePKCE: false, // clave: PKCE prendido, es cliente público
  oidc: false, // este servidor es OAuth2 puro, no expone id_token/OIDC
};
auth.service.ts
import { Injectable } from '@angular/core';
import { OAuthService } from 'angular-oauth2-oidc';

@Injectable({ providedIn: 'root' })
export class AuthService {
  constructor(private oauth: OAuthService) {}

  login() {
    this.oauth.initCodeFlow(); // redirige a /oauth/authorize con PKCE
  }

  async me() {
    const token = this.oauth.getAccessToken();
    const res = await fetch('http://localhost:8000/api/user', {
      headers: { Authorization: `Bearer ${token}` },
    });
    return res.json(); // { id, name, email, roles, app_roles, ... }
  }
}

Si preferís no traer una librería, el flujo manual es: generar code_verifier (random) y code_challenge (SHA-256 + base64url del verifier), armar la URL de /oauth/authorize con esos parámetros + state, y en el callback hacer POST /oauth/token con grant_type=authorization_code, el code recibido y el code_verifier original.

5. NestJS — backend con login de usuarios

Tu backend actúa de cliente confidencial (guarda el secret) y maneja el flujo Authorization Code por el usuario — patrón "backend for frontend": el navegador nunca ve el access token, solo una cookie de sesión propia de tu app.

auth.controller.ts
@Controller('auth')
export class AuthController {
  private readonly IAM = 'http://localhost:8000';
  private readonly CLIENT_ID = process.env.IAM_CLIENT_ID;
  private readonly CLIENT_SECRET = process.env.IAM_CLIENT_SECRET;

  @Get('login')
  login(@Res() res: Response) {
    const params = new URLSearchParams({
      client_id: this.CLIENT_ID,
      redirect_uri: 'https://mi-backend.com/auth/callback',
      response_type: 'code',
      scope: 'users:read',
      state: crypto.randomUUID(),
    });
    res.redirect(`${this.IAM}/oauth/authorize?${params}`);
  }

  @Get('callback')
  async callback(@Query('code') code: string, @Session() session: Record) {
    const { data } = await axios.post(`${this.IAM}/oauth/token`, {
      grant_type: 'authorization_code',
      client_id: this.CLIENT_ID,
      client_secret: this.CLIENT_SECRET,
      code,
      redirect_uri: 'https://mi-backend.com/auth/callback',
    });

    session.accessToken = data.access_token; // guardalo del lado del servidor
    session.refreshToken = data.refresh_token;
  }
}

Como es cliente confidencial, no hace falta PKCE (aunque no molesta agregarlo igual). El token queda guardado en la sesión del backend, nunca expuesto al navegador.

6. NestJS — servicio backend-a-backend (Client Credentials)

Para un servicio que consulta/gestiona usuarios por su cuenta, sin un usuario logueado de por medio — scheduler, worker, integración entre sistemas.

iam-client.service.ts
@Injectable()
export class IamClientService {
  private token: string | null = null;
  private expiresAt = 0;

  private async getToken(): Promise {
    if (this.token && Date.now() < this.expiresAt) return this.token;

    const { data } = await axios.post('http://localhost:8000/oauth/token', {
      grant_type: 'client_credentials',
      client_id: process.env.IAM_SERVICE_CLIENT_ID,
      client_secret: process.env.IAM_SERVICE_CLIENT_SECRET,
      scope: 'users:read users:write',
    });

    this.token = data.access_token;
    this.expiresAt = Date.now() + (data.expires_in - 60) * 1000; // renovar 1 min antes
    return this.token;
  }

  async listUsers() {
    const token = await this.getToken();
    const { data } = await axios.get('http://localhost:8000/api/users', {
      headers: { Authorization: `Bearer ${token}` },
    });
    return data;
  }
}
Un token de client_credentials no tiene usuario asociado: actúa como "servicio de confianza". En este servidor eso significa que puede usar /api/users y /api/apps con el scope como único chequeo (sin necesitar un permiso de usuario admin) — pero no puede tocar recursos "de cuenta propia" (/api/clients, personal access tokens, apps autorizadas), porque esos son inherentemente de un usuario y no tiene ninguno asociado.

7. Flutter / apps móviles

Con el paquete flutter_appauth (implementa Authorization Code + PKCE con el navegador del sistema, que es lo recomendado — evita usar un WebView embebido).

auth_service.dart
final appAuth = FlutterAppAuth();

Future<String?> login() async {
  final result = await appAuth.authorizeAndExchangeCode(
    AuthorizationTokenRequest(
      '{tu_client_id}',
      'com.miapp://callback', // custom scheme registrado en tu app
      serviceConfiguration: const AuthorizationServiceConfiguration(
        authorizationEndpoint: 'http://localhost:8000/oauth/authorize',
        tokenEndpoint: 'http://localhost:8000/oauth/token',
      ),
      scopes: ['users:read'],
    ),
  );

  return result.accessToken;
}

Future<Map<String, dynamic>> me(String token) async {
  final res = await http.get(
    Uri.parse('http://localhost:8000/api/user'),
    headers: {'Authorization': 'Bearer $token'},
  );
  return jsonDecode(res.body);
}

El redirect_uri (com.miapp://callback) tiene que estar registrado exactamente igual en el cliente OAuth (sección 2) y configurado en tu app nativa (Android: intent filter; iOS: URL scheme en el Info.plist).

8. Validar el token en tu backend

Cuando tu backend recibe un request con Authorization: Bearer {token} y necesita confirmar que es válido, hay dos caminos:

Opción A — Preguntarle a este servidor (más simple)

GET /api/user
Authorization: Bearer {token}

Si responde 200 con los datos del usuario, el token es válido. Es un request de red extra por cada llamada, pero es siempre exacto: refleja revocaciones al instante.

Opción B — Validar el JWT localmente (sin red)

Los access tokens de este servidor son JWT firmados con RS256 (no opacos). Tu backend puede verificar la firma con la clave pública de Passport (storage/oauth-public.key en este proyecto — pedísela al equipo que administra este servidor) sin llamarlo por red:

// Node.js, con la librería "jsonwebtoken"
import jwt from 'jsonwebtoken';
import fs from 'fs';

const publicKey = fs.readFileSync('oauth-public.key');

function verify(token: string) {
  return jwt.verify(token, publicKey, { algorithms: ['RS256'] });
  // -> { aud, jti, iat, nbf, exp, sub, scopes: string[] }
}

Claims relevantes: sub = id del usuario (ausente en tokens de client_credentials), aud = client_id que lo pidió, scopes = array de scopes otorgados, exp = expiración (unix timestamp).

Trade-off: la validación local es más rápida (sin red) pero no se entera si el token fue revocado antes de su expiración natural — solo valida firma y expiración. Para casos sensibles (borrar acceso ya mismo), usá la Opción A o combiná: validación local + chequeo periódico contra el servidor.

9. Roles: global vs. por-app

GET /api/user (o /api/users/{id}) devuelve, entre otros campos:

{
  "id": 2,
  "name": "Carla Gómez",
  "email": "carla@example.com",
  "roles": ["user"],
  "app_roles": ["editor"]
}

Un admin asigna estos roles desde /admin/users → editar usuario → "Roles por app", eligiendo tu app de la lista. Interpretalos como quieras del lado tuyo (un guard de NestJS, un route guard de Angular, etc.) — este servidor solo los guarda y te los devuelve.

nestjs guard (ejemplo)
@Injectable()
export class AppRoleGuard implements CanActivate {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const req = context.switchToHttp().getRequest();
    const user = await this.iam.me(req.headers.authorization);
    return user.app_roles.includes('editor');
  }
}

10. Refrescar y revocar tokens

Los access tokens expiran (15 días por defecto en este servidor). Si tu cliente pidió el scope refresh_token implícito del grant (viene incluido en authorization_code y password), la respuesta de /oauth/token trae un refresh_token — usalo así, sin volver a pasar por el login:

curl -X POST http://localhost:8000/oauth/token \
  -d grant_type=refresh_token \
  -d refresh_token={tu_refresh_token} \
  -d client_id={tu_client_id} \
  -d client_secret={tu_client_secret}   # si tu cliente es confidencial

client_credentials no tiene refresh token — simplemente pedís uno nuevo cuando el anterior expira (ver el ejemplo de NestJS en la sección 6).

El usuario puede revocarte el acceso en cualquier momento desde /authorized-apps en el panel de este servidor — a partir de ahí tus requests con ese token empiezan a devolver 401.

11. Checklist de seguridad

12. Referencia rápida

EndpointPara qué
GET /oauth/authorizeInicio del flujo Authorization Code (redirige al login/consentimiento).
POST /oauth/tokenCanjear código, credenciales o refresh token por un access token.
GET /api/userWhoami — usuario del token actual + roles global y por-app.
GET/POST/PUT/DELETE /api/usersDirectorio de usuarios (scope users:read/write).
GET/POST/DELETE /api/users/{id}/app-rolesRoles por-app de un usuario (para asignarlos vos mismo por API, si tenés permiso).
GET /api/appsListado de apps (clientes OAuth) registradas.
GET/POST/PUT/DELETE /api/clientsGestionar tus propios clientes OAuth por API.
ScopeDa acceso a
users:readLeer usuarios y sus roles (global + por-app).
users:writeCrear/editar/borrar usuarios, asignar roles por-app.
account:readLeer tus propios clientes OAuth, tokens y apps autorizadas.
account:writeGestionar tus propios clientes OAuth, tokens y apps autorizadas.

Documentación interactiva (Swagger, para probar todo esto desde el navegador) en /api/documentation. Manual completo del panel de administración en docs/manual-usuario.pdf.