Skip to content

Commit

Permalink
BREAKING (IDP): generalize the IDP ServiceProviders
Browse files Browse the repository at this point in the history
Before the list of service providers for the IDP was in a map. This
commit replaces the map with an interface that performs the lookup
at runtime.
  • Loading branch information
crewjam committed May 19, 2017
1 parent 08dd8e9 commit f102ca0
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 37 deletions.
50 changes: 33 additions & 17 deletions identity_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"compress/flate"
"crypto"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/xml"
"fmt"
Expand All @@ -13,12 +14,11 @@ import (
"log"
"net/http"
"net/url"
"os"
"regexp"
"text/template"
"time"

"crypto/x509"

"github.com/beevik/etree"
"github.com/crewjam/saml/xmlenc"
dsig "github.com/russellhaering/goxmldsig"
Expand Down Expand Up @@ -53,6 +53,16 @@ type SessionProvider interface {
GetSession(w http.ResponseWriter, r *http.Request, req *IdpAuthnRequest) *Session
}

// ServiceProviderProvider is an interface used by IdentityProvider to look up
// service provider metadata for a request.
type ServiceProviderProvider interface {
// GetServiceProvider returns the Service Provider metadata for the
// service provider ID, which is typically the service provider's
// metadata URL. If an appropriate service provider cannot be found then
// the returned error must be os.ErrNotExist.
GetServiceProvider(r *http.Request, serviceProviderID string) (*Metadata, error)
}

// IdentityProvider implements the SAML Identity Provider role (IDP).
//
// An identity provider receives SAML assertion requests and responds
Expand All @@ -61,19 +71,19 @@ type SessionProvider interface {
// You must provide a keypair that is used to
// sign assertions.
//
// For each service provider that is able to use this
// IDP you must add their metadata to the ServiceProviders map.
// You must provide an implementation of ServiceProviderProvider which
// returns
//
// You must provide an implementation of the SessionProvider which
// handles the actual authentication (i.e. prompting for a username
// and password).
type IdentityProvider struct {
Key crypto.PrivateKey
Certificate *x509.Certificate
MetadataURL url.URL
SSOURL url.URL
ServiceProviders map[string]*Metadata
SessionProvider SessionProvider
Key crypto.PrivateKey
Certificate *x509.Certificate
MetadataURL url.URL
SSOURL url.URL
ServiceProviderProvider ServiceProviderProvider
SessionProvider SessionProvider
}

// Metadata returns the metadata structure for this identity provider.
Expand Down Expand Up @@ -206,12 +216,16 @@ func (idp *IdentityProvider) ServeIDPInitiated(w http.ResponseWriter, r *http.Re
return
}

var ok bool
req.ServiceProviderMetadata, ok = idp.ServiceProviders[serviceProviderID]
if !ok {
var err error
req.ServiceProviderMetadata, err = idp.ServiceProviderProvider.GetServiceProvider(r, serviceProviderID)
if err == os.ErrNotExist {
log.Printf("cannot find service provider: %s", serviceProviderID)
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
} else if err != nil {
log.Printf("cannot find service provider %s: %v", serviceProviderID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}

for _, endpoint := range req.ServiceProviderMetadata.SPSSODescriptor.AssertionConsumerService {
Expand Down Expand Up @@ -303,10 +317,12 @@ func (req *IdpAuthnRequest) Validate() error {
}

// find the service provider
serviceProvider, serviceProviderFound := req.IDP.ServiceProviders[req.Request.Issuer.Value]
if !serviceProviderFound {
return fmt.Errorf("cannot handle request from unknown service provider %s",
req.Request.Issuer.Value)
serviceProviderID := req.Request.Issuer.Value
serviceProvider, err := req.IDP.ServiceProviderProvider.GetServiceProvider(req.HTTPRequest, serviceProviderID)
if err == os.ErrNotExist {
return fmt.Errorf("cannot handle request from unknown service provider %s", serviceProviderID)
} else if err != nil {
return fmt.Errorf("cannot find service provider %s: %v", serviceProviderID, err)
}
req.ServiceProviderMetadata = serviceProvider

Expand Down
28 changes: 22 additions & 6 deletions identity_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import (
"strings"
"time"

"os"

"github.com/beevik/etree"
"github.com/crewjam/saml/testsaml"
"github.com/crewjam/saml/xmlenc"
Expand Down Expand Up @@ -121,11 +123,18 @@ OwJlNCASPZRH/JmF8tX0hoHuAQ==
test.Certificate = mustParseCertificate("-----BEGIN CERTIFICATE-----\nMIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJV\nUzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0\nMB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMx\nCzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCB\nnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9\nibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmH\nO8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKv\nRsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgk\nakpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeT\nQLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvn\nOwJlNCASPZRH/JmF8tX0hoHuAQ==\n-----END CERTIFICATE-----\n")

test.IDP = IdentityProvider{
Key: test.Key,
Certificate: test.Certificate,
MetadataURL: mustParseURL("https://idp.example.com/saml/metadata"),
SSOURL: mustParseURL("https://idp.example.com/saml/sso"),
ServiceProviders: map[string]*Metadata{},
Key: test.Key,
Certificate: test.Certificate,
MetadataURL: mustParseURL("https://idp.example.com/saml/metadata"),
SSOURL: mustParseURL("https://idp.example.com/saml/sso"),
ServiceProviderProvider: &mockServiceProviderProvider{
GetServiceProviderFunc: func(r *http.Request, serviceProviderID string) (*Metadata, error) {
if serviceProviderID == test.SP.MetadataURL.String() {
return test.SP.Metadata(), nil
}
return nil, os.ErrNotExist
},
},
SessionProvider: &mockSessionProvider{
GetSessionFunc: func(w http.ResponseWriter, r *http.Request, req *IdpAuthnRequest) *Session {
return nil
Expand All @@ -135,7 +144,6 @@ OwJlNCASPZRH/JmF8tX0hoHuAQ==

// bind the service provider and the IDP
test.SP.IDPMetadata = test.IDP.Metadata()
test.IDP.ServiceProviders[test.SP.MetadataURL.String()] = test.SP.Metadata()
}

type mockSessionProvider struct {
Expand All @@ -146,6 +154,14 @@ func (msp *mockSessionProvider) GetSession(w http.ResponseWriter, r *http.Reques
return msp.GetSessionFunc(w, r, req)
}

type mockServiceProviderProvider struct {
GetServiceProviderFunc func(r *http.Request, serviceProviderID string) (*Metadata, error)
}

func (mspp *mockServiceProviderProvider) GetServiceProvider(r *http.Request, serviceProviderID string) (*Metadata, error) {
return mspp.GetServiceProviderFunc(r, serviceProviderID)
}

func (test *IdentityProviderTest) TestCanProduceMetadata(c *C) {
c.Assert(test.IDP.Metadata(), DeepEquals, &Metadata{
ValidUntil: TimeNow().Add(DefaultValidDuration),
Expand Down
18 changes: 10 additions & 8 deletions samlidp/samlidp.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,10 @@ type Options struct {
// /shortcuts - RESTful interface to Shortcut objects
type Server struct {
http.Handler
idpConfigMu sync.RWMutex // protects calls into the IDP
IDP saml.IdentityProvider // the underlying IDP
Store Store // the data store
idpConfigMu sync.RWMutex // protects calls into the IDP
serviceProviders map[string]*saml.Metadata
IDP saml.IdentityProvider // the underlying IDP
Store Store // the data store
}

// New returns a new Server
Expand All @@ -46,16 +47,17 @@ func New(opts Options) (*Server, error) {
ssoURL := opts.URL
ssoURL.Path = ssoURL.Path + "/sso"
s := &Server{
serviceProviders: map[string]*saml.Metadata{},
IDP: saml.IdentityProvider{
Key: opts.Key,
Certificate: opts.Certificate,
MetadataURL: metadataURL,
SSOURL: ssoURL,
ServiceProviders: map[string]*saml.Metadata{},
Key: opts.Key,
Certificate: opts.Certificate,
MetadataURL: metadataURL,
SSOURL: ssoURL,
},
Store: opts.Store,
}
s.IDP.SessionProvider = s
s.IDP.ServiceProviderProvider = s

if err := s.initializeServices(); err != nil {
return nil, err
Expand Down
2 changes: 1 addition & 1 deletion samlidp/samlidp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ OwJlNCASPZRH/JmF8tX0hoHuAQ==
c.Assert(err, IsNil)

test.SP.IDPMetadata = test.Server.IDP.Metadata()
test.Server.IDP.ServiceProviders["https://sp.example.com/saml2/metadata"] = test.SP.Metadata()
test.Server.serviceProviders["https://sp.example.com/saml2/metadata"] = test.SP.Metadata()
}

func (test *ServerTest) TestHTTPCanHandleMetadataRequest(c *C) {
Expand Down
21 changes: 18 additions & 3 deletions samlidp/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"log"
"net/http"
"os"

"github.com/crewjam/saml"
"github.com/zenazn/goji/web"
Expand All @@ -20,6 +21,20 @@ type Service struct {
Metadata saml.Metadata
}

// GetServiceProvider returns the Service Provider metadata for the
// service provider ID, which is typically the service provider's
// metadata URL. If an appropriate service provider cannot be found then
// the returned error must be os.ErrNotExist.
func (s *Server) GetServiceProvider(r *http.Request, serviceProviderID string) (*saml.Metadata, error) {
s.idpConfigMu.RLock()
defer s.idpConfigMu.RUnlock()
rv, ok := s.serviceProviders[serviceProviderID]
if !ok {
return nil, os.ErrNotExist
}
return rv, nil
}

// HandleListServices handles the `GET /services/` request and responds with a JSON formatted list
// of service names.
func (s *Server) HandleListServices(c web.C, w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -66,7 +81,7 @@ func (s *Server) HandlePutService(c web.C, w http.ResponseWriter, r *http.Reques
}

s.idpConfigMu.Lock()
s.IDP.ServiceProviders[service.Metadata.EntityID] = &service.Metadata
s.serviceProviders[service.Metadata.EntityID] = &service.Metadata
s.idpConfigMu.Unlock()

w.WriteHeader(http.StatusNoContent)
Expand All @@ -89,7 +104,7 @@ func (s *Server) HandleDeleteService(c web.C, w http.ResponseWriter, r *http.Req
}

s.idpConfigMu.Lock()
delete(s.IDP.ServiceProviders, service.Metadata.EntityID)
delete(s.serviceProviders, service.Metadata.EntityID)
s.idpConfigMu.Unlock()

w.WriteHeader(http.StatusNoContent)
Expand All @@ -109,7 +124,7 @@ func (s *Server) initializeServices() error {
}

s.idpConfigMu.Lock()
s.IDP.ServiceProviders[service.Metadata.EntityID] = &service.Metadata
s.serviceProviders[service.Metadata.EntityID] = &service.Metadata
s.idpConfigMu.Unlock()
}
return nil
Expand Down
4 changes: 2 additions & 2 deletions samlidp/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func (test *ServerTest) TestServicesCrud(c *C) {
c.Assert(w.Code, Equals, http.StatusOK)
c.Assert(string(w.Body.Bytes()), Equals, "{\"services\":[\"sp\"]}\n")

c.Assert(test.Server.IDP.ServiceProviders, HasLen, 2)
c.Assert(test.Server.serviceProviders, HasLen, 2)

w = httptest.NewRecorder()
r, _ = http.NewRequest("DELETE", "https://idp.example.com/services/sp", nil)
Expand All @@ -46,5 +46,5 @@ func (test *ServerTest) TestServicesCrud(c *C) {
test.Server.ServeHTTP(w, r)
c.Assert(w.Code, Equals, http.StatusOK)
c.Assert(string(w.Body.Bytes()), Equals, "{\"services\":[]}\n")
c.Assert(test.Server.IDP.ServiceProviders, HasLen, 1)
c.Assert(test.Server.serviceProviders, HasLen, 1)
}

0 comments on commit f102ca0

Please sign in to comment.