fleet/server/sso/authorization_request.go
2021-11-23 08:25:43 -05:00

153 lines
3.9 KiB
Go

package sso
import (
"bytes"
"compress/flate"
"encoding/base64"
"encoding/xml"
"errors"
"fmt"
"io"
"net/url"
"time"
)
const (
samlVersion = "2.0"
cacheLifetime = 300 // five minutes
)
// RelayState sets optional relay state
func RelayState(v string) func(*opts) {
return func(o *opts) {
o.relayState = v
}
}
type opts struct {
relayState string
}
// CreateAuthorizationRequest creates a url suitable for use to satisfy the SAML
// redirect binding. It returns the URL of the identity provider, configured to
// initiate the authentication request.
// See http://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf Section 3.4
func CreateAuthorizationRequest(settings *Settings, issuer string, options ...func(o *opts)) (string, error) {
var optionalParams opts
for _, opt := range options {
opt(&optionalParams)
}
if settings.Metadata == nil {
return "", errors.New("missing settings metadata")
}
requestID, err := generateSAMLValidID()
if err != nil {
return "", fmt.Errorf("creating auth request id: %w", err)
}
destinationURL, err := getDestinationURL(settings)
if err != nil {
return "", fmt.Errorf("creating auth request: %w", err)
}
request := AuthnRequest{
XMLName: xml.Name{
Local: "samlp:AuthnRequest",
},
ID: requestID,
SAMLP: "urn:oasis:names:tc:SAML:2.0:protocol",
SAML: "urn:oasis:names:tc:SAML:2.0:assertion",
AssertionConsumerServiceURL: settings.AssertionConsumerServiceURL,
Destination: destinationURL,
IssueInstant: time.Now().UTC().Format("2006-01-02T15:04:05Z"),
Version: samlVersion,
ProviderName: "Fleet",
Issuer: Issuer{
XMLName: xml.Name{
Local: "saml:Issuer",
},
Url: issuer,
},
}
var reader bytes.Buffer
err = xml.NewEncoder(&reader).Encode(settings.Metadata)
if err != nil {
return "", fmt.Errorf("encoding metadata creating auth request: %w", err)
}
// cache metadata so we can check the signatures on the response we get from the IDP
err = settings.SessionStore.create(requestID,
settings.OriginalURL,
reader.String(),
cacheLifetime,
)
if err != nil {
return "", fmt.Errorf("caching metadata while creating auth request: %w", err)
}
u, err := url.Parse(destinationURL)
if err != nil {
return "", fmt.Errorf("parsing destination url: %w", err)
}
qry := u.Query()
var writer bytes.Buffer
err = xml.NewEncoder(&writer).Encode(request)
if err != nil {
return "", fmt.Errorf("encoding auth request xml: %w", err)
}
authQueryVal, err := deflate(&writer)
if err != nil {
return "", fmt.Errorf("unable to compress auth info: %w", err)
}
qry.Set("SAMLRequest", authQueryVal)
if optionalParams.relayState != "" {
qry.Set("RelayState", optionalParams.relayState)
}
u.RawQuery = qry.Encode()
return u.String(), nil
}
func getDestinationURL(settings *Settings) (string, error) {
for _, sso := range settings.Metadata.IDPSSODescriptor.SingleSignOnService {
if sso.Binding == RedirectBinding {
return sso.Location, nil
}
}
return "", errors.New("IDP does not support redirect binding")
}
// See SAML Bindings http://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf
// Section 3.4.4.1
func deflate(xmlBuffer *bytes.Buffer) (string, error) {
// Gzip
var deflated bytes.Buffer
writer, err := flate.NewWriter(&deflated, flate.DefaultCompression)
if err != nil {
return "", fmt.Errorf("create flate writer: %w", err)
}
count := xmlBuffer.Len()
n, err := io.Copy(writer, xmlBuffer)
if err != nil {
_ = writer.Close()
return "", fmt.Errorf("compressing auth request: %w", err)
}
if int(n) != count {
_ = writer.Close()
return "", errors.New("incomplete write during compression")
}
if err := writer.Close(); err != nil {
return "", fmt.Errorf("close flate writer: %w", err)
}
// Base64
encbuff := deflated.Bytes()
encoded := base64.StdEncoding.EncodeToString(encbuff)
return encoded, nil
}