Permalink
Browse files

Implement SAML 2 provider

  • Loading branch information...
mraerino committed Aug 21, 2018
1 parent 2983e32 commit 43405a3ac89c20e0961dec72ac27da43eb74184d
Showing with 269 additions and 6 deletions.
  1. +9 −0 api/api.go
  2. +16 −4 api/external.go
  3. +71 −0 api/external_saml.go
  4. +144 −0 api/provider/saml.go
  5. +2 −0 api/settings.go
  6. +8 −0 conf/configuration.go
  7. +16 −2 glide.lock
  8. +3 −0 glide.yaml
View
@@ -121,6 +121,15 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati
})
})
})
r.Route("/saml", func(r *router) {
r.Route("/acs", func(r *router) {
r.Use(api.loadSAMLState)
r.Post("/", api.ExternalProviderCallback)
})
r.Get("/metadata", api.SAMLMetadata)
})
})
if globalConfig.MultiInstanceMode {
View
@@ -88,14 +88,24 @@ func (a *API) internalExternalProviderCallback(w http.ResponseWriter, r *http.Re
instanceID := getInstanceID(ctx)
providerType := getExternalProviderType(ctx)
userData, err := a.oAuthCallback(r, ctx, providerType)
if err != nil {
return err
var userData *provider.UserProvidedData
if providerType == "saml" {
samlUserData, err := a.samlCallback(r, ctx)
if err != nil {
return err
}
userData = samlUserData
} else {
oAuthUserData, err := a.oAuthCallback(r, ctx, providerType)
if err != nil {
return err
}
userData = oAuthUserData
}
var user *models.User
var token *AccessTokenResponse
err = a.db.Transaction(func(tx *storage.Connection) error {
err := a.db.Transaction(func(tx *storage.Connection) error {
var terr error
inviteToken := getInviteToken(ctx)
if inviteToken != "" {
@@ -266,6 +276,8 @@ func (a *API) Provider(ctx context.Context, name string) (provider.Provider, err
return provider.NewGoogleProvider(config.External.Google)
case "facebook":
return provider.NewFacebookProvider(config.External.Facebook)
case "saml":
return provider.NewSamlProvider(config.External.Saml)
default:
return nil, fmt.Errorf("Provider %s could not be found", name)
}
View
@@ -0,0 +1,71 @@
package api
import (
"context"
"net/http"
"github.com/netlify/gotrue/api/provider"
"github.com/pkg/errors"
)
func (a *API) loadSAMLState(w http.ResponseWriter, r *http.Request) (context.Context, error) {
state := r.FormValue("RelayState")
if state == "" {
return nil, errors.New("SAML RelayState is missing")
}
ctx := r.Context()
return a.loadExternalState(ctx, state)
}
func (a *API) samlCallback(r *http.Request, ctx context.Context) (*provider.UserProvidedData, error) {
config := a.getConfig(ctx)
samlProvider, err := provider.NewSamlProvider(config.External.Saml)
if err != nil {
return nil, badRequestError("Could not initialize SAML provider: %+v", err).WithInternalError(err)
}
samlResponse := r.FormValue("SAMLResponse")
if samlResponse == "" {
return nil, badRequestError("SAML Response is missing")
}
assertionInfo, err := samlProvider.ServiceProvider.RetrieveAssertionInfo(samlResponse)
if err != nil {
return nil, internalServerError("Parsing SAML assertion failed: %+v", err).WithInternalError(err)
}
if assertionInfo.WarningInfo.InvalidTime {
return nil, forbiddenError("SAML response has invalid time")
}
if assertionInfo.WarningInfo.NotInAudience {
return nil, forbiddenError("SAML response is not in audience")
}
if assertionInfo == nil {
return nil, internalServerError("SAML Assertion is missing")
}
userData := &provider.UserProvidedData{
Email: assertionInfo.NameID,
Verified: true,
}
return userData, nil
}
func (a *API) SAMLMetadata(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
config := getConfig(ctx)
samlProvider, err := provider.NewSamlProvider(config.External.Saml)
if err != nil {
return internalServerError("Could not create SAML Provider: %+v", err).WithInternalError(err)
}
metadata, err := samlProvider.SPMetadata()
w.Header().Set("Content-Type", "application/xml")
w.Write(metadata)
return nil
}
View
@@ -0,0 +1,144 @@
package provider
import (
"crypto/x509"
"encoding/base64"
"encoding/xml"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
"github.com/netlify/gotrue/conf"
saml2 "github.com/russellhaering/gosaml2"
"github.com/russellhaering/gosaml2/types"
dsig "github.com/russellhaering/goxmldsig"
"golang.org/x/oauth2"
)
type SamlProvider struct {
ServiceProvider *saml2.SAMLServiceProvider
}
func getMetadata(url string) (*types.EntityDescriptor, error) {
res, err := http.Get(url)
if err != nil {
return nil, err
}
rawMetadata, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
metadata := &types.EntityDescriptor{}
err = xml.Unmarshal(rawMetadata, metadata)
if err != nil {
return nil, err
}
// TODO: cache in memory
return metadata, nil
}
// NewSamlProvider creates a Saml account provider.
func NewSamlProvider(ext conf.SamlProviderConfiguration) (*SamlProvider, error) {
if !ext.Enabled {
return nil, errors.New("SAML Provider is not enabled")
}
if _, err := url.Parse(ext.MetadataURL); err != nil {
return nil, fmt.Errorf("Metadata URL is invalid: %+v", err)
}
meta, err := getMetadata(ext.MetadataURL)
if err != nil {
return nil, err
}
baseURI, err := url.Parse(strings.Trim(ext.APIBase, "/"))
if err != nil || ext.APIBase == "" {
return nil, fmt.Errorf("Invalid API base URI: %s", ext.APIBase)
}
var ssoService types.SingleSignOnService
foundService := false
for _, service := range meta.IDPSSODescriptor.SingleSignOnServices {
if service.Binding == "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" {
ssoService = service
foundService = true
break
}
}
if !foundService {
return nil, errors.New("No valid SSO service found in IDP metadata")
}
certStore := dsig.MemoryX509CertificateStore{
Roots: []*x509.Certificate{},
}
for _, kd := range meta.IDPSSODescriptor.KeyDescriptors {
for _, xcert := range kd.KeyInfo.X509Data.X509Certificates {
if xcert.Data == "" {
continue
}
certData, err := base64.StdEncoding.DecodeString(xcert.Data)
if err != nil {
continue
}
idpCert, err := x509.ParseCertificate(certData)
if err != nil {
continue
}
certStore.Roots = append(certStore.Roots, idpCert)
}
}
// TODO: generate keys once, save them in the database and use here
randomKeyStore := dsig.RandomKeyStoreForTest()
sp := &saml2.SAMLServiceProvider{
IdentityProviderSSOURL: ssoService.Location,
IdentityProviderIssuer: meta.EntityID,
AssertionConsumerServiceURL: baseURI.String() + "/saml/acs",
ServiceProviderIssuer: baseURI.String() + "/saml",
SignAuthnRequests: true,
AudienceURI: baseURI.String() + "/saml",
IDPCertificateStore: &certStore,
SPKeyStore: randomKeyStore,
AllowMissingAttributes: true,
}
p := &SamlProvider{
ServiceProvider: sp,
}
return p, nil
}
func (p SamlProvider) AuthCodeURL(tokenString string, args ...oauth2.AuthCodeOption) string {
url, err := p.ServiceProvider.BuildAuthURL(tokenString)
if err != nil {
return ""
}
return url
}
func (p SamlProvider) SPMetadata() ([]byte, error) {
metadata, err := p.ServiceProvider.Metadata()
if err != nil {
return nil, err
}
rawMetadata, err := xml.Marshal(metadata)
if err != nil {
return nil, err
}
return rawMetadata, nil
}
View
@@ -9,6 +9,7 @@ type ProviderSettings struct {
Google bool `json:"google"`
Facebook bool `json:"facebook"`
Email bool `json:"email"`
SAML bool `json:"saml"`
}
type Settings struct {
@@ -28,6 +29,7 @@ func (a *API) Settings(w http.ResponseWriter, r *http.Request) error {
Google: config.External.Google.Enabled,
Facebook: config.External.Facebook.Enabled,
Email: !config.External.Email.Disabled,
SAML: config.External.Saml.Enabled,
},
DisableSignup: config.DisableSignup,
Autoconfirm: config.Mailer.Autoconfirm,
View
@@ -25,6 +25,13 @@ type EmailProviderConfiguration struct {
Disabled bool `json:"disabled"`
}
type SamlProviderConfiguration struct {
Enabled bool `json:"enabled"`
MetadataURL string `json:"metadata_url" envconfig:"METADATA_URL"`
APIBase string `json:"api_base" envconfig:"API_BASE"`
Name string `json:"name"`
}
// DBConfiguration holds all the database related configuration.
type DBConfiguration struct {
Driver string `json:"driver" required:"true"`
@@ -73,6 +80,7 @@ type ProviderConfiguration struct {
Google OAuthProviderConfiguration `json:"google"`
Facebook OAuthProviderConfiguration `json:"facebook"`
Email EmailProviderConfiguration `json:"email"`
Saml SamlProviderConfiguration `json:"saml"`
RedirectURL string `json:"redirect_url"`
}
View

Some generated files are not rendered by default. Learn more.

Oops, something went wrong.
View
@@ -51,3 +51,6 @@ import:
version: v1.2.0
- package: github.com/gobuffalo/pop
version: 3.42.1
- package: github.com/russellhaering/gosaml2
version: ~0.3.1
- package: github.com/russellhaering/goxmldsig

0 comments on commit 43405a3

Please sign in to comment.