mattermost-community-enterp.../vendor/github.com/mattermost/gosaml2/build_request.go

560 lines
18 KiB
Go

// Copyright 2016 Russell Haering et al.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package saml2
import (
"bytes"
"compress/flate"
"encoding/base64"
"fmt"
"html/template"
"net/http"
"net/url"
"github.com/beevik/etree"
"github.com/mattermost/gosaml2/uuid"
)
const issueInstantFormat = "2006-01-02T15:04:05Z"
func (sp *SAMLServiceProvider) buildAuthnRequest(includeSig bool) (*etree.Document, error) {
authnRequest := &etree.Element{
Space: "samlp",
Tag: "AuthnRequest",
}
authnRequest.CreateAttr("xmlns:samlp", "urn:oasis:names:tc:SAML:2.0:protocol")
authnRequest.CreateAttr("xmlns:saml", "urn:oasis:names:tc:SAML:2.0:assertion")
arId := uuid.NewV4()
authnRequest.CreateAttr("ID", "_"+arId.String())
authnRequest.CreateAttr("Version", "2.0")
authnRequest.CreateAttr("ProtocolBinding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST")
authnRequest.CreateAttr("AssertionConsumerServiceURL", sp.AssertionConsumerServiceURL)
authnRequest.CreateAttr("IssueInstant", sp.Clock.Now().UTC().Format(issueInstantFormat))
authnRequest.CreateAttr("Destination", sp.IdentityProviderSSOURL)
// NOTE(russell_h): In earlier versions we mistakenly sent the IdentityProviderIssuer
// in the AuthnRequest. For backwards compatibility we will fall back to that
// behavior when ServiceProviderIssuer isn't set.
if sp.ServiceProviderIssuer != "" {
authnRequest.CreateElement("saml:Issuer").SetText(sp.ServiceProviderIssuer)
} else {
authnRequest.CreateElement("saml:Issuer").SetText(sp.IdentityProviderIssuer)
}
nameIdPolicy := authnRequest.CreateElement("samlp:NameIDPolicy")
nameIdPolicy.CreateAttr("AllowCreate", "true")
if sp.NameIdFormat != "" {
nameIdPolicy.CreateAttr("Format", sp.NameIdFormat)
}
if sp.RequestedAuthnContext != nil {
requestedAuthnContext := authnRequest.CreateElement("samlp:RequestedAuthnContext")
requestedAuthnContext.CreateAttr("Comparison", sp.RequestedAuthnContext.Comparison)
for _, context := range sp.RequestedAuthnContext.Contexts {
authnContextClassRef := requestedAuthnContext.CreateElement("saml:AuthnContextClassRef")
authnContextClassRef.SetText(context)
}
}
if sp.ScopingIDPProviderId != "" && sp.ScopingIDPProviderName != "" {
scoping := authnRequest.CreateElement("samlp:Scoping")
idpList := scoping.CreateElement("samlp:IDPList")
idpEntry := idpList.CreateElement("samlp:IDPEntry")
idpEntry.CreateAttr("ProviderID", sp.ScopingIDPProviderId)
idpEntry.CreateAttr("Name", sp.ScopingIDPProviderName)
}
doc := etree.NewDocument()
// Only POST binding includes <Signature> in <AuthnRequest> (includeSig)
if sp.SignAuthnRequests && includeSig {
signed, err := sp.SignAuthnRequest(authnRequest)
if err != nil {
return nil, err
}
doc.SetRoot(signed)
} else {
doc.SetRoot(authnRequest)
}
return doc, nil
}
func (sp *SAMLServiceProvider) BuildAuthRequestDocument() (*etree.Document, error) {
return sp.buildAuthnRequest(true)
}
func (sp *SAMLServiceProvider) BuildAuthRequestDocumentNoSig() (*etree.Document, error) {
return sp.buildAuthnRequest(false)
}
// SignAuthnRequest takes a document, builds a signature, creates another document
// and inserts the signature in it. According to the schema, the position of the
// signature is right after the Issuer [1] then all other children.
//
// [1] https://docs.oasis-open.org/security/saml/v2.0/saml-schema-protocol-2.0.xsd
func (sp *SAMLServiceProvider) SignAuthnRequest(el *etree.Element) (*etree.Element, error) {
ctx := sp.SigningContext()
sig, err := ctx.ConstructSignature(el, true)
if err != nil {
return nil, err
}
ret := el.Copy()
var children []etree.Token
children = append(children, ret.Child[0]) // issuer is always first
children = append(children, sig) // next is the signature
children = append(children, ret.Child[1:]...) // then all other children
ret.Child = children
return ret, nil
}
// BuildAuthRequest builds <AuthnRequest> for identity provider
func (sp *SAMLServiceProvider) BuildAuthRequest() (string, error) {
doc, err := sp.BuildAuthRequestDocument()
if err != nil {
return "", err
}
return doc.WriteToString()
}
func (sp *SAMLServiceProvider) buildAuthURLFromDocument(relayState, binding string, doc *etree.Document) (string, error) {
parsedUrl, err := url.Parse(sp.IdentityProviderSSOURL)
if err != nil {
return "", err
}
authnRequest, err := doc.WriteToString()
if err != nil {
return "", err
}
buf := &bytes.Buffer{}
fw, err := flate.NewWriter(buf, flate.DefaultCompression)
if err != nil {
return "", fmt.Errorf("flate NewWriter error: %v", err)
}
_, err = fw.Write([]byte(authnRequest))
if err != nil {
return "", fmt.Errorf("flate.Writer Write error: %v", err)
}
err = fw.Close()
if err != nil {
return "", fmt.Errorf("flate.Writer Close error: %v", err)
}
qs := parsedUrl.Query()
qs.Add("SAMLRequest", base64.StdEncoding.EncodeToString(buf.Bytes()))
if relayState != "" {
qs.Add("RelayState", relayState)
}
if sp.SignAuthnRequests && binding == BindingHttpRedirect {
// Sign URL encoded query (see Section 3.4.4.1 DEFLATE Encoding of saml-bindings-2.0-os.pdf)
ctx := sp.SigningContext()
qs.Add("SigAlg", ctx.GetSignatureMethodIdentifier())
var rawSignature []byte
if rawSignature, err = ctx.SignString(signatureInputString(qs.Get("SAMLRequest"), qs.Get("RelayState"), qs.Get("SigAlg"))); err != nil {
return "", fmt.Errorf("unable to sign query string of redirect URL: %v", err)
}
// Now add base64 encoded Signature
qs.Add("Signature", base64.StdEncoding.EncodeToString(rawSignature))
}
//Here the parameters may appear in any order.
parsedUrl.RawQuery = qs.Encode()
return parsedUrl.String(), nil
}
func (sp *SAMLServiceProvider) BuildAuthURLFromDocument(relayState string, doc *etree.Document) (string, error) {
return sp.buildAuthURLFromDocument(relayState, BindingHttpPost, doc)
}
func (sp *SAMLServiceProvider) BuildAuthURLRedirect(relayState string, doc *etree.Document) (string, error) {
return sp.buildAuthURLFromDocument(relayState, BindingHttpRedirect, doc)
}
func (sp *SAMLServiceProvider) buildAuthBodyPostFromDocument(relayState string, doc *etree.Document) ([]byte, error) {
reqBuf, err := doc.WriteToBytes()
if err != nil {
return nil, err
}
encodedReqBuf := base64.StdEncoding.EncodeToString(reqBuf)
var tmpl *template.Template
var rv bytes.Buffer
if relayState != "" {
tmpl = template.Must(template.New("saml-post-form").Parse(`` +
`<form method="POST" action="{{.URL}}" id="SAMLRequestForm">` +
`<input type="hidden" name="SAMLRequest" value="{{.SAMLRequest}}" />` +
`<input type="hidden" name="RelayState" value="{{.RelayState}}" />` +
`<input id="SAMLSubmitButton" type="submit" value="Submit" />` +
`</form>` +
`<script>document.getElementById('SAMLSubmitButton').style.visibility="hidden";` +
`document.getElementById('SAMLRequestForm').submit();</script>`))
data := struct {
URL string
SAMLRequest string
RelayState string
}{
URL: sp.IdentityProviderSSOURL,
SAMLRequest: encodedReqBuf,
RelayState: relayState,
}
if err = tmpl.Execute(&rv, data); err != nil {
return nil, err
}
} else {
tmpl = template.Must(template.New("saml-post-form").Parse(`` +
`<form method="POST" action="{{.URL}}" id="SAMLRequestForm">` +
`<input type="hidden" name="SAMLRequest" value="{{.SAMLRequest}}" />` +
`<input id="SAMLSubmitButton" type="submit" value="Submit" />` +
`</form>` +
`<script>document.getElementById('SAMLSubmitButton').style.visibility="hidden";` +
`document.getElementById('SAMLRequestForm').submit();</script>`))
data := struct {
URL string
SAMLRequest string
}{
URL: sp.IdentityProviderSSOURL,
SAMLRequest: encodedReqBuf,
}
if err = tmpl.Execute(&rv, data); err != nil {
return nil, err
}
}
return rv.Bytes(), nil
}
//BuildAuthBodyPost builds the POST body to be sent to IDP.
func (sp *SAMLServiceProvider) BuildAuthBodyPost(relayState string) ([]byte, error) {
var doc *etree.Document
var err error
if sp.SignAuthnRequests {
doc, err = sp.BuildAuthRequestDocument()
} else {
doc, err = sp.BuildAuthRequestDocumentNoSig()
}
if err != nil {
return nil, err
}
return sp.buildAuthBodyPostFromDocument(relayState, doc)
}
//BuildAuthBodyPostFromDocument builds the POST body to be sent to IDP.
//It takes the AuthnRequest xml as input.
func (sp *SAMLServiceProvider) BuildAuthBodyPostFromDocument(relayState string, doc *etree.Document) ([]byte, error) {
return sp.buildAuthBodyPostFromDocument(relayState, doc)
}
// BuildAuthURL builds redirect URL to be sent to principal
func (sp *SAMLServiceProvider) BuildAuthURL(relayState string) (string, error) {
doc, err := sp.BuildAuthRequestDocument()
if err != nil {
return "", err
}
return sp.BuildAuthURLFromDocument(relayState, doc)
}
// AuthRedirect takes a ResponseWriter and Request from an http interaction and
// redirects to the SAMLServiceProvider's configured IdP, including the
// relayState provided, if any.
func (sp *SAMLServiceProvider) AuthRedirect(w http.ResponseWriter, r *http.Request, relayState string) (err error) {
url, err := sp.BuildAuthURL(relayState)
if err != nil {
return err
}
http.Redirect(w, r, url, http.StatusFound)
return nil
}
func (sp *SAMLServiceProvider) buildLogoutRequest(includeSig bool, nameID string, sessionIndex string) (*etree.Document, error) {
logoutRequest := &etree.Element{
Space: "samlp",
Tag: "LogoutRequest",
}
logoutRequest.CreateAttr("xmlns:samlp", "urn:oasis:names:tc:SAML:2.0:protocol")
logoutRequest.CreateAttr("xmlns:saml", "urn:oasis:names:tc:SAML:2.0:assertion")
arId := uuid.NewV4()
logoutRequest.CreateAttr("ID", "_"+arId.String())
logoutRequest.CreateAttr("Version", "2.0")
logoutRequest.CreateAttr("IssueInstant", sp.Clock.Now().UTC().Format(issueInstantFormat))
logoutRequest.CreateAttr("Destination", sp.IdentityProviderSLOURL)
// NOTE(russell_h): In earlier versions we mistakenly sent the IdentityProviderIssuer
// in the AuthnRequest. For backwards compatibility we will fall back to that
// behavior when ServiceProviderIssuer isn't set.
// TODO: Throw error in case Issuer is empty.
if sp.ServiceProviderIssuer != "" {
logoutRequest.CreateElement("saml:Issuer").SetText(sp.ServiceProviderIssuer)
} else {
logoutRequest.CreateElement("saml:Issuer").SetText(sp.IdentityProviderIssuer)
}
nameId := logoutRequest.CreateElement("saml:NameID")
nameId.SetText(nameID)
nameId.CreateAttr("Format", sp.NameIdFormat)
//Section 3.7.1 - http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf says
//SessionIndex is optional. If the IDP supports SLO then it must send SessionIndex as per
//Section 4.1.4.2 of https://docs.oasis-open.org/security/saml/v2.0/saml-profiles-2.0-os.pdf.
//As per section 4.4.3.1 of //docs.oasis-open.org/security/saml/v2.0/saml-profiles-2.0-os.pdf,
//a LogoutRequest issued by Session Participant to Identity Provider, must contain
//at least one SessionIndex element needs to be included.
nameId = logoutRequest.CreateElement("samlp:SessionIndex")
nameId.SetText(sessionIndex)
doc := etree.NewDocument()
if includeSig {
signed, err := sp.SignLogoutRequest(logoutRequest)
if err != nil {
return nil, err
}
doc.SetRoot(signed)
} else {
doc.SetRoot(logoutRequest)
}
return doc, nil
}
func (sp *SAMLServiceProvider) SignLogoutRequest(el *etree.Element) (*etree.Element, error) {
ctx := sp.SigningContext()
sig, err := ctx.ConstructSignature(el, true)
if err != nil {
return nil, err
}
ret := el.Copy()
var children []etree.Token
children = append(children, ret.Child[0]) // issuer is always first
children = append(children, sig) // next is the signature
children = append(children, ret.Child[1:]...) // then all other children
ret.Child = children
return ret, nil
}
func (sp *SAMLServiceProvider) BuildLogoutRequestDocumentNoSig(nameID string, sessionIndex string) (*etree.Document, error) {
return sp.buildLogoutRequest(false, nameID, sessionIndex)
}
func (sp *SAMLServiceProvider) BuildLogoutRequestDocument(nameID string, sessionIndex string) (*etree.Document, error) {
return sp.buildLogoutRequest(true, nameID, sessionIndex)
}
//BuildLogoutBodyPostFromDocument builds the POST body to be sent to IDP.
//It takes the LogoutRequest xml as input.
func (sp *SAMLServiceProvider) BuildLogoutBodyPostFromDocument(relayState string, doc *etree.Document) ([]byte, error) {
return sp.buildLogoutBodyPostFromDocument(relayState, doc)
}
func (sp *SAMLServiceProvider) buildLogoutBodyPostFromDocument(relayState string, doc *etree.Document) ([]byte, error) {
reqBuf, err := doc.WriteToBytes()
if err != nil {
return nil, err
}
encodedReqBuf := base64.StdEncoding.EncodeToString(reqBuf)
var tmpl *template.Template
var rv bytes.Buffer
if relayState != "" {
tmpl = template.Must(template.New("saml-post-form").Parse(`` +
`<form method="POST" action="{{.URL}}" id="SAMLRequestForm">` +
`<input type="hidden" name="SAMLRequest" value="{{.SAMLRequest}}" />` +
`<input type="hidden" name="RelayState" value="{{.RelayState}}" />` +
`<input id="SAMLSubmitButton" type="submit" value="Submit" />` +
`</form>` +
`<script>document.getElementById('SAMLSubmitButton').style.visibility="hidden";` +
`document.getElementById('SAMLRequestForm').submit();</script>`))
data := struct {
URL string
SAMLRequest string
RelayState string
}{
URL: sp.IdentityProviderSLOURL,
SAMLRequest: encodedReqBuf,
RelayState: relayState,
}
if err = tmpl.Execute(&rv, data); err != nil {
return nil, err
}
} else {
tmpl = template.Must(template.New("saml-post-form").Parse(`` +
`<form method="POST" action="{{.URL}}" id="SAMLRequestForm">` +
`<input type="hidden" name="SAMLRequest" value="{{.SAMLRequest}}" />` +
`<input id="SAMLSubmitButton" type="submit" value="Submit" />` +
`</form>` +
`<script>document.getElementById('SAMLSubmitButton').style.visibility="hidden";` +
`document.getElementById('SAMLRequestForm').submit();</script>`))
data := struct {
URL string
SAMLRequest string
}{
URL: sp.IdentityProviderSLOURL,
SAMLRequest: encodedReqBuf,
}
if err = tmpl.Execute(&rv, data); err != nil {
return nil, err
}
}
return rv.Bytes(), nil
}
func (sp *SAMLServiceProvider) BuildLogoutURLRedirect(relayState string, doc *etree.Document) (string, error) {
return sp.buildLogoutURLFromDocument(relayState, BindingHttpRedirect, doc)
}
func (sp *SAMLServiceProvider) buildLogoutURLFromDocument(relayState, binding string, doc *etree.Document) (string, error) {
parsedUrl, err := url.Parse(sp.IdentityProviderSLOURL)
if err != nil {
return "", err
}
logoutRequest, err := doc.WriteToString()
if err != nil {
return "", err
}
buf := &bytes.Buffer{}
fw, err := flate.NewWriter(buf, flate.DefaultCompression)
if err != nil {
return "", fmt.Errorf("flate NewWriter error: %v", err)
}
_, err = fw.Write([]byte(logoutRequest))
if err != nil {
return "", fmt.Errorf("flate.Writer Write error: %v", err)
}
err = fw.Close()
if err != nil {
return "", fmt.Errorf("flate.Writer Close error: %v", err)
}
qs := parsedUrl.Query()
qs.Add("SAMLRequest", base64.StdEncoding.EncodeToString(buf.Bytes()))
if relayState != "" {
qs.Add("RelayState", relayState)
}
if binding == BindingHttpRedirect {
// Sign URL encoded query (see Section 3.4.4.1 DEFLATE Encoding of saml-bindings-2.0-os.pdf)
ctx := sp.SigningContext()
qs.Add("SigAlg", ctx.GetSignatureMethodIdentifier())
var rawSignature []byte
//qs.Encode() sorts the keys (See https://golang.org/pkg/net/url/#Values.Encode).
//If RelayState parameter is present then RelayState parameter
//will be put first by Encode(). Hence encode them separately and concatenate.
//Signature string has to have parameters in the order - SAMLRequest=value&RelayState=value&SigAlg=value.
//(See Section 3.4.4.1 saml-bindings-2.0-os.pdf).
var orderedParams = []string{"SAMLRequest", "RelayState", "SigAlg"}
var paramValueMap = make(map[string]string)
paramValueMap["SAMLRequest"] = base64.StdEncoding.EncodeToString(buf.Bytes())
if relayState != "" {
paramValueMap["RelayState"] = relayState
}
paramValueMap["SigAlg"] = ctx.GetSignatureMethodIdentifier()
ss := ""
for _, k := range orderedParams {
v, ok := paramValueMap[k]
if ok {
//Add the value after URL encoding.
u := url.Values{}
u.Add(k, v)
e := u.Encode()
if ss != "" {
ss += "&" + e
} else {
ss = e
}
}
}
//Now generate the signature on the string of ordered parameters.
if rawSignature, err = ctx.SignString(ss); err != nil {
return "", fmt.Errorf("unable to sign query string of redirect URL: %v", err)
}
// Now add base64 encoded Signature
qs.Add("Signature", base64.StdEncoding.EncodeToString(rawSignature))
}
//Here the parameters may appear in any order.
parsedUrl.RawQuery = qs.Encode()
return parsedUrl.String(), nil
}
// signatureInputString constructs the string to be fed into the signature algorithm, as described
// in section 3.4.4.1 of
// https://www.oasis-open.org/committees/download.php/56779/sstc-saml-bindings-errata-2.0-wd-06.pdf
func signatureInputString(samlRequest, relayState, sigAlg string) string {
var params [][2]string
if relayState == "" {
params = [][2]string{{"SAMLRequest", samlRequest}, {"SigAlg", sigAlg}}
} else {
params = [][2]string{{"SAMLRequest", samlRequest}, {"RelayState", relayState}, {"SigAlg", sigAlg}}
}
var buf bytes.Buffer
for _, kv := range params {
k, v := kv[0], kv[1]
if buf.Len() > 0 {
buf.WriteByte('&')
}
buf.WriteString(url.QueryEscape(k) + "=" + url.QueryEscape(v))
}
return buf.String()
}