Base 3:16
Guides

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 labelValue
IdP metadata URLhttps://api.base316.com/saml/idp/{environmentId}/metadata
IdP entity IDhttps://api.base316.com/saml/idp/{environmentId}
SSO URLhttps://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

FieldWhat goes in it
NameYours, to recognise it by.
Entity IDThe SP entity ID, exactly as the application sends it as the Issuer. It becomes the Audience of every assertion.
ACS URLsOne per line, https only. Matched exactly — see below.
NameID formatEmail address, Persistent or Unspecified.
NameID valueEmail, User ID or External ID — what the NameID is filled from.
Attribute mappingThe attributes the application expects; see Attributes.
Require signed requestsRefuse unsigned AuthnRequests. Needs the Signing certificate.
Signing certificateThe application's request-signing certificate, in PEM.
Require consentAsk each person before their attributes are sent.
Allow IdP-initiated sign-inOff by default. Read IdP-initiated sign-in first.
Default RelayStateOnly for IdP-initiated sign-in. At most 80 bytes.
EnabledShown 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/metadata

If 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, department

Register 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 nameFilled from
emailemail
first_namename
departmentCustom 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.

SourceValue
emailThe email address
email_verifiedtrue or false
nameThe display name
user_idThe Base316 user id
external_idYour id for the user, if you set one
pictureThe image URL
phone_numberThe phone number
rolesEvery role the person holds — one value each
organization_idsThe tenants the person belongs to — one value each
organization_namesTheir names
organization_external_idsTheir external ids, where set
planThe plan key of the person's tenant
subscription_statusThat 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

FormatMeaning
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 (SigAlg and Signature), 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.

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, as prompt=login does for OIDC.
  • IsPassive="true" with nobody signed in is answered with a NoPassive error 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 Issuer is not a registered, enabled application's Entity ID;
  • its AssertionConsumerServiceURL is 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 Destination is present and is not this environment's SSO URL;
  • its IssueInstant is more than 5 minutes old, allowing 3 minutes of clock skew either way;
  • its request ID has 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 Response and the Assertion are both signed, RSA-SHA256 with exclusive canonicalisation, with the active Signing certificate.
  • Audience is the application's Entity ID.
  • Recipient and Destination are the ACS URL the response is posted to.
  • InResponseTo names the request (absent for an IdP-initiated response).
  • The assertion is valid for 5 minutes.
  • AuthnInstant is 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.

MethodPathDoes
GET/v1/saml-applicationsList applications
POST/v1/saml-applicationsRegister one — fields, or metadataXml, or both
GET/v1/saml-applications/:idRead one
PATCH/v1/saml-applications/:idUpdate one
DELETE/v1/saml-applications/:idDelete one
GET/v1/saml-applications/idpThe IdP's entity ID, metadata URL, SSO URL and certificates
POST/v1/saml-applications/idp/rotateRotate 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 });
MethodDoes
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

On this page