package auth import ( "crypto" "crypto/tls" "crypto/x509" "encoding/base64" "encoding/xml" "fmt" "net/http" "net/url" "os" "strings" "github.com/crewjam/saml" "github.com/crewjam/saml/samlsp" "wucher/internal/config" ) var SAMLMiddleware *samlsp.Middleware func InitSAML(cfg config.Config) error { keyPair, err := tls.LoadX509KeyPair(cfg.SAMLCertFile, cfg.SAMLKeyFile) if err != nil { return fmt.Errorf("load saml keypair: %w", err) } cert, err := x509.ParseCertificate(keyPair.Certificate[0]) if err != nil { return fmt.Errorf("parse saml certificate: %w", err) } signer, ok := keyPair.PrivateKey.(crypto.Signer) if !ok { return fmt.Errorf("saml private key is not a crypto.Signer") } idpMetadataBytes, err := os.ReadFile(cfg.SAMLMetadataFile) if err != nil { return fmt.Errorf("read idp metadata: %w", err) } var idpMetadata saml.EntityDescriptor if err := xml.Unmarshal(idpMetadataBytes, &idpMetadata); err != nil { return fmt.Errorf("parse idp metadata xml: %w", err) } rootURL, err := url.Parse(cfg.SAMLRootURL) if err != nil { return fmt.Errorf("invalid SAML_ROOT_URL: %w", err) } m, err := samlsp.New(samlsp.Options{ EntityID: cfg.SAMLRootURL, URL: *rootURL, Key: signer, Certificate: cert, IDPMetadata: &idpMetadata, AllowIDPInitiated: true, CookieSameSite: http.SameSiteLaxMode, DefaultRedirectURI: cfg.FrontendURL + "/dashboard", }) if err != nil { return fmt.Errorf("init saml middleware: %w", err) } m.ServiceProvider.MetadataURL.Path = "/api/v1/auth/saml/metadata" m.ServiceProvider.AcsURL.Path = "/api/v1/auth/microsoft/callback" m.ServiceProvider.SloURL.Path = "/api/v1/auth/microsoft/logout" m.ServiceProvider.ValidateAudienceRestriction = func(assertion *saml.Assertion) error { if assertion == nil || assertion.Conditions == nil || len(assertion.Conditions.AudienceRestrictions) == 0 { return nil } expected := strings.TrimRight(firstNonEmpty(m.ServiceProvider.EntityID, m.ServiceProvider.MetadataURL.String()), "/") for _, audienceRestriction := range assertion.Conditions.AudienceRestrictions { if strings.TrimRight(audienceRestriction.Audience.Value, "/") == expected { return nil } } return fmt.Errorf("assertion Conditions AudienceRestriction does not contain %q", expected) } SAMLMiddleware = m return nil } func firstNonEmpty(values ...string) string { for _, v := range values { if strings.TrimSpace(v) != "" { return v } } return "" } func getIDPCertificates() ([]*x509.Certificate, error) { if SAMLMiddleware == nil || SAMLMiddleware.ServiceProvider.IDPMetadata == nil { return nil, fmt.Errorf("saml middleware not initialized") } var certificates []*x509.Certificate for _, idp := range SAMLMiddleware.ServiceProvider.IDPMetadata.IDPSSODescriptors { for _, kd := range idp.KeyDescriptors { for _, certData := range kd.KeyInfo.X509Data.X509Certificates { raw := strings.TrimSpace(certData.Data) if raw == "" { continue } der, err := base64.StdEncoding.DecodeString(raw) if err != nil { continue } cert, err := x509.ParseCertificate(der) if err != nil { continue } certificates = append(certificates, cert) } } } if len(certificates) == 0 { return nil, fmt.Errorf("no idp certificate in metadata") } return certificates, nil }