SAML identity provider
Signing your users in to applications that only speak SAML — a helpdesk, an LMS, a vendor portal — with the account they already have in Base316.
Some applications your users need only accept sign-in over SAML 2.0. Base316 can be the identity provider for them: you register each application as a service provider, and Base316 signs your users in to it with the identity they already have here — whichever way they signed in, second factor and all.
This is the opposite direction from enterprise SSO. There, one of your customers' identity providers (Okta, Entra) signs people in to Base316 and Base316 is the service provider. Here, Base316 is the identity provider and the SAML application is the service provider. An environment can do both at once.
Where it lives
Each environment has its own identity provider. In the dashboard, open the project, then Settings → Applications, and scroll to SAML applications, below the OIDC clients.
The panel at the top shows what every SAML application asks for:
| Dashboard label | Value |
|---|---|
| IdP metadata URL | https://api.base316.com/saml/idp/{environmentId}/metadata |
| IdP entity ID | https://api.base316.com/saml/idp/{environmentId} |
| SSO URL | https://api.base316.com/saml/idp/{environmentId}/sso |
The SSO URL accepts AuthnRequests over both the HTTP-Redirect and HTTP-POST bindings. Responses always go back over HTTP-POST; a request that asks for any other response binding is refused rather than left waiting.
Below the URLs is the Signing certificate: its SHA-256 fingerprint, its expiry, and Download certificate for the PEM.
Registering an application
Choose Add SAML application.
1. Import its metadata
Most applications publish SP metadata — an XML document, usually downloadable from their SAML settings page. Paste it into Import metadata, or upload the file, and choose Read metadata. The dialog fills in the Entity ID, ACS URLs, Signing certificate, Require signed requests and NameID format from it. Nothing is saved until you choose Add SAML application at the bottom, so check the fields first.
Metadata is only ever pasted or uploaded. Base316 never fetches it from a URL you give it — a fetch-by-URL field is a request-forgery primitive pointed at whatever that URL resolves to.
2. Or fill it in by hand
| Field | What goes in it |
|---|---|
| Name | Yours, to recognise it by. |
| Entity ID | The SP entity ID, exactly as the application sends it as the Issuer. It becomes the Audience of every assertion. |
| ACS URLs | One per line, https only. Matched exactly — see below. |
| NameID format | Email address, Persistent or Unspecified. |
| NameID value | Email, User ID or External ID — what the NameID is filled from. |
| Attribute mapping | The attributes the application expects; see Attributes. |
| Require signed requests | Refuse unsigned AuthnRequests. Needs the Signing certificate. |
| Signing certificate | The application's request-signing certificate, in PEM. |
| Require consent | Ask each person before their attributes are sent. |
| Allow IdP-initiated sign-in | Off by default. Read IdP-initiated sign-in first. |
| Default RelayState | Only for IdP-initiated sign-in. At most 80 bytes. |
| Enabled | Shown when editing. A disabled application is refused at sign-in; its settings are kept. |
3. Configure the application
Give the application Base316's details. If it takes a metadata URL, that is all it needs:
https://api.base316.com/saml/idp/env_1a2b3c/metadataIf it asks for each value separately, give it the SSO URL, the IdP entity ID, and the certificate from Download certificate. Prefer the metadata URL where there is a choice: an application that reads it picks up a rotated certificate on its own.
A worked example
A helpdesk documents its SAML settings as:
SP Entity ID https://support.example.com/saml/metadata
ACS URL https://support.example.com/saml/consume
NameID urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
Attributes email, first_name, departmentRegister it with Entity ID https://support.example.com/saml/metadata,
ACS URLs https://support.example.com/saml/consume, NameID format
Email address (which fixes NameID value to Email), and three rows
under Attribute mapping:
| Attribute name | Filled from |
|---|---|
email | email |
first_name | name |
department | Custom field… department |
Then, in the helpdesk's admin screen, paste the IdP metadata URL — or the SSO URL, the IdP entity ID and the downloaded certificate — and turn SAML on. The next person who chooses "Sign in with SSO" there lands on your hosted sign-in page, and arrives back at the helpdesk signed in.
Attributes
Each row under Attribute mapping sends one <saml:Attribute>, named as the
application expects it (email, or a URN — whatever it documents), filled from
a fact about the person. The sources are the same facts the OIDC provider puts
in claims, read the same way, so a SAML attribute and an OIDC claim about one
person cannot disagree.
| Source | Value |
|---|---|
email | The email address |
email_verified | true or false |
name | The display name |
user_id | The Base316 user id |
external_id | Your id for the user, if you set one |
picture | The image URL |
phone_number | The phone number |
roles | Every role the person holds — one value each |
organization_ids | The tenants the person belongs to — one value each |
organization_names | Their names |
organization_external_ids | Their external ids, where set |
plan | The plan key of the person's tenant |
subscription_status | That subscription's status |
field:<key> | One of your custom user fields — Custom field… in the dashboard |
entitlement:<featureKey> | What the tenant's plan grants for a feature — Entitlement… in the dashboard. An unlimited limit is sent as unlimited. |
A source with no value for the person leaves that attribute out of the assertion, rather than sending an empty string. An attribute name may be mapped once.
NameID
| Format | Meaning |
|---|---|
| Email address | …nameid-format:emailAddress. Always filled from the email address. |
| Persistent | …nameid-format:persistent. With User ID, an identifier that never changes, even if the address does. |
| Unspecified | …nameid-format:unspecified. For applications that do not care. |
A request whose NameIDPolicy asks for a different format is answered with an
InvalidNameIDPolicy error response. A request asking for unspecified, or not
asking, gets the registered format.
Signed AuthnRequests
Turn on Require signed requests and paste the application's Signing certificate, and every AuthnRequest must be signed with it:
- HTTP-Redirect — the query-string signature (
SigAlgandSignature), checked over the parameters exactly as they were encoded on the wire. RSA-SHA256, RSA-SHA512 and ECDSA-SHA256 are accepted. - HTTP-POST — an enveloped XML signature on the AuthnRequest. RSA-SHA256 and RSA-SHA512, with SHA-256 or SHA-512 digests.
SHA-1 is refused in both. A signature that is present is always checked, even when not required — a request carrying a bad one is a forgery or a broken application, and neither is a request to act on.
Consent
With Require consent, the first time a person signs in to the application
they are shown the attributes it will receive and asked to approve. Their
answer is remembered; if you later add attributes to the mapping, they are
asked again, because they agreed to what was listed then. Declining sends the
application a signed RequestDenied error response instead of an assertion.
IdP-initiated sign-in
Normally the application starts sign-in (SP-initiated): it sends an AuthnRequest, and the response answers that request. With Allow IdP-initiated sign-in on, Base316 can also start it, from a link you can put on your own portal — the Launch URL shown under the application in the list:
https://api.base316.com/saml/idp/{environmentId}/start/{applicationId}It is off by default for a reason. An unsolicited response answers no request, so the application cannot tie it to the browser that receives it: a captured one can be replayed until it expires, and an attacker can push a victim's browser into the application signed in as the attacker (login CSRF). Turn it on only for an application that cannot start sign-in itself.
The response goes to the first ACS URL. The RelayState comes from Default RelayState on the registration, never from the launch URL: the link is unauthenticated, and letting it choose where the application sends the person next would make it an open redirect.
Single sign-on
Signing in to one application signs the browser in to Base316 as well, so the
next SAML application — or OIDC client — in the same environment goes straight
through without a form. This is the same session /oidc/{env}/authorize
reuses.
ForceAuthn="true"asks for credentials again, asprompt=logindoes for OIDC.IsPassive="true"with nobody signed in is answered with aNoPassiveerror response rather than a sign-in form.
The assertion's AuthnInstant is when the person actually authenticated,
not when the assertion was built. An application reusing a single sign-on
session therefore sees how long ago the person really proved who they are, and
can ask for ForceAuthn if that is too long.
Single logout is not supported
Signing out of Base316 does not end sessions at SAML applications, and Base316 does not send
or accept SAML LogoutRequests. Set a short session lifetime in each application, so that a
person who has been disabled or signed out here loses access there soon after.
Disabling a person does stop every new sign-in immediately — no assertion is issued for a disabled account.
What is checked
Every AuthnRequest is refused, with a message page rather than a response, if:
- its
Issueris not a registered, enabled application's Entity ID; - its
AssertionConsumerServiceURLis not exactly one of the registered ACS URLs — never a prefix match. With no URL, the registered first one is used; with an index, the registration's own order; - its
Destinationis present and is not this environment's SSO URL; - its
IssueInstantis more than 5 minutes old, allowing 3 minutes of clock skew either way; - its request
IDhas already been seen for that application — a replayed request is refused; - it contains a DTD or entity declaration;
- it is unsigned, badly signed or SHA-1-signed when a signature is required.
The assertion
- The
Responseand theAssertionare both signed, RSA-SHA256 with exclusive canonicalisation, with the active Signing certificate. Audienceis the application's Entity ID.RecipientandDestinationare the ACS URL the response is posted to.InResponseTonames the request (absent for an IdP-initiated response).- The assertion is valid for 5 minutes.
AuthnInstantis the real sign-in time, as above.
Certificate rotation
Rotate certificate mints a new certificate and signs every new response with it straight away. The old one is marked Retiring and stays published in the metadata for 14 days, then is dropped.
An application that reads the IdP metadata URL picks up the new certificate within that window by itself. One configured with a pasted certificate needs the new one — Download certificate beside it — before the old one leaves the metadata; until then, it rejects the new signatures.
Over the API
Everything the dashboard does is available with a secret key at
/v1/saml-applications. A publishable key is refused: registering an ACS URL
decides where a signed assertion about a user is posted.
| Method | Path | Does |
|---|---|---|
GET | /v1/saml-applications | List applications |
POST | /v1/saml-applications | Register one — fields, or metadataXml, or both |
GET | /v1/saml-applications/:id | Read one |
PATCH | /v1/saml-applications/:id | Update one |
DELETE | /v1/saml-applications/:id | Delete one |
GET | /v1/saml-applications/idp | The IdP's entity ID, metadata URL, SSO URL and certificates |
POST | /v1/saml-applications/idp/rotate | Rotate the signing certificate |
With metadataXml, the document supplies the entity ID, ACS URLs, signing
certificate, signed-request flag and NameID format, and any field you also send
wins over it.
In the SDK these are under base316.saml:
import { readFile } from "node:fs/promises";
import { Base316 } from "@base316/sdk";
// Reads BASE316_SECRET_KEY from the environment.
const base316 = new Base316();
const idp = await base316.saml.idp();
console.log(idp.metadataUrl);
const helpdesk = await base316.saml.createApplication({
name: "Helpdesk",
metadataXml: await readFile("helpdesk-sp-metadata.xml", "utf8"),
attributeMapping: [
{ name: "email", source: "email" },
{ name: "department", source: "field:department" },
{ name: "seats", source: "entitlement:seats" },
],
});
await base316.saml.updateApplication(helpdesk.id, { requireConsent: true });| Method | Does |
|---|---|
saml.idp() | IdP URLs and certificates |
saml.rotateCertificate() | Rotate the signing certificate |
saml.applications() | List applications |
saml.createApplication(input) | Register one |
saml.application(id) | Read one |
saml.updateApplication(id, input) | Update one |
saml.deleteApplication(id) | Delete one |

