-
Notifications
You must be signed in to change notification settings - Fork 0
Office365SSO
Office365SSO extends the base OAuth2 class to provide Microsoft Entra ID (formerly Azure AD) Single Sign-On with Microsoft Graph API integration. It handles the full OAuth 2.0 authorisation code flow, ID token parsing, and user profile retrieval.
use Razy\Office365SSO;
$sso = new Office365SSO(
clientId: 'your-app-client-id',
clientSecret: 'your-app-client-secret',
redirectUri: 'https://app.example.com/auth/callback',
tenantId: 'your-tenant-id',
);
// Step 1: Redirect user to Microsoft login
header('Location: ' . $sso->getAuthorizeUrl());
exit;
// Step 2: Handle callback (in /auth/callback)
$sso->requestAccessToken($_GET['code']);
// Step 3: Get user info
$email = $sso->getUserEmail();
$name = $sso->getUserDisplayName();$sso = new Office365SSO(
clientId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
clientSecret: 'your-client-secret-value',
redirectUri: 'https://app.example.com/auth/callback',
tenantId: 'your-tenant-id', // or 'common' for multi-tenant
);The Office365SSO class automatically builds Microsoft Entra ID endpoints from the tenant ID:
| Endpoint | URL Pattern |
| --- | --- |
| Authorise | https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/authorize |
| Token | https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token |
| Sign-out | https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/logout |
// Use 'common' for any Microsoft account or Azure AD account
$sso = new Office365SSO(
clientId: 'xxx',
clientSecret: 'xxx',
redirectUri: 'https://app.example.com/auth/callback',
tenantId: 'common',
);
// Use 'consumers' for personal Microsoft accounts only
// Use 'organizations' for Azure AD accounts only// Basic
$url = $sso->getAuthorizeUrl();
// With state parameter (CSRF protection)
$state = bin2hex(random_bytes(16));
$_SESSION['oauth_state'] = $state;
$url = $sso->getAuthorizeUrl(state: $state);
header('Location: ' . $url);
exit;// In the callback handler
if ($_GET['state'] !== $_SESSION['oauth_state']) {
throw new \RuntimeException('State mismatch');
}
// Exchange authorisation code for access token
$sso->requestAccessToken($_GET['code']);
// The access token is stored internally
// You can also get the raw token data:
$token = $sso->getAccessToken(); // access_token string
$refresh = $sso->getRefreshToken(); // refresh_token (if offline_access scope)
$expires = $sso->getTokenExpiresAt(); // DateTime// Create a session from the current token
$sso->createSession();
// Stores token data in $_SESSION for subsequent requests
// Check if session has expired
if (Office365SSO::isSessionExpired()) {
// Re-authenticate
header('Location: ' . $sso->getAuthorizeUrl());
exit;
}After authentication, retrieve user profile data from Microsoft Graph:
// Full user info (all available fields)
$userInfo = $sso->getUserInfo();
/*
[
'displayName' => 'Alice Smith',
'mail' => 'alice@example.com',
'givenName' => 'Alice',
'surname' => 'Smith',
'jobTitle' => 'Software Engineer',
'userPrincipalName' => 'alice@example.onmicrosoft.com',
...
]
*/
// Individual fields
$email = $sso->getUserEmail();
$displayName = $sso->getUserDisplayName();
$firstName = $sso->getUserGivenName();
$lastName = $sso->getUserSurname();
$jobTitle = $sso->getUserJobTitle();
// Profile photo (binary data)
$photoData = $sso->getUserPhoto();
if ($photoData) {
file_put_contents('avatar.jpg', $photoData);
}The ID token returned by Microsoft Entra ID contains user claims. You can parse and validate it:
// Parse the ID token (JWT)
$claims = $sso->parseIdToken();
/*
[
'aud' => 'client-id',
'iss' => 'https://login.microsoftonline.com/{tenant}/v2.0',
'sub' => 'user-subject-id',
'email' => 'alice@example.com',
'name' => 'Alice Smith',
'preferred_username' => 'alice@example.com',
'tid' => 'tenant-id',
'iat' => 1705350000,
'exp' => 1705353600,
...
]
*/
// Validate the ID token signature and claims
$valid = $sso->validateIdToken();
// Returns true if token is valid, unexpired, and audience matchesControl which Microsoft Graph permissions are requested:
// Default scope includes: openid, profile, email, User.Read
$sso->setGraphScope('openid profile email User.Read Mail.Read');
// Access calendar
$sso->setGraphScope('openid profile email User.Read Calendars.Read');
// Get current scope
$scope = $sso->getGraphScope();Pre-fill the email field on the Microsoft login page:
$sso->setLoginHint('alice@example.com');
$url = $sso->getAuthorizeUrl();
// Login page shows alice@example.com pre-filledControl the authentication prompt:
// Force re-authentication (even if already signed in)
$sso->setPrompt('login');
// Show consent prompt
$sso->setPrompt('consent');
// Silent authentication (no UI, fails if interaction needed)
$sso->setPrompt('none');
// Default (Microsoft decides based on session state)
$sso->setPrompt('select_account');Hint the Azure AD tenant for faster login:
$sso->setDomainHint('example.com');
// Skips the "pick an account" screen for users in this domain// Get the sign-out URL
$signOutUrl = Office365SSO::getSignOutUrl(
tenantId: 'your-tenant-id',
postLogoutRedirectUri: 'https://app.example.com/logged-out',
);
// Redirect user to sign out
header('Location: ' . $signOutUrl);
exit;This signs the user out of both your application and Microsoft's session.
use Razy\Office365SSO;
// config.php
$sso = new Office365SSO(
clientId: getenv('AZURE_CLIENT_ID'),
clientSecret: getenv('AZURE_CLIENT_SECRET'),
redirectUri: getenv('AZURE_REDIRECT_URI'),
tenantId: getenv('AZURE_TENANT_ID'),
);
// login.php → Initiate login
session_start();
$_SESSION['oauth_state'] = bin2hex(random_bytes(16));
$sso->setPrompt('select_account');
$sso->setGraphScope('openid profile email User.Read');
header('Location: ' . $sso->getAuthorizeUrl(state: $_SESSION['oauth_state']));
exit;
// callback.php → Handle Microsoft response
session_start();
if ($_GET['state'] !== $_SESSION['oauth_state']) {
die('Invalid state parameter');
}
try {
$sso->requestAccessToken($_GET['code']);
$sso->createSession();
$userInfo = $sso->getUserInfo();
// Find or create local user
$user = User::firstOrCreate(
['email' => $sso->getUserEmail()],
[
'name' => $sso->getUserDisplayName(),
'first_name' => $sso->getUserGivenName(),
'last_name' => $sso->getUserSurname(),
'job_title' => $sso->getUserJobTitle(),
],
);
$_SESSION['user_id'] = $user->id;
header('Location: /dashboard');
} catch (\Razy\Exception\OAuthException $e) {
error_log('SSO failed: ' . $e->getMessage());
header('Location: /login?error=sso_failed');
}
// logout.php → Sign out
session_start();
session_destroy();
header('Location: ' . Office365SSO::getSignOutUrl(
tenantId: getenv('AZURE_TENANT_ID'),
postLogoutRedirectUri: getenv('APP_URL') . '/login',
));
exit;
// middleware → Check session on protected routes
if (Office365SSO::isSessionExpired()) {
header('Location: /login');
exit;
}| Method | Signature | Returns |
| --- | --- | --- |
| __construct | (string $clientId, string $clientSecret, string $redirectUri, string $tenantId) | |
| getTenantId | (): string | Tenant ID |
| setGraphScope | (string $scope): static | Set Graph API scopes |
| getGraphScope | (): string | Current scopes |
| setPrompt | (string $prompt): static | Auth prompt type |
| setLoginHint | (string $email): static | Pre-fill email |
| setDomainHint | (string $domain): static | Domain hint |
| getUserInfo | (): array | Full Graph profile |
| getUserEmail | (): ?string | Email address |
| getUserDisplayName | (): ?string | Display name |
| getUserGivenName | (): ?string | First name |
| getUserSurname | (): ?string | Last name |
| getUserJobTitle | (): ?string | Job title |
| getUserPhoto | (): ?string | Photo binary data |
| parseIdToken | (): array | Decode JWT claims |
| validateIdToken | (): bool | Verify JWT |
| createSession | (): void | Store token in session |
| httpPost | (string $url, array $data): array | HTTP POST helper |
| Method | Signature | Returns |
| --- | --- | --- |
| isSessionExpired | (): bool | Token expired? |
| getSignOutUrl | (string $tenantId, ?string $postLogoutRedirectUri): string | Sign-out URL |
| Method | Signature | Returns |
| --- | --- | --- |
| getAuthorizeUrl | (?string $state = null): string | Auth URL |
| requestAccessToken | (string $code): void | Exchange code |
| getAccessToken | (): ?string | Access token |
| getRefreshToken | (): ?string | Refresh token |
| getTokenExpiresAt | (): ?DateTime | Token expiry |
| refreshAccessToken | (): void | Refresh flow |