Documentation
¶
Overview ¶
Package sso is enterprise single sign-on: OpenID Connect and SAML 2.0 identity providers that an app registers at run time, one per customer organization or email domain, the shape of better-auth's sso plugin.
plugin/oauth signs people in with providers fixed at build time (Google, GitHub, one Okta). This plugin keeps providers as rows in auth_sso_provider, so a customer's admin can connect their own Okta, Entra ID or ADFS without a deploy, and routes a sign-in to the right one by provider id, email domain, domain or organization. OpenID Connect runs on plugin/oauth's OAuth2 type (PKCE, ID-token signature, issuer and nonce checks); SAML runs on the SAMLEngine interface, gosaml2 by default, with every assertion rule that decides who signs in (audience, recipient, time window, InResponseTo, replay) checked here, in code this package's tests break one by one.
A provider account is linked to a user in auth_sso_account, keyed by the provider and the IdP's subject (the OIDC sub or the SAML NameID). An existing user with the same email is linked automatically only when the provider's domain is verified and the address is in it; otherwise the sign-in is refused, because linking would hand the account to whoever runs that IdP.
Index ¶
- Constants
- Variables
- func DeriveIdPEntityID(cfg SAMLConfig, maxMetadata int) (string, error)
- func DiscoveryURL(issuer string, cfg OIDCConfig) string
- func DomainMatches(domains []string, d string) bool
- func DomainRecordName(prefix, providerID, domain string) string
- func LogoutDocument(kind, id, issuer, destination, nameID, nameIDFormat string, ...) *etree.Document
- func NeedsRuntimeDiscovery(cfg OIDCConfig) bool
- func NormalizeEndpoint(issuer, endpoint string) (string, error)
- func PublicAddr(ip netip.Addr) bool
- func RecordHoldsToken(records []string, label, token string) bool
- func RedirectURL(dest, param string, msg *etree.Document, relay string, key crypto.Signer, ...) (string, error)
- func SPMetadataXML(sp SPSettings) ([]byte, error)
- func SelectTokenAuth(methods []string) (oauth.TokenAuth, error)
- func ValidateDiscoveryDocument(doc DiscoveryDocument, issuer string) error
- type ACSRequest
- type Account
- type Assignment
- type BindingMessage
- type CallbackRequest
- type CallbackResult
- type CheckRequest
- type Decision
- type DeprecatedPolicy
- type DiscoveryCode
- type DiscoveryDocument
- type DiscoveryError
- type DomainTokenResult
- type DomainVerification
- type Endpoint
- type GoSAML2
- func (g GoSAML2) AuthnRequestPOST(sp SPSettings, idp IdPSettings, requestID, relayState string) (PostForm, error)
- func (g GoSAML2) AuthnRequestURL(sp SPSettings, idp IdPSettings, requestID, relayState string) (string, error)
- func (g GoSAML2) VerifyResponse(sp SPSettings, idp IdPSettings, samlResponse string, now time.Time) (*SAMLResponse, error)
- type IdPMetadata
- type IdPSettings
- type InvitationChecker
- type LogoutMessage
- type LogoutRequest
- type LogoutResult
- type MetadataResult
- type MutationAction
- type MutationGuard
- type MutationInput
- type OIDCConfig
- type OIDCMapping
- type Option
- func DefaultOverrideUserInfo() Option
- func DisableImplicitSignUp() Option
- func PerProviderPaths() Option
- func ProvidersLimit(n int) Option
- func ProvisionUserOnEveryLogin() Option
- func RequireTrustedOrigins() Option
- func ReserveProviderIDs(ids ...string) Option
- func TrustEmailVerified() Option
- func TrustProviders(ids ...string) Option
- func WithACSURL(u string) Option
- func WithCallbackURL(u string) Option
- func WithDefaultProviders(ps ...Provider) Option
- func WithDomainVerification(d DomainVerification) Option
- func WithErrorURL(path string) Option
- func WithHTTPClient(hc *http.Client) Option
- func WithMutationGuard(fn MutationGuard) Option
- func WithOrganizations(o Organizations, prov OrganizationProvisioning) Option
- func WithPrivateKeyResolver(fn PrivateKeyResolver) Option
- func WithProviderFields(fields ...ProviderField) Option
- func WithProvidersLimit(fn func(ctx context.Context, u auth.User) int) Option
- func WithProvisionUser(fn ProvisionFunc) Option
- func WithResolveUser(fn ResolveUserFunc) Option
- func WithSAMLEngine(e SAMLEngine) Option
- func WithSAMLOptions(o SAMLOptions) Option
- func WithStateTTL(d time.Duration) Option
- func WithTrustedOrigins(origins ...string) Option
- type OrganizationProvisioning
- type Organizations
- type Plugin
- func (p *Plugin) ACS(ctx context.Context, in ACSRequest) (CallbackResult, error)
- func (p *Plugin) Accounts(ctx context.Context, userID string) ([]Account, error)
- func (p *Plugin) AssignByVerifiedDomain(ctx context.Context, userID, email string, emailVerified bool) (Assignment, error)
- func (p *Plugin) AssignOAuth(ctx context.Context, ev oauth.ResolvedEvent) (Assignment, error)
- func (p *Plugin) Callback(ctx context.Context, in CallbackRequest) (CallbackResult, error)
- func (p *Plugin) DeleteProvider(ctx context.Context, providerID string) error
- func (p *Plugin) Discover(ctx context.Context, issuer string, cfg *OIDCConfig) error
- func (p *Plugin) DomainToken(ctx context.Context, pr *Provider) (string, error)
- func (p *Plugin) FetchDiscoveryDocument(ctx context.Context, u string) (DiscoveryDocument, error)
- func (p *Plugin) GetProvider(ctx context.Context, providerID string) (SanitizedProvider, error)
- func (p *Plugin) Init(a *auth.Auth) error
- func (p *Plugin) InitiateSLO(ctx context.Context, in LogoutRequest) (LogoutResult, error)
- func (p *Plugin) ListProviders(ctx context.Context) (ProvidersResult, error)
- func (p *Plugin) Name() string
- func (p *Plugin) OAuthAssignment() oauth.ResolvedHook
- func (p *Plugin) OperationHooks() []auth.OperationHook
- func (p *Plugin) Operations() []auth.Operation
- func (p *Plugin) OptionErr() error
- func (p *Plugin) ProviderByID(ctx context.Context, providerID string) (Provider, error)
- func (p *Plugin) ProviderForDomain(ctx context.Context, d string) (Provider, error)
- func (p *Plugin) ProviderPaths(mount string, next http.Handler) http.Handler
- func (p *Plugin) RateLimits() []auth.RateRule
- func (p *Plugin) RegisterProvider(ctx context.Context, in RegisterRequest) (RegisterResult, error)
- func (p *Plugin) RequestDomainVerification(ctx context.Context, providerID string) (DomainTokenResult, error)
- func (p *Plugin) SLO(ctx context.Context, in SLORequest) (LogoutResult, error)
- func (p *Plugin) SPMetadata(ctx context.Context, providerID string) (MetadataResult, error)
- func (p *Plugin) SetProviderFields(ctx context.Context, providerID string, values map[string]any) error
- func (p *Plugin) SignIn(ctx context.Context, in SignInRequest) (SignInResult, error)
- func (p *Plugin) SignsIn() bool
- func (p *Plugin) Tables() []sqlb.Table
- func (p *Plugin) TypedMigrations(engine string, t auth.SQLTypes) ([]auth.Migration, error)
- func (p *Plugin) UpdateProvider(ctx context.Context, in UpdateRequest) (SanitizedProvider, error)
- func (p *Plugin) UserTables() []auth.UserTable
- func (p *Plugin) Validate(c *auth.Config) error
- func (p *Plugin) VerifyDomain(ctx context.Context, providerID string) error
- type PostForm
- type PrivateKeyRequest
- type PrivateKeyResolver
- type Protocol
- type Provider
- type ProviderField
- type ProviderRequest
- type ProvidersResult
- type ProvisionEvent
- type ProvisionFunc
- type RawFacts
- type RegisterRequest
- type RegisterResult
- type ResolveAction
- type ResolveInput
- type ResolveUserFunc
- type SAMLAssertion
- type SAMLCode
- type SAMLConfig
- type SAMLEngine
- type SAMLError
- type SAMLMapping
- type SAMLOptions
- type SAMLResponse
- type SLORequest
- type SPMetadata
- type SPPolicy
- type SPSettings
- type SanitizedOIDC
- type SanitizedProvider
- type SanitizedSAML
- type SignInRequest
- type SignInResult
- type SubjectConfirmation
- type TXTResolver
- type UpdateRequest
- type UserInfo
Constants ¶
const ( RoleOwner = "owner" RoleAdmin = "admin" RoleMember = "member" )
Organization roles the plugin names. Only an owner or admin may register or manage an organization's provider.
const ( KindLogoutRequest = "LogoutRequest" KindLogoutResponse = "LogoutResponse" // StatusSuccess is the SAML success status. StatusSuccess = "urn:oasis:names:tc:SAML:2.0:status:Success" )
Logout message kinds.
const ( BindingRedirect = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" BindingPOST = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" NameIDEmail = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" NameIDUnspecified = "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" NameIDPersistent = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" )
SAML bindings and formats.
const ( AlgRSASHA1 = "http://www.w3.org/2000/09/xmldsig#rsa-sha1" AlgRSASHA256 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" AlgRSASHA384 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384" AlgRSASHA512 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512" AlgECDSASHA256 = "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256" AlgECDSASHA384 = "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384" AlgECDSASHA512 = "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512" DigestSHA1 = "http://www.w3.org/2000/09/xmldsig#sha1" DigestSHA256 = "http://www.w3.org/2001/04/xmlenc#sha256" DigestSHA384 = "http://www.w3.org/2001/04/xmldsig-more#sha384" DigestSHA512 = "http://www.w3.org/2001/04/xmlenc#sha512" )
Signature and digest algorithm URIs.
const ( KeyEncRSA15 = "http://www.w3.org/2001/04/xmlenc#rsa-1_5" KeyEncRSAOAEP = "http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p" KeyEncRSAOAEP11 = "http://www.w3.org/2009/xmlenc11#rsa-oaep" DataEnc3DESCBC = "http://www.w3.org/2001/04/xmlenc#tripledes-cbc" DataEncAES128CBC = "http://www.w3.org/2001/04/xmlenc#aes128-cbc" DataEncAES192CBC = "http://www.w3.org/2001/04/xmlenc#aes192-cbc" DataEncAES256CBC = "http://www.w3.org/2001/04/xmlenc#aes256-cbc" DataEncAES128GCM = "http://www.w3.org/2009/xmlenc11#aes128-gcm" DataEncAES192GCM = "http://www.w3.org/2009/xmlenc11#aes192-gcm" DataEncAES256GCM = "http://www.w3.org/2009/xmlenc11#aes256-gcm" )
Key-transport and data-encryption algorithms of an EncryptedAssertion.
const ( AuthnBindingRedirect = "redirect" AuthnBindingPOST = "post" )
Bindings an AuthnRequest may use (SPMetadata.Binding).
const ( // PurposeSAMLSession finds an app session from (provider, NameID, // SessionIndex); Meta is the JSON samlSession. PurposeSAMLSession auth.Purpose = "sso.saml_session" // PurposeSAMLSessionByID finds the SAML session of an app session id. PurposeSAMLSessionByID auth.Purpose = "sso.saml_session_id" // PurposeSAMLLogout is an SP-initiated LogoutRequest's ID, consumed by // the IdP's LogoutResponse. PurposeSAMLLogout auth.Purpose = "sso.saml_logout" )
Verification purposes of single logout.
const ( EndpointLogout auth.EndpointID = "sso.saml-logout" EndpointSLO auth.EndpointID = "sso.saml-slo" EndpointSLOPost auth.EndpointID = "sso.saml-slo-post" PathLogout = "/saml2/logout" PathSLO = "/saml2/sp/slo" )
Operation ids and routes of single logout.
const ( // PurposeState is an OIDC flow's state; Identifier is the provider id, // Meta the JSON flowMeta. PurposeState auth.Purpose = "sso.state" // PurposeSAMLRequest is an SP-initiated AuthnRequest; the token is the // request ID, so the response's InResponseTo consumes it exactly once. PurposeSAMLRequest auth.Purpose = "sso.saml_request" // PurposeSAMLRelay is the RelayState of an SP-initiated SAML flow; Meta // is the JSON flowMeta. PurposeSAMLRelay auth.Purpose = "sso.saml_relay" // PurposeSAMLAssertion reserves an accepted assertion's ID until it // expires: a second response carrying the same ID is a replay. PurposeSAMLAssertion auth.Purpose = "sso.saml_assertion" )
Verification purposes. Each is a single-use core verification row.
const ( EndpointRegister auth.EndpointID = "sso.register" EndpointSignIn auth.EndpointID = "sso.sign-in" EndpointCallback auth.EndpointID = "sso.callback" EndpointProviders auth.EndpointID = "sso.providers" EndpointGetProvider auth.EndpointID = "sso.get-provider" EndpointUpdate auth.EndpointID = "sso.update-provider" EndpointDelete auth.EndpointID = "sso.delete-provider" EndpointSPMetadata auth.EndpointID = "sso.saml-metadata" EndpointACS auth.EndpointID = "sso.saml-acs" EndpointRequestDomain auth.EndpointID = "sso.request-domain-verification" EndpointVerifyDomain auth.EndpointID = "sso.verify-domain" )
Operation ids.
const ( PathRegister = "/register" PathSignIn = "/sign-in" PathCallback = "/callback" PathProviders = "/providers" PathGetProvider = "/get-provider" PathUpdate = "/update-provider" PathDelete = "/delete-provider" PathSPMetadata = "/saml2/sp/metadata" PathACS = "/saml2/sp/acs" PathRequestDomain = "/request-domain-verification" PathVerifyDomain = "/verify-domain" )
Routes, relative to the plugin's mount ("/auth/sso" by default). The OIDC callback and the SAML ACS are shared by every provider: the core routes exact paths, so the provider travels in the state, the RelayState or (for an IdP-initiated sign-in) a providerId query parameter.
const ( // DefaultProvidersLimit is how many providers one user may register. DefaultProvidersLimit = 10 // DefaultStateTTL bounds an OIDC flow and an SP-initiated SAML request. DefaultStateTTL = 10 * time.Minute // DefaultSAMLRequestTTL is how long an AuthnRequest's ID is accepted // as InResponseTo. DefaultSAMLRequestTTL = 5 * time.Minute // DefaultClockSkew is the tolerance on every SAML timestamp. DefaultClockSkew = 5 * time.Minute // DefaultMaxResponseSize bounds a base64-decoded SAML response. DefaultMaxResponseSize = 256 * 1024 // DefaultMaxMetadataSize bounds IdP or SP metadata XML. DefaultMaxMetadataSize = 100 * 1024 // DefaultReplayWindow is how long an assertion ID stays reserved when // the assertion states no NotOnOrAfter. DefaultReplayWindow = 15 * time.Minute // DefaultDomainTokenTTL is how long a domain-verification token is // accepted (better-auth: one week). DefaultDomainTokenTTL = 7 * 24 * time.Hour // DefaultDomainTokenPrefix names the DNS TXT record of a domain // verification: _<prefix>-<providerId>.<domain>. DefaultDomainTokenPrefix = "ubgo-auth-token" // DefaultHTTPTimeout bounds every call to an identity provider. DefaultHTTPTimeout = 10 * time.Second )
Defaults, each better-auth's.
const ( TableNameProvider = "auth_sso_provider" TableAliasProvider = "ssop" TableNameAccount = "auth_sso_account" TableAliasAccount = "ssoa" ColID = "id" ColProviderID = "provider_id" ColIssuer = "issuer" ColDomain = "domain" ColOrganizationID = "organization_id" ColUserID = "user_id" // ColOIDCConfig and ColSAMLConfig hold JSON. The OIDC client secret // inside is sealed (auth.KeyRing, additional data the provider id), so // a database dump does not hand out every customer's IdP credential. ColOIDCConfig = "oidc_config" ColSAMLConfig = "saml_config" ColDomainVerified = "domain_verified" ColCreatedAt = "created_at" ColUpdatedAt = "updated_at" ColSubject = "subject" // ColDomainToken and ColDomainTokenExpires hold the pending // domain-verification token (” and NULL when none). ColDomainToken = "domain_token" ColDomainTokenExpires = "domain_token_expires_at" // ColFields holds the WithProviderFields values as a JSON object, NULL // when none is set. ColFields = "fields" ColEmail = "email" )
Table labels and columns. auth_sso_provider holds the registered providers; auth_sso_account links an IdP subject to a user. Neither has a foreign key (ADR-0002): DeleteUser removes a user's sso accounts through UserTables, and a provider outlives the admin who registered it.
const DefaultLogoutRequestTTL = 5 * time.Minute
DefaultLogoutRequestTTL bounds an SP-initiated LogoutRequest.
const DefaultMountPrefix = "/auth"
DefaultMountPrefix is where authhttp mounts plugins by default; the callback and ACS URLs are built under it unless WithCallbackURL or WithACSURL say otherwise.
const PluginName = "sso"
PluginName is the plugin's permanent identity and its route segment.
Variables ¶
var ( ErrFieldNotAllowed = auth.NewError("sso.field_not_allowed", http.StatusBadRequest, "A provider field may not be set here.") ErrBadFields = errors.New("sso: a provider field name is empty, repeated, or a built-in key") )
Errors of additional fields.
var ( ErrSLOUnsupported = auth.NewError("sso.idp_slo_unsupported", http.StatusBadRequest, "This identity provider does not support single logout.") ErrSAMLSessionNotFound = auth.NewError("sso.saml_session_not_found", http.StatusNotFound, "This session did not start with SAML single sign-on.") ErrInvalidLogoutRequest = auth.NewError("sso.invalid_logout_request", http.StatusBadRequest, "The logout request was not accepted.") ErrInvalidLogoutResponse = auth.NewError("sso.invalid_logout_response", http.StatusBadRequest, "The logout response was not accepted.") ErrLogoutFailedAtIdP = auth.NewError("sso.logout_failed_at_idp", http.StatusBadGateway, "The identity provider could not sign you out.") )
Errors of single logout.
var ( ErrProviderNotFound = auth.NewError("sso.provider_not_found", http.StatusNotFound, "No sign-in provider was found for this account.") ErrProviderExists = auth.NewError("sso.provider_exists", http.StatusUnprocessableEntity, "A sign-in provider with this id already exists.") ErrProviderIDReserved = auth.NewError("sso.provider_id_reserved", http.StatusUnprocessableEntity, "This provider id is reserved.") ErrBadProvider = auth.NewError("sso.bad_provider", http.StatusBadRequest, "The provider configuration is not valid.") ErrRegistrationOff = auth.NewError("sso.registration_disabled", http.StatusForbidden, "Sign-in provider registration is disabled.") ErrProvidersLimit = auth.NewError("sso.providers_limit", http.StatusForbidden, "You have reached the maximum number of sign-in providers.") ErrForbidden = auth.NewError("sso.forbidden", http.StatusForbidden, "You cannot manage this sign-in provider.") ErrNotMember = auth.NewError("sso.not_member", http.StatusBadRequest, "You are not a member of this organization.") ErrSignInTarget = auth.NewError("sso.target_required", http.StatusBadRequest, "Give an email, a domain, an organization or a provider id.") ErrDomainUnverified = auth.NewError("sso.domain_unverified", http.StatusUnauthorized, "This provider's domain has not been verified.") ErrProtocol = auth.NewError("sso.protocol_unavailable", http.StatusBadRequest, "This provider does not offer that sign-in protocol.") ErrBadParams = auth.NewError("sso.bad_params", http.StatusBadRequest, "A sign-in parameter is not allowed.") ErrBadReturnTo = auth.NewError("sso.bad_return_to", http.StatusBadRequest, "The return address must be a path on this site or a trusted origin.") ErrStateInvalid = auth.NewError("sso.state_invalid", http.StatusBadRequest, "This sign-in link has expired. Start again.") ErrProviderChanged = auth.NewError("sso.provider_changed", http.StatusConflict, "The sign-in provider changed during sign-in. Start again.") ErrProviderDenied = auth.NewError("sso.denied", http.StatusBadRequest, "Sign-in was cancelled at the provider.") ErrTokenNotVerified = auth.NewError("sso.token_not_verified", http.StatusUnauthorized, "The provider's sign-in token was not accepted.") ErrNoIDToken = auth.NewError("sso.token_response_not_found", http.StatusBadGateway, "The provider's answer carried no sign-in token.") ErrNoPrivateKey = auth.NewError("sso.no_private_key_available", http.StatusInternalServerError, "The sign-in key for this provider is not available.") ErrTokenExchange = auth.NewError("sso.token_response_error", http.StatusBadGateway, "The provider did not complete the sign-in. Try again.") ErrProviderFailed = auth.NewError("sso.provider_failed", http.StatusBadGateway, "The sign-in provider could not be reached. Try again.") ErrAccountExists = auth.NewError("sso.account_exists", http.StatusConflict, "An account with this email already exists. Sign in, then link this provider.") ErrLinkedElsewhere = auth.NewError("sso.linked_elsewhere", http.StatusConflict, "This provider account is linked to another user.") ErrSignUpDisabled = auth.NewError("sso.sign_up_disabled", http.StatusForbidden, "No account exists for you here. Ask for an invitation or sign up first.") ErrUserRejected = auth.NewError("sso.user_rejected", http.StatusForbidden, "This account cannot sign in here.") ErrBoundaryLocked = auth.NewError("sso.boundary_locked", http.StatusConflict, "Cannot change a provider's identity fields while accounts are linked to it.") ErrMutationRejected = auth.NewError("sso.mutation_rejected", http.StatusConflict, "This change to the sign-in provider was refused.") ErrSAMLInvalid = auth.NewError("sso.saml_invalid", http.StatusBadRequest, "The sign-in response was not accepted.") ErrSAMLReplay = auth.NewError("sso.saml_replay", http.StatusBadRequest, "This sign-in response was already used.") ErrSAMLUnsolicited = auth.NewError("sso.saml_unsolicited", http.StatusBadRequest, "Sign-in must start on this site.") ErrSAMLTooLarge = auth.NewError("sso.saml_too_large", http.StatusBadRequest, "The sign-in response is too large.") ErrNoIdentity = auth.NewError("sso.no_identity", http.StatusBadRequest, "The provider sent no user id or email.") ErrDomainVerified = auth.NewError("sso.domain_verified", http.StatusConflict, "This domain is already verified.") ErrDomainNotProven = auth.NewError("sso.domain_not_proven", http.StatusBadGateway, "The domain's DNS record was not found. It can take a while to appear.") ErrNoPendingDomain = auth.NewError("sso.no_pending_verification", http.StatusNotFound, "No domain verification is pending for this provider. Request a new token.") ErrIdentifierTooLong = auth.NewError("sso.identifier_too_long", http.StatusBadRequest, "The provider id is too long for a DNS verification record.") )
Errors a caller sees. Codes are permanent.
var ( ErrNoCallbackURL = errors.New("sso: callback URLs need auth.WithBaseURL or sso.WithCallbackURL") ErrBadDefault = errors.New("sso: a default provider is not valid") ErrPrefixTooLong = errors.New("sso: the domain token prefix leaves no room for a provider id in a DNS label (63 bytes)") // ErrNoSAMLEngine was WithSAMLEngine(nil). // // Deprecated: no longer returned. WithSAMLEngine(nil) fails auth.New // with auth.ErrNilOption naming the option. ErrNoSAMLEngine = errors.New("sso: SAML needs a SAMLEngine (sso.WithSAMLEngine)") ErrKeyDerivation = errors.New("sso: provider secrets need auth.WithSecret, so they are sealed at rest") ErrBadAlgorithmSet = errors.New("sso: an allowed SAML algorithm is not one this plugin knows") )
Configuration errors, returned by auth.New.
var ( TableProvider = sqlb.Table{Name: TableNameProvider, Alias: TableAliasProvider} ProviderRowID = sqlb.Col[string](TableProvider, ColID) ProviderProviderID = sqlb.Col[string](TableProvider, ColProviderID) ProviderIssuer = sqlb.Col[string](TableProvider, ColIssuer) ProviderDomain = sqlb.Col[string](TableProvider, ColDomain) ProviderOrganizationID = sqlb.Col[string](TableProvider, ColOrganizationID) ProviderUserID = sqlb.Col[string](TableProvider, ColUserID) ProviderOIDCConfig = sqlb.Col[string](TableProvider, ColOIDCConfig) ProviderSAMLConfig = sqlb.Col[string](TableProvider, ColSAMLConfig) ProviderDomainVerified = sqlb.Col[bool](TableProvider, ColDomainVerified) ProviderCreatedAt = sqlb.Col[time.Time](TableProvider, ColCreatedAt) ProviderUpdatedAt = sqlb.Col[time.Time](TableProvider, ColUpdatedAt) ProviderDomainToken = sqlb.Col[string](TableProvider, ColDomainToken) ProviderDomainTokenExp = sqlb.Col[*time.Time](TableProvider, ColDomainTokenExpires) ProviderFields = sqlb.Col[*string](TableProvider, ColFields) TableAccount = sqlb.Table{Name: TableNameAccount, Alias: TableAliasAccount} AccountID = sqlb.Col[string](TableAccount, ColID) AccountUserID = sqlb.Col[string](TableAccount, ColUserID) AccountProviderID = sqlb.Col[string](TableAccount, ColProviderID) AccountIssuer = sqlb.Col[string](TableAccount, ColIssuer) AccountSubject = sqlb.Col[string](TableAccount, ColSubject) AccountEmail = sqlb.Col[string](TableAccount, ColEmail) AccountCreatedAt = sqlb.Col[time.Time](TableAccount, ColCreatedAt) )
Typed tables and columns, for an app that joins them.
var AssignmentValues = []Assignment{AssignAssigned, AssignAlreadyMember, AssignAmbiguous, AssignIneligibleSource, AssignInvitationPending, AssignNoTarget, AssignDisabled, AssignUnverified}
AssignmentValues is every Assignment.
var DataEncryptionAlgorithms = []string{DataEnc3DESCBC, DataEncAES128CBC, DataEncAES192CBC, DataEncAES256CBC, DataEncAES128GCM, DataEncAES192GCM, DataEncAES256GCM}
DataEncryptionAlgorithms are the data-encryption algorithms this plugin knows; 3DES is deprecated. AES-192-CBC is listed so a response naming it is refused as unsupported by the engine rather than as unknown.
var DeprecatedPolicyValues = []DeprecatedPolicy{DeprecatedWarn, DeprecatedReject, DeprecatedAllow}
DeprecatedPolicyValues is every DeprecatedPolicy.
var DigestAlgorithms = []string{DigestSHA1, DigestSHA256, DigestSHA384, DigestSHA512}
DigestAlgorithms are the digest algorithms this plugin knows; SHA-1 is deprecated.
var DiscoveryCodeValues = []DiscoveryCode{DiscoveryIssuerMismatch, DiscoveryIncomplete, DiscoveryNotFound, DiscoveryTimeout, DiscoveryInvalidURL, DiscoveryPrivateHost, DiscoveryInvalidJSON, DiscoveryRedirect, DiscoveryUnsupportedAuth, DiscoveryUnexpectedFailed, DiscoveryUntrustedOrigin}
DiscoveryCodeValues is every DiscoveryCode.
var ErrSPMetadataMismatch = auth.NewError("sso.sp_metadata_mismatch", http.StatusBadRequest, "The SP metadata does not match the provider's settings.")
ErrSPMetadataMismatch is custom SP metadata that contradicts the config.
var KeyEncryptionAlgorithms = []string{KeyEncRSA15, KeyEncRSAOAEP, KeyEncRSAOAEP11}
KeyEncryptionAlgorithms are the key-transport algorithms this plugin knows; RSA-1.5 is deprecated.
var MutationActionValues = []MutationAction{MutationUpdate, MutationDelete}
MutationActionValues is every MutationAction.
var ProtocolValues = []Protocol{ProtocolOIDC, ProtocolSAML}
ProtocolValues is every Protocol.
var ResolveActionValues = []ResolveAction{ResolveContinue, ResolveLink}
ResolveActionValues is every ResolveAction.
var SignatureAlgorithms = []string{AlgRSASHA1, AlgRSASHA256, AlgRSASHA384, AlgRSASHA512, AlgECDSASHA256, AlgECDSASHA384, AlgECDSASHA512}
SignatureAlgorithms are the signature algorithms this plugin knows; RSA-SHA1 is deprecated (SAMLOptions.OnDeprecated).
Functions ¶
func DeriveIdPEntityID ¶
func DeriveIdPEntityID(cfg SAMLConfig, maxMetadata int) (string, error)
DeriveIdPEntityID is the IdP entity id of a config, by the parser sign-in uses (better-auth's deriveSAMLIdentityProviderEntityID).
func DiscoveryURL ¶
func DiscoveryURL(issuer string, cfg OIDCConfig) string
DiscoveryURL is where the document of cfg is read: DiscoveryEndpoint, or issuer + /.well-known/openid-configuration.
func DomainMatches ¶
DomainMatches reports whether email domain d is one of domains or a subdomain of one.
func DomainRecordName ¶
DomainRecordName is where the TXT record of a provider's domain lives.
func LogoutDocument ¶
func LogoutDocument(kind, id, issuer, destination, nameID, nameIDFormat string, sessionIndexes []string, inResponseTo, status string, now time.Time) *etree.Document
LogoutDocument builds a LogoutRequest (nameID set) or LogoutResponse (inResponseTo and status set) from this SP.
func NeedsRuntimeDiscovery ¶
func NeedsRuntimeDiscovery(cfg OIDCConfig) bool
NeedsRuntimeDiscovery reports whether an endpoint the flow needs is missing, so discovery runs again at sign-in.
func NormalizeEndpoint ¶
NormalizeEndpoint resolves a discovered endpoint against the issuer: an absolute URL is kept, a path is joined to the issuer's origin and path ("/oauth/token" under "https://idp/tenant" is "https://idp/tenant/oauth/token").
func PublicAddr ¶
PublicAddr reports whether ip is publicly routable: not loopback, private (RFC 1918, fc00::/7), link-local (169.254.0.0/16, which holds the cloud metadata address, and fe80::/10), carrier-grade NAT, multicast or unspecified.
func RecordHoldsToken ¶
RecordHoldsToken reports whether a TXT record set proves the token: a record that is the token, or "<label>=<token>". Compared in constant time, like every secret.
func RedirectURL ¶
func RedirectURL(dest, param string, msg *etree.Document, relay string, key crypto.Signer, sigAlg string) (string, error)
RedirectURL encodes a message for the HTTP-Redirect binding at dest, signing the query when key is set.
func SPMetadataXML ¶
func SPMetadataXML(sp SPSettings) ([]byte, error)
SPMetadataXML renders this app's metadata for one provider: entity id, POST ACS, the NameID format, the signing and encryption certificate when set, and the two signing flags.
func SelectTokenAuth ¶
SelectTokenAuth picks the token-endpoint authentication from the methods a discovery document lists: client_secret_basic, then client_secret_post, then private_key_jwt; an empty list means client_secret_basic, the specification's default. A list naming none of the three is DiscoveryUnsupportedAuth.
func ValidateDiscoveryDocument ¶
func ValidateDiscoveryDocument(doc DiscoveryDocument, issuer string) error
ValidateDiscoveryDocument checks a document against the configured issuer: the same issuer (trailing slash aside), and the authorization, token and JWKS endpoints present.
Types ¶
type ACSRequest ¶
type ACSRequest struct {
SAMLResponse string `json:"SAMLResponse"`
RelayState string `json:"RelayState,omitempty"`
// ProviderID is set by ProviderPaths from a per-provider ACS URL.
ProviderID string `json:"providerId,omitempty"`
}
ACSRequest is the IdP's POST to the assertion consumer service.
type Account ¶
type Account struct {
ID string
UserID string
ProviderID string
// Issuer is the ID token's iss or the IdP entity id at link time.
Issuer string
// Subject is the OIDC sub or SAML NameID, compared exactly.
Subject string
Email string
CreatedAt time.Time
}
Account is one IdP account linked to a user: a row of auth_sso_account.
type Assignment ¶
type Assignment string
Assignment is the outcome of AssignByVerifiedDomain, better-auth's organization-assignment outcomes.
const ( AssignAssigned Assignment = "assigned" AssignAlreadyMember Assignment = "already-member" AssignAmbiguous Assignment = "ambiguous-target" AssignIneligibleSource Assignment = "ineligible-source" AssignInvitationPending Assignment = "invitation-pending" AssignNoTarget Assignment = "no-target" AssignDisabled Assignment = "provisioning-disabled" AssignUnverified Assignment = "unverified-identity" )
type BindingMessage ¶
type BindingMessage struct {
// Redirect is true for HTTP-Redirect (deflated, query-signed), false
// for HTTP-POST (enveloped XML signature).
Redirect bool
Value string
Param string
RelayState string
SigAlg string
Signature string
}
BindingMessage is a SAML message as it arrived: the redirect binding's query values, or the POST binding's form values.
type CallbackRequest ¶
type CallbackRequest struct {
Code string `json:"code,omitempty"`
State string `json:"state,omitempty"`
Error string `json:"error,omitempty"`
ErrorDescription string `json:"error_description,omitempty"`
// Iss is RFC 9207's issuer parameter.
Iss string `json:"iss,omitempty"`
// ProviderID names the provider of an IdP-initiated callback, which has
// no state: register the redirect URI with ?providerId=<id> at the IdP.
ProviderID string `json:"providerId,omitempty"`
}
CallbackRequest is the OIDC callback's query.
type CallbackResult ¶
type CallbackResult struct {
User auth.UserView `json:"user"`
Token auth.Token `json:"-"`
RedirectTo string `json:"redirectTo"`
// Created is true when the sign-in created the user.
Created bool `json:"created"`
SecondFactorRequired bool `json:"secondFactorRequired,omitempty"`
}
CallbackResult is a completed sign-in.
type CheckRequest ¶
type CheckRequest struct {
IdPEntityID string
Audience string
ACSURL string
// RequestID is the AuthnRequest ID of an SP-initiated sign-in; "" for
// an IdP-initiated one, whose InResponseTo must then be empty.
RequestID string
WantAssertionsSigned bool
Now time.Time
ClockSkew time.Duration
RequireTimestamps bool
}
CheckRequest is what CheckAssertion compares a response against.
type Decision ¶
type Decision struct {
Action ResolveAction
// UserID is the user to link to, for ResolveLink.
UserID string
// UpdateProfile copies the IdP's name and image onto that user.
UpdateProfile bool
}
Decision is what a WithResolveUser function decided.
type DeprecatedPolicy ¶
type DeprecatedPolicy string
DeprecatedPolicy is what to do with a SHA-1 signature or digest.
const ( DeprecatedWarn DeprecatedPolicy = "warn" DeprecatedReject DeprecatedPolicy = "reject" DeprecatedAllow DeprecatedPolicy = "allow" )
type DiscoveryCode ¶
type DiscoveryCode string
DiscoveryCode names why discovery failed; each is better-auth's code.
const ( DiscoveryIssuerMismatch DiscoveryCode = "issuer_mismatch" DiscoveryIncomplete DiscoveryCode = "discovery_incomplete" DiscoveryNotFound DiscoveryCode = "discovery_not_found" DiscoveryTimeout DiscoveryCode = "discovery_timeout" DiscoveryInvalidURL DiscoveryCode = "discovery_invalid_url" DiscoveryPrivateHost DiscoveryCode = "discovery_private_host" DiscoveryInvalidJSON DiscoveryCode = "discovery_invalid_json" DiscoveryRedirect DiscoveryCode = "oidc_endpoint_redirect" DiscoveryUnsupportedAuth DiscoveryCode = "unsupported_token_auth_method" DiscoveryUnexpectedFailed DiscoveryCode = "discovery_unexpected_error" DiscoveryUntrustedOrigin DiscoveryCode = "discovery_untrusted_origin" )
type DiscoveryDocument ¶
type DiscoveryDocument struct {
Issuer string `json:"issuer"`
AuthorizationEndpoint string `json:"authorization_endpoint"`
TokenEndpoint string `json:"token_endpoint"`
UserinfoEndpoint string `json:"userinfo_endpoint"`
JWKSURI string `json:"jwks_uri"`
TokenAuthMethods []string `json:"token_endpoint_auth_methods_supported"`
}
DiscoveryDocument is the part of an OpenID Connect discovery document the plugin reads.
type DiscoveryError ¶
type DiscoveryError struct {
Code DiscoveryCode
Err error
}
DiscoveryError is a failed discovery: the code, and the cause for logs.
func (*DiscoveryError) Error ¶
func (e *DiscoveryError) Error() string
func (*DiscoveryError) Unwrap ¶
func (e *DiscoveryError) Unwrap() []error
Unwrap returns the answer for the code, so errors.Is(err, the *auth.Error) holds and the transport answers it.
type DomainTokenResult ¶
type DomainTokenResult struct {
DomainVerificationToken string `json:"domainVerificationToken"`
// RecordName is the TXT record's name for the first domain.
RecordName string `json:"recordName"`
}
DomainTokenResult is the token to publish.
type DomainVerification ¶
type DomainVerification struct {
// TokenPrefix names the record; empty is DefaultDomainTokenPrefix.
TokenPrefix string
// Resolver looks the record up; nil is net.DefaultResolver.
Resolver TXTResolver
}
DomainVerification turns on proving a provider's domains through DNS: registration returns a token (DomainToken), the customer publishes it as a TXT record at _<prefix>-<providerId>.<domain>, and verify-domain checks it. Until then the provider cannot sign anyone in, and only a verified provider links to existing users by email.
type GoSAML2 ¶
type GoSAML2 struct{}
GoSAML2 is the default SAMLEngine, on github.com/russellhaering/gosaml2 and its XML-signature library goxmldsig.
Why this library: it keeps every setting on a per-provider struct (no package-level clock or skew, which crewjam/saml has), it returns only the content its signature check covered (the unverified element is replaced by the verified one, the defence against signature wrapping), it bounds decompression and XML token counts, and it is fuzzed in OSS-Fuzz. It is built per call from the provider's settings and holds no state.
func (GoSAML2) AuthnRequestPOST ¶
func (g GoSAML2) AuthnRequestPOST(sp SPSettings, idp IdPSettings, requestID, relayState string) (PostForm, error)
AuthnRequestPOST builds the POST-binding form of an AuthnRequest whose ID is requestID, with an enveloped signature when asked.
func (GoSAML2) AuthnRequestURL ¶
func (g GoSAML2) AuthnRequestURL(sp SPSettings, idp IdPSettings, requestID, relayState string) (string, error)
AuthnRequestURL builds the HTTP-Redirect URL of an AuthnRequest whose ID is requestID; the library signs the query when asked.
func (GoSAML2) VerifyResponse ¶
func (g GoSAML2) VerifyResponse(sp SPSettings, idp IdPSettings, samlResponse string, now time.Time) (*SAMLResponse, error)
VerifyResponse verifies the signature and returns the verified content. The library's own audience and time checks are warnings it leaves to the caller; CheckAssertion applies them.
type IdPMetadata ¶
type IdPMetadata struct {
// Metadata is the IdP's metadata XML; its entity id, signing
// certificates and SSO endpoint are read from it.
Metadata string `json:"metadata,omitempty"`
// EntityID is required when Metadata is empty.
EntityID string `json:"entityID,omitempty"`
Cert string `json:"cert,omitempty"`
SingleSignOnService []Endpoint `json:"singleSignOnService,omitempty"`
// SingleLogoutService is the IdP's SLO endpoint (HTTP-Redirect).
SingleLogoutService []Endpoint `json:"singleLogoutService,omitempty"`
}
IdPMetadata is the IdP's side: its metadata XML, or the fields written out.
type IdPSettings ¶
type IdPSettings struct {
EntityID string
SSOURL string
// SLOURL is the IdP's single logout URL (HTTP-Redirect); "" when it has
// none.
SLOURL string
Certs []*x509.Certificate
}
IdPSettings are the IdP's settings.
type InvitationChecker ¶
type InvitationChecker interface {
PendingInvitation(ctx context.Context, orgID, userID string) (bool, error)
}
InvitationChecker is an optional Organizations upgrade: an organization system that knows pending invitations implements it, and joining at sign-in then leaves a user with a pending invitation to accept it, as better-auth does, so the invited role is not overwritten by the default.
type LogoutMessage ¶
type LogoutMessage struct {
Kind string
ID string
InResponseTo string
Issuer string
Destination string
NameID string
NameIDFormat string
SessionIndexes []string
NotOnOrAfter time.Time
Status string
Signed bool
}
LogoutMessage is a LogoutRequest or LogoutResponse, after its signature was checked (Signed says whether it carried one).
func DecodeLogout ¶
func DecodeLogout(m BindingMessage, certs []*x509.Certificate, now time.Time, maxSize int) (*LogoutMessage, error)
DecodeLogout reads a logout message and checks its signature against the IdP's certificates: the query signature for the redirect binding, the enveloped XML signature for POST, whose verified element is the only content read. An unsigned message comes back with Signed false; the caller decides whether that is acceptable. maxSize bounds the XML.
func PeekLogout ¶
func PeekLogout(m BindingMessage, maxSize int) (*LogoutMessage, error)
PeekLogout reads a logout message without checking its signature, only to learn which provider's certificates must check it.
type LogoutRequest ¶
type LogoutRequest struct {
// CallbackURL is where the browser lands after the IdP answers.
CallbackURL string `json:"callbackURL,omitempty"`
}
LogoutRequest starts an SP-initiated single logout.
type LogoutResult ¶
LogoutResult is where to send the browser: the IdP's SLO URL.
type MetadataResult ¶
type MetadataResult struct {
XML string `json:"metadata"`
}
MetadataResult is SP metadata XML.
type MutationAction ¶
type MutationAction string
MutationAction is what a MutationGuard is asked about.
const ( MutationUpdate MutationAction = "update" MutationDelete MutationAction = "delete" )
type MutationGuard ¶
MutationGuard is the WithMutationGuard hook.
type MutationInput ¶
type MutationInput struct {
Action MutationAction
Provider Provider
// Next is the provider after an update; zero for a delete.
Next Provider
// BoundaryChange is true when an update changes an authentication
// boundary field (issuer, endpoints, client id, entity ids, certs).
BoundaryChange bool
}
MutationInput describes a provider update or delete.
type OIDCConfig ¶
type OIDCConfig struct {
ClientID string `json:"clientId"`
// ClientSecret is required unless TokenEndpointAuthentication is
// private_key_jwt or none. Stored sealed; never returned by a read.
ClientSecret string `json:"clientSecret,omitempty"`
AuthorizationEndpoint string `json:"authorizationEndpoint,omitempty"`
TokenEndpoint string `json:"tokenEndpoint,omitempty"`
UserInfoEndpoint string `json:"userInfoEndpoint,omitempty"`
JWKSEndpoint string `json:"jwksEndpoint,omitempty"`
// DiscoveryEndpoint defaults to <issuer>/.well-known/openid-configuration.
DiscoveryEndpoint string `json:"discoveryEndpoint,omitempty"`
// SkipDiscovery never fetches the discovery document: the endpoints
// above must all be given (JWKS included, since the ID token is
// verified).
SkipDiscovery bool `json:"skipDiscovery,omitempty"`
// TokenEndpointAuthentication is client_secret_basic,
// client_secret_post, private_key_jwt or none; empty takes the best
// method the discovery document lists (basic, then post, then
// private_key_jwt), and client_secret_basic without discovery.
TokenEndpointAuthentication oauth.TokenAuth `json:"tokenEndpointAuthentication,omitempty"`
// PrivateKeyID and PrivateKeyAlgorithm are handed to the
// WithPrivateKeyResolver function for private_key_jwt; the key itself
// never lives in the row.
PrivateKeyID string `json:"privateKeyId,omitempty"`
PrivateKeyAlgorithm string `json:"privateKeyAlgorithm,omitempty"`
// Scopes default to openid email profile offline_access.
Scopes []string `json:"scopes,omitempty"`
// PKCE defaults to true; false only for an IdP that refuses it.
PKCE *bool `json:"pkce,omitempty"`
// OverrideUserInfo copies the IdP's name and image onto the user at
// every sign-in, not only at creation.
OverrideUserInfo bool `json:"overrideUserInfo,omitempty"`
Mapping OIDCMapping `json:"mapping,omitempty"`
// AllowIdPInitiated answers a callback with a code and no state (an
// IdP dashboard tile) by starting a fresh flow with new state and PKCE;
// the code it carried is never redeemed.
AllowIdPInitiated bool `json:"allowIdpInitiated,omitempty"`
}
OIDCConfig is an OpenID Connect provider's settings. Every endpoint left empty is filled from the issuer's discovery document at registration (and again at sign-in if still missing); a value set here wins.
type OIDCMapping ¶
type OIDCMapping struct {
ID string `json:"id,omitempty"`
Email string `json:"email,omitempty"`
EmailVerified string `json:"emailVerified,omitempty"`
Name string `json:"name,omitempty"`
Image string `json:"image,omitempty"`
// ExtraFields copies claims into UserInfo.Extra, keyed by the map's
// key: {"department": "dept"}.
ExtraFields map[string]string `json:"extraFields,omitempty"`
}
OIDCMapping names the claims read for each profile field; an empty name takes the standard claim (sub, email, email_verified, name, picture).
type Option ¶
type Option func(*Plugin)
Option configures the plugin.
func DefaultOverrideUserInfo ¶
func DefaultOverrideUserInfo() Option
DefaultOverrideUserInfo turns OIDCConfig.OverrideUserInfo on for every provider: the IdP's name and image are copied at every sign-in.
func DisableImplicitSignUp ¶
func DisableImplicitSignUp() Option
DisableImplicitSignUp refuses to create a user unless the sign-in asked for it (SignInRequest.RequestSignUp).
func PerProviderPaths ¶
func PerProviderPaths() Option
PerProviderPaths gives every provider its own OIDC redirect URI, ACS and SLO URL, better-auth's /sso/callback/:providerId shape: the shared URL plus "/<providerId>". The core routes exact paths, so the app mounts Plugin.ProviderPaths in front of its auth handler, which turns each such URL into the shared route with the provider id as a parameter. The id is then a second check: a callback or response for one provider arriving at another's URL is refused.
func ProvidersLimit ¶
ProvidersLimit caps how many providers one user may register; 0 turns registration off (ErrRegistrationOff). The default is DefaultProvidersLimit.
func ProvisionUserOnEveryLogin ¶
func ProvisionUserOnEveryLogin() Option
ProvisionUserOnEveryLogin runs the WithProvisionUser function at every sign-in, not only the first.
func RequireTrustedOrigins ¶
func RequireTrustedOrigins() Option
RequireTrustedOrigins accepts only IdP URLs whose origin WithTrustedOrigins lists, better-auth's trustedOrigins rule for discovery: the discovery URL and every discovered endpoint. For an app whose customers' IdPs are known in advance.
func ReserveProviderIDs ¶
ReserveProviderIDs refuses these provider ids at registration, besides every plugin name and every plugin/oauth provider name (always reserved, so an SSO provider can never shadow another sign-in method).
func TrustEmailVerified ¶
func TrustEmailVerified() Option
TrustEmailVerified takes the IdP's email_verified claim (or the SAML mapping's emailVerified attribute) as proof the person controls the address. Off by default, as in better-auth: an enterprise IdP often asserts an address it never verified.
func TrustProviders ¶
TrustProviders lets these providers link to an existing user with the same verified email in one of their domains even when domain verification is off. A default provider and a domain-verified one are trusted already.
func WithACSURL ¶
WithACSURL sets the SAML assertion consumer service URL. The default is BaseURL + DefaultMountPrefix + "/sso/saml2/sp/acs".
func WithCallbackURL ¶
WithCallbackURL sets the OIDC redirect URI registered at every IdP. The default is BaseURL + DefaultMountPrefix + "/sso/callback".
func WithDefaultProviders ¶
WithDefaultProviders adds providers defined in code (better-auth's defaultSSO): matched by provider id or domain before any row, never stored, never editable through the API, and treated as domain-verified, since only the app's own code can add one.
func WithDomainVerification ¶
func WithDomainVerification(d DomainVerification) Option
WithDomainVerification turns domain verification on.
func WithErrorURL ¶
WithErrorURL sends a failed sign-in's browser to path (with the error code) instead of answering JSON, as oauth.ErrorURL does.
func WithHTTPClient ¶
WithHTTPClient replaces the client every IdP call uses. The default refuses redirects and private, loopback and link-local addresses (the SSRF guard), resolved at connect time so a DNS answer that changes after a check cannot slip through. An app that replaces it owns that guard; WithTrustedOrigins is the narrower escape hatch.
func WithMutationGuard ¶
func WithMutationGuard(fn MutationGuard) Option
WithMutationGuard runs fn before every provider update or delete; an error refuses it (an *auth.Error as it is, anything else as ErrMutationRejected).
func WithOrganizations ¶
func WithOrganizations(o Organizations, prov OrganizationProvisioning) Option
WithOrganizations connects an organization system: registration under an organization then needs the caller to be its owner or admin, sign-in can name an organization by slug, and a user who signs in through an organization's provider joins it (OrganizationProvisioning).
func WithPrivateKeyResolver ¶
func WithPrivateKeyResolver(fn PrivateKeyResolver) Option
WithPrivateKeyResolver supplies the key for private_key_jwt token authentication, so the key can live in an HSM or KMS rather than a row.
func WithProviderFields ¶
func WithProviderFields(fields ...ProviderField) Option
WithProviderFields declares the provider's additional fields.
func WithProvidersLimit ¶
WithProvidersLimit computes the cap per user, for plans that differ.
func WithProvisionUser ¶
func WithProvisionUser(fn ProvisionFunc) Option
WithProvisionUser runs fn after a user signs in through a provider for the first time (or every time, with ProvisionUserOnEveryLogin), inside the sign-in transaction: an error refuses the sign-in and rolls back the user it created.
func WithResolveUser ¶
func WithResolveUser(fn ResolveUserFunc) Option
WithResolveUser decides, before the built-in rules, whose account a first sign-in through a provider belongs to: continue with the rules, link to a user the app names, or refuse by returning an error (an *auth.Error is answered as it is). It runs in the sign-in transaction.
func WithSAMLEngine ¶
func WithSAMLEngine(e SAMLEngine) Option
WithSAMLEngine replaces the XML-signature engine (GoSAML2 by default).
func WithSAMLOptions ¶
func WithSAMLOptions(o SAMLOptions) Option
WithSAMLOptions sets the plugin-wide SAML settings; zero fields keep their defaults.
func WithStateTTL ¶
WithStateTTL bounds an OIDC flow; the default is DefaultStateTTL.
func WithTrustedOrigins ¶
WithTrustedOrigins lists IdP origins ("https://idp.internal:8443") the SSRF guard lets through even on a private address, and that may use plain http: an on-premises ADFS, a local test IdP.
type OrganizationProvisioning ¶
type OrganizationProvisioning struct {
// Disabled turns joining off.
Disabled bool
// DefaultRole is the new member's role; empty is RoleMember.
DefaultRole string
// Role computes the role per sign-in, overriding DefaultRole.
Role func(ctx context.Context, ev ProvisionEvent) (string, error)
}
OrganizationProvisioning controls joining a provider's organization at sign-in.
type Organizations ¶
type Organizations interface {
// Role returns userID's role in orgID; ok is false when the user is not
// a member.
Role(ctx context.Context, orgID, userID string) (role string, ok bool, err error)
// BySlug returns the id of the organization with slug; ok is false
// when there is none.
BySlug(ctx context.Context, slug string) (orgID string, ok bool, err error)
// AddMember adds userID to orgID with role.
AddMember(ctx context.Context, orgID, userID, role string) error
}
Organizations is the organization system the plugin consults. The organization plugin is a separate module this one does not import; an app adapts it (or its own) in a few lines, shown in the README.
type Plugin ¶
type Plugin struct {
// contains filtered or unexported fields
}
Plugin is the sso plugin. Build it with New.
func (*Plugin) ACS ¶
func (p *Plugin) ACS(ctx context.Context, in ACSRequest) (CallbackResult, error)
ACS is POST /saml2/sp/acs (acsEndpoint).
func (*Plugin) AssignByVerifiedDomain ¶
func (p *Plugin) AssignByVerifiedDomain(ctx context.Context, userID, email string, emailVerified bool) (Assignment, error)
AssignByVerifiedDomain adds a user who signed in some other way (social OAuth, a password) to the organization whose domain-verified provider covers their verified email: exactly one such organization, or nothing. An unverified address never assigns, since anyone can type one.
func (*Plugin) AssignOAuth ¶
func (p *Plugin) AssignOAuth(ctx context.Context, ev oauth.ResolvedEvent) (Assignment, error)
AssignOAuth is the assignment of one plugin/oauth sign-in: a link flow or a provider-initiated sign-in (nothing ties it to a person of this app) is AssignIneligibleSource; anything else AssignByVerifiedDomain.
func (*Plugin) Callback ¶
func (p *Plugin) Callback(ctx context.Context, in CallbackRequest) (CallbackResult, error)
Callback is GET /callback (callbackSSO, callbackSSOShared).
func (*Plugin) DeleteProvider ¶
DeleteProvider is POST /delete-provider (deleteSSOProvider).
func (*Plugin) Discover ¶
Discover fills cfg's empty endpoints and token authentication from issuer's discovery document; values already set win. The document must name issuer exactly (trailing slash aside) and list the authorization, token and JWKS endpoints; relative endpoint paths are resolved against the issuer. With SkipDiscovery nothing is fetched and a missing endpoint is DiscoveryIncomplete.
func (*Plugin) DomainToken ¶
DomainToken returns the provider's pending verification token, issuing a new one when none is pending or it expired (better-auth reuses an active token and gives each one week, DefaultDomainTokenTTL). The token lives on the provider row: it is public once published, and the row is the one place a verify must find it.
func (*Plugin) FetchDiscoveryDocument ¶
fetchDiscovery GETs and decodes the document.
func (*Plugin) GetProvider ¶
GetProvider is GET /get-provider (getSSOProvider).
func (*Plugin) Init ¶
Init captures the core, builds the guarded HTTP client and fills the default URLs.
func (*Plugin) InitiateSLO ¶
func (p *Plugin) InitiateSLO(ctx context.Context, in LogoutRequest) (LogoutResult, error)
InitiateSLO is POST /saml2/logout (initiateSLO).
func (*Plugin) ListProviders ¶
func (p *Plugin) ListProviders(ctx context.Context) (ProvidersResult, error)
ListProviders is GET /providers (listSSOProviders).
func (*Plugin) OAuthAssignment ¶
func (p *Plugin) OAuthAssignment() oauth.ResolvedHook
OAuthAssignment is AssignByVerifiedDomain as a plugin/oauth hook: pass it to oauth.OnResolved and a social sign-in with a verified address in a verified domain joins that domain's organization. A link flow is not a sign-in and is left alone.
func (*Plugin) OperationHooks ¶
func (p *Plugin) OperationHooks() []auth.OperationHook
OperationHooks forgets the SAML session record when the app session is signed out, under EnableSingleLogout.
func (*Plugin) Operations ¶
Operations lists every operation; the two domain-verification ones only under WithDomainVerification.
func (*Plugin) OptionErr ¶
OptionErr is every error the options recorded, such as auth.ErrNilOption naming an option given nil; auth.New refuses to start while it is not nil (auth.OptionChecker).
func (*Plugin) ProviderByID ¶
ProviderByID returns a provider by its provider id: a default provider first, then a row. Not found is ErrProviderNotFound.
func (*Plugin) ProviderForDomain ¶
ProviderForDomain returns the provider for an email domain: a default provider first, then a stored one whose domain is exactly d, then one whose domain list holds d or a parent of it (a provider for company.com serves eng.company.com). Under WithDomainVerification an unverified provider is passed over in the list scan, so a domain one tenant claimed without proof cannot capture another tenant's routing.
func (*Plugin) ProviderPaths ¶
ProviderPaths wraps the app's auth handler (authhttp.Handler) for PerProviderPaths and better-auth's metadata route. mount is where the plugin's routes live ("/auth/sso" for authhttp.Prefix("/auth")):
- GET <mount>/callback/<id> → GET <mount>/callback?providerId=<id>
- POST <mount>/saml2/sp/acs/<id> → POST <mount>/saml2/sp/acs with providerId=<id>
- GET|POST <mount>/saml2/sp/slo/<id> → the shared SLO route with providerId=<id>
- GET <mount>/saml2/sp/metadata?providerId=<id> → the SP metadata as application/xml, as better-auth serves it
Every other request passes through untouched.
func (*Plugin) RateLimits ¶
RateLimits bounds the public flow starts, the ACS, registration and DNS verification, each of which costs a row, a network call or a signature check.
func (*Plugin) RegisterProvider ¶
func (p *Plugin) RegisterProvider(ctx context.Context, in RegisterRequest) (RegisterResult, error)
RegisterProvider is POST /register (registerSSOProvider).
func (*Plugin) RequestDomainVerification ¶
func (p *Plugin) RequestDomainVerification(ctx context.Context, providerID string) (DomainTokenResult, error)
RequestDomainVerification is POST /request-domain-verification.
func (*Plugin) SLO ¶
func (p *Plugin) SLO(ctx context.Context, in SLORequest) (LogoutResult, error)
SLO is the SLO endpoint (sloEndpoint), for the redirect binding.
func (*Plugin) SPMetadata ¶
SPMetadata is GET /saml2/sp/metadata (spMetadata).
func (*Plugin) SetProviderFields ¶
func (p *Plugin) SetProviderFields(ctx context.Context, providerID string, values map[string]any) error
SetProviderFields sets additional fields from Go, NoInput ones included; a field not declared is refused.
func (*Plugin) SignIn ¶
func (p *Plugin) SignIn(ctx context.Context, in SignInRequest) (SignInResult, error)
SignIn is POST /sign-in (signInSSO).
func (*Plugin) TypedMigrations ¶
TypedMigrations creates both tables for engine. provider_id is UNIQUE; (provider_id, subject) is UNIQUE: one IdP account belongs to one user.
func (*Plugin) UpdateProvider ¶
func (p *Plugin) UpdateProvider(ctx context.Context, in UpdateRequest) (SanitizedProvider, error)
UpdateProvider is POST /update-provider (updateSSOProvider).
func (*Plugin) UserTables ¶
UserTables declares that auth_sso_account rows belong to their user, so DeleteUser removes them. Providers are not listed: an organization's provider must survive the admin who registered it.
type PostForm ¶
type PostForm struct {
Action string `json:"action"`
SAMLRequest string `json:"SAMLRequest"`
RelayState string `json:"RelayState"`
}
PostForm is an HTTP-POST binding message: the browser posts SAMLRequest and RelayState to Action (an auto-submitting form).
type PrivateKeyRequest ¶
PrivateKeyRequest names the key private_key_jwt needs.
type PrivateKeyResolver ¶
type PrivateKeyResolver func(ctx context.Context, r PrivateKeyRequest) (key crypto.Signer, kid string, err error)
PrivateKeyResolver returns the signer and key id for a provider's private_key_jwt client assertion (ECDSA P-256 or RSA, as oauth.ClientAssertion accepts).
type Provider ¶
type Provider struct {
// ID is the row id; "" for a default provider.
ID string `json:"-"`
// ProviderID is the app-chosen permanent name, unique across the app.
ProviderID string `json:"providerId"`
Issuer string `json:"issuer"`
Domain string `json:"domain"`
OrganizationID string `json:"organizationId,omitempty"`
UserID string `json:"userId,omitempty"`
OIDC *OIDCConfig `json:"oidcConfig,omitempty"`
SAML *SAMLConfig `json:"samlConfig,omitempty"`
// DomainVerified is set once the domain's DNS record was found
// (WithDomainVerification); a default provider counts as verified.
DomainVerified bool `json:"domainVerified"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
// DomainToken is the pending domain-verification token and
// DomainTokenExpiresAt its expiry; "" and nil when none is pending.
DomainToken string `json:"-"`
// Fields are the WithProviderFields values.
Fields map[string]any `json:"additionalFields,omitempty"`
DomainTokenExpiresAt *time.Time `json:"-"`
// Default marks a WithDefaultProviders entry: code, not a row.
Default bool `json:"-"`
}
Provider is one registered identity provider: a row of auth_sso_provider, or a WithDefaultProviders entry. A provider may offer OIDC, SAML or both.
Multi-shape fields: OIDC and SAML are each nil when the provider does not offer that protocol; at least one is set. Domain is one domain or a comma-separated list. UserID is who registered it ("" for a default provider); OrganizationID is "" for a provider not tied to one.
func (Provider) Domains ¶
Domains returns the provider's domains, lower-cased and trimmed, empty entries dropped.
func (Provider) Reference ¶
Reference is the provider's fingerprint: the SHA-256 of its authentication boundary (issuer, endpoints, client id, SAML entity ids, certificates), secrets left out. A flow carries it from start to callback and is refused when it changed in between, so an admin who repoints a provider mid-flow cannot have a response from the old IdP accepted under the new configuration.
type ProviderField ¶
type ProviderField struct {
Name string
// Required refuses a registration without it.
Required bool
// NoInput refuses it in a register or update body (better-auth's
// input: false): only the app's Go code sets it, with SetProviderFields.
NoInput bool
}
ProviderField is an app-defined field on every provider (better-auth's schema.ssoProvider.additionalFields), stored as JSON beside the provider.
type ProviderRequest ¶
type ProviderRequest struct {
ProviderID string `json:"providerId"`
}
ProviderRequest names one provider.
type ProvidersResult ¶
type ProvidersResult struct {
Providers []SanitizedProvider `json:"providers"`
}
ProvidersResult lists the providers the caller may manage.
type ProvisionEvent ¶
type ProvisionEvent struct {
User auth.User
Info UserInfo
Provider Provider
Protocol Protocol
// Created is true when this sign-in created the user.
Created bool
}
ProvisionEvent is a completed SSO sign-in, handed to WithProvisionUser.
type ProvisionFunc ¶
ProvisionFunc is the WithProvisionUser hook.
type RawFacts ¶
type RawFacts struct {
Assertions int
SignatureAlgorithms []string
DigestAlgorithms []string
// KeyEncryption and DataEncryption are the EncryptionMethod algorithms
// of an EncryptedKey and of the EncryptedData around it.
KeyEncryption []string
DataEncryption []string
}
RawFacts are what InspectResponse reads from the undecoded XML, before any signature check: how many assertions it holds anywhere in the tree, and every signature and digest algorithm it names.
func InspectResponse ¶
InspectResponse decodes a base64 SAMLResponse and scans its XML. Counting assertions over the whole tree, not only where a parser expects them, is the defence in depth against signature wrapping: a wrapped response carries a second Assertion (or EncryptedAssertion) somewhere, and is refused here whatever the signature engine makes of it.
type RegisterRequest ¶
type RegisterRequest struct {
ProviderID string `json:"providerId"`
Issuer string `json:"issuer"`
Domain string `json:"domain"`
OrganizationID string `json:"organizationId,omitempty"`
OIDCConfig *OIDCConfig `json:"oidcConfig,omitempty"`
SAMLConfig *SAMLConfig `json:"samlConfig,omitempty"`
// OverrideUserInfo sets OIDCConfig.OverrideUserInfo.
OverrideUserInfo bool `json:"overrideUserInfo,omitempty"`
// AdditionalFields are WithProviderFields values.
AdditionalFields map[string]any `json:"additionalFields,omitempty"`
}
RegisterRequest registers a provider for the signed-in user, or for an organization the user owns or administers.
type RegisterResult ¶
type RegisterResult struct {
SanitizedProvider
DomainVerificationToken string `json:"domainVerificationToken,omitempty"`
}
RegisterResult is the new provider, plus the domain-verification token when WithDomainVerification is on.
type ResolveAction ¶
type ResolveAction string
ResolveAction is a WithResolveUser decision.
const ( // ResolveContinue applies the built-in rules. ResolveContinue ResolveAction = "continue" // ResolveLink links the IdP account to Decision.UserID. ResolveLink ResolveAction = "link" )
type ResolveInput ¶
type ResolveInput struct {
Protocol Protocol
Provider Provider
// Issuer and Subject are the account key: the ID token's iss and sub,
// or the IdP entity id and NameID.
Issuer string
Subject string
Info UserInfo
}
ResolveInput is a first sign-in through a provider: no account is linked to (provider, subject) yet.
type ResolveUserFunc ¶
ResolveUserFunc is the WithResolveUser hook. To refuse the sign-in, return an error: an *auth.Error is answered as it is, anything else as ErrUserRejected with the reason logged.
type SAMLAssertion ¶
type SAMLAssertion struct {
ID string
Issuer string
Signed bool
NameID string
NameIDFormat string
// HasConditions is false when the assertion has no Conditions element.
HasConditions bool
NotBefore time.Time
NotOnOrAfter time.Time
// Audiences holds each AudienceRestriction's audiences.
Audiences [][]string
Confirmations []SubjectConfirmation
SessionIndex string
Attributes map[string][]string
}
SAMLAssertion is a verified assertion.
func CheckAssertion ¶
func CheckAssertion(r *SAMLResponse, req CheckRequest) (SAMLAssertion, error)
CheckAssertion applies every rule that decides whether a verified response signs anyone in, and returns its one assertion:
- exactly one assertion, signed itself when WantAssertionsSigned and otherwise covered by a signed Response;
- the Response's and the Assertion's Issuer are the IdP entity id;
- the Response's Destination, when present, is the ACS URL;
- InResponseTo, on the Response and the bearer confirmation, is the request this browser started, or absent for an IdP-initiated one;
- an AudienceRestriction exists and every one names the audience;
- a bearer SubjectConfirmation whose Recipient is the ACS URL and whose NotOnOrAfter has not passed;
- Conditions NotBefore has come and NotOnOrAfter has not passed, each with ClockSkew, and both present under RequireTimestamps.
type SAMLCode ¶
type SAMLCode string
SAMLCode names why a response was refused; each is logged, the caller sees ErrSAMLInvalid (or ErrSAMLReplay, ErrSAMLUnsolicited, ErrSAMLTooLarge).
const ( SAMLInvalidEncoding SAMLCode = "SAML_INVALID_ENCODING" SAMLInvalidXML SAMLCode = "SAML_INVALID_XML" SAMLNoAssertion SAMLCode = "SAML_NO_ASSERTION" SAMLMultipleAssertions SAMLCode = "SAML_MULTIPLE_ASSERTIONS" SAMLSignatureInvalid SAMLCode = "SAML_SIGNATURE_INVALID" SAMLAssertionSigRequired SAMLCode = "SAML_ASSERTION_SIGNATURE_REQUIRED" SAMLIssuerMismatch SAMLCode = "SAML_ISSUER_MISMATCH" SAMLAudienceMissing SAMLCode = "SAML_AUDIENCE_MISSING" SAMLAudienceMismatch SAMLCode = "SAML_AUDIENCE_MISMATCH" SAMLBearerMissing SAMLCode = "SAML_BEARER_CONFIRMATION_MISSING" SAMLRecipientMismatch SAMLCode = "SAML_RECIPIENT_MISMATCH" SAMLDestinationMismatch SAMLCode = "SAML_DESTINATION_MISMATCH" SAMLNotYetValid SAMLCode = "SAML_NOT_YET_VALID" SAMLExpired SAMLCode = "SAML_EXPIRED" SAMLMissingTimestamps SAMLCode = "SAML_MISSING_TIMESTAMPS" SAMLInResponseToMismatch SAMLCode = "SAML_IN_RESPONSE_TO_MISMATCH" SAMLUnknownRequest SAMLCode = "SAML_UNKNOWN_REQUEST" SAMLReplay SAMLCode = "SAML_REPLAY" SAMLUnsolicited SAMLCode = "SAML_UNSOLICITED" SAMLDeprecatedAlgorithm SAMLCode = "SAML_DEPRECATED_ALGORITHM" SAMLAlgorithmNotAllowed SAMLCode = "SAML_ALGORITHM_NOT_ALLOWED" SAMLUnknownAlgorithm SAMLCode = "SAML_UNKNOWN_ALGORITHM" SAMLTooLarge SAMLCode = "SAML_TOO_LARGE" SAMLMissingSubjectOrEmail SAMLCode = "SAML_MISSING_IDENTITY" )
type SAMLConfig ¶
type SAMLConfig struct {
// EntryPoint is the IdP's single sign-on URL (HTTP-Redirect binding),
// used when IdPMetadata names no SingleSignOnService.
EntryPoint string `json:"entryPoint,omitempty"`
// Cert is the IdP's signing certificate, PEM; several PEM blocks are
// several certificates, any of which may sign (rotation).
// IdPMetadata.Cert wins when both are set.
Cert string `json:"cert,omitempty"`
// Audience is the AudienceRestriction every assertion must carry;
// empty is the SP entity id.
Audience string `json:"audience,omitempty"`
// CallbackURL is where an IdP-initiated sign-in lands; empty falls
// back to SAMLOptions.IdPInitiatedCallbackURL, then the site root.
CallbackURL string `json:"callbackUrl,omitempty"`
// IdPInitiatedCallbackURL overrides CallbackURL for IdP-initiated
// sign-ins only.
IdPInitiatedCallbackURL string `json:"idpInitiatedCallbackUrl,omitempty"`
IdPMetadata IdPMetadata `json:"idpMetadata,omitempty"`
SPMetadata SPMetadata `json:"spMetadata,omitempty"`
// WantAssertionsSigned requires the Assertion's own signature; a
// signed Response around an unsigned Assertion is then refused.
WantAssertionsSigned bool `json:"wantAssertionsSigned,omitempty"`
// AuthnRequestsSigned signs every AuthnRequest with PrivateKey.
AuthnRequestsSigned bool `json:"authnRequestsSigned,omitempty"`
// SignatureAlgorithm signs AuthnRequests: a URI or a short form
// (sha256, sha384, sha512); empty is RSA-SHA256.
SignatureAlgorithm string `json:"signatureAlgorithm,omitempty"`
// DigestAlgorithm is advertised for AuthnRequest signatures.
DigestAlgorithm string `json:"digestAlgorithm,omitempty"`
// IdentifierFormat is the NameID format requested and advertised.
IdentifierFormat string `json:"identifierFormat,omitempty"`
// PrivateKey and SPCertificate are the SP's key pair, PEM: they sign
// AuthnRequests and decrypt an EncryptedAssertion. The key is stored
// sealed and never returned by a read.
PrivateKey string `json:"privateKey,omitempty"`
SPCertificate string `json:"spCertificate,omitempty"`
// DecryptionKey is a separate decryption key, PEM, when the IdP
// encrypts to another key than the signing one; sealed like PrivateKey.
DecryptionKey string `json:"decryptionKey,omitempty"`
// DecryptionCertificate goes with DecryptionKey (PEM); it is advertised
// as the encryption key in SP metadata.
DecryptionCertificate string `json:"decryptionCertificate,omitempty"`
Mapping SAMLMapping `json:"mapping,omitempty"`
// AllowIdPInitiated accepts an unsolicited response for this provider
// even when SAMLOptions.AllowIdPInitiated is off.
AllowIdPInitiated bool `json:"allowIdpInitiated,omitempty"`
}
SAMLConfig is a SAML 2.0 provider's settings: this app is the service provider (SP), the customer's IdP signs the assertions.
func (SAMLConfig) IdP ¶
func (s SAMLConfig) IdP(maxMetadata int) (IdPSettings, error)
IdP returns the IdP's entity id, SSO URL and signing certificates, read from the metadata XML when set and from the written fields otherwise; fields written out win over metadata.
type SAMLEngine ¶
type SAMLEngine interface {
// AuthnRequestURL returns the IdP's SSO URL carrying an AuthnRequest
// with requestID, HTTP-Redirect binding, signed when
// sp.AuthnRequestsSigned.
AuthnRequestURL(sp SPSettings, idp IdPSettings, requestID, relayState string) (string, error)
// AuthnRequestPOST returns the form of an HTTP-POST AuthnRequest with
// requestID, its enveloped signature made when sp.AuthnRequestsSigned.
AuthnRequestPOST(sp SPSettings, idp IdPSettings, requestID, relayState string) (PostForm, error)
// VerifyResponse decodes a base64 SAMLResponse, verifies its signature
// (on the Response, or on each Assertion) against idp.Certs, decrypts an
// EncryptedAssertion with sp.DecryptionKey, and returns only verified
// content. Any failure is an error.
VerifyResponse(sp SPSettings, idp IdPSettings, samlResponse string, now time.Time) (*SAMLResponse, error)
}
SAMLEngine is the XML-signature work: building an AuthnRequest and verifying a response's signature. It returns what the signature covers; every rule about whether that content signs anyone in is checked by the plugin (CheckAssertion), so an engine swap cannot drop one.
type SAMLMapping ¶
type SAMLMapping struct {
// ID is the attribute taken as the subject instead of the NameID.
ID string `json:"id,omitempty"`
// Email defaults to "email", then the NameID when it is an address.
Email string `json:"email,omitempty"`
// EmailVerified is read only under TrustEmailVerified.
EmailVerified string `json:"emailVerified,omitempty"`
// Name defaults to "displayName", used when first and last are absent.
Name string `json:"name,omitempty"`
// FirstName defaults to "givenName", LastName to "surname".
FirstName string `json:"firstName,omitempty"`
LastName string `json:"lastName,omitempty"`
ExtraFields map[string]string `json:"extraFields,omitempty"`
}
SAMLMapping names the attributes read for each profile field.
type SAMLOptions ¶
type SAMLOptions struct {
// AllowIdPInitiated accepts unsolicited responses (an IdP dashboard
// tile). Off by default: an unsolicited response has no request ID to
// bind it to this browser, so only replay protection and the time
// window stand between a stolen response and a session.
AllowIdPInitiated bool
// RequestTTL bounds an AuthnRequest; DefaultSAMLRequestTTL when zero.
RequestTTL time.Duration
// ClockSkew is tolerated on every timestamp; DefaultClockSkew when zero.
ClockSkew time.Duration
// RequireTimestamps refuses an assertion without NotBefore and
// NotOnOrAfter conditions.
RequireTimestamps bool
// OnDeprecated decides SHA-1 signatures and digests; empty is warn.
OnDeprecated DeprecatedPolicy
// AllowedSignatureAlgorithms and AllowedDigestAlgorithms, when set,
// refuse every other algorithm.
AllowedSignatureAlgorithms []string
AllowedDigestAlgorithms []string
// AllowedKeyEncryptionAlgorithms and AllowedDataEncryptionAlgorithms,
// when set, refuse an EncryptedAssertion using any other.
AllowedKeyEncryptionAlgorithms []string
AllowedDataEncryptionAlgorithms []string
// MaxResponseSize and MaxMetadataSize bound the decoded XML.
MaxResponseSize int
MaxMetadataSize int
// IdPInitiatedCallbackURL is where an IdP-initiated sign-in lands when
// its provider names none.
IdPInitiatedCallbackURL string
// EnableSingleLogout turns on SAML single logout: the logout route, the
// SLO endpoint, SLO in SP metadata, and the session records.
EnableSingleLogout bool
// LogoutRequestTTL bounds an SP-initiated LogoutRequest;
// DefaultLogoutRequestTTL when zero.
LogoutRequestTTL time.Duration
// AllowUnsignedLogoutRequests and AllowUnsignedLogoutResponses accept
// an IdP logout message without a signature. Off by default (better-auth
// defaults to accepting): an unsigned LogoutRequest lets anyone who
// knows a NameID and SessionIndex end that person's session. A message
// that carries a signature is always checked.
AllowUnsignedLogoutRequests bool
AllowUnsignedLogoutResponses bool
// SkipInResponseTo accepts an SP-initiated response without matching it
// to its AuthnRequest (better-auth's enableInResponseToValidation:
// false), for an IdP that drops InResponseTo. Replay protection and the
// time window still apply; a warning is logged at startup.
SkipInResponseTo bool
}
SAMLOptions are the plugin-wide SAML settings.
func (SAMLOptions) CheckAlgorithms ¶
func (o SAMLOptions) CheckAlgorithms(f RawFacts, warn func(alg string)) error
CheckAlgorithms applies the algorithm policy to what a response names: an unknown algorithm is refused, an allow-list refuses everything else, and a SHA-1 one follows OnDeprecated (warn logs and continues).
type SAMLResponse ¶
type SAMLResponse struct {
ID string
InResponseTo string
Destination string
Issuer string
Signed bool
Assertions []SAMLAssertion
}
SAMLResponse is a verified response.
type SLORequest ¶
type SLORequest struct {
SAMLRequest string `json:"SAMLRequest,omitempty"`
SAMLResponse string `json:"SAMLResponse,omitempty"`
RelayState string `json:"RelayState,omitempty"`
SigAlg string `json:"SigAlg,omitempty"`
Signature string `json:"Signature,omitempty"`
// ProviderID names the provider when the IdP's SLO URL carries it
// (?providerId=); otherwise the message's issuer or InResponseTo does.
ProviderID string `json:"providerId,omitempty"`
}
SLORequest is what the IdP sends to the SLO endpoint: a LogoutRequest (IdP-initiated) or a LogoutResponse (to an SP-initiated logout), by the redirect binding (query) or the POST binding (form).
type SPMetadata ¶
type SPMetadata struct {
// EntityID is the SP entity id; empty is the custom metadata's, then
// the provider's Issuer.
EntityID string `json:"entityID,omitempty"`
// Metadata is custom SP metadata XML, served as it is instead of the
// generated document. Its entity id, NameID format and
// WantAssertionsSigned are read from it; it must list this app's ACS
// URL with the POST binding, and may not advertise unsigned assertions
// when WantAssertionsSigned is on (ErrSPMetadataMismatch).
Metadata string `json:"metadata,omitempty"`
// Binding is how AuthnRequests travel: "redirect" (the default, the
// query signed) or "post" (an auto-submitting form, the XML signed,
// which is where DigestAlgorithm applies).
Binding string `json:"binding,omitempty"`
}
SPMetadata is this app's side.
type SPPolicy ¶
type SPPolicy struct {
EntityID string
WantAssertionsSigned bool
NameIDFormat string
// ACSURLs are the HTTP-POST assertion consumer services it lists.
ACSURLs []string
}
SPPolicy is what custom SP metadata says.
type SPSettings ¶
type SPSettings struct {
EntityID string
ACSURL string
Audience string
NameIDFormat string
AuthnRequestsSigned bool
SignatureAlgorithm string
SigningKey crypto.Signer
Certificate *x509.Certificate
DecryptionKey crypto.Signer
DecryptionCert *x509.Certificate
WantAssertionsSigned bool
MaxResponseSize int
// SLOURL is the SP's single logout endpoint; "" when single logout is
// off, and then left out of the metadata.
SLOURL string
// CustomMetadata is SPMetadata.Metadata, served instead of the
// generated document.
CustomMetadata string
// POSTBinding sends AuthnRequests by HTTP-POST (SPMetadata.Binding).
POSTBinding bool
// Now is the core's clock (auth.WithClock) when an AuthnRequest is
// built, so its IssueInstant follows the injected clock.
Now time.Time
}
SPSettings are this app's SAML settings for one provider.
type SanitizedOIDC ¶
type SanitizedOIDC struct {
DiscoveryEndpoint string `json:"discoveryEndpoint,omitempty"`
ClientIDLastFour string `json:"clientIdLastFour"`
PKCE bool `json:"pkce"`
AuthorizationEndpoint string `json:"authorizationEndpoint,omitempty"`
TokenEndpoint string `json:"tokenEndpoint,omitempty"`
UserInfoEndpoint string `json:"userInfoEndpoint,omitempty"`
JWKSEndpoint string `json:"jwksEndpoint,omitempty"`
Scopes []string `json:"scopes,omitempty"`
TokenEndpointAuthentication oauth.TokenAuth `json:"tokenEndpointAuthentication,omitempty"`
}
SanitizedOIDC is an OIDCConfig without its secret.
type SanitizedProvider ¶
type SanitizedProvider struct {
ProviderID string `json:"providerId"`
Type Protocol `json:"type"`
Protocols []Protocol `json:"protocols"`
Issuer string `json:"issuer"`
Domain string `json:"domain"`
OrganizationID string `json:"organizationId,omitempty"`
DomainVerified bool `json:"domainVerified"`
OIDC *SanitizedOIDC `json:"oidcConfig,omitempty"`
SAML *SanitizedSAML `json:"samlConfig,omitempty"`
// AdditionalFields are the WithProviderFields values.
AdditionalFields map[string]any `json:"additionalFields,omitempty"`
// RedirectURI is the OIDC redirect URI to register at the IdP.
RedirectURI string `json:"redirectURI,omitempty"`
// ACSURL and SPMetadataURL are what to enter at a SAML IdP.
ACSURL string `json:"acsURL,omitempty"`
SPMetadataURL string `json:"spMetadataURL,omitempty"`
}
SanitizedProvider is what a read returns: no secret, the client id cut to its last four characters.
type SanitizedSAML ¶
type SanitizedSAML struct {
EntryPoint string `json:"entryPoint,omitempty"`
CallbackURL string `json:"callbackUrl,omitempty"`
Audience string `json:"audience,omitempty"`
IdPEntityID string `json:"idpEntityId,omitempty"`
SPEntityID string `json:"spEntityId,omitempty"`
WantAssertionsSigned bool `json:"wantAssertionsSigned"`
AuthnRequestsSigned bool `json:"authnRequestsSigned"`
IdentifierFormat string `json:"identifierFormat,omitempty"`
// CertificatesCount is how many IdP signing certificates are on file.
CertificatesCount int `json:"certificatesCount"`
}
SanitizedSAML is a SAMLConfig without its keys.
type SignInRequest ¶
type SignInRequest struct {
Email string `json:"email,omitempty"`
OrganizationSlug string `json:"organizationSlug,omitempty"`
ProviderID string `json:"providerId,omitempty"`
Domain string `json:"domain,omitempty"`
// CallbackURL is where to land after sign-in: a path on this site or a
// trusted origin; "" is the site root.
CallbackURL string `json:"callbackURL,omitempty"`
// ErrorCallbackURL is where a failed sign-in lands, with the error code.
ErrorCallbackURL string `json:"errorCallbackURL,omitempty"`
// NewUserCallbackURL replaces CallbackURL when the sign-in created the
// user.
NewUserCallbackURL string `json:"newUserCallbackURL,omitempty"`
// Scopes replace the provider's scopes for this sign-in (OIDC).
Scopes []string `json:"scopes,omitempty"`
// LoginHint is sent as login_hint (OIDC); Email is sent when it is "".
LoginHint string `json:"loginHint,omitempty"`
// AdditionalParams are added to the authorization URL (OIDC); a
// parameter the flow sets itself is ErrBadParams.
AdditionalParams map[string]string `json:"additionalParams,omitempty"`
// RequestSignUp allows creating a user under DisableImplicitSignUp.
RequestSignUp bool `json:"requestSignUp,omitempty"`
// ProviderType picks the protocol of a provider that offers both;
// empty is OIDC when offered.
ProviderType Protocol `json:"providerType,omitempty"`
}
SignInRequest starts an SSO sign-in. One of Email, Domain, OrganizationSlug or ProviderID names the provider; ProviderID wins, then OrganizationSlug, then Domain, then the domain of Email.
type SignInResult ¶
type SignInResult struct {
URL string `json:"url"`
Redirect bool `json:"redirect"`
// Form is set, and Redirect false, for a SAML provider on the POST
// binding: post its fields to Form.Action from an auto-submitting form.
Form *PostForm `json:"form,omitempty"`
}
SignInResult is where to send the browser.
type SubjectConfirmation ¶
type SubjectConfirmation struct {
Method string
Recipient string
InResponseTo string
NotOnOrAfter time.Time
NotBefore time.Time
}
SubjectConfirmation is a SubjectConfirmation and its data.
type TXTResolver ¶
TXTResolver looks up DNS TXT records; *net.Resolver implements it.
type UpdateRequest ¶
type UpdateRequest struct {
ProviderID string `json:"providerId"`
Issuer *string `json:"issuer,omitempty"`
Domain *string `json:"domain,omitempty"`
OIDCConfig *OIDCConfig `json:"oidcConfig,omitempty"`
SAMLConfig *SAMLConfig `json:"samlConfig,omitempty"`
// AdditionalFields are merged into the provider's; a NoInput field is
// ErrFieldNotAllowed.
AdditionalFields map[string]any `json:"additionalFields,omitempty"`
}
UpdateRequest changes a provider; a nil field is left as it is. A domain change resets DomainVerified. An empty OIDC client secret keeps the stored one.
type UserInfo ¶
type UserInfo struct {
Subject string
Email string
EmailVerified bool
Name string
Image string
// Extra holds the mapping's ExtraFields, keyed by the app's names.
// OIDC values are JSON-decoded claims; SAML values are []string.
Extra map[string]any
// Claims is the verified ID token's payload merged with userinfo, for
// OIDC; nil for SAML.
Claims map[string]any
// Attributes are the signed assertion's attributes, every value kept,
// for SAML; nil for OIDC.
Attributes map[string][]string
// SessionIndex is the SAML AuthnStatement's SessionIndex; NameID and
// NameIDFormat are the assertion's subject, which single logout names.
SessionIndex string
NameID string
NameIDFormat string
}
UserInfo is what an IdP said about the person, after the provider's mapping. Subject is the account key: the OIDC sub (or the mapped id claim) or the SAML NameID, compared exactly.
func MapAttributes ¶
func MapAttributes(m SAMLMapping, a SAMLAssertion) UserInfo
MapAttributes turns a verified assertion into UserInfo: the subject is the NameID (or the mapping's id attribute), the email the mapped attribute or an address-shaped NameID.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package ssotest runs a fake OpenID Connect provider and a fake SAML identity provider for tests of an app's SSO wiring: the OIDC side serves discovery, JWKS, a token endpoint that checks PKCE and the client, and userinfo; the SAML side signs responses with a fresh certificate.
|
Package ssotest runs a fake OpenID Connect provider and a fake SAML identity provider for tests of an app's SSO wiring: the OIDC side serves discovery, JWKS, a token endpoint that checks PKCE and the client, and userinfo; the SAML side signs responses with a fresh certificate. |