Commit 2cd5254c authored by ale's avatar ale

Set the logout URL on the SAML server descriptor

Update the crewjam/saml library too.
parent 01df95f6
......@@ -305,6 +305,12 @@ func NewSAMLIDP(config *Config) (http.Handler, error) {
svc += "/"
}
logoutURL, err := url.Parse(
strings.TrimRight(config.SSOLoginServerURL, "/") + "/logout")
if err != nil {
return nil, err
}
sp, err := newSessionProvider(config)
if err != nil {
return nil, err
......@@ -319,6 +325,7 @@ func NewSAMLIDP(config *Config) (http.Handler, error) {
Logger: logger.DefaultLogger,
MetadataURL: metadataURL,
SSOURL: ssoURL,
LogoutURL: *logoutURL,
ServiceProviderProvider: config,
SessionProvider: sp,
}
......
# Gopkg.toml example
#
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
[[constraint]]
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
name = "github.com/beevik/etree"
version = "1.0.0"
packages = ["."]
revision = "9d7e8feddccb4ed1b8afb54e368bd323d2ff652c"
version = "v1.0.1"
[[constraint]]
[[projects]]
name = "github.com/crewjam/saml"
packages = [
".",
"logger",
"samlidp",
"samlsp",
"testsaml",
"xmlenc"
]
revision = "6b5dd2d26974f7f5e59132ef5921fab7993794d7"
version = "0.2.0"
[[projects]]
branch = "master"
name = "github.com/dchest/uniuri"
packages = ["."]
revision = "8902c56451e9b58ff940bbe5fec35d5f9c04584a"
[[constraint]]
[[projects]]
name = "github.com/dgrijalva/jwt-go"
version = "3.0.0"
packages = ["."]
revision = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e"
version = "v3.2.0"
[[constraint]]
branch = "master"
[[projects]]
name = "github.com/jonboulle/clockwork"
packages = ["."]
revision = "2eee05ed794112d45db504eb05aa693efd2b8b09"
version = "v0.1.0"
[[projects]]
name = "github.com/kr/pretty"
packages = ["."]
revision = "73f6ac0b30a98e433b289500d779f50c1a6f0712"
version = "v0.1.0"
[[projects]]
name = "github.com/kr/text"
packages = ["."]
revision = "e2ffdb16a802fe2bb95e2e35ff34f0e53aeef34f"
version = "v0.1.0"
[[constraint]]
[[projects]]
branch = "master"
name = "github.com/russellhaering/goxmldsig"
packages = [
".",
"etreeutils",
"types"
]
revision = "7acd5e4a6ef74fe1b082c20f119556adf70c3944"
[[constraint]]
[[projects]]
name = "github.com/zenazn/goji"
version = "1.0.0"
packages = [
".",
"bind",
"graceful",
"graceful/listener",
"web",
"web/middleware",
"web/mutil"
]
revision = "64eb34159fe53473206c2b3e70fe396a639452f2"
version = "v1.0"
[[constraint]]
[[projects]]
branch = "master"
name = "golang.org/x/crypto"
packages = [
"bcrypt",
"blowfish",
"ripemd160"
]
revision = "c126467f60eb25f8f27e5a981f32a87e3965053f"
[[constraint]]
[[projects]]
branch = "v1"
name = "gopkg.in/check.v1"
packages = ["."]
revision = "788fd78401277ebd861206a03c884797c6ec5541"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "e95ae53367b806651c34d425c22f40bade70c9db018c9b619b37f4c8405acb65"
solver-name = "gps-cdcl"
solver-version = 1
* https://github.com/lestrrat/go-libxml2
* https://github.com/onelogin/python-saml
- reads settings from a JSON file (yuck)
- BSD License (3-clause)
* TODO: understand xml bomb (https://pypi.python.org/pypi/defusedxml)
* Providers for SAML SP & IDP endpoints
* Methods for generating and authenticating various SAML flows
Current working demo:
term1: go run ./example/idp/idp.go -bind :8001
term2: ngrok http 8001
term4: ngrok http 8000
edit example.go and fill in values for baseURL (term4) and idpMetadataURL (term2)
term3: go run ./example/example.go -bind :8000
term5: curl -v https://$SP.ngrok.io/saml/metadata | curl -v -H "Content-type: text/xml" --data-binary @- https://$IDP.ngrok.io/register-sp
browser: https://$SP.ngrok.io
# SAML
[![](https://godoc.org/github.com/crewjam/saml?status.svg)](http://godoc.org/github.com/crewjam/saml)
[![Build Status](https://travis-ci.org/crewjam/saml.svg?branch=master)](https://travis-ci.org/crewjam/saml)
Package saml contains a partial implementation of the SAML standard in golang.
SAML is a standard for identity federation, i.e. either allowing a third party to authenticate your users or allowing third parties to rely on us to authenticate their users.
## Introduction
In SAML parlance an **Identity Provider** (IDP) is a service that knows how to authenticate users. A **Service Provider** (SP) is a service that delegates authentication to an IDP. If you are building a service where users log in with someone else's credentials, then you are a **Service Provider**. This package supports implementing both service providers and identity providers.
The core package contains the implementation of SAML. The package samlsp provides helper middleware suitable for use in Service Provider applications. The package samlidp provides a rudimentary IDP service that is useful for testing or as a starting point for other integrations.
## Breaking Changes
......@@ -20,18 +33,9 @@ In various places where keys and certificates were modeled as `string`
<= version 0.1.0 (what was I thinking?!) they are now modeled as
`*rsa.PrivateKey`, `*x509.Certificate`, or `crypto.PrivateKey` as appropriate.
## Introduction
Package saml contains a partial implementation of the SAML standard in golang.
SAML is a standard for identity federation, i.e. either allowing a third party to authenticate your users or allowing third parties to rely on us to authenticate their users.
In SAML parlance an **Identity Provider** (IDP) is a service that knows how to authenticate users. A **Service Provider** (SP) is a service that delegates authentication to an IDP. If you are building a service where users log in with someone else's credentials, then you are a **Service Provider**. This package supports implementing both service providers and identity providers.
The core package contains the implementation of SAML. The package samlsp provides helper middleware suitable for use in Service Provider applications. The package samlidp provides a rudimentary IDP service that is useful for testing or as a starting point for other integrations.
## Getting Started as a Service Provider
Let us assume we have a simple web appliation to protect. We'll modify this application so it uses SAML to authenticate users.
Let us assume we have a simple web application to protect. We'll modify this application so it uses SAML to authenticate users.
```golang
package main
......@@ -68,7 +72,7 @@ import (
)
func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %s!", r.Header.Get("X-Saml-Cn"))
fmt.Fprintf(w, "Hello, %s!", samlsp.Token(r.Context()).Attributes.Get("cn"))
}
func main() {
......@@ -104,26 +108,26 @@ func main() {
}
```
Next we'll have to register our service provider with the identiy provider to establish trust from the service provider to the IDP. For [testshib.org](https://www.testshib.org/), you can do something like:
Next we'll have to register our service provider with the identity provider to establish trust from the service provider to the IDP. For [testshib.org](https://www.testshib.org/), you can do something like:
mdpath=saml-test-$USER-$HOST.xml
curl localhost:8000/saml/metadata > $mdpath
Naviate to https://www.testshib.org/register.html and upload the file you fetched.
Navigate to https://www.testshib.org/register.html and upload the file you fetched.
Now you should be able to authenticate. The flow should look like this:
1. You browse to `localhost:8000/hello`
2. The middleware redirects you to `https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO`
1. The middleware redirects you to `https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO`
3. testshib.org prompts you for a username and password.
1. testshib.org prompts you for a username and password.
4. testshib.org returns you an HTML document which contains an HTML form setup to POST to `localhost:8000/saml/acs`. The form is automatically submitted if you have javascript enabled.
1. testshib.org returns you an HTML document which contains an HTML form setup to POST to `localhost:8000/saml/acs`. The form is automatically submitted if you have javascript enabled.
5. The local service validates the response, issues a session cookie, and redirects you to the original URL, `localhost:8000/hello`.
1. The local service validates the response, issues a session cookie, and redirects you to the original URL, `localhost:8000/hello`.
6. This time when `localhost:8000/hello` is requested there is a valid session and so the main content is served.
1. This time when `localhost:8000/hello` is requested there is a valid session and so the main content is served.
## Getting Started as an Identity Provider
......@@ -139,7 +143,7 @@ The package supports signed and encrypted SAML assertions. It does not support s
## RelayState
The *RelayState* parameter allows you to pass user state information across the authentication flow. The most common use for this is to allow a user to request a deep link into your site, be redirected through the SAML login flow, and upon successful completion, be directed to the originaly requested link, rather than the root.
The *RelayState* parameter allows you to pass user state information across the authentication flow. The most common use for this is to allow a user to request a deep link into your site, be redirected through the SAML login flow, and upon successful completion, be directed to the originally requested link, rather than the root.
Unfortunately, *RelayState* is less useful than it could be. Firstly, it is **not** authenticated, so anything you supply must be signed to avoid XSS or CSRF. Secondly, it is limited to 80 bytes in length, which precludes signing. (See section 3.6.3.1 of SAMLProfiles.)
......@@ -159,4 +163,4 @@ The SAML specification is a collection of PDFs (sadly):
## Security Issues
Please do not report security issues in the issue tracker. Rather, please contact me directly at ross@kndr.org ([PGP Key `8EA205C01C425FF195A5E9A43FA0768F26FD2554`](https://keybase.io/crewjam)).
Please do not report security issues in the issue tracker. Rather, please contact me directly at ross@kndr.org ([PGP Key `78B6038B3B9DFB88`](https://keybase.io/crewjam)).
......@@ -91,18 +91,21 @@ type IdentityProvider struct {
Key crypto.PrivateKey
Logger logger.Interface
Certificate *x509.Certificate
Intermediates []*x509.Certificate
MetadataURL url.URL
SSOURL url.URL
LogoutURL url.URL
ServiceProviderProvider ServiceProviderProvider
SessionProvider SessionProvider
AssertionMaker AssertionMaker
SignatureMethod string
}
// Metadata returns the metadata structure for this identity provider.
func (idp *IdentityProvider) Metadata() *EntityDescriptor {
certStr := base64.StdEncoding.EncodeToString(idp.Certificate.Raw)
return &EntityDescriptor{
ed := &EntityDescriptor{
EntityID: idp.MetadataURL.String(),
ValidUntil: TimeNow().Add(DefaultValidDuration),
CacheDuration: DefaultValidDuration,
......@@ -147,6 +150,17 @@ func (idp *IdentityProvider) Metadata() *EntityDescriptor {
},
},
}
if idp.LogoutURL.String() != "" {
ed.IDPSSODescriptors[0].SSODescriptor.SingleLogoutServices = []Endpoint{
{
Binding: HTTPRedirectBinding,
Location: idp.LogoutURL.String(),
},
}
}
return ed
}
// Handler returns an http.Handler that serves the metadata and SSO
......@@ -228,6 +242,7 @@ func (idp *IdentityProvider) ServeIDPInitiated(w http.ResponseWriter, r *http.Re
IDP: idp,
HTTPRequest: r,
RelayState: relayState,
Now: TimeNow(),
}
session := idp.SessionProvider.GetSession(w, r, req)
......@@ -298,6 +313,7 @@ type IdpAuthnRequest struct {
Assertion *Assertion
AssertionEl *etree.Element
ResponseEl *etree.Element
Now time.Time
}
// NewIdpAuthnRequest returns a new IdpAuthnRequest for the given HTTP request to the authorization
......@@ -306,6 +322,7 @@ func NewIdpAuthnRequest(idp *IdentityProvider, r *http.Request) (*IdpAuthnReques
req := &IdpAuthnRequest{
IDP: idp,
HTTPRequest: r,
Now: TimeNow(),
}
switch r.Method {
......@@ -375,7 +392,7 @@ func (req *IdpAuthnRequest) Validate() error {
}
}
if req.Request.IssueInstant.Add(MaxIssueDelay).Before(TimeNow()) {
if req.Request.IssueInstant.Add(MaxIssueDelay).Before(req.Now) {
return fmt.Errorf("request expired at %s",
req.Request.IssueInstant.Add(MaxIssueDelay))
}
......@@ -426,6 +443,36 @@ func (req *IdpAuthnRequest) getACSEndpoint() error {
}
}
// Some service providers, like the Microsoft Azure AD service provider, issue
// assertion requests that don't specify an ACS url at all.
if req.Request.AssertionConsumerServiceURL == "" && req.Request.AssertionConsumerServiceIndex == "" {
// find a default ACS binding in the metadata that we can use
for _, spssoDescriptor := range req.ServiceProviderMetadata.SPSSODescriptors {
for _, spAssertionConsumerService := range spssoDescriptor.AssertionConsumerServices {
if spAssertionConsumerService.IsDefault != nil && *spAssertionConsumerService.IsDefault {
switch spAssertionConsumerService.Binding {
case HTTPPostBinding, HTTPRedirectBinding:
req.SPSSODescriptor = &spssoDescriptor
req.ACSEndpoint = &spAssertionConsumerService
return nil
}
}
}
}
// if we can't find a default, use *any* ACS binding
for _, spssoDescriptor := range req.ServiceProviderMetadata.SPSSODescriptors {
for _, spAssertionConsumerService := range spssoDescriptor.AssertionConsumerServices {
switch spAssertionConsumerService.Binding {
case HTTPPostBinding, HTTPRedirectBinding:
req.SPSSODescriptor = &spssoDescriptor
req.ACSEndpoint = &spAssertionConsumerService
return nil
}
}
}
}
return os.ErrNotExist // no ACS url found or specified
}
......@@ -591,8 +638,8 @@ func (DefaultAssertionMaker) MakeAssertion(req *IdpAuthnRequest, session *Sessio
// allow for some clock skew in the validity period using the
// issuer's apparent clock.
notBefore := TimeNow().Add(-1 * MaxClockSkew)
notOnOrAfterAfter := notBefore.Add(MaxClockSkew).Add(MaxIssueDelay)
notBefore := req.Now.Add(-1 * MaxClockSkew)
notOnOrAfterAfter := req.Now.Add(MaxIssueDelay)
if notBefore.Before(req.Request.IssueInstant) {
notBefore = req.Request.IssueInstant
notOnOrAfterAfter = notBefore.Add(MaxIssueDelay)
......@@ -619,7 +666,7 @@ func (DefaultAssertionMaker) MakeAssertion(req *IdpAuthnRequest, session *Sessio
SubjectConfirmationData: &SubjectConfirmationData{
Address: req.HTTPRequest.RemoteAddr,
InResponseTo: req.Request.ID,
NotOnOrAfter: TimeNow().Add(MaxIssueDelay),
NotOnOrAfter: req.Now.Add(MaxIssueDelay),
Recipient: req.ACSEndpoint.Location,
},
},
......@@ -669,11 +716,19 @@ func (req *IdpAuthnRequest) MakeAssertionEl() error {
PrivateKey: req.IDP.Key,
Leaf: req.IDP.Certificate,
}
for _, cert := range req.IDP.Intermediates {
keyPair.Certificate = append(keyPair.Certificate, cert.Raw)
}
keyStore := dsig.TLSCertKeyStore(keyPair)
signatureMethod := req.IDP.SignatureMethod
if signatureMethod == "" {
signatureMethod = dsig.RSASHA1SignatureMethod
}
signingContext := dsig.NewDefaultSigningContext(keyStore)
signingContext.Canonicalizer = dsig.MakeC14N10ExclusiveCanonicalizerWithPrefixList(canonicalizerPrefixList)
if err := signingContext.SetSignatureMethod(dsig.RSASHA1SignatureMethod); err != nil {
if err := signingContext.SetSignatureMethod(signatureMethod); err != nil {
return err
}
......@@ -842,7 +897,7 @@ func (req *IdpAuthnRequest) MakeResponse() error {
Destination: req.ACSEndpoint.Location,
ID: fmt.Sprintf("id-%x", randomBytes(20)),
InResponseTo: req.Request.ID,
IssueInstant: TimeNow(),
IssueInstant: req.Now,
Version: "2.0",
Issuer: &Issuer{
Format: "urn:oasis:names:tc:SAML:2.0:nameid-format:entity",
......@@ -865,11 +920,19 @@ func (req *IdpAuthnRequest) MakeResponse() error {
PrivateKey: req.IDP.Key,
Leaf: req.IDP.Certificate,
}
for _, cert := range req.IDP.Intermediates {
keyPair.Certificate = append(keyPair.Certificate, cert.Raw)
}
keyStore := dsig.TLSCertKeyStore(keyPair)
signatureMethod := req.IDP.SignatureMethod
if signatureMethod == "" {
signatureMethod = dsig.RSASHA1SignatureMethod
}
signingContext := dsig.NewDefaultSigningContext(keyStore)
signingContext.Canonicalizer = dsig.MakeC14N10ExclusiveCanonicalizerWithPrefixList(canonicalizerPrefixList)
if err := signingContext.SetSignatureMethod(dsig.RSASHA1SignatureMethod); err != nil {
if err := signingContext.SetSignatureMethod(signatureMethod); err != nil {
return err
}
......
//
// Package saml contains a partial implementation of the SAML standard in golang.
// SAML is a standard for identity federation, i.e. either allowing a third party to authenticate your users or allowing third parties to rely on us to authenticate their users.
//
//
// Introduction
//
// In SAML parlance an Identity Provider (IDP) is a service that knows how to authenticate users. A Service Provider (SP) is a service that delegates authentication to an IDP. If you are building a service where users log in with someone else's credentials, then you are a Service Provider. This package supports implementing both service providers and identity providers.
//
//
// The core package contains the implementation of SAML. The package samlsp provides helper middleware suitable for use in Service Provider applications. The package samlidp provides a rudimentary IDP service that is useful for testing or as a starting point for other integrations.
//
//
// Breaking Changes
//
// Note: between version 0.2.0 and the current master include changes to the API
// that will break your existing code a little.
//
// This change turned some fields from pointers to a single optional struct into
// the more correct slice of struct, and to pluralize the field name. For example,
// `IDPSSODescriptor *IDPSSODescriptor` has become
// `IDPSSODescriptors []IDPSSODescriptor`. This more accurately reflects the
// standard.
//
// The struct `Metadata` has been renamed to `EntityDescriptor`. In 0.2.0 and before,
// every struct derived from the standard has the same name as in the standard,
// *except* for `Metadata` which should always have been called `EntityDescriptor`.
//
// In various places `url.URL` is now used where `string` was used <= version 0.1.0.
//
// In various places where keys and certificates were modeled as `string`
// <= version 0.1.0 (what was I thinking?!) they are now modeled as
// `*rsa.PrivateKey`, `*x509.Certificate`, or `crypto.PrivateKey` as appropriate.
//
// Getting Started as a Service Provider
//
//
// Let us assume we have a simple web appliation to protect. We'll modify this application so it uses SAML to authenticate users.
//
// package main
//
// import "net/http"
//
// func hello(w http.ResponseWriter, r *http.Request) {
// fmt.Fprintf(w, "Hello, World!")
// })
//
// func main() {
// app := http.HandlerFunc(hello)
// http.Handle("/hello", app)
// http.ListenAndServe(":8000", nil)
// }
//
// ```golang
// package main
//
// import "net/http"
//
// func hello(w http.ResponseWriter, r *http.Request) {
// fmt.Fprintf(w, "Hello, World!")
// }
//
// func main() {
// app := http.HandlerFunc(hello)
// http.Handle("/hello", app)
// http.ListenAndServe(":8000", nil)
// }
// ```
// Each service provider must have an self-signed X.509 key pair established. You can generate your own with something like this:
//
//
// openssl req -x509 -newkey rsa:2048 -keyout myservice.key -out myservice.cert -days 365 -nodes -subj "/CN=myservice.example.com"
//
// We will use `samlsp.Middleware` to wrap the endpoint we want to protect. Middleware provides both an `http.Handler` to serve the SAML specific URLs and a set of wrappers to require the user to be logged in. We also provide the URL where the service provider can fetch the metadata from the IDP at startup. In our case, we'll use [testshib.org](testshib.org), an identity provider designed for testing.
//
// package main
//
// import (
// "fmt"
// "io/ioutil"
// "net/http"
//
// "github.com/crewjam/saml/samlsp"
// )
//
// func hello(w http.ResponseWriter, r *http.Request) {
// fmt.Fprintf(w, "Hello, %s!", r.Header.Get("X-Saml-Cn"))
// }
//
// func main() {
// key, _ := ioutil.ReadFile("myservice.key")
// cert, _ := ioutil.ReadFile("myservice.cert")
// samlSP, _ := samlsp.New(samlsp.Options{
// IDPMetadataURL: "https://www.testshib.org/metadata/testshib-providers.xml",
// URL: "http://localhost:8000",
// Key: string(key),
// Certificate: string(cert),
// })
// app := http.HandlerFunc(hello)
// http.Handle("/hello", samlSP.RequireAccount(app))
// http.Handle("/saml/", samlSP)
// http.ListenAndServe(":8000", nil)
// }
//
//
// Next we'll have to register our service provider with the identiy provider to establish trust from the service provider to the IDP. For [testshib.org](testshib.org), you can do something like:
//
//
// We will use `samlsp.Middleware` to wrap the endpoint we want to protect. Middleware provides both an `http.Handler` to serve the SAML specific URLs and a set of wrappers to require the user to be logged in. We also provide the URL where the service provider can fetch the metadata from the IDP at startup. In our case, we'll use [testshib.org](https://www.testshib.org/), an identity provider designed for testing.
//
// ```golang
// package main
//
// import (
// "crypto/rsa"
// "crypto/tls"
// "crypto/x509"
// "fmt"
// "net/http"
// "net/url"
//
// "github.com/crewjam/saml/samlsp"
// )
//
// func hello(w http.ResponseWriter, r *http.Request) {
// claims := samlsp.Claims(r.Context())
// fmt.Fprintf(w, "Hello, %s!", claims.Attributes["cn"][0])
// }
//
// func main() {
// keyPair, err := tls.LoadX509KeyPair("myservice.cert", "myservice.key")
// if err != nil {
// panic(err) // TODO handle error
// }
// keyPair.Leaf, err = x509.ParseCertificate(keyPair.Certificate[0])
// if err != nil {
// panic(err) // TODO handle error
// }
//
// idpMetadataURL, err := url.Parse("https://www.testshib.org/metadata/testshib-providers.xml")
// if err != nil {
// panic(err) // TODO handle error
// }
//
// rootURL, err := url.Parse("http://localhost:8000")
// if err != nil {
// panic(err) // TODO handle error
// }
//
// samlSP, _ := samlsp.New(samlsp.Options{
// URL: *rootURL,
// Key: keyPair.PrivateKey.(*rsa.PrivateKey),
// Certificate: keyPair.Leaf,
// IDPMetadataURL: idpMetadataURL,
// })
// app := http.HandlerFunc(hello)
// http.Handle("/hello", samlSP.RequireAccount(app))
// http.Handle("/saml/", samlSP)
// http.ListenAndServe(":8000", nil)
// }
// ```
//
// Next we'll have to register our service provider with the identiy provider to establish trust from the service provider to the IDP. For [testshib.org](https://www.testshib.org/), you can do something like:
//
// mdpath=saml-test-$USER-$HOST.xml
// curl localhost:8000/saml/metadata > $mdpath
// curl -i -F userfile=@$mdpath https://www.testshib.org/procupload.php
//
//
// Naviate to https://www.testshib.org/register.html and upload the file you fetched.
//
// Now you should be able to authenticate. The flow should look like this:
//
//
// 1. You browse to `localhost:8000/hello`
//
// 2. The middleware redirects you to `https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO`
//