114 lines
3.2 KiB
Go
114 lines
3.2 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/crewjam/saml"
|
|
"github.com/crewjam/saml/samlsp"
|
|
)
|
|
|
|
// getSAMLServiceProvider generates a new service provider object with the
|
|
// (optionally) provided descriptor (metadata) for the identity provider.
|
|
func (a *API) getSAMLServiceProvider(identityProvider *saml.EntityDescriptor, idpInitiated bool) *saml.ServiceProvider {
|
|
var externalURL *url.URL
|
|
|
|
if a.config.SAML.ExternalURL != "" {
|
|
url, err := url.ParseRequestURI(a.config.SAML.ExternalURL)
|
|
if err != nil {
|
|
// this should not fail as a.config should have been validated using #Validate()
|
|
panic(err)
|
|
}
|
|
|
|
externalURL = url
|
|
} else {
|
|
url, err := url.ParseRequestURI(a.config.API.ExternalURL)
|
|
if err != nil {
|
|
// this should not fail as a.config should have been validated using #Validate()
|
|
panic(err)
|
|
}
|
|
|
|
externalURL = url
|
|
}
|
|
|
|
if !strings.HasSuffix(externalURL.Path, "/") {
|
|
externalURL.Path += "/"
|
|
}
|
|
|
|
externalURL.Path += "sso/"
|
|
|
|
provider := samlsp.DefaultServiceProvider(samlsp.Options{
|
|
URL: *externalURL,
|
|
Key: a.config.SAML.RSAPrivateKey,
|
|
Certificate: a.config.SAML.Certificate,
|
|
SignRequest: true,
|
|
AllowIDPInitiated: idpInitiated,
|
|
IDPMetadata: identityProvider,
|
|
})
|
|
|
|
provider.AuthnNameIDFormat = saml.PersistentNameIDFormat
|
|
|
|
return &provider
|
|
}
|
|
|
|
// SAMLMetadata serves GoTrue's SAML Service Provider metadata file.
|
|
func (a *API) SAMLMetadata(w http.ResponseWriter, r *http.Request) error {
|
|
serviceProvider := a.getSAMLServiceProvider(nil, true)
|
|
|
|
metadata := serviceProvider.Metadata()
|
|
|
|
if r.FormValue("download") == "true" {
|
|
// 5 year expiration, comparable to what GSuite does
|
|
metadata.ValidUntil = time.Now().UTC().AddDate(5, 0, 0)
|
|
}
|
|
|
|
for i := range metadata.SPSSODescriptors {
|
|
// we set this to false since the IdP initiated flow can only
|
|
// sign the Assertion, and not the full Request
|
|
// unfortunately this is hardcoded in the crewjam library if
|
|
// signatures (instead of encryption) are supported
|
|
// https://github.com/crewjam/saml/blob/v0.4.8/service_provider.go#L217
|
|
metadata.SPSSODescriptors[i].AuthnRequestsSigned = nil
|
|
|
|
// advertize the requested NameID formats (either persistent or email address)
|
|
metadata.SPSSODescriptors[i].NameIDFormats = []saml.NameIDFormat{
|
|
saml.EmailAddressNameIDFormat,
|
|
saml.PersistentNameIDFormat,
|
|
}
|
|
}
|
|
|
|
for i := range metadata.SPSSODescriptors {
|
|
spd := &metadata.SPSSODescriptors[i]
|
|
|
|
var keyDescriptors []saml.KeyDescriptor
|
|
|
|
for _, kd := range spd.KeyDescriptors {
|
|
// only advertize key as usable for encryption if allowed
|
|
if kd.Use == "signing" || (a.config.SAML.AllowEncryptedAssertions && kd.Use == "encryption") {
|
|
keyDescriptors = append(keyDescriptors, kd)
|
|
}
|
|
}
|
|
|
|
spd.KeyDescriptors = keyDescriptors
|
|
}
|
|
|
|
metadataXML, err := xml.Marshal(metadata)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/xml")
|
|
w.Header().Set("Cache-Control", "public, max-age=600") // cache at CDN for 10 minutes
|
|
|
|
if r.FormValue("download") == "true" {
|
|
w.Header().Set("Content-Disposition", "attachment; filename=\"metadata.xml\"")
|
|
}
|
|
|
|
_, err = w.Write(metadataXML)
|
|
|
|
return err
|
|
}
|