laramicrosoft / auth
LaraMicrosoft-Auth: autenticación social con Microsoft Entra ID (Office 365). Integrable con frontends Vue, Nuxt o React.
Requires
- php: >=8.4
- league/oauth2-client: ^2.7
Requires (Dev)
- phpunit/phpunit: ^11.0
README
Librería PHP para autenticación social con Microsoft Entra ID (Office 365) mediante OAuth 2.0. Pensada para backends en PHP (Laravel, Slim, etc.) que exponen la lógica de login; el frontend (Vue, Nuxt, React) solo redirige al usuario a Microsoft y envía el código de autorización de vuelta al backend.
- PHP 8.4+
- Flujo: Authorization Code (backend confidencial)
- Frontend: Cualquier SPA o SSR (Vue, Nuxt, React, etc.)
Índice
- Requisitos
- Instalación
- Flujo de autenticación
- Registro en Azure (Entra ID)
- Configuración en el backend
- Endpoints del backend
- Integración en el frontend
- Referencia de la API
- Datos que devuelve Office 365
- Opciones de configuración
- Solución de problemas
- Seguridad
- Licencia
Requisitos
- PHP >= 8.4
- Composer 2.x
- Aplicación registrada en Azure Portal (Microsoft Entra ID) con Client ID, Client Secret y URI de redirección configurados
Instalación
composer require laramicrosoft/auth
Flujo de autenticación
┌─────────────┐ GET /auth/entra/url ┌─────────────┐
│ Frontend │ ──────────────────────────► │ Backend │
│ (Vue/React) │ ◄────────────────────────── │ (PHP) │
└──────┬──────┘ { url, state } └──────┬──────┘
│ │
│ redirect usuario a url │ guarda state en sesión
▼ │
┌─────────────┐ │
│ Microsoft │ usuario inicia sesión │
│ Entra ID │ y acepta permisos │
└──────┬──────┘ │
│ redirect a redirect_uri?code=...&state=... │
▼ │
┌─────────────┐ POST /auth/entra/callback │
│ Frontend │ { code, state } ─────────────────►│
│ (callback) │ ◄───────────────────────────────────│ valida state, canjea
└─────────────┘ sesión / token / cookie │ code por token y usuario
- El frontend pide al backend la URL de autorización (y opcionalmente un
state). - El backend genera la URL de Microsoft, guarda el
stateen sesión y devuelve{ url, state }. - El frontend redirige al usuario a esa URL; el usuario inicia sesión en Microsoft.
- Microsoft redirige a tu
redirect_uriconcodeystateen la query. - El frontend envía
codeystateal backend; el backend valida elstate, canjea elcodepor tokens y usuario, y establece la sesión (o devuelve un token).
Registro en Azure (Entra ID)
Pasos
-
Entra en Azure Portal → Microsoft Entra ID → Registros de aplicaciones → Nuevo registro.
-
Nombre: por ejemplo
Mi AppoLaraMicrosoft-Auth Demo. -
Tipos de cuenta admitidos:
- Solo mi organización: solo usuarios de tu tenant.
- Cualquier organización: cuentas laborales o escolares de cualquier tenant.
- Cuentas personales y laborales: incluye cuentas Microsoft personales (outlook.com, etc.).
-
URI de redirección:
- Tipo Web.
- URL donde Microsoft enviará al usuario tras el login. Debe coincidir exactamente con la ruta de callback de tu app (backend o frontend), por ejemplo:
- Producción:
https://tu-dominio.com/auth/entra/callback - Local:
http://localhost:3000/auth/callback(ajusta puerto y ruta a tu app).
- Producción:
-
Tras crear el registro, anota:
- Id. de aplicación (cliente) → lo usarás como
client_id. - Id. de directorio (inquilino) → lo usarás como
tenant(ocommonpara multi-tenant).
- Id. de aplicación (cliente) → lo usarás como
-
Certificados y secretos → Nuevo secreto de cliente → copia el Valor (solo se muestra una vez) →
client_secret. -
Permisos de API → Agregar un permiso → Microsoft Graph → Permisos delegados. Añade al menos:
openidprofileemailUser.Read
Resumen de valores
| Parámetro | Dónde se obtiene |
|---|---|
client_id |
Registro de la aplicación → Información esencial |
client_secret |
Certificados y secretos → Valor del secreto |
tenant |
Información esencial → Id. de directorio, o common |
redirect_uri |
La URL que configuraste en URI de redirección |
Configuración en el backend
Crea la configuración a partir de un array (por ejemplo variables de entorno):
use LaraMicrosoft\Auth\Config\EntraIdConfig; use LaraMicrosoft\Auth\EntraIdAuthService; $config = EntraIdConfig::fromArray([ 'client_id' => getenv('ENTRA_CLIENT_ID'), 'client_secret' => getenv('ENTRA_CLIENT_SECRET'), 'redirect_uri' => getenv('ENTRA_REDIRECT_URI'), // ej. https://tu-dominio.com/auth/entra/callback 'tenant' => getenv('ENTRA_TENANT_ID') ?: 'common', 'scopes' => ['openid', 'profile', 'email', 'User.Read'], ]); $entraAuth = new EntraIdAuthService($config);
Ejemplo de .env:
ENTRA_CLIENT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx ENTRA_CLIENT_SECRET=tu~secreto~de~cliente ENTRA_REDIRECT_URI=https://tu-dominio.com/auth/entra/callback ENTRA_TENANT_ID=common
La librería exige que client_id, client_secret y redirect_uri no estén vacíos; si lo están, se lanzará una excepción al crear la config.
Endpoints del backend
Tu aplicación debe exponer al menos dos rutas que usen EntraIdAuthService.
1. Obtener URL de login
El frontend llama a esta ruta para obtener la URL a la que redirigir al usuario.
Ejemplo (pseudo-framework):
// GET /api/auth/entra/url $state = bin2hex(random_bytes(16)); $_SESSION['entra_oauth_state'] = $state; // o Redis, etc. $result = $entraAuth->getAuthorizationUrl($state); return [ 'url' => $result['url'], 'state' => $result['state'], ];
El frontend guarda state (por ejemplo en sessionStorage) y redirige al usuario a url.
2. Callback: canjear código por token y usuario
Microsoft redirige al usuario a tu redirect_uri con ?code=...&state=.... El frontend (o el backend si el callback es una ruta del propio backend) debe enviar code y state a esta ruta.
Ejemplo (pseudo-framework):
// POST /api/auth/entra/callback // Body: { "code": "...", "state": "..." } $code = $request->input('code'); $state = $request->input('state'); $expectedState = $_SESSION['entra_oauth_state'] ?? null; try { $result = $entraAuth->exchangeCodeAndGetUser($code, $state, $expectedState); } catch (\LaraMicrosoft\Auth\Exception\InvalidStateException $e) { // State no coincide: posible CSRF return response()->json(['error' => 'invalid_state'], 400); } catch (\LaraMicrosoft\Auth\Exception\TokenExchangeException $e) { // Error al canjear el código (código expirado, revocado, etc.) return response()->json(['error' => 'token_exchange_failed'], 400); } $accessToken = $result['token']; $user = $result['user']; // Ejemplo de datos del usuario $user->getId(); // sub/oid de Microsoft $user->getEmail(); // email o preferred_username $user->getName(); // nombre completo $user->getGivenName(); $user->getFamilyName(); $user->toArray(); // todos los claims // Aquí: crear o actualizar usuario en tu BD, iniciar sesión, devolver cookie o JWT al frontend
Integración en el frontend
Vue 3 o React (fetch)
Iniciar login:
async function loginWithEntra() { const res = await fetch('/api/auth/entra/url', { credentials: 'include' }); const { url, state } = await res.json(); sessionStorage.setItem('entra_oauth_state', state); window.location.href = url; }
Página de callback (ruta que hayas configurado como redirect_uri en Azure):
const params = new URLSearchParams(window.location.search); const code = params.get('code'); const state = params.get('state'); if (code && state) { const res = await fetch('/api/auth/entra/callback', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ code, state }), }); if (res.ok) { // Backend habrá establecido cookie/sesión; redirigir al dashboard window.location.href = '/dashboard'; } }
Nuxt 3
Página de login (pages/login.vue):
<script setup> async function loginWithEntra() { const { data } = await useFetch('/api/auth/entra/url'); if (data.value?.url) { sessionStorage.setItem('entra_oauth_state', data.value.state); await navigateTo(data.value.url, { external: true }); } } </script> <template> <button @click="loginWithEntra">Iniciar sesión con Office 365</button> </template>
Página de callback (pages/auth/callback.vue): la URL de esta página debe ser exactamente la configurada como redirect_uri en Azure.
<script setup> const route = useRoute(); const code = route.query.code; const state = route.query.state; if (code && state) { await $fetch('/api/auth/entra/callback', { method: 'POST', body: { code, state }, }); await navigateTo('/'); } </script>
Referencia de la API
| Método | Descripción |
|---|---|
getAuthorizationUrl(?string $state = null) |
Devuelve ['url' => string, 'state' => string]. Si no pasas state, se genera uno aleatorio. |
exchangeCodeForToken(string $code, string $state, ?string $expectedState) |
Canjea el código por un AccessToken. Lanza InvalidStateException o TokenExchangeException si falla. |
getUser(AccessToken $token) |
Obtiene el usuario desde Microsoft (userinfo) como EntraIdResourceOwner. |
exchangeCodeAndGetUser(string $code, string $state, ?string $expectedState) |
Equivalente a exchangeCodeForToken + getUser; devuelve ['token' => AccessToken, 'user' => EntraIdResourceOwner]. |
exchangeCodeAndGetFullResponse(string $code, string $state, ?string $expectedState) |
Igual que arriba pero devuelve todos los datos crudos de O365: ['token_response' => array, 'user' => array]. Ver Datos que devuelve Office 365. |
getConfig() |
Devuelve la instancia de EntraIdConfig usada. |
EntraIdResourceOwner (usuario)
getId()– identificador (sub/oid)getEmail()– email o preferred_usernamegetName()– nombre completogetGivenName()– nombregetFamilyName()– apellidotoArray()– array con todos los claims que devuelve Microsoft (userinfo)
Datos que devuelve Office 365
Para ver toda la respuesta de Entra ID (tokens + usuario), usa exchangeCodeAndGetFullResponse():
$full = $entraAuth->exchangeCodeAndGetFullResponse($code, $state, $expectedState); // Respuesta del endpoint de tokens (POST .../oauth2/v2.0/token) $full['token_response']; // Ejemplo: access_token, refresh_token, expires, token_type, scope, id_token (si aplica), etc. // Todos los claims del usuario (userinfo / OpenID Connect) $full['user']; // Ejemplo: sub, oid, name, given_name, family_name, email, preferred_username, etc.
Campos típicos en token_response
| Campo | Descripción |
|---|---|
access_token |
Token para llamar a APIs (p. ej. Microsoft Graph). |
refresh_token |
Presente si pediste el scope offline_access; sirve para renovar el access_token. |
expires |
Timestamp Unix de expiración del access_token. |
token_type |
Normalmente Bearer. |
scope |
Scopes concedidos (separados por espacio). |
id_token |
JWT con claims del usuario (si pediste openid). |
Campos típicos en user (userinfo)
| Campo | Descripción |
|---|---|
sub |
Identificador único del usuario (claim estándar OIDC). |
oid |
Object ID en Microsoft Entra ID. |
name |
Nombre completo. |
given_name |
Nombre. |
family_name |
Apellido. |
email |
Email (puede no venir si no está configurado). |
preferred_username |
UPN o email que usa para iniciar sesión. |
tenant_id / tid |
ID del tenant. |
Microsoft puede devolver más campos según permisos y configuración del tenant. Con exchangeCodeAndGetFullResponse() o $user->toArray() tienes siempre el array completo para inspeccionar o guardar.
Opciones de configuración
| Clave | Tipo | Requerido | Descripción |
|---|---|---|---|
client_id |
string | Sí | Application (client) ID de Azure. |
client_secret |
string | Sí | Valor del secreto de cliente. |
redirect_uri |
string | Sí | URI de redirección registrada en Azure. |
tenant |
string | No | common, organizations, consumers o ID del tenant. Por defecto: common. |
scopes |
string[] | No | Scopes OAuth; por defecto: ['openid', 'profile', 'email', 'User.Read']. |
prompt |
string | No | Comportamiento de login: login, none, consent, select_account. |
Solución de problemas
AADSTS900144 — "The request body must contain the following parameter: 'client_id'"
Significa que la petición al endpoint de tokens de Microsoft no incluye client_id.
- Comprueba que en tu
.env(o config) tengas ENTRA_CLIENT_ID con el valor del Id. de aplicación (cliente) del registro en Azure. - Asegúrate de que la app lee esa variable al construir
EntraIdConfigy que no esté vacía. Si está vacía, la librería lanzará una excepción al crear la config.
AADSTS50011 — "Reply URL does not match"
La redirect_uri que envías no coincide con ninguna de las URLs configuradas en el registro de la aplicación.
- En Azure → Registro de la aplicación → Autenticación → URI de redirección, añade exactamente la misma URL que usas en
redirect_uri(incluyendo protocolo, dominio, puerto y path).
Invalid state / Estado inválido
- El
stateque envías en el callback debe ser el mismo que guardaste al generar la URL (por ejemplo en sesión). - Asegúrate de que el frontend envía el mismo
stateque recibió al pedir la URL y de que el backend compara con el guardado (por sesión o almacén equivalente).
Código ya canjeado o expirado
Los códigos de autorización son de un solo uso y caducan en poco tiempo (aprox. 1 minuto). Si el usuario recarga la página de callback o se envía el mismo código dos veces, Microsoft devolverá error; en ese caso hay que pedir de nuevo la URL de login e iniciar el flujo otra vez.
Seguridad
- State: Siempre genera y valida un
statealeatorio para evitar ataques CSRF. Guarda elstateen sesión (o almacén vinculado al usuario) al generar la URL y compáralo en el callback. - Client secret: No expongas nunca el
client_secreten el frontend; úsalo solo en el backend. - HTTPS: En producción usa siempre HTTPS para
redirect_uriy para las rutas de tu API. - Redirect URI: Registra solo las URLs que realmente uses; evita wildcards si no son necesarios.
Licencia
MIT.