Documentation
¶
Overview ¶
Package samlidp makes the app a SAML 2.0 identity provider: other services (SAML service providers, "SPs") send their users here to sign in, and receive a signed assertion naming the user. It is the SAML sibling of plugin/oauthprovider, and the mirror of plugin/sso, which makes the app a service provider of someone else's IdP.
It is opt-in and its own module. The protocol endpoints (metadata, the single sign-on service in the HTTP-Redirect and HTTP-POST bindings, the continue step after sign-in, and IdP-initiated sign-on) are served by Plugin.Handler; each is a self-served auth operation underneath (auth.OpInfo.SelfServed), so it runs the same pipeline as every other operation: rate limits, hooks, tracing.
Security rules, each pinned by a test: an AuthnRequest from an unknown issuer is refused; its AssertionConsumerServiceURL must equal a registered one exactly, and an error is never sent to an unregistered URL; its ID is single use (claimed in a store that refuses duplicates) and its IssueInstant must be recent; a signed request is verified against the SP's registered certificate and only the verified element is read (the defence against XML signature wrapping); a request whose SP requires signing is refused unsigned. Every assertion is signed (RSA-SHA256 by default), names the SP's entity ID as its only audience, the ACS URL as its recipient, the request's ID as InResponseTo, and expires after AssertionTTL. A browser that is not signed in is sent to LoginPage and never gets an assertion.
Single logout (SLO) is not implemented yet: an SP's logout does not end the IdP session, and the metadata advertises no SingleLogoutService.
Index ¶
- Constants
- Variables
- type AccessFunc
- type Attribute
- type AttributesFunc
- type ContinueRequest
- type InitiateRequest
- type NameIDFunc
- type Option
- func Access(fn AccessFunc) Option
- func AssertionTTL(d time.Duration) Option
- func BasePath(path string) Option
- func ClockSkew(d time.Duration) Option
- func EntityID(id string) Option
- func Issuer(iss string) Option
- func LoginPage(u string) Option
- func LoginTTL(d time.Duration) Option
- func MaxMessageSize(n int) Option
- func RateLimit(endpoint auth.EndpointID, limit int, window time.Duration) Option
- func RequestMaxAge(d time.Duration) Option
- func ReturnParam(name string) Option
- func ServiceProviders(sps ...ServiceProvider) Option
- func SigningKey(key crypto.Signer, cert *x509.Certificate) Option
- func WantAuthnRequestsSigned() Option
- func WithRequestIDGuard(g RequestIDGuard) Option
- func WithServiceProviderStore(store ServiceProviderStore) Option
- type Plugin
- func (p *Plugin) Continue(ctx context.Context, in ContinueRequest) (Result, error)
- func (p *Plugin) ContinueURL(handle string) string
- func (p *Plugin) Handler(opts ...authhttp.Option) http.Handler
- func (p *Plugin) IdPEntityID() string
- func (p *Plugin) Init(a *auth.Auth) error
- func (p *Plugin) Initiate(ctx context.Context, in InitiateRequest) (Result, error)
- func (p *Plugin) Metadata() []byte
- func (p *Plugin) MetadataURL() string
- func (p *Plugin) Name() string
- func (p *Plugin) Operations() []auth.Operation
- func (p *Plugin) OptionErr() error
- func (p *Plugin) RateLimits() []auth.RateRule
- func (p *Plugin) SSO(ctx context.Context, in SSORequest) (Result, error)
- func (p *Plugin) SSOURL() string
- func (p *Plugin) Validate(*auth.Config) error
- type RequestClaim
- type RequestIDGuard
- type ResponseForm
- type Result
- type SSORequest
- type ServiceProvider
- type ServiceProviderStore
- type Subject
Constants ¶
const ( BindingRedirect = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" BindingPOST = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" )
SAML bindings.
const ( AlgRSASHA256 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" AlgRSASHA512 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512" AlgECDSASHA256 = "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256" )
Signature algorithms (XML Signature, RFC 6931) accepted on a request and used on a response. SHA-1 is not accepted.
const ( ParamSAMLRequest = "SAMLRequest" ParamSAMLResponse = "SAMLResponse" ParamRelayState = "RelayState" ParamSigAlg = "SigAlg" ParamSignature = "Signature" )
Binding parameter names (SAML bindings 3.4.4 and 3.5.4).
const ( StatusSuccess = "urn:oasis:names:tc:SAML:2.0:status:Success" StatusResponder = "urn:oasis:names:tc:SAML:2.0:status:Responder" StatusNoPassive = "urn:oasis:names:tc:SAML:2.0:status:NoPassive" )
Status codes (SAML core 3.2.2.2).
const ( EndpointSSO auth.EndpointID = "samlidp.sso" EndpointContinue auth.EndpointID = "samlidp.continue" EndpointInitiate auth.EndpointID = "samlidp.initiate" )
Operation ids. All are self-served: Handler serves them, no transport mounts a JSON copy.
const ( // PurposeRequestID reserves an AuthnRequest's ID so it is answered at // most once; the token is the SP's entity ID and the request ID. PurposeRequestID auth.Purpose = "samlidp.request_id" // PurposePending is an accepted AuthnRequest waiting for the user to // sign in; the token is the handle in the continue URL, Meta the // request. It is spent when the assertion is issued, and rotated while // the browser is still signed out. PurposePending auth.Purpose = "samlidp.pending" )
Verification purposes this plugin files rows under.
const ( // DefaultBasePath is where Handler serves the endpoints under the // issuer: <issuer>/saml/metadata, /saml/sso, /saml/continue, // /saml/initiate. //ds:def id=samlidp-defaultbasepath-q4m8zt2k owner=@khanakia stability=stable DefaultBasePath = "/saml" // DefaultAssertionTTL is how long an assertion is valid (Conditions and // SubjectConfirmationData NotOnOrAfter). Five minutes is what most IdPs // use: the browser posts it to the SP within seconds. //ds:def id=samlidp-defaultassertionttl-w7n3kp5x owner=@khanakia stability=stable DefaultAssertionTTL = 5 * time.Minute // DefaultRequestMaxAge is how old an AuthnRequest's IssueInstant may be // when it arrives. An SP builds the request just before redirecting. //ds:def id=samlidp-defaultrequestmaxage-h2v9qm4r owner=@khanakia stability=stable DefaultRequestMaxAge = 5 * time.Minute // DefaultClockSkew is the tolerance for an SP clock ahead of ours, and // how far NotBefore is set back for an SP clock behind ours. //ds:def id=samlidp-defaultclockskew-z5k8wn3t owner=@khanakia stability=stable DefaultClockSkew = time.Minute // DefaultLoginTTL is how long an accepted request waits for the user to // sign in before the continue link stops working. //ds:def id=samlidp-defaultloginttl-p3x7mq9v owner=@khanakia stability=stable DefaultLoginTTL = 15 * time.Minute // DefaultReturnParam is the query parameter LoginPage receives the // continue URL in. DefaultReturnParam = "return_to" // DefaultMaxMessageSize bounds an AuthnRequest after decoding and // inflating; real ones are a few kilobytes. DefaultMaxMessageSize = 64 << 10 // MaxRelayState bounds the RelayState stored with a pending request. // SAML bindings 3.4.3 limits it to 80 bytes; real SPs send more, so the // bound is generous but finite. MaxRelayState = 1024 )
Defaults. Each has an option.
const ( ParamPrompt = "prompt" PromptLogin = "login" )
ParamPrompt is added to the LoginPage URL as prompt=login when the SP asked for ForceAuthn and the current session is older than the request: the page must make the user authenticate again even though a session exists.
const ( NameIDUnspecified = "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" NameIDEmail = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" NameIDPersistent = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" NameIDTransient = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" )
NameID formats (SAML core 8.3). The SP's NameIDFormat picks which one its assertions carry.
const ( AttrNameFormatBasic = "urn:oasis:names:tc:SAML:2.0:attrname-format:basic" AttrNameFormatURI = "urn:oasis:names:tc:SAML:2.0:attrname-format:uri" AttrNameFormatUnspecified = "urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified" )
Attribute name formats.
const ( // ContentTypeMetadata is the media type of SAML metadata (SAML // metadata 4.1.1). ContentTypeMetadata = "application/samlmetadata+xml" )
HTTP constants.
const PluginName = "samlidp"
PluginName is the plugin's permanent identity.
Variables ¶
var ( ErrInvalidRequest = auth.NewError("samlidp.invalid_request", http.StatusBadRequest, "The SAML request is malformed.") ErrUnknownSP = auth.NewError("samlidp.unknown_service_provider", http.StatusBadRequest, "The service provider is not registered.") ErrACSNotRegistered = auth.NewError("samlidp.acs_not_registered", http.StatusBadRequest, "The assertion consumer service URL is not registered for this service provider.") ErrUnsupportedBinding = auth.NewError("samlidp.unsupported_binding", http.StatusBadRequest, "Only the HTTP-POST binding is supported for the response.") ErrWrongDestination = auth.NewError("samlidp.wrong_destination", http.StatusBadRequest, "The request is addressed to another endpoint.") ErrRequestExpired = auth.NewError("samlidp.request_expired", http.StatusBadRequest, "The SAML request is too old or issued in the future.") ErrRequestReplayed = auth.NewError("samlidp.request_replayed", http.StatusBadRequest, "The SAML request was already used.") ErrSignatureRequired = auth.NewError("samlidp.signature_required", http.StatusBadRequest, "This service provider must sign its requests.") ErrInvalidSignature = auth.NewError("samlidp.invalid_signature", http.StatusBadRequest, "The request signature does not verify.") ErrNameIDPolicy = auth.NewError("samlidp.invalid_name_id_policy", http.StatusBadRequest, "The requested NameID format is not the one configured for this service provider.") ErrRequestNotFound = auth.NewError("samlidp.request_not_found", http.StatusBadRequest, "The sign-in request is unknown, used or expired. Start again from the application.") ErrIdPInitiatedOff = auth.NewError("samlidp.idp_initiated_disabled", http.StatusForbidden, "IdP-initiated sign-on is not enabled for this service provider.") ErrAccessDenied = auth.NewError("samlidp.access_denied", http.StatusForbidden, "You may not sign in to this service.") ErrRelayStateTooLong = auth.NewError("samlidp.relay_state_too_long", http.StatusBadRequest, "The RelayState is too long.") ErrMessageTooLarge = auth.NewError("samlidp.message_too_large", http.StatusRequestEntityTooLarge, "The SAML message is too large.") ErrUnsupportedSigAlg = auth.NewError("samlidp.unsupported_signature_algorithm", http.StatusBadRequest, "The signature algorithm is not accepted.") ErrMissingRequestParam = auth.NewError("samlidp.missing_request", http.StatusBadRequest, "SAMLRequest is missing.") )
Errors. Every one is shown to the user (Handler renders an error page) and never sent to an ACS URL, so an attacker cannot use the IdP to post to a URL of their choosing.
var ( ErrNoIssuer = errors.New("samlidp: Issuer is required: an absolute https URL (http only for a loopback host) with no query or fragment") ErrNoLoginPage = errors.New("samlidp: LoginPage is required: where a browser that is not signed in is sent") ErrNoSigningKey = errors.New("samlidp: SigningKey is required: an RSA or ECDSA crypto.Signer and its certificate, whose public key must match") ErrBadSP = errors.New("samlidp: invalid service provider") ErrBadTTL = errors.New("samlidp: every lifetime is positive") ErrBadBasePath = errors.New("samlidp: BasePath must start with / and not end with one") ErrNoExclusive = errors.New("samlidp: the verification store does not refuse duplicate tokens (auth.ExclusiveVerificationStore), so a request ID cannot be claimed once; give its storage SetIfAbsent or pass samlidp.WithRequestIDGuard") )
Configuration errors, returned by auth.New.
var DefaultRateLimits = map[auth.EndpointID]int{
EndpointSSO: 30,
EndpointContinue: 60,
EndpointInitiate: 30,
}
DefaultRateLimits are the calls per client IP per minute each endpoint accepts unless RateLimit changes it. The single sign-on service writes a row per accepted request, so it is bounded even though every request must come from a registered SP; continue allows the extra hops of a login.
var NameIDFormats = []string{NameIDPersistent, NameIDEmail, NameIDTransient, NameIDUnspecified}
NameIDFormats is every format the IdP can issue, in the order the metadata lists them.
Functions ¶
This section is empty.
Types ¶
type AccessFunc ¶
AccessFunc decides whether a signed-in user may sign in to sp: a nil answer allows, an error refuses (ErrAccessDenied unless it is an *auth.Error of its own). It is the seam for any authorization model (roles, groups, an organization's app assignments, a policy engine).
type Attribute ¶
type Attribute struct {
Name string
NameFormat string // AttrNameFormatBasic when empty
FriendlyName string
Values []string
}
Attribute is one SAML attribute an assertion releases to an SP.
func DefaultAttributes ¶
DefaultAttributes releases "email" (when verified) and "name" in the basic name format, the attribute names most SPs map by default.
type AttributesFunc ¶
AttributesFunc builds the attributes an SP receives. It runs on every assertion; an error refuses the sign-in.
type ContinueRequest ¶
type ContinueRequest struct {
Request string `json:"request"`
}
ContinueRequest names a pending request by the handle in the continue URL.
type InitiateRequest ¶
type InitiateRequest struct {
// ServiceProvider is the SP's entity ID.
ServiceProvider string `json:"sp"`
// ACSURL picks a registered ACS URL; the first when empty.
ACSURL string `json:"acs_url,omitempty"`
RelayState string `json:"relay_state,omitempty"`
}
InitiateRequest starts IdP-initiated sign-on to an SP.
type NameIDFunc ¶
NameIDFunc replaces the NameID value an SP receives. It returns the value for sp.NameIDFormat; "" refuses the sign-in with ErrNameIDUnavailable.
type Option ¶
type Option func(*Plugin)
Option configures the plugin; pass options to New.
func Access ¶
func Access(fn AccessFunc) Option
Access decides per user and SP whether an assertion may be issued; see AccessFunc. Without it every signed-in user may sign in to every registered SP.
func AssertionTTL ¶
AssertionTTL overrides DefaultAssertionTTL.
func EntityID ¶
EntityID replaces the IdP's SAML entity ID, which defaults to the metadata URL (<issuer><base>/metadata). Set it when SPs were configured with another value; it is the Issuer of every response.
func Issuer ¶
Issuer is the app's public origin the endpoints live under, such as "https://id.example.com": an absolute https URL (http only for a loopback host) with no query or fragment. Required.
func LoginPage ¶
LoginPage is where a browser that is not signed in is sent, with the continue URL in the ReturnParam query parameter (and prompt=login when the SP asked for ForceAuthn). After signing the user in, the page sends the browser to that URL. Required: a same-site path or a URL on a trusted origin.
func MaxMessageSize ¶
MaxMessageSize overrides DefaultMaxMessageSize.
func RateLimit ¶
RateLimit replaces an endpoint's default limit per client IP; a limit of 0 removes it (for an app that limits in front of the library).
func RequestMaxAge ¶
RequestMaxAge overrides DefaultRequestMaxAge.
func ReturnParam ¶
ReturnParam renames the query parameter LoginPage receives the continue URL in (DefaultReturnParam).
func ServiceProviders ¶
func ServiceProviders(sps ...ServiceProvider) Option
ServiceProviders registers SPs in code. They are checked by auth.New.
func SigningKey ¶
func SigningKey(key crypto.Signer, cert *x509.Certificate) Option
SigningKey is the key that signs every response and assertion, and its certificate, published in the metadata for SPs to trust. key is any crypto.Signer (an *rsa.PrivateKey, or a KMS or HSM handle) whose public key is cert's. RSA signs with RSA-SHA256, ECDSA with ECDSA-SHA256. Required.
func WantAuthnRequestsSigned ¶
func WantAuthnRequestsSigned() Option
WantAuthnRequestsSigned requires every SP to sign its AuthnRequests and says so in the metadata. Every SP then needs a Certificate. A single SP can require it with ServiceProvider.RequireSignedRequests.
func WithRequestIDGuard ¶
func WithRequestIDGuard(g RequestIDGuard) Option
WithRequestIDGuard replaces the default single-use claim of request IDs (a verification row that the store refuses to insert twice). Use it when the verification store cannot refuse duplicates.
func WithServiceProviderStore ¶
func WithServiceProviderStore(store ServiceProviderStore) Option
WithServiceProviderStore looks SPs up in store (a database table, an admin UI's registry, a config service) instead of, or after, the ones ServiceProviders registered. Each SP it returns is checked on every lookup, as auth.New checks static ones.
type Plugin ¶
type Plugin struct {
// contains filtered or unexported fields
}
Plugin is the SAML identity provider. Build it with New.
func (*Plugin) Continue ¶
Continue answers a pending request for the caller's session: the login redirect while signed out, the response form once signed in.
func (*Plugin) ContinueURL ¶
ContinueURL is the continue step for a pending request's handle.
func (*Plugin) Handler ¶
Handler serves the endpoints at their paths under the issuer: the metadata, the single sign-on service (GET for HTTP-Redirect, POST for HTTP-POST), the continue step and IdP-initiated sign-on. opts are the authhttp options the app's auth handler uses (cookie prefix, trusted proxies), so the continue step reads the same session. Errors are shown as a page and never sent to an SP. An unknown path answers 404.
func (*Plugin) IdPEntityID ¶
IdPEntityID is the entity ID the IdP signs as.
func (*Plugin) Init ¶
Init captures the core, checks the login page against the trusted origins, and installs the default request-ID guard.
func (*Plugin) Initiate ¶
Initiate builds an unsolicited response for the signed-in caller to an SP that allows IdP-initiated sign-on.
func (*Plugin) Metadata ¶
Metadata is the IdP's SAML metadata: its entity ID, signing certificate, NameID formats and single sign-on service in both bindings. It is what an SP administrator uploads or points their SP at (MetadataURL). It needs the plugin to have passed auth.New, which checked the certificate.
func (*Plugin) MetadataURL ¶
MetadataURL is where Handler serves the IdP metadata.
func (*Plugin) Operations ¶
Operations lists the self-served operations.
func (*Plugin) RateLimits ¶
RateLimits declares the limits, in a fixed order so the rule list is the same on every start.
func (*Plugin) SSO ¶
SSO accepts an AuthnRequest from Go: the redirect to the continue step, or an error to show the user.
type RequestClaim ¶
type RequestClaim struct {
// SP is the requesting SP's entity ID; Request the request's ID.
SP, Request string
// Until is when the claim may be forgotten: past it the request's
// IssueInstant is too old to be accepted anyway.
Until time.Time
}
RequestClaim is one AuthnRequest ID to be answered at most once.
type RequestIDGuard ¶
type RequestIDGuard interface {
Claim(ctx context.Context, c RequestClaim) error
}
RequestIDGuard claims request IDs. Claim answers nil the first time a claim is made and ErrRequestReplayed every later time, even under concurrent calls.
type ResponseForm ¶
type ResponseForm struct {
ACSURL string `json:"acs_url"`
SAMLResponse string `json:"saml_response"`
RelayState string `json:"relay_state,omitempty"`
}
ResponseForm is what the browser posts to the SP's ACS URL (HTTP-POST binding).
type Result ¶
type Result struct {
RedirectTo string `json:"redirect_to,omitempty"`
Form *ResponseForm `json:"form,omitempty"`
}
Result is what an endpoint answers: a redirect (to the continue step or the login page), or the response form the browser posts to the SP. Exactly one is set.
type SSORequest ¶
type SSORequest struct {
// Binding is BindingRedirect (SAMLRequest deflated, any signature in
// the query) or BindingPOST (SAMLRequest base64 XML, any signature
// enveloped in it).
Binding string `json:"binding"`
SAMLRequest string `json:"saml_request"`
RelayState string `json:"relay_state,omitempty"`
// SigAlg and Signature are the redirect binding's query signature.
SigAlg string `json:"sig_alg,omitempty"`
Signature string `json:"signature,omitempty"`
// RawQuery is the redirect binding's query exactly as received, whose
// octets the signature covers (SAML bindings 3.4.4.1). Without it the
// signed string is rebuilt from the decoded values.
RawQuery string `json:"raw_query,omitempty"`
}
SSORequest is an AuthnRequest as it arrived at the single sign-on service.
type ServiceProvider ¶
type ServiceProvider struct {
// EntityID is the SP's entity ID: the Issuer of its AuthnRequests and
// the only Audience of the assertions it receives. Required.
EntityID string
// ACSURLs are the SP's assertion consumer service URLs (HTTP-POST
// binding). A request's AssertionConsumerServiceURL must equal one of
// them exactly; a request without one is answered at the first. An
// AssertionConsumerServiceIndex selects by position. Required; each an
// absolute https URL (http only for a loopback host).
ACSURLs []string
// Certificates verify the SP's signed AuthnRequests: a request that
// carries a signature is verified against them, and refused when it
// does not verify. Without Certificates a signature cannot be checked
// and the request counts as unsigned. RequireSignedRequests refuses an
// unsigned one.
Certificates []*x509.Certificate
// RequireSignedRequests refuses an AuthnRequest that is not signed by
// one of Certificates.
RequireSignedRequests bool
// NameIDFormat is the NameID format the SP receives; NameIDPersistent
// when empty. Persistent and unspecified carry the user's ID unless
// NameID replaces it, email the primary verified email address,
// transient a fresh random value per assertion.
NameIDFormat string
// NameID, when set, produces the NameID value (a pairwise identifier,
// an employee number).
NameID NameIDFunc
// Attributes, when set, builds the attributes released to the SP.
// Without it the assertion carries none. DefaultAttributes releases the
// email address and the name.
Attributes AttributesFunc
// AllowIdPInitiated lets Plugin.Initiate send an unsolicited assertion
// to this SP (IdP-initiated sign-on). Off by default: an unsolicited
// assertion has no InResponseTo to bind it to, so an SP that accepts
// them is open to assertion injection on its side.
AllowIdPInitiated bool
// UnsignedResponse signs only the assertion and leaves the Response
// envelope unsigned, for an SP that refuses two signatures. By default
// both are signed.
UnsignedResponse bool
}
ServiceProvider is one registered SAML SP.
type ServiceProviderStore ¶
type ServiceProviderStore interface {
ServiceProvider(ctx context.Context, entityID string) (ServiceProvider, error)
}
ServiceProviderStore finds a registered SP by entity ID. It answers ErrUnknownSP (or an error wrapping it) for an entity ID it does not know.
type Subject ¶
type Subject struct {
User auth.User
// Email is the user's primary verified email address, "" when there is
// none. Unverified addresses are never released.
Email string
// Session is the IdP session the assertion is issued from.
Session auth.Session
}
Subject is what an SP's NameID and Attributes functions see about the user being signed in.