mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-01-04 03:09:33 -06:00
Bumps [github.com/crewjam/saml](https://github.com/crewjam/saml) from 0.4.13 to 0.4.14. - [Commits](https://github.com/crewjam/saml/compare/v0.4.13...v0.4.14) --- updated-dependencies: - dependency-name: github.com/crewjam/saml dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com>
254 lines
8.6 KiB
Go
254 lines
8.6 KiB
Go
package samlsp
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/xml"
|
|
"net/http"
|
|
|
|
"github.com/crewjam/saml"
|
|
)
|
|
|
|
// Middleware implements middleware than allows a web application
|
|
// to support SAML.
|
|
//
|
|
// It implements http.Handler so that it can provide the metadata and ACS endpoints,
|
|
// typically /saml/metadata and /saml/acs, respectively.
|
|
//
|
|
// It also provides middleware RequireAccount which redirects users to
|
|
// the auth process if they do not have session credentials.
|
|
//
|
|
// When redirecting the user through the SAML auth flow, the middleware assigns
|
|
// a temporary cookie with a random name beginning with "saml_". The value of
|
|
// the cookie is a signed JSON Web Token containing the original URL requested
|
|
// and the SAML request ID. The random part of the name corresponds to the
|
|
// RelayState parameter passed through the SAML flow.
|
|
//
|
|
// When validating the SAML response, the RelayState is used to look up the
|
|
// correct cookie, validate that the SAML request ID, and redirect the user
|
|
// back to their original URL.
|
|
//
|
|
// Sessions are established by issuing a JSON Web Token (JWT) as a session
|
|
// cookie once the SAML flow has succeeded. The JWT token contains the
|
|
// authenticated attributes from the SAML assertion.
|
|
//
|
|
// When the middleware receives a request with a valid session JWT it extracts
|
|
// the SAML attributes and modifies the http.Request object adding a Context
|
|
// object to the request context that contains attributes from the initial
|
|
// SAML assertion.
|
|
//
|
|
// When issuing JSON Web Tokens, a signing key is required. Because the
|
|
// SAML service provider already has a private key, we borrow that key
|
|
// to sign the JWTs as well.
|
|
type Middleware struct {
|
|
ServiceProvider saml.ServiceProvider
|
|
OnError func(w http.ResponseWriter, r *http.Request, err error)
|
|
Binding string // either saml.HTTPPostBinding or saml.HTTPRedirectBinding
|
|
ResponseBinding string // either saml.HTTPPostBinding or saml.HTTPArtifactBinding
|
|
RequestTracker RequestTracker
|
|
Session SessionProvider
|
|
}
|
|
|
|
// ServeHTTP implements http.Handler and serves the SAML-specific HTTP endpoints
|
|
// on the URIs specified by m.ServiceProvider.MetadataURL and
|
|
// m.ServiceProvider.AcsURL.
|
|
func (m *Middleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == m.ServiceProvider.MetadataURL.Path {
|
|
m.ServeMetadata(w, r)
|
|
return
|
|
}
|
|
|
|
if r.URL.Path == m.ServiceProvider.AcsURL.Path {
|
|
m.ServeACS(w, r)
|
|
return
|
|
}
|
|
|
|
http.NotFoundHandler().ServeHTTP(w, r)
|
|
}
|
|
|
|
// ServeMetadata handles requests for the SAML metadata endpoint.
|
|
func (m *Middleware) ServeMetadata(w http.ResponseWriter, _ *http.Request) {
|
|
buf, _ := xml.MarshalIndent(m.ServiceProvider.Metadata(), "", " ")
|
|
w.Header().Set("Content-Type", "application/samlmetadata+xml")
|
|
if _, err := w.Write(buf); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
|
|
// ServeACS handles requests for the SAML ACS endpoint.
|
|
func (m *Middleware) ServeACS(w http.ResponseWriter, r *http.Request) {
|
|
err := r.ParseForm()
|
|
if err != nil {
|
|
m.OnError(w, r, err)
|
|
return
|
|
}
|
|
|
|
possibleRequestIDs := []string{}
|
|
if m.ServiceProvider.AllowIDPInitiated {
|
|
possibleRequestIDs = append(possibleRequestIDs, "")
|
|
}
|
|
|
|
trackedRequests := m.RequestTracker.GetTrackedRequests(r)
|
|
for _, tr := range trackedRequests {
|
|
possibleRequestIDs = append(possibleRequestIDs, tr.SAMLRequestID)
|
|
}
|
|
|
|
assertion, err := m.ServiceProvider.ParseResponse(r, possibleRequestIDs)
|
|
if err != nil {
|
|
m.OnError(w, r, err)
|
|
return
|
|
}
|
|
|
|
m.CreateSessionFromAssertion(w, r, assertion, m.ServiceProvider.DefaultRedirectURI)
|
|
}
|
|
|
|
// RequireAccount is HTTP middleware that requires that each request be
|
|
// associated with a valid session. If the request is not associated with a valid
|
|
// session, then rather than serve the request, the middleware redirects the user
|
|
// to start the SAML auth flow.
|
|
func (m *Middleware) RequireAccount(handler http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
session, err := m.Session.GetSession(r)
|
|
if session != nil {
|
|
r = r.WithContext(ContextWithSession(r.Context(), session))
|
|
handler.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
if err == ErrNoSession {
|
|
m.HandleStartAuthFlow(w, r)
|
|
return
|
|
}
|
|
|
|
m.OnError(w, r, err)
|
|
})
|
|
}
|
|
|
|
// HandleStartAuthFlow is called to start the SAML authentication process.
|
|
func (m *Middleware) HandleStartAuthFlow(w http.ResponseWriter, r *http.Request) {
|
|
// If we try to redirect when the original request is the ACS URL we'll
|
|
// end up in a loop. This is a programming error, so we panic here. In
|
|
// general this means a 500 to the user, which is preferable to a
|
|
// redirect loop.
|
|
if r.URL.Path == m.ServiceProvider.AcsURL.Path {
|
|
panic("don't wrap Middleware with RequireAccount")
|
|
}
|
|
|
|
var binding, bindingLocation string
|
|
if m.Binding != "" {
|
|
binding = m.Binding
|
|
bindingLocation = m.ServiceProvider.GetSSOBindingLocation(binding)
|
|
} else {
|
|
binding = saml.HTTPRedirectBinding
|
|
bindingLocation = m.ServiceProvider.GetSSOBindingLocation(binding)
|
|
if bindingLocation == "" {
|
|
binding = saml.HTTPPostBinding
|
|
bindingLocation = m.ServiceProvider.GetSSOBindingLocation(binding)
|
|
}
|
|
}
|
|
|
|
authReq, err := m.ServiceProvider.MakeAuthenticationRequest(bindingLocation, binding, m.ResponseBinding)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// relayState is limited to 80 bytes but also must be integrity protected.
|
|
// this means that we cannot use a JWT because it is way to long. Instead
|
|
// we set a signed cookie that encodes the original URL which we'll check
|
|
// against the SAML response when we get it.
|
|
relayState, err := m.RequestTracker.TrackRequest(w, r, authReq.ID)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
if binding == saml.HTTPRedirectBinding {
|
|
redirectURL, err := authReq.Redirect(relayState, &m.ServiceProvider)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Header().Add("Location", redirectURL.String())
|
|
w.WriteHeader(http.StatusFound)
|
|
return
|
|
}
|
|
if binding == saml.HTTPPostBinding {
|
|
w.Header().Add("Content-Security-Policy", ""+
|
|
"default-src; "+
|
|
"script-src 'sha256-AjPdJSbZmeWHnEc5ykvJFay8FTWeTeRbs9dutfZ0HqE='; "+
|
|
"reflected-xss block; referrer no-referrer;")
|
|
w.Header().Add("Content-type", "text/html")
|
|
var buf bytes.Buffer
|
|
buf.WriteString(`<!DOCTYPE html><html><body>`)
|
|
buf.Write(authReq.Post(relayState))
|
|
buf.WriteString(`</body></html>`)
|
|
if _, err := w.Write(buf.Bytes()); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
return
|
|
}
|
|
panic("not reached")
|
|
}
|
|
|
|
// CreateSessionFromAssertion is invoked by ServeHTTP when we have a new, valid SAML assertion.
|
|
func (m *Middleware) CreateSessionFromAssertion(w http.ResponseWriter, r *http.Request, assertion *saml.Assertion, redirectURI string) {
|
|
if trackedRequestIndex := r.Form.Get("RelayState"); trackedRequestIndex != "" {
|
|
trackedRequest, err := m.RequestTracker.GetTrackedRequest(r, trackedRequestIndex)
|
|
if err != nil {
|
|
if err == http.ErrNoCookie && m.ServiceProvider.AllowIDPInitiated {
|
|
if uri := r.Form.Get("RelayState"); uri != "" {
|
|
redirectURI = uri
|
|
}
|
|
} else {
|
|
m.OnError(w, r, err)
|
|
return
|
|
}
|
|
} else {
|
|
if err := m.RequestTracker.StopTrackingRequest(w, r, trackedRequestIndex); err != nil {
|
|
m.OnError(w, r, err)
|
|
return
|
|
}
|
|
|
|
redirectURI = trackedRequest.URI
|
|
}
|
|
}
|
|
|
|
if err := m.Session.CreateSession(w, r, assertion); err != nil {
|
|
m.OnError(w, r, err)
|
|
return
|
|
}
|
|
|
|
http.Redirect(w, r, redirectURI, http.StatusFound)
|
|
}
|
|
|
|
// RequireAttribute returns a middleware function that requires that the
|
|
// SAML attribute `name` be set to `value`. This can be used to require
|
|
// that a remote user be a member of a group. It relies on the Claims assigned
|
|
// to to the context in RequireAccount.
|
|
//
|
|
// For example:
|
|
//
|
|
// goji.Use(m.RequireAccount)
|
|
// goji.Use(RequireAttributeMiddleware("eduPersonAffiliation", "Staff"))
|
|
func RequireAttribute(name, value string) func(http.Handler) http.Handler {
|
|
return func(handler http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if session := SessionFromContext(r.Context()); session != nil {
|
|
// this will panic if we have the wrong type of Session, and that is OK.
|
|
sessionWithAttributes := session.(SessionWithAttributes)
|
|
attributes := sessionWithAttributes.GetAttributes()
|
|
if values, ok := attributes[name]; ok {
|
|
for _, v := range values {
|
|
if v == value {
|
|
handler.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
|
|
})
|
|
}
|
|
}
|