# crewjam/saml - SAML 2.0 Library for Go This library provides a comprehensive implementation of the SAML 2.0 standard in Go, enabling applications to act as both Service Providers (SP) and Identity Providers (IDP). SAML (Security Assertion Markup Language) is a standard for identity federation that allows third-party authentication, enabling single sign-on (SSO) across multiple services. The package supports the Web SSO profile with HTTP-Redirect and HTTP-POST bindings, handles signed and encrypted assertions, and provides middleware for easy integration with Go web applications. It includes the core `saml` package for protocol implementation, `samlsp` for Service Provider middleware helpers, and `samlidp` for Identity Provider services. The library implements the interoperable SAML subset and handles metadata exchange, authentication requests, assertion validation, session management, and single logout flows. ## Service Provider Implementation ### Creating a Basic Service Provider Complete service provider setup with middleware that protects endpoints and extracts user attributes from SAML assertions. ```go package main import ( "context" "crypto/rsa" "crypto/tls" "crypto/x509" "fmt" "net/http" "net/url" "github.com/crewjam/saml/samlsp" ) func main() { // Load X.509 key pair for signing keyPair, err := tls.LoadX509KeyPair("myservice.cert", "myservice.key") if err != nil { panic(err) } keyPair.Leaf, err = x509.ParseCertificate(keyPair.Certificate[0]) if err != nil { panic(err) } // Fetch IDP metadata idpMetadataURL, err := url.Parse("https://samltest.id/saml/idp") if err != nil { panic(err) } idpMetadata, err := samlsp.FetchMetadata( context.Background(), http.DefaultClient, *idpMetadataURL, ) if err != nil { panic(err) } // Configure service provider rootURL, _ := url.Parse("http://localhost:8000") samlSP, _ := samlsp.New(samlsp.Options{ URL: *rootURL, Key: keyPair.PrivateKey.(*rsa.PrivateKey), Certificate: keyPair.Leaf, IDPMetadata: idpMetadata, }) // Protected endpoint app := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { displayName := samlsp.AttributeFromContext(r.Context(), "displayName") email := samlsp.AttributeFromContext(r.Context(), "mail") fmt.Fprintf(w, "Hello, %s (%s)!", displayName, email) }) // Mount handlers http.Handle("/hello", samlSP.RequireAccount(app)) http.Handle("/saml/", samlSP) http.ListenAndServe(":8000", nil) } // Expected flow: // 1. User visits /hello // 2. Redirected to IDP login at https://samltest.id/ // 3. After auth, IDP POSTs SAML response to /saml/acs // 4. Session cookie created, user redirected to /hello // 5. Protected content served with user attributes ``` ### Advanced Service Provider Configuration Service provider with custom session handling, request tracking, and signature verification. ```go package main import ( "context" "crypto/rsa" "crypto/tls" "crypto/x509" "net/http" "net/url" "time" "github.com/crewjam/saml" "github.com/crewjam/saml/samlsp" ) func main() { keyPair, _ := tls.LoadX509KeyPair("myservice.cert", "myservice.key") keyPair.Leaf, _ = x509.ParseCertificate(keyPair.Certificate[0]) idpMetadataURL, _ := url.Parse("https://idp.example.com/metadata") idpMetadata, _ := samlsp.FetchMetadata( context.Background(), http.DefaultClient, *idpMetadataURL, ) rootURL, _ := url.Parse("https://myservice.example.com") // Advanced configuration samlSP, _ := samlsp.New(samlsp.Options{ EntityID: "https://myservice.example.com", URL: *rootURL, Key: keyPair.PrivateKey.(*rsa.PrivateKey), Certificate: keyPair.Leaf, IDPMetadata: idpMetadata, // Force re-authentication ForceAuthn: true, // Allow IDP-initiated flows AllowIDPInitiated: true, DefaultRedirectURI: "/dashboard", // Custom cookie settings for sessions CookieSecure: true, CookieSameSite: http.SameSiteStrictMode, CookieDomain: ".example.com", CookieMaxAge: time.Hour * 8, // Sign authentication requests SignRequest: true, }) // Custom error handler samlSP.Middleware.OnError = func(w http.ResponseWriter, r *http.Request, err error) { http.Error(w, "Authentication failed: "+err.Error(), http.StatusForbidden) } // Require specific attribute value adminOnly := samlsp.RequireAttribute("role", "admin") adminHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Admin panel")) }) http.Handle("/admin", samlSP.RequireAccount(adminOnly(adminHandler))) http.Handle("/saml/", samlSP) http.ListenAndServe(":443", nil) } // Features demonstrated: // - IDP-initiated SSO support // - Force re-authentication for sensitive operations // - Custom error handling // - Role-based access control with attribute checks // - Secure cookie configuration for production ``` ### Parsing and Validating SAML Responses Low-level SAML response parsing with manual assertion validation. ```go package main import ( "crypto/rsa" "crypto/tls" "crypto/x509" "encoding/xml" "fmt" "net/http" "net/url" "time" "github.com/crewjam/saml" ) func handleACS(sp *saml.ServiceProvider) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { err := r.ParseForm() if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } // Track request IDs to prevent replay attacks possibleRequestIDs := []string{"id-abc123", "id-def456"} // Parse SAML response assertion, err := sp.ParseResponse(r, possibleRequestIDs) if err != nil { // Handle different error types if invalidErr, ok := err.(*saml.InvalidResponseError); ok { fmt.Printf("Invalid at %s: %v\n", invalidErr.Now, invalidErr.PrivateErr) fmt.Printf("Response XML: %s\n", invalidErr.Response) } if badStatus, ok := err.(saml.ErrBadStatus); ok { fmt.Printf("Bad status: %s\n", badStatus.Status) } http.Error(w, "Invalid SAML response", http.StatusForbidden) return } // Extract user attributes userID := assertion.Subject.NameID.Value email := "" groups := []string{} for _, stmt := range assertion.AttributeStatements { for _, attr := range stmt.Attributes { if attr.Name == "email" && len(attr.Values) > 0 { email = attr.Values[0].Value } if attr.Name == "groups" { for _, val := range attr.Values { groups = append(groups, val.Value) } } } } // Validate assertion conditions now := time.Now() if assertion.Conditions.NotBefore.After(now) { http.Error(w, "Assertion not yet valid", http.StatusForbidden) return } if assertion.Conditions.NotOnOrAfter.Before(now) { http.Error(w, "Assertion expired", http.StatusForbidden) return } // Create session (implement your own session management) fmt.Fprintf(w, "Authenticated: %s (%s), Groups: %v", userID, email, groups) } } func main() { keyPair, _ := tls.LoadX509KeyPair("myservice.cert", "myservice.key") keyPair.Leaf, _ = x509.ParseCertificate(keyPair.Certificate[0]) // Manual ServiceProvider setup sp := &saml.ServiceProvider{ Key: keyPair.PrivateKey.(*rsa.PrivateKey), Certificate: keyPair.Leaf, MetadataURL: url.URL{Scheme: "https", Host: "sp.example.com", Path: "/saml/metadata"}, AcsURL: url.URL{Scheme: "https", Host: "sp.example.com", Path: "/saml/acs"}, IDPMetadata: &saml.EntityDescriptor{}, // Load IDP metadata } http.HandleFunc("/saml/acs", handleACS(sp)) http.ListenAndServe(":8000", nil) } // Output example: // Authenticated: user@example.com (user@example.com), Groups: [users developers] ``` ### Creating Authentication Requests Generate SAML authentication requests for both redirect and POST bindings. ```go package main import ( "crypto/rsa" "crypto/tls" "crypto/x509" "fmt" "net/url" "github.com/crewjam/saml" "github.com/russellhaering/goxmldsig" ) func main() { keyPair, _ := tls.LoadX509KeyPair("myservice.cert", "myservice.key") keyPair.Leaf, _ = x509.ParseCertificate(keyPair.Certificate[0]) sp := &saml.ServiceProvider{ Key: keyPair.PrivateKey.(*rsa.PrivateKey), Certificate: keyPair.Leaf, MetadataURL: url.URL{Scheme: "https", Host: "sp.example.com", Path: "/saml/metadata"}, AcsURL: url.URL{Scheme: "https", Host: "sp.example.com", Path: "/saml/acs"}, SignatureMethod: dsig.RSASHA256SignatureMethod, AuthnNameIDFormat: saml.EmailAddressNameIDFormat, IDPMetadata: &saml.EntityDescriptor{ EntityID: "https://idp.example.com", IDPSSODescriptors: []saml.IDPSSODescriptor{{ SingleSignOnServices: []saml.Endpoint{ { Binding: saml.HTTPRedirectBinding, Location: "https://idp.example.com/sso", }, { Binding: saml.HTTPPostBinding, Location: "https://idp.example.com/sso", }, }, }}, }, } // HTTP-Redirect binding (most common) redirectURL, err := sp.MakeRedirectAuthenticationRequest("state-123") if err != nil { panic(err) } fmt.Printf("Redirect URL: %s\n", redirectURL.String()) // HTTP-POST binding postHTML, err := sp.MakePostAuthenticationRequest("state-123") if err != nil { panic(err) } fmt.Printf("POST form HTML:\n%s\n", string(postHTML)) // Low-level: Create custom authentication request forceAuthn := true req := &saml.AuthnRequest{ ID: "id-" + randomID(), IssueInstant: time.Now(), Version: "2.0", Destination: "https://idp.example.com/sso", Issuer: &saml.Issuer{ Format: "urn:oasis:names:tc:SAML:2.0:nameid-format:entity", Value: "https://sp.example.com", }, AssertionConsumerServiceURL: "https://sp.example.com/saml/acs", ProtocolBinding: saml.HTTPPostBinding, ForceAuthn: &forceAuthn, NameIDPolicy: &saml.NameIDPolicy{ AllowCreate: &[]bool{true}[0], Format: &[]string{string(saml.EmailAddressNameIDFormat)}[0], }, } // Convert to redirect URL reqURL, _ := req.Redirect("relayState", sp) fmt.Printf("Custom request URL: %s\n", reqURL.String()) } func randomID() string { return "abc123def456" // In production, use crypto/rand } // Output: // Redirect URL: https://idp.example.com/sso?SAMLRequest=nZFBa8MwDIb...&RelayState=state-123&SigAlg=...&Signature=... // POST form HTML: //
... // Custom request URL: https://idp.example.com/sso?SAMLRequest=...&RelayState=relayState ``` ### Handling Single Logout Implement single logout (SLO) flows for both service provider and identity provider initiated logout. ```go package main import ( "crypto/rsa" "crypto/tls" "crypto/x509" "fmt" "net/http" "net/url" "github.com/crewjam/saml" "github.com/crewjam/saml/samlsp" ) func main() { keyPair, _ := tls.LoadX509KeyPair("myservice.cert", "myservice.key") keyPair.Leaf, _ = x509.ParseCertificate(keyPair.Certificate[0]) sp := &saml.ServiceProvider{ Key: keyPair.PrivateKey.(*rsa.PrivateKey), Certificate: keyPair.Leaf, MetadataURL: url.URL{Scheme: "https", Host: "sp.example.com", Path: "/saml/metadata"}, AcsURL: url.URL{Scheme: "https", Host: "sp.example.com", Path: "/saml/acs"}, SloURL: url.URL{Scheme: "https", Host: "sp.example.com", Path: "/saml/slo"}, IDPMetadata: &saml.EntityDescriptor{ EntityID: "https://idp.example.com", IDPSSODescriptors: []saml.IDPSSODescriptor{{ SingleLogoutServices: []saml.Endpoint{ { Binding: saml.HTTPRedirectBinding, Location: "https://idp.example.com/slo", }, }, }}, }, } samlMiddleware, _ := samlsp.New(samlsp.Options{ URL: url.URL{Scheme: "https", Host: "sp.example.com"}, Key: keyPair.PrivateKey.(*rsa.PrivateKey), Certificate: keyPair.Leaf, IDPMetadata: sp.IDPMetadata, SignRequest: true, }) // SP-initiated logout logoutHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Get user's NameID from session nameID := samlsp.AttributeFromContext(r.Context(), "urn:oasis:names:tc:SAML:attribute:subject-id") // Create logout request logoutURL, err := sp.MakeRedirectLogoutRequest(nameID, "") if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // Delete local session err = samlMiddleware.Session.DeleteSession(w, r) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // Redirect to IDP logout http.Redirect(w, r, logoutURL.String(), http.StatusFound) }) // IDP-initiated logout (handle LogoutRequest from IDP) sloHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Parse logout request from IDP err := r.ParseForm() if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } // For POST binding, parse the LogoutRequest // For Redirect binding, parse from query parameter if r.Method == "POST" { // Handle POST logout request // Validate signature, extract NameID, session index } // Delete local session for the user err = samlMiddleware.Session.DeleteSession(w, r) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // Send LogoutResponse back to IDP logoutResponseURL, err := sp.MakeRedirectLogoutResponse("request-id-from-idp", "") if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } http.Redirect(w, r, logoutResponseURL.String(), http.StatusFound) }) http.Handle("/logout", samlMiddleware.RequireAccount(logoutHandler)) http.Handle("/saml/slo", sloHandler) http.ListenAndServe(":8000", nil) } // Logout flow: // 1. User clicks logout button -> /logout // 2. SP creates LogoutRequest, deletes session, redirects to IDP // 3. IDP processes logout, sends LogoutResponse // 4. SP validates response, completes logout ``` ## Identity Provider Implementation ### Creating a Basic Identity Provider Implement a complete identity provider with authentication and assertion generation. ```go package main import ( "crypto/rsa" "crypto/tls" "crypto/x509" "encoding/xml" "fmt" "net/http" "net/url" "os" "time" "github.com/crewjam/saml" "github.com/crewjam/saml/logger" ) // SessionProvider returns user session information type MySessionProvider struct{} func (sp *MySessionProvider) GetSession(w http.ResponseWriter, r *http.Request, req *saml.IdpAuthnRequest) *saml.Session { // Check if user is already authenticated (e.g., check cookie) cookie, err := r.Cookie("user_session") if err != nil { // Not authenticated - show login form w.WriteHeader(http.StatusOK) w.Write([]byte(`
`)) return nil } // User authenticated - return session userID := cookie.Value return &saml.Session{ ID: userID, CreateTime: time.Now(), ExpireTime: time.Now().Add(time.Hour * 8), Index: "session-index-123", NameID: userID, NameIDFormat: string(saml.EmailAddressNameIDFormat), SubjectID: userID, Groups: []string{"users", "developers"}, UserName: userID, UserEmail: fmt.Sprintf("%s@example.com", userID), UserCommonName: "John Doe", UserSurname: "Doe", UserGivenName: "John", CustomAttributes: []saml.Attribute{ { Name: "department", NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic", Values: []saml.AttributeValue{ {Type: "xs:string", Value: "Engineering"}, }, }, }, } } // ServiceProviderProvider looks up registered service providers type MyServiceProviderProvider struct { sps map[string]*saml.EntityDescriptor } func (spp *MyServiceProviderProvider) GetServiceProvider(r *http.Request, serviceProviderID string) (*saml.EntityDescriptor, error) { sp, ok := spp.sps[serviceProviderID] if !ok { return nil, os.ErrNotExist } return sp, nil } func main() { // Load IDP certificate keyPair, _ := tls.LoadX509KeyPair("idp.cert", "idp.key") keyPair.Leaf, _ = x509.ParseCertificate(keyPair.Certificate[0]) // Create IDP idp := &saml.IdentityProvider{ Key: keyPair.PrivateKey.(*rsa.PrivateKey), Certificate: keyPair.Leaf, Logger: logger.DefaultLogger, MetadataURL: url.URL{Scheme: "https", Host: "idp.example.com", Path: "/metadata"}, SSOURL: url.URL{Scheme: "https", Host: "idp.example.com", Path: "/sso"}, LogoutURL: url.URL{Scheme: "https", Host: "idp.example.com", Path: "/slo"}, ServiceProviderProvider: &MyServiceProviderProvider{ sps: map[string]*saml.EntityDescriptor{ "https://sp.example.com": { EntityID: "https://sp.example.com", SPSSODescriptors: []saml.SPSSODescriptor{{ AssertionConsumerServices: []saml.IndexedEndpoint{ { Binding: saml.HTTPPostBinding, Location: "https://sp.example.com/saml/acs", Index: 1, }, }, }}, }, }, }, SessionProvider: &MySessionProvider{}, } // Serve IDP endpoints http.Handle("/metadata", http.HandlerFunc(idp.ServeMetadata)) http.Handle("/sso", http.HandlerFunc(idp.ServeSSO)) // Metadata endpoint returns IDP configuration http.ListenAndServe(":8080", nil) } // When SP redirects user to /sso: // 1. ServeSSO parses SAMLRequest // 2. SessionProvider.GetSession checks authentication // 3. If not authenticated, show login form // 4. After login, create SAML assertion // 5. POST assertion back to SP's ACS URL ``` ### Custom Assertion Generation Create custom SAML assertions with specific attributes and conditions. ```go package main import ( "fmt" "time" "github.com/crewjam/saml" ) // CustomAssertionMaker implements saml.AssertionMaker type CustomAssertionMaker struct{} func (cam CustomAssertionMaker) MakeAssertion(req *saml.IdpAuthnRequest, session *saml.Session) error { // Build custom attributes based on business logic attributes := []saml.Attribute{ { FriendlyName: "Email", Name: "email", NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic", Values: []saml.AttributeValue{ {Type: "xs:string", Value: session.UserEmail}, }, }, { FriendlyName: "Roles", Name: "roles", NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic", Values: []saml.AttributeValue{ {Type: "xs:string", Value: "admin"}, {Type: "xs:string", Value: "user"}, }, }, } // Add department if present in custom attributes for _, attr := range session.CustomAttributes { attributes = append(attributes, attr) } now := time.Now() req.Assertion = &saml.Assertion{ ID: fmt.Sprintf("id-%x", randomBytes(20)), IssueInstant: now, Version: "2.0", Issuer: saml.Issuer{ Format: "urn:oasis:names:tc:SAML:2.0:nameid-format:entity", Value: req.IDP.MetadataURL.String(), }, Subject: &saml.Subject{ NameID: &saml.NameID{ Format: string(saml.EmailAddressNameIDFormat), Value: session.UserEmail, NameQualifier: req.IDP.MetadataURL.String(), SPNameQualifier: req.ServiceProviderMetadata.EntityID, }, SubjectConfirmations: []saml.SubjectConfirmation{ { Method: "urn:oasis:names:tc:SAML:2.0:cm:bearer", SubjectConfirmationData: &saml.SubjectConfirmationData{ NotOnOrAfter: now.Add(5 * time.Minute), Recipient: req.ACSEndpoint.Location, InResponseTo: req.Request.ID, }, }, }, }, Conditions: &saml.Conditions{ NotBefore: now.Add(-1 * time.Minute), NotOnOrAfter: now.Add(5 * time.Minute), AudienceRestrictions: []saml.AudienceRestriction{ { Audience: saml.Audience{ Value: req.ServiceProviderMetadata.EntityID, }, }, }, }, AuthnStatements: []saml.AuthnStatement{ { AuthnInstant: session.CreateTime, SessionIndex: session.Index, AuthnContext: saml.AuthnContext{ AuthnContextClassRef: &saml.AuthnContextClassRef{ Value: "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport", }, }, }, }, AttributeStatements: []saml.AttributeStatement{ {Attributes: attributes}, }, } return nil } func randomBytes(n int) []byte { // In production, use crypto/rand b := make([]byte, n) for i := range b { b[i] = byte(i) } return b } // Use custom assertion maker: // idp.AssertionMaker = CustomAssertionMaker{} ``` ## Metadata Management ### Fetching and Parsing IDP Metadata Retrieve and parse identity provider metadata from URLs or files. ```go package main import ( "context" "encoding/xml" "fmt" "io/ioutil" "net/http" "net/url" "time" "github.com/crewjam/saml" "github.com/crewjam/saml/samlsp" ) func main() { // Fetch metadata from URL ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() metadataURL, _ := url.Parse("https://idp.example.com/metadata") metadata, err := samlsp.FetchMetadata(ctx, http.DefaultClient, *metadataURL) if err != nil { panic(err) } fmt.Printf("IDP Entity ID: %s\n", metadata.EntityID) fmt.Printf("Valid Until: %s\n", metadata.ValidUntil) // Extract SSO URLs for _, descriptor := range metadata.IDPSSODescriptors { fmt.Println("Single Sign-On Services:") for _, sso := range descriptor.SingleSignOnServices { fmt.Printf(" %s: %s\n", sso.Binding, sso.Location) } fmt.Println("Certificates:") for _, keyDescriptor := range descriptor.KeyDescriptors { if keyDescriptor.Use == "signing" { fmt.Printf(" Signing cert: %s\n", keyDescriptor.KeyInfo.X509Data.X509Certificates[0].Data[:60]+"...") } } } // Parse metadata from file metadataBytes, _ := ioutil.ReadFile("idp-metadata.xml") metadata2, err := samlsp.ParseMetadata(metadataBytes) if err != nil { panic(err) } // Handle EntitiesDescriptor (multiple entities) // ParseMetadata automatically handles both EntityDescriptor and EntitiesDescriptor // Marshal metadata back to XML output, _ := xml.MarshalIndent(metadata2, "", " ") fmt.Printf("Metadata XML:\n%s\n", string(output)) } // Output: // IDP Entity ID: https://idp.example.com // Valid Until: 2025-01-15 10:30:00 +0000 UTC // Single Sign-On Services: // urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect: https://idp.example.com/sso // urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST: https://idp.example.com/sso // Certificates: // Signing cert: MIIDXTCCAkWgAwIBAgIJAKZ... ``` ### Generating Service Provider Metadata Generate and serve service provider metadata for IDP registration. ```go package main import ( "crypto/rsa" "crypto/tls" "crypto/x509" "encoding/xml" "fmt" "net/http" "net/url" "time" "github.com/crewjam/saml" "github.com/russellhaering/goxmldsig" ) func main() { keyPair, _ := tls.LoadX509KeyPair("myservice.cert", "myservice.key") keyPair.Leaf, _ = x509.ParseCertificate(keyPair.Certificate[0]) sp := &saml.ServiceProvider{ EntityID: "https://sp.example.com", Key: keyPair.PrivateKey.(*rsa.PrivateKey), Certificate: keyPair.Leaf, MetadataURL: url.URL{Scheme: "https", Host: "sp.example.com", Path: "/saml/metadata"}, AcsURL: url.URL{Scheme: "https", Host: "sp.example.com", Path: "/saml/acs"}, SloURL: url.URL{Scheme: "https", Host: "sp.example.com", Path: "/saml/slo"}, AuthnNameIDFormat: saml.EmailAddressNameIDFormat, MetadataValidDuration: 48 * time.Hour, SignatureMethod: dsig.RSASHA256SignatureMethod, LogoutBindings: []string{saml.HTTPRedirectBinding, saml.HTTPPostBinding}, IDPMetadata: &saml.EntityDescriptor{}, // Set to avoid nil pointer } // Generate metadata metadata := sp.Metadata() fmt.Printf("Entity ID: %s\n", metadata.EntityID) fmt.Printf("Valid Until: %s\n", metadata.ValidUntil) // Inspect SP configuration for _, desc := range metadata.SPSSODescriptors { fmt.Printf("AuthnRequestsSigned: %v\n", *desc.AuthnRequestsSigned) fmt.Printf("WantAssertionsSigned: %v\n", *desc.WantAssertionsSigned) fmt.Println("Assertion Consumer Services:") for _, acs := range desc.AssertionConsumerServices { fmt.Printf(" [%d] %s: %s\n", acs.Index, acs.Binding, acs.Location) } fmt.Println("Single Logout Services:") for _, slo := range desc.SingleLogoutServices { fmt.Printf(" %s: %s\n", slo.Binding, slo.Location) } } // Serve metadata endpoint http.HandleFunc("/saml/metadata", func(w http.ResponseWriter, r *http.Request) { buf, _ := xml.MarshalIndent(metadata, "", " ") w.Header().Set("Content-Type", "application/samlmetadata+xml") w.Write(buf) }) http.ListenAndServe(":8000", nil) } // curl http://localhost:8000/saml/metadata > sp-metadata.xml // Upload sp-metadata.xml to IDP for registration // Output: // Entity ID: https://sp.example.com // Valid Until: 2025-01-10 10:00:00 +0000 UTC // AuthnRequestsSigned: true // WantAssertionsSigned: true // Assertion Consumer Services: // [1] urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST: https://sp.example.com/saml/acs // [2] urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact: https://sp.example.com/saml/acs // Single Logout Services: // urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect: https://sp.example.com/saml/slo // urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST: https://sp.example.com/saml/slo ``` ## Advanced Features ### Working with Encrypted Assertions Handle encrypted SAML assertions with automatic decryption. ```go package main import ( "crypto/rsa" "crypto/tls" "crypto/x509" "fmt" "net/http" "net/url" "github.com/crewjam/saml" ) func main() { keyPair, _ := tls.LoadX509KeyPair("myservice.cert", "myservice.key") keyPair.Leaf, _ = x509.ParseCertificate(keyPair.Certificate[0]) sp := &saml.ServiceProvider{ Key: keyPair.PrivateKey.(*rsa.PrivateKey), Certificate: keyPair.Leaf, MetadataURL: url.URL{Scheme: "https", Host: "sp.example.com", Path: "/saml/metadata"}, AcsURL: url.URL{Scheme: "https", Host: "sp.example.com", Path: "/saml/acs"}, IDPMetadata: &saml.EntityDescriptor{ EntityID: "https://idp.example.com", IDPSSODescriptors: []saml.IDPSSODescriptor{{ KeyDescriptors: []saml.KeyDescriptor{ { Use: "encryption", KeyInfo: saml.KeyInfo{ X509Data: saml.X509Data{ X509Certificates: []saml.X509Certificate{ {Data: "MIIDXTCCAkWgAwIBAgIJAKZ..."}, }, }, }, }, }, }}, }, } http.HandleFunc("/saml/acs", func(w http.ResponseWriter, r *http.Request) { err := r.ParseForm() if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } // ParseResponse automatically handles encrypted assertions // It will decrypt using sp.Key if the assertion is encrypted assertion, err := sp.ParseResponse(r, []string{"request-id-123"}) if err != nil { http.Error(w, "Failed to parse encrypted response", http.StatusForbidden) return } // Assertion is now decrypted and validated userID := assertion.Subject.NameID.Value fmt.Fprintf(w, "Decrypted user ID: %s", userID) }) http.ListenAndServe(":8000", nil) } // IDP encrypts assertions using SP's public certificate // SP automatically decrypts using its private key during ParseResponse // Encryption provides confidentiality for sensitive user attributes ``` ### Custom Signature Verification Implement custom signature verification logic for special requirements. ```go package main import ( "crypto/x509" "fmt" "github.com/beevik/etree" "github.com/crewjam/saml" dsig "github.com/russellhaering/goxmldsig" ) // CustomSignatureVerifier implements saml.SignatureVerifier type CustomSignatureVerifier struct { trustedCerts []*x509.Certificate } func (csv *CustomSignatureVerifier) VerifySignature(validationContext *dsig.ValidationContext, el *etree.Element) error { // Custom verification logic // Example: Allow only specific signature algorithms sigEl := el.FindElement("./Signature") if sigEl == nil { return fmt.Errorf("no signature found") } methodEl := sigEl.FindElement("./SignedInfo/SignatureMethod") if methodEl == nil { return fmt.Errorf("no signature method found") } algorithm := methodEl.SelectAttrValue("Algorithm", "") // Only allow SHA-256 or stronger allowedAlgorithms := []string{ dsig.RSASHA256SignatureMethod, dsig.RSASHA384SignatureMethod, dsig.RSASHA512SignatureMethod, } allowed := false for _, alg := range allowedAlgorithms { if algorithm == alg { allowed = true break } } if !allowed { return fmt.Errorf("signature algorithm %s not allowed", algorithm) } // Perform standard validation _, err := validationContext.Validate(el) if err != nil { return fmt.Errorf("signature validation failed: %w", err) } return nil } func main() { sp := &saml.ServiceProvider{ // ... standard configuration ... SignatureVerifier: &CustomSignatureVerifier{}, } // When ParseResponse is called, it will use the custom verifier // This allows enforcing organizational security policies _ = sp } // Use cases: // - Enforce minimum signature algorithm strength // - Add custom certificate validation logic // - Implement certificate pinning // - Log signature verification for audit trails ``` ### Attribute Mapping and Session Management Extract and map SAML attributes to application-specific user models. ```go package main import ( "encoding/json" "fmt" "net/http" "time" "github.com/crewjam/saml" "github.com/crewjam/saml/samlsp" ) // User represents your application's user model type User struct { ID string Email string FirstName string LastName string Roles []string Department string LoginTime time.Time } func mapAssertionToUser(assertion *saml.Assertion) (*User, error) { user := &User{ ID: assertion.Subject.NameID.Value, LoginTime: time.Now(), } // Extract attributes from assertion for _, stmt := range assertion.AttributeStatements { for _, attr := range stmt.Attributes { if len(attr.Values) == 0 { continue } switch attr.Name { case "email", "mail", "emailAddress": user.Email = attr.Values[0].Value case "givenName", "firstName": user.FirstName = attr.Values[0].Value case "surname", "lastName", "familyName": user.LastName = attr.Values[0].Value case "department": user.Department = attr.Values[0].Value case "roles", "groups", "memberOf": for _, val := range attr.Values { user.Roles = append(user.Roles, val.Value) } } } } // Validate required fields if user.Email == "" { return nil, fmt.Errorf("email attribute required") } return user, nil } func main() { samlSP, _ := samlsp.New(samlsp.Options{ // ... configuration ... }) // Custom ACS handler with attribute mapping http.HandleFunc("/saml/acs", func(w http.ResponseWriter, r *http.Request) { r.ParseForm() assertion, err := samlSP.ServiceProvider.ParseResponse(r, []string{}) if err != nil { http.Error(w, "Authentication failed", http.StatusForbidden) return } // Map SAML attributes to user model user, err := mapAssertionToUser(assertion) if err != nil { http.Error(w, "Invalid attributes", http.StatusBadRequest) return } // Create application session (example using JWT or database) sessionToken := createAppSession(user) // Set session cookie http.SetCookie(w, &http.Cookie{ Name: "app_session", Value: sessionToken, Path: "/", MaxAge: 28800, // 8 hours HttpOnly: true, Secure: true, SameSite: http.SameSiteStrictMode, }) http.Redirect(w, r, "/dashboard", http.StatusFound) }) // Protected endpoint using mapped user http.HandleFunc("/dashboard", func(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie("app_session") if err != nil { http.Redirect(w, r, "/login", http.StatusFound) return } user := getUserFromSession(cookie.Value) json.NewEncoder(w).Encode(user) }) http.ListenAndServe(":8000", nil) } func createAppSession(user *User) string { // Implement your session management (JWT, Redis, database, etc.) return "session-token-123" } func getUserFromSession(token string) *User { // Retrieve user from session return &User{Email: "user@example.com"} } // Output example: // {"ID":"user@example.com","Email":"user@example.com","FirstName":"John", // "LastName":"Doe","Roles":["admin","users"],"Department":"Engineering", // "LoginTime":"2025-01-08T10:00:00Z"} ``` ## Summary This SAML library provides production-ready implementations for both Service Providers and Identity Providers in Go applications. The core `saml` package handles low-level protocol operations including request/response parsing, signature verification, encryption/decryption, and metadata management. The `samlsp` package offers high-level middleware for Service Providers with session management, request tracking, and attribute handling built-in. Key integration patterns include: (1) Using the `samlsp.Middleware` for simple SP integration with automatic session handling; (2) Implementing custom `SessionProvider` and `AssertionMaker` interfaces for fine-grained control over authentication flows; (3) Leveraging the `ServiceProvider` and `IdentityProvider` structs directly for maximum flexibility; (4) Handling encrypted assertions transparently through the library's built-in cryptographic operations. The library supports all major SAML bindings (HTTP-Redirect, HTTP-POST, HTTP-Artifact, SOAP), signature algorithms (RSA-SHA256/384/512, ECDSA), and encryption standards, making it suitable for enterprise SSO deployments with strict security requirements.