LinOTP integration for SimpleSAMLphp | by Greg Harvey
TL;DR
- This guide covers how to plug LinOTP into your SimpleSAMLphp setup to get that sweet multi-factor authentication going. You'll learn about authproc filters, configuring the linotp api, and making sure your saml assertions are actually secure. We also look at common pitfalls when dealing with identity providers and how to test your config without losing your mind.
Why bother with LinOTP and SimpleSAMLphp?
Ever tried explaining to a ceo why a six-digit password isn't enough anymore? It's like trying to stop a flood with a paper towel, honestly.
Passwords are basically a "please hack me" sign for modern attackers. If you're running a portal for healthcare or even just a retail backend, you need layers.
- Security is a moving target: A 2024 report by Verizon found that over 60% of breaches involved compromised credentials, which is just wild.
- LinOTP is the secret sauce: It's an open-source backend that handles otp tokens without locking you into one vendor.
- simplesamlphp is the glue: It's still the go-to for connecting different apps because it just works with almost any federated identity setup.
In practice, a finance firm might use this to bridge their legacy api with modern saml. It keeps things tight without breaking the bank on licenses.
Next, we'll dive into getting the actual bits installed.
Getting the Software on the Box
Before we talk config, you actually need the code on your server. For LinOTP, if you're on Ubuntu, you're usually adding their repository and grabbing the packages.
# Add the LinOTP repo and install
add-apt-repository ppa:linotp/stable
apt-get update
apt-get install linotp2 linotp2-adminclient-cli
For simplesamlphp, you usually just download the tarball or clone it into /var/simplesamlphp. You'll also need to point your web server (like apache or nginx) to the public directory so it actually loads.
The most important part for our integration is getting the specific linotp module. Don't just download random zips; use composer in your simplesamlphp directory:
composer require pulsesecurity/simplesamlphp-module-linotp
This pulls in the logic needed to talk to the linotp api without you having to write custom php from scratch.
Setting up the LinOTP side of things
So, you got the software installed but now comes the part where most people start pullin' their hair out—actually making LinOTP talk to your users. If LinOTP doesn't know who your users are, it can't exactly give them a token, right?
The UserIdResolver is basically the bridge between linotp and your user database. You usually set this up in the LinOTP Management Web UI under the "LinOTP Config" -> "UserIdResolver" tab. You'll define your connection string there. For example, an LDAP resolver might look like this in the backend config:
ldap.uri = ldap://localhost, ldap.binddn = cn=admin,dc=example,dc=com
- Connect to your source: You'll need to define if you are using LDAP, SQL, or even a flat file. Most enterprise setups go with LDAP because it's just easier to manage at scale.
- Create a Realm: Think of a realm as a logical container. You might have one realm for "Healthcare_Staff" and another for "Contractors" to keep things organized.
- Test with curl: Don't just hope it works. Use a simple api call to see if the resolver actually sees your account.
Here is a quick example of how you'd check a user status using the api via terminal (make sure you use the right parameter name!):
curl -k https://localhost/admin/show?user=greg.harvey&resolver=my_ldap_resolver
It's also worth noting that complexity is a huge barrier. A 2023 report by Thales found that 55% of IT professionals see it as a top reason for mfa deployment failing. Keeping your resolvers simple helps avoid that mess.
Next, we're gonna look at how to actually link this up to simpleSAMLphp so the login flow actually completes.
SimpleSAMLphp Configuration and Authproc Filters
Now that LinOTP knows who your users are, we gotta make simpleSAMLphp actually ask for that second factor. It’s honestly not as scary as it sounds—you’re basically just dropping a specialized filter into your auth chain.
Important Note: The "authproc" filters don't work in a vacuum. You first need a primary authentication source defined in config/authsources.php (like an LDAP bind). The filter only runs after the user has successfully logged in with their first password.
Once your primary auth is working, you need to enable the module in your config/config.php by making sure 'linotp' is in the module.enable array.
The real magic happens in your metadata/saml20-idp-hosted.php (or your config.php if you want it global). You’ll add an authproc filter. This is just a fancy way of saying "hey, before you finish logging this person in, run this extra check."
- Set the parameters: You need to tell the filter where your LinOTP server lives (the api url).
- Attribute Mapping: This is where things usually break. You have to ensure the username simpleSAMLphp got from your primary auth (like LDAP) matches the "User" LinOTP is looking for.
- SSL stuff: If you're using self-signed certs in a lab, you might need to toggle
ssl_verifyto false, but please don't do that in production, okay?
Here is a basic look at what that filter config looks like:
'authproc' => [
10 => [
'class' => 'linotp:OtpFilter',
'linotp_url' => 'https://linotp.example.com/validate/check',
'ssl_verify' => true,
'username_attribute' => 'uid',
'realm' => 'Healthcare_Staff',
],
],
I've seen so many retail setups fail because the IdP sends email but LinOTP expects sAMAccountName. If they don't line up, LinOTP just shrugs and says "user not found," and your login loop begins.
As that Thales report mentioned, complexity is a killer for these projects. Getting these attributes right the first time saves you a massive headache later.
In a high-stakes environment like a hospital system, you might even add logic to only trigger this for certain "sensitive" apps. It keeps the doctors happy because they aren't typing codes just to check the cafeteria menu.
Testing and Validating your SSO Flow
It's one thing to see your config file look pretty, but it's another to actually see a login work without blowing up in your face. I can't tell you how many times I've walked away from a "finished" setup only to get a frantic Slack message ten minutes later because someone in the retail branch can't sync their scanner.
To really see what's happening under the hood, you need to look at the actual saml assertions being passed back and forth. Tools like the SAML Tracer extension for Firefox or Chrome are absolute lifesavers here.
- Watch the Redirects: You want to see the user hit simpleSAMLphp, get kicked to the OTP page, and then finally land at the app with a valid signature.
- Check the Attributes: Make sure the
eduPersonPrincipalNameoruidmatches exactly what LinOTP saw during the test we did earlier with curl. - Timestamp drift: If your server clocks are off by even a minute, the assertion will fail. Use ntp to keep things synced up.
If you're seeing a "State Lost" error, it usually means your session cookies are fighting each other or you've got a load balancer misconfigured. Since complexity stalls these rollouts, keep your testing environment identical to production to catch these quirks early.
Security Best Practices and Final Thoughts
Look, you can have the fanciest mfa setup in the world, but if you leave your api keys sitting in a public git repo, you’re basically handing the keys to the kingdom to any script kiddie with a scanner. it happens more than you'd think in fast-paced retail or healthcare dev cycles.
First rule of fight club: don't hardcode secrets. Use environment variables or a proper vault. If you're putting your LinOTP admin password directly into authsources.php, just stop.
- Environment Vars: pull your sensitive strings from the system.
- Log Monitoring: setup alerts for "500 Internal Server Error" or repeated failed 2fa hits. If a finance app suddenly sees 200 failures from one ip, you've got a problem.
- Stay Updated: simpleSAMLphp puts out security patches for a reason. Don't be that person running a three-year-old version because you're "scared to break the theme."
A 2023 report by IBM found that the average cost of a data breach reached $4.45 million, so yeah, those patches are worth the ten minutes of downtime.
Anyway, the goal here isn't just to "check a box" for compliance. It’s about making things hard enough that attackers go look for an easier target. When you combine the flexibility of LinOTP with the reliability of simpleSAMLphp, you’re building a pretty solid wall.
Just remember to keep testing. Use those tools we talked about earlier and don't assume "no news is good news." Stay safe out there and keep those tokens rotating.