chatai/auth_v2.169.0/internal/conf/saml.go

137 lines
3.7 KiB
Go

package conf
import (
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"errors"
"fmt"
"math/big"
"net"
"net/url"
"time"
)
// SAMLConfiguration holds configuration for native SAML support.
type SAMLConfiguration struct {
Enabled bool `json:"enabled"`
PrivateKey string `json:"-" split_words:"true"`
AllowEncryptedAssertions bool `json:"allow_encrypted_assertions" split_words:"true"`
RelayStateValidityPeriod time.Duration `json:"relay_state_validity_period" split_words:"true"`
RSAPrivateKey *rsa.PrivateKey `json:"-"`
RSAPublicKey *rsa.PublicKey `json:"-"`
Certificate *x509.Certificate `json:"-"`
ExternalURL string `json:"external_url,omitempty" split_words:"true"`
RateLimitAssertion float64 `default:"15" split_words:"true"`
}
func (c *SAMLConfiguration) Validate() error {
if c.Enabled {
bytes, err := base64.StdEncoding.DecodeString(c.PrivateKey)
if err != nil {
return errors.New("SAML private key not in standard Base64 format")
}
privateKey, err := x509.ParsePKCS1PrivateKey(bytes)
if err != nil {
return errors.New("SAML private key not in PKCS#1 format")
}
err = privateKey.Validate()
if err != nil {
return errors.New("SAML private key is not valid")
}
if privateKey.E != 0x10001 {
return errors.New("SAML private key should use the 65537 (0x10001) RSA public exponent")
}
if privateKey.N.BitLen() < 2048 {
return errors.New("SAML private key must be at least RSA 2048")
}
if c.RelayStateValidityPeriod < 0 {
return errors.New("SAML RelayState validity period should be a positive duration")
}
if c.ExternalURL != "" {
_, err := url.ParseRequestURI(c.ExternalURL)
if err != nil {
return err
}
}
}
return nil
}
// PopulateFields fills the configuration details based off the provided
// parameters.
func (c *SAMLConfiguration) PopulateFields(externalURL string) error {
// errors are intentionally ignored since they should have been handled
// within #Validate()
bytes, _ := base64.StdEncoding.DecodeString(c.PrivateKey)
privateKey, _ := x509.ParsePKCS1PrivateKey(bytes)
c.RSAPrivateKey = privateKey
c.RSAPublicKey = privateKey.Public().(*rsa.PublicKey)
parsedURL, err := url.ParseRequestURI(externalURL)
if err != nil {
return fmt.Errorf("saml: unable to parse external URL for SAML, check API_EXTERNAL_URL: %w", err)
}
host := ""
host, _, err = net.SplitHostPort(parsedURL.Host)
if err != nil {
host = parsedURL.Host
}
// SAML does not care much about the contents of the certificate, it
// only uses it as a vessel for the public key; therefore we set these
// fixed values.
// Please avoid modifying or adding new values to this template as they
// will change the exposed SAML certificate, requiring users of
// GoTrue to re-establish a connection between their Identity Provider
// and their running GoTrue instances.
certTemplate := &x509.Certificate{
SerialNumber: big.NewInt(0),
IsCA: false,
DNSNames: []string{
"_samlsp." + host,
},
KeyUsage: x509.KeyUsageDigitalSignature,
NotBefore: time.UnixMilli(0).UTC(),
NotAfter: time.UnixMilli(0).UTC().AddDate(200, 0, 0),
Subject: pkix.Name{
CommonName: "SAML 2.0 Certificate for " + host,
},
}
if c.AllowEncryptedAssertions {
certTemplate.KeyUsage = certTemplate.KeyUsage | x509.KeyUsageDataEncipherment
}
certDer, err := x509.CreateCertificate(nil, certTemplate, certTemplate, c.RSAPublicKey, c.RSAPrivateKey)
if err != nil {
return err
}
cert, err := x509.ParseCertificate(certDer)
if err != nil {
return err
}
c.Certificate = cert
if c.RelayStateValidityPeriod == 0 {
c.RelayStateValidityPeriod = 2 * time.Minute
}
return nil
}