137 lines
3.7 KiB
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
|
|
}
|