Skip to main content

OAuth2 Authorization Code Grant

Capital Check In API implementa el flujo OAuth2 Authorization Code Grant según el estándar RFC 6749. Este método es ideal para aplicaciones de terceros que necesitan acceder a recursos de usuarios de Capital Check In.

Flujo OAuth2

Endpoints OAuth2

1. Solicitud de Autorización

2. Aprobar Autorización

3. Denegar Autorización

4. Intercambiar Código por Token

Scopes Disponibles

ScopeDescripciónPermisos
read:profileLeer perfil de usuarioObtener información básica del usuario
read:groupsLeer gruposObtener lista de grupos y colaboradores
write:profileModificar perfilActualizar información del usuario
write:groupsModificar gruposCrear, actualizar y eliminar grupos
read:reportsLeer reportesAcceder a reportes de check-in
write:reportsCrear reportesGenerar y modificar reportes

Configuración de Cliente OAuth2

Registrar Aplicación Cliente

Para usar OAuth2, necesitas registrar tu aplicación:

Implementación en JavaScript

Cliente OAuth2

class CapitalCheckInOAuth2 {
  constructor(clientId, clientSecret, redirectUri) {
    this.clientId = clientId;
    this.clientSecret = clientSecret;
    this.redirectUri = redirectUri;
    this.baseURL = 'https://api.capitalcheckin.app';
  }

  // Paso 1: Redirigir al usuario para autorización
  initiateAuthorization(scope = 'read:profile', state = null) {
    const params = new URLSearchParams({
      client_id: this.clientId,
      redirect_uri: this.redirectUri,
      response_type: 'code',
      scope: scope
    });

    if (state) {
      params.append('state', state);
    }

    const authUrl = `${this.baseURL}/oauth/authorize?${params.toString()}`;
    window.location.href = authUrl;
  }

  // Paso 2: Intercambiar código por token
  async exchangeCodeForToken(code) {
    const response = await fetch(`${this.baseURL}/oauth/token`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json'
      },
      body: JSON.stringify({
        grant_type: 'authorization_code',
        client_id: this.clientId,
        client_secret: this.clientSecret,
        code: code,
        redirect_uri: this.redirectUri
      })
    });

    if (!response.ok) {
      const error = await response.json();
      throw new Error(error.message || 'Token exchange failed');
    }

    return response.json();
  }

  // Paso 3: Usar el token para acceder a la API
  async makeAuthenticatedRequest(endpoint, token) {
    const response = await fetch(`${this.baseURL}${endpoint}`, {
      headers: {
        'Authorization': `Bearer ${token}`,
        'Accept': 'application/json'
      }
    });

    if (!response.ok) {
      const error = await response.json();
      throw new Error(error.message || 'Request failed');
    }

    return response.json();
  }

  // Paso 4: Renovar token usando refresh token
  async refreshToken(refreshToken) {
    const response = await fetch(`${this.baseURL}/oauth/token`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json'
      },
      body: JSON.stringify({
        grant_type: 'refresh_token',
        client_id: this.clientId,
        client_secret: this.clientSecret,
        refresh_token: refreshToken
      })
    });

    if (!response.ok) {
      const error = await response.json();
      throw new Error(error.message || 'Token refresh failed');
    }

    return response.json();
  }
}

// Uso
const oauth2 = new CapitalCheckInOAuth2(
  'your_client_id',
  'your_client_secret',
  'https://your-app.com/callback'
);

// Iniciar flujo de autorización
oauth2.initiateAuthorization('read:profile read:groups', 'random_state');

// En la página de callback
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const state = urlParams.get('state');

if (code) {
  try {
    const tokenResponse = await oauth2.exchangeCodeForToken(code);
    console.log('Token obtenido:', tokenResponse);
    
    // Usar el token para acceder a la API
    const profile = await oauth2.makeAuthenticatedRequest(
      '/v1/users/profile',
      tokenResponse.access_token
    );
    console.log('Perfil:', profile);
  } catch (error) {
    console.error('Error:', error);
  }
}

Página de Autorización

Implementación del Frontend

// Página de autorización OAuth2
class OAuthAuthorizationPage {
  constructor() {
    this.stateId = null;
    this.client = null;
    this.scopesRequested = '';
    this.state = '';
  }

  async initialize() {
    // Obtener parámetros de la URL
    const urlParams = new URLSearchParams(window.location.search);
    const clientId = urlParams.get('client_id');
    const redirectUri = urlParams.get('redirect_uri');
    const scope = urlParams.get('scope');
    const state = urlParams.get('state');

    // Validar parámetros requeridos
    if (!clientId || !redirectUri) {
      this.showError('Parámetros OAuth2 inválidos');
      return;
    }

    // Obtener información del cliente
    try {
      const response = await fetch(`/api/oauth/authorize?${window.location.search}`);
      const data = await response.json();

      if (data.success) {
        this.stateId = data.state_id;
        this.client = data.client;
        this.scopesRequested = data.scopes_requested;
        this.state = data.state;
        this.renderAuthorizationPage();
      } else {
        this.showError(data.message);
      }
    } catch (error) {
      this.showError('Error al inicializar autorización');
    }
  }

  renderAuthorizationPage() {
    const container = document.getElementById('oauth-container');
    container.innerHTML = `
      <div class="oauth-card">
        <div class="client-info">
          <img src="${this.client.logo}" alt="${this.client.name}" class="client-logo">
          <h2>${this.client.name}</h2>
          <p>${this.client.description}</p>
        </div>
        
        <div class="permissions">
          <h3>Permisos Solicitados:</h3>
          <ul>
            ${this.scopesRequested.split(' ').map(scope => 
              `<li>${this.getScopeDescription(scope)}</li>`
            ).join('')}
          </ul>
        </div>

        <div class="actions">
          <button onclick="authorizeApp.approve()" class="btn-approve">
            Autorizar
          </button>
          <button onclick="authorizeApp.deny()" class="btn-deny">
            Denegar
          </button>
        </div>
      </div>
    `;
  }

  getScopeDescription(scope) {
    const scopeDescriptions = {
      'read:profile': 'Leer información de tu perfil',
      'read:groups': 'Leer información de grupos',
      'write:profile': 'Modificar tu perfil',
      'write:groups': 'Modificar grupos',
      'read:reports': 'Leer reportes',
      'write:reports': 'Crear reportes'
    };
    return scopeDescriptions[scope] || scope;
  }

  async approve() {
    try {
      const response = await fetch('/api/oauth/authorize', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          state_id: this.stateId,
          jwt: this.getUserToken()
        })
      });

      const data = await response.json();
      if (data.success) {
        window.location.href = data.redirect_url;
      } else {
        this.showError(data.message);
      }
    } catch (error) {
      this.showError('Error al autorizar aplicación');
    }
  }

  async deny() {
    try {
      const response = await fetch('/api/oauth/authorize', {
        method: 'DELETE',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          state_id: this.stateId,
          jwt: this.getUserToken()
        })
      });

      const data = await response.json();
      if (data.success) {
        window.location.href = data.redirect_url;
      } else {
        this.showError(data.message);
      }
    } catch (error) {
      this.showError('Error al denegar autorización');
    }
  }

  getUserToken() {
    // Obtener token del usuario desde localStorage o cookies
    return localStorage.getItem('access_token') || '';
  }

  showError(message) {
    const container = document.getElementById('oauth-container');
    container.innerHTML = `
      <div class="error-message">
        <h3>Error</h3>
        <p>${message}</p>
        <button onclick="window.close()">Cerrar</button>
      </div>
    `;
  }
}

// Inicializar página de autorización
const authorizeApp = new OAuthAuthorizationPage();
authorizeApp.initialize();

Seguridad

Mejores Prácticas

  1. State Parameter: Siempre usa el parámetro state para prevenir ataques CSRF
  2. PKCE: Implementa PKCE para aplicaciones públicas
  3. HTTPS: Usa HTTPS en todas las comunicaciones
  4. Validación de Redirect URI: Valida que la URI de redirección esté autorizada
  5. Scope Validation: Valida los permisos solicitados
  6. Token Storage: Almacena tokens de forma segura

Validaciones de Seguridad

  • Client ID Validation: Verificación del ID del cliente
  • Redirect URI Validation: Validación de URI de redirección
  • Scope Validation: Validación de permisos solicitados
  • State Parameter: Prevención de ataques CSRF
  • JWT Validation: Validación del token JWT del usuario

Manejo de Errores

Errores Comunes