Skip to main content

Overview

Satélite API uses a two-tier authentication system:
  1. Azure AD Authentication - Initial authentication using Microsoft Azure AD
  2. JWT Token Authentication - API-specific JWT tokens for subsequent requests
This approach provides enterprise-grade security while maintaining performance and scalability.
All API requests (except the authentication mutation) require a valid JWT token in the Authorization header.

Authentication Flow

1

Obtain Azure AD Token

Authenticate with Microsoft Azure AD to get an access token. This is typically handled by your organization’s identity provider.
# Your application obtains an Azure AD token
# This process varies depending on your auth library
2

Exchange for Satélite Token

Call the authenticate mutation with your Azure AD token to receive a Satélite API JWT token.
mutation {
  authenticate {
    token
    usuario {
      id
      nombre
      email
    }
  }
}
3

Use Satélite Token

Include the Satélite JWT token in the Authorization header for all subsequent API requests.
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

JWT Configuration

The API validates JWT tokens using the following parameters (configured in Program.cs:319-354):
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidAudience = builder.Configuration["Jwt:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])
            )
        };
    });

Token Validation

The API validates:
  • Issuer - Token must come from the configured issuer
  • Audience - Token must be intended for this API
  • Lifetime - Token must not be expired
  • Signing Key - Token must be signed with the correct key

Token Extraction

The API supports two methods for providing authentication tokens:
Authorization: Bearer YOUR_JWT_TOKEN
The API also checks for a satelite_session cookie:
document.cookie = "satelite_session=YOUR_JWT_TOKEN; path=/";
This is implemented in Program.cs:322-341:
options.Events = new JwtBearerEvents
{
    OnMessageReceived = context =>
    {
        var token = context.Request.Headers["Authorization"].FirstOrDefault();
        if (!string.IsNullOrEmpty(token) && token.StartsWith("Bearer "))
        {
            context.Token = token.Substring("Bearer ".Length);
        }
        else
        {
            token = context.Request.Cookies["satelite_session"];
            if (!string.IsNullOrEmpty(token))
            {
                context.Token = token;
            }
        }
        return Task.CompletedTask;
    }
};

Authentication Mutation

The authenticate mutation is the entry point for obtaining a Satélite API token.

Mutation Signature

type Mutation {
  authenticate: SessionDTO!
}

Request

mutation {
  authenticate {
    token
    usuario {
      id
      nombre
      email
      activo
      idRol
      rol {
        id
        nombre
        descripcion
      }
    }
    permisos {
      id
      idUsuario
      idPermiso
      permiso {
        id
        nombre
        activo
      }
    }
    menu {
      id
      nombre
      ruta
      icono
      orden
    }
  }
}

cURL Example

curl -X POST https://your-api-endpoint/graphql \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_AZURE_AD_TOKEN" \
  -d '{
    "query": "mutation { authenticate { token usuario { id nombre email } permisos { permiso { nombre } } menu { nombre ruta } } }"
  }'

Response Schema

interface SessionDTO {
  token: string;                    // JWT token for API access
  usuario: UsuarioDTO;              // User information
  permisos: UsuarioPermisosDTO[];   // User permissions
  menu: MenuDTO[];                  // Available menu items
}

interface UsuarioDTO {
  id: number;
  nombre: string;
  email: string;
  activo: boolean;
  idRol: number | null;
  rol: RolesDTO | null;
}

interface UsuarioPermisosDTO {
  id: number;
  idUsuario: number;
  idPermiso: number;
  permiso: PermisoDTO;
}

interface MenuDTO {
  id: number;
  nombre: string;
  ruta: string;
  icono: string | null;
  orden: number;
}

Success Response

{
  "data": {
    "authenticate": {
      "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsImVtYWlsIjoidXNlckBtb2Rlcm5hLmNvbS5lYyIsInJvbCI6IjMiLCJleHAiOjE3MDk4NDMyMDB9.signature",
      "usuario": {
        "id": 42,
        "nombre": "Juan Pérez",
        "email": "[email protected]",
        "activo": true,
        "idRol": 3,
        "rol": {
          "id": 3,
          "nombre": "Usuario Estándar",
          "descripcion": "Acceso a funcionalidades básicas"
        }
      },
      "permisos": [
        {
          "id": 1,
          "idUsuario": 42,
          "idPermiso": 5,
          "permiso": {
            "id": 5,
            "nombre": "Gestión de Órdenes",
            "activo": true
          }
        },
        {
          "id": 2,
          "idUsuario": 42,
          "idPermiso": 8,
          "permiso": {
            "id": 8,
            "nombre": "Consulta de Reportes",
            "activo": true
          }
        }
      ],
      "menu": [
        {
          "id": 1,
          "nombre": "Dashboard",
          "ruta": "/dashboard",
          "icono": "home",
          "orden": 1
        },
        {
          "id": 2,
          "nombre": "Órdenes de Compra",
          "ruta": "/ordenes",
          "icono": "shopping-cart",
          "orden": 2
        }
      ]
    }
  }
}

Token Generation Process

The API generates JWT tokens with the following claims (implemented in AuthenticationRepository.cs:106-141):
public string GenerateToken(UsuarioDTO user, List<UsuarioPermisosDTO> permissions)
{
    var securityKey = new SymmetricSecurityKey(
        Encoding.UTF8.GetBytes(_config["Jwt:Key"])
    );
    var credentials = new SigningCredentials(
        securityKey, 
        SecurityAlgorithms.HmacSha256
    );
    
    List<Claim> claims = new()
    {
        new Claim("sub", user.Id.ToString()),
        new Claim("email", user.Email),
        new Claim("rol", user.IdRol?.ToString() ?? ""),
    };
    
    // Add special scopes for users with specific permissions
    if (permissions.Find(x => 
        x.Permiso != null && 
        x.Permiso.Nombre.ToLower().Contains("vista") && 
        x.Permiso.Nombre.ToLower().Contains("usuario")) != null)
    {
        claims.Add(new Claim("scopes", "user:impersonate"));
    }
    
    var token = new JwtSecurityToken(
        _config["Jwt:Issuer"],
        _config["Jwt:Audience"],
        claims,
        expires: DateTime.UtcNow.AddMinutes(SESSION_MINUTES),
        signingCredentials: credentials
    );
    
    return new JwtSecurityTokenHandler().WriteToken(token);
}

Token Claims

ClaimDescriptionExample
subUser ID"42"
emailUser email address"[email protected]"
rolUser role ID"3"
scopesSpecial permissions (optional)"user:impersonate"
issToken issuerConfigured in appsettings.json
audToken audienceConfigured in appsettings.json
expExpiration timestampUnix timestamp

Token Lifetime

Tokens are valid for 120 minutes (2 hours) from issuance:
private const double SESSION_MINUTES = 120;
Tokens cannot be refreshed. When a token expires, users must re-authenticate through the authenticate mutation.

Email Domain Validation

The API only accepts users with @moderna.com.ec email domains (implemented in AuthenticationRepository.cs:41-44):
if (emailClaim == null || !emailClaim.EndsWith("@moderna.com.ec"))
{
    throw GraphQLErrors.Unauthorized("Sessión no válida");
}
This validation happens during the Azure AD token exchange, ensuring only authorized users can access the API.

User Auto-Registration

If a user doesn’t exist in the database but has a valid Azure AD token, they are automatically registered (AuthenticationRepository.cs:49-60):
var user = await context.Usuarios
    .Include(x => x.Rol)
    .Where(x => x.Email == emailClaim)
    .FirstOrDefaultAsync();

if (user == null) {
    var name = jwtToken.Claims.FirstOrDefault(c => c.Type == "name")?.Value;
    user = new UsuarioDTO()
    {
        Email = emailClaim,
        Activo = true,
        CreatedAt = DateTime.Now,
        Nombre = name,
    };
    await context.Usuarios.AddAsync(user);
    await context.SaveChangesAsync();
}

Permission-Based Authorization

After authentication, the API enforces permissions at the query/mutation level:

Using [Authorize] Attribute

Many queries require authorization:
[Authorize]
public async Task<IEnumerable<UsuarioDTO>> GetUsuarios()
{
    return await _userRepository.GetAll();
}

Permission Checks

The authentication response includes user permissions that should be checked client-side:
function hasPermission(session: SessionDTO, permissionName: string): boolean {
  return session.permisos.some(
    p => p.permiso.nombre.toLowerCase().includes(permissionName.toLowerCase())
  );
}

if (hasPermission(session, "gestión de órdenes")) {
  // User can manage orders
}

Error Scenarios

Invalid Azure AD Token

{
  "errors": [
    {
      "message": "Sessión no válida",
      "extensions": {
        "code": "AUTH_NOT_AUTHORIZED"
      }
    }
  ]
}

User Without Permissions

{
  "errors": [
    {
      "message": "No tiene rol asignado. Por favor, contacte al administrador del sistema para que le asigne un rol",
      "extensions": {
        "code": "AUTH_NOT_AUTHORIZED"
      }
    }
  ]
}

Inactive User Account

{
  "errors": [
    {
      "message": "Tu acceso se encuentra deshabilitado. Contacta a un administrador si se trata de un error",
      "extensions": {
        "code": "AUTH_NOT_AUTHORIZED"
      }
    }
  ]
}

Expired Token

When using an expired token:
{
  "errors": [
    {
      "message": "The token is expired.",
      "extensions": {
        "code": "AUTH_NOT_AUTHENTICATED"
      }
    }
  ]
}

Best Practices

Token Storage

Store tokens securely using:
  • HTTP-only cookies (for web apps)
  • Secure storage APIs (for mobile apps)
  • Memory only (for SPAs)
Never store tokens in localStorage for production applications.

Token Refresh

Implement a token refresh strategy:
  • Monitor token expiration
  • Re-authenticate before expiry
  • Handle 401 errors gracefully
  • Provide clear user feedback

HTTPS Only

Always use HTTPS in production:
  • Protects tokens in transit
  • Prevents man-in-the-middle attacks
  • Required for secure cookies

Error Handling

Implement robust error handling:
  • Check for authentication errors
  • Redirect to login on 401
  • Log authentication failures
  • Show user-friendly messages

Code Examples

JavaScript/TypeScript

class SateliteAuthClient {
  private token: string | null = null;
  private tokenExpiry: Date | null = null;
  
  async authenticate(azureToken: string): Promise<SessionDTO> {
    const response = await fetch('https://your-api/graphql', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${azureToken}`,
      },
      body: JSON.stringify({
        query: `
          mutation {
            authenticate {
              token
              usuario { id nombre email }
              permisos { permiso { nombre } }
            }
          }
        `,
      }),
    });
    
    const { data, errors } = await response.json();
    
    if (errors) {
      throw new Error(errors[0].message);
    }
    
    this.token = data.authenticate.token;
    // Token expires in 120 minutes
    this.tokenExpiry = new Date(Date.now() + 120 * 60 * 1000);
    
    return data.authenticate;
  }
  
  isTokenValid(): boolean {
    return this.token !== null && 
           this.tokenExpiry !== null && 
           this.tokenExpiry > new Date();
  }
  
  async query(query: string, variables?: any): Promise<any> {
    if (!this.isTokenValid()) {
      throw new Error('Token expired or not available');
    }
    
    const response = await fetch('https://your-api/graphql', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${this.token}`,
      },
      body: JSON.stringify({ query, variables }),
    });
    
    const result = await response.json();
    
    if (result.errors) {
      // Check if it's an authentication error
      if (result.errors[0]?.extensions?.code === 'AUTH_NOT_AUTHENTICATED') {
        this.token = null;
        this.tokenExpiry = null;
        throw new Error('Authentication required');
      }
      throw new Error(result.errors[0].message);
    }
    
    return result.data;
  }
}

// Usage
const client = new SateliteAuthClient();
const session = await client.authenticate(azureToken);
console.log(`Authenticated as: ${session.usuario.nombre}`);

const users = await client.query(`
  query {
    getUsuarios {
      id
      nombre
      email
    }
  }
`);

C#

using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

public class SateliteAuthClient
{
    private readonly HttpClient _httpClient;
    private string? _token;
    private DateTime? _tokenExpiry;
    
    public SateliteAuthClient(string apiEndpoint)
    {
        _httpClient = new HttpClient
        {
            BaseAddress = new Uri(apiEndpoint)
        };
    }
    
    public async Task<SessionDTO> AuthenticateAsync(string azureToken)
    {
        var query = new
        {
            query = @"
                mutation {
                    authenticate {
                        token
                        usuario { id nombre email }
                        permisos { permiso { nombre } }
                    }
                }
            "
        };
        
        var content = new StringContent(
            JsonConvert.SerializeObject(query),
            Encoding.UTF8,
            "application/json"
        );
        
        _httpClient.DefaultRequestHeaders.Authorization = 
            new AuthenticationHeaderValue("Bearer", azureToken);
        
        var response = await _httpClient.PostAsync("/graphql", content);
        response.EnsureSuccessStatusCode();
        
        var result = await response.Content.ReadAsStringAsync();
        var json = JObject.Parse(result);
        
        if (json["errors"] != null)
        {
            throw new Exception(json["errors"][0]["message"].ToString());
        }
        
        _token = json["data"]["authenticate"]["token"].ToString();
        _tokenExpiry = DateTime.UtcNow.AddMinutes(120);
        
        return JsonConvert.DeserializeObject<SessionDTO>(
            json["data"]["authenticate"].ToString()
        );
    }
    
    public bool IsTokenValid()
    {
        return !string.IsNullOrEmpty(_token) && 
               _tokenExpiry.HasValue && 
               _tokenExpiry.Value > DateTime.UtcNow;
    }
    
    public async Task<JObject> QueryAsync(string query)
    {
        if (!IsTokenValid())
        {
            throw new InvalidOperationException("Token expired or not available");
        }
        
        var content = new StringContent(
            JsonConvert.SerializeObject(new { query }),
            Encoding.UTF8,
            "application/json"
        );
        
        _httpClient.DefaultRequestHeaders.Authorization = 
            new AuthenticationHeaderValue("Bearer", _token);
        
        var response = await _httpClient.PostAsync("/graphql", content);
        response.EnsureSuccessStatusCode();
        
        var result = await response.Content.ReadAsStringAsync();
        var json = JObject.Parse(result);
        
        if (json["errors"] != null)
        {
            throw new Exception(json["errors"][0]["message"].ToString());
        }
        
        return (JObject)json["data"];
    }
}

// Usage
var client = new SateliteAuthClient("https://your-api-endpoint");
var session = await client.AuthenticateAsync(azureToken);
Console.WriteLine($"Authenticated as: {session.Usuario.Nombre}");

var result = await client.QueryAsync(@"
    query {
        getUsuarios {
            id
            nombre
            email
        }
    }
");

Next Steps

GraphQL Overview

Learn about the GraphQL schema and available operations

Error Handling

Understand how to handle authentication and authorization errors

API Reference

Explore the complete authentication API reference

Security Best Practices

Learn about production security configuration

Build docs developers (and LLMs) love