Overview
Satélite API uses a two-tier authentication system:
Azure AD Authentication - Initial authentication using Microsoft Azure AD
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
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
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
}
}
}
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
The API supports two methods for providing authentication tokens:
Authorization: Bearer YOUR_JWT_TOKEN
2. Cookie-based Authentication
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
Claim Description Example subUser ID "42"emailUser email address "[email protected] "rolUser role ID "3"scopesSpecial permissions (optional) "user:impersonate"issToken issuer Configured in appsettings.json audToken audience Configured in appsettings.json expExpiration timestamp Unix 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
}
}
` );
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