When I first set out to connect Azure SAML Single Sign-On (SSO) with my Angular app, I thought it would be quick and easy. After all, SSO is a standard, Angular is powerful, and Azure is… well, Azure.
But a few hours in, I realized this wasn’t going to be a plug-and-play situation. Angular lives entirely in the frontend world, while SAML is built for server-side processing. The two don’t naturally talk to each other.
I banged my head against this for a while before coming to the conclusion that I needed a bridge — something that could handle the server-side heavy lifting. That’s where PHP middleware came in.
And honestly? Once I made that shift, everything clicked. PHP takes care of the SAML handshake, validates the user, and then hands Angular a JWT token. From there, Angular just sees it like any other login flow.
[NOTE]: You can’t integrate SAML login directly with Angular or any frontend framework; you need support from a server-side technology.
Here’s exactly how I pieced it together.
Why I Needed PHP in the Middle
Angular simply can’t handle SAML responses. They’re big XML blobs full of sensitive data (name, email, attributes), and exposing that directly in the browser is a bad idea.
So PHP’s job is to:
- Accept the SAML response from Azure
- Pull out the useful attributes (first name, last name, email)
- Ask my backend, “Hey, is this user valid?”
- If yes, grab a JWT from the backend
- Redirect Angular with that token
Once I introduced that JWT step, Angular stopped caring about SAML entirely. It just saw a token and went about business as usual.
Step 1: Check Session in Angular
The first check happens on the Angular side. If a token already exists, no need to send the user to Azure again. If it doesn’t, I punt them to my PHP login page.
// Quick Angular session check
if (!sessionStorage.getItem('jwtToken')) {
window.location.href = 'https://your-php-app.com/login.php';
}That simple guard saved me from sending people through the SSO loop unnecessarily.
Step 2: The PHP Login File
On the PHP side, I leaned on the OneLogin SAML PHP library. Here’s a trimmed-down version of login.php that I started with:
<?php
session_start(); // don’t forget this
require_once __DIR__ . '/vendor/autoload.php';
use OneLogin\Saml2\Auth;
$settings = require('settings.php');
$auth = new Auth($settings);
if ($auth->isAuthenticated()) {
$userEmail = $auth->getAttributes()['email'][0];
}
// Handle logout
if (isset($_GET['logout'])) {
$auth->logout('http://localhost');
exit;
}
?>The biggest “aha” moment here was session_start(). I wasted a solid hour wondering why my sessions weren’t persisting before realizing I had left it out.
Step 3: Handle the SAML Response
Once Azure complete the login process, it return back a SAMLResponse. That’s when PHP needs to parse it, get the attributes.
Here’s what that looked like for me:
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['SAMLResponse'])) {
$samlResponse = base64_decode($_POST['SAMLResponse']);
$dom = new DOMDocument();
$dom->loadXML($samlResponse);
$xpath = new DOMXPath($dom);
$xpath->registerNamespace('saml', 'urn:oasis:names:tc:SAML:2.0:assertion');
$emailNodes = $xpath->query("//saml:Attribute[@Name='http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name']/saml:AttributeValue");
$email = $emailNodes->length > 0 ? $emailNodes->item(0)->nodeValue : null;
if ($email) {
$postData = ['email' => $email];
$apiUrl = 'https://backend-url.com/sso-login';
$ch = curl_init($apiUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Accept: application/json',
'Content-Type: application/json'
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postData));
$response = curl_exec($ch);
curl_close($ch);
$responseData = json_decode($response, true);
if (isset($responseData['token'])) {
$token = urlencode($responseData['token']);
header("Location: https://yourdomain.com/#/saml/login/$token");
exit;
} else {
echo "User Error: " . $response;
}
} else {
echo 'Email not found in SAML response.';
}
} else {
header("Location: login.php");
exit;
}
?>The key detail? URL-encode the token. The first time I didn’t, the redirect broke in such a confusing way I thought my whole flow was wrong.
Step 4: Grabbing the Token in Angular
At this point, Angular just needs to capture the token from the redirect and stash it in storage. That’s as simple as:
import { ActivatedRoute, Router } from '@angular/router';
constructor(private route: ActivatedRoute, private router: Router) {}
ngOnInit() {
this.route.params.subscribe(params => {
const token = params['token'];
if (token) {
sessionStorage.setItem('jwtToken', token);
this.router.navigate(['/dashboard']);
}
});
}From here, Angular behaves like it always does — the SAML dance is already behind the scenes.
Step 5: Secure the API Calls
Every call request to the backend needs the JWT. Just a header:
const headers = new HttpHeaders().set(
'Authorization',
'Bearer ' + sessionStorage.getItem('jwtToken')
);
this.http.get('https://backend-url.com/protected', { headers }).subscribe(...);Step 6: LogOut Cleanly
You need to clear both Angular’s token and the Azure session. I ended up with:
logout() {
sessionStorage.removeItem('jwtToken');
window.location.href = 'https://your-php-app.com/login.php?logout=true';
}Why This Setup Works for Me
- Safe: The sensitive SAML Data stays on the server side, far away from the browser.
- Flexible: Angular doesn’t care — this could just as easily be any frontend language.
- Smooth: If a session already exists, users will redirect into Angular without extra steps.
Wrapping Up
If you try to with Azure SAML straight into Angular, you’re in for a frustrating ride. It’s not designed for that, and honestly, it’s not safe either.
Dropping a PHP middleware in between gave me a clean solution:
- Azure + PHP handle the authentication,
- the backend validates the user and issues a JWT,
- Angular just does what it’s good at — managing the user experience.
For me, this setup was a lifesaver. If you’re wrestling with SAML and a modern frontend, I can’t recommend it enough. It’ll save you hours of confusion and give you a flow that just works.
