125 lines
3.3 KiB
Go
125 lines
3.3 KiB
Go
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
|
|
}
|