Depedency: Bump crewjam/saml to the latest master (#20126)
* Dependency: Bump our SAML Library to the latest version Fixes a non-obligatory check for RSA encrypted assertions. Previously they required a certificate embedded in the assertion, this is not mandatory according to the SAML standard.
This commit is contained in:
+4
@@ -1,3 +1,7 @@
|
||||
coverage.out
|
||||
coverage.html
|
||||
vendor/
|
||||
|
||||
# IDE-specific settings
|
||||
.idea
|
||||
.vscode
|
||||
-98
@@ -1,98 +0,0 @@
|
||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/beevik/etree"
|
||||
packages = ["."]
|
||||
revision = "9d7e8feddccb4ed1b8afb54e368bd323d2ff652c"
|
||||
version = "v1.0.1"
|
||||
|
||||
[[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"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/dgrijalva/jwt-go"
|
||||
packages = ["."]
|
||||
revision = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e"
|
||||
version = "v3.2.0"
|
||||
|
||||
[[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"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/russellhaering/goxmldsig"
|
||||
packages = [
|
||||
".",
|
||||
"etreeutils",
|
||||
"types"
|
||||
]
|
||||
revision = "7acd5e4a6ef74fe1b082c20f119556adf70c3944"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/zenazn/goji"
|
||||
packages = [
|
||||
".",
|
||||
"bind",
|
||||
"graceful",
|
||||
"graceful/listener",
|
||||
"web",
|
||||
"web/middleware",
|
||||
"web/mutil"
|
||||
]
|
||||
revision = "64eb34159fe53473206c2b3e70fe396a639452f2"
|
||||
version = "v1.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = [
|
||||
"bcrypt",
|
||||
"blowfish",
|
||||
"ripemd160"
|
||||
]
|
||||
revision = "c126467f60eb25f8f27e5a981f32a87e3965053f"
|
||||
|
||||
[[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
|
||||
-70
@@ -1,70 +0,0 @@
|
||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/beevik/etree"
|
||||
packages = ["."]
|
||||
revision = "15a30b44cfd6c5a16a7ddfe271bf146aaf2d3195"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/dchest/uniuri"
|
||||
packages = ["."]
|
||||
revision = "8902c56451e9b58ff940bbe5fec35d5f9c04584a"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/dgrijalva/jwt-go"
|
||||
packages = ["."]
|
||||
revision = "d2709f9f1f31ebcda9651b03077758c1f3a0018c"
|
||||
version = "v3.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/jonboulle/clockwork"
|
||||
packages = ["."]
|
||||
revision = "2eee05ed794112d45db504eb05aa693efd2b8b09"
|
||||
version = "v0.1.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/kr/pretty"
|
||||
packages = ["."]
|
||||
revision = "cfb55aafdaf3ec08f0db22699ab822c50091b1c4"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/kr/text"
|
||||
packages = ["."]
|
||||
revision = "7cafcd837844e784b526369c9bce262804aebc60"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/russellhaering/goxmldsig"
|
||||
packages = [".","etreeutils","types"]
|
||||
revision = "b7efc6231e45b10bfd779852831c8bb59b350ec5"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/zenazn/goji"
|
||||
packages = [".","bind","graceful","graceful/listener","web","web/middleware","web/mutil"]
|
||||
revision = "64eb34159fe53473206c2b3e70fe396a639452f2"
|
||||
version = "v1.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = ["bcrypt","blowfish","ripemd160"]
|
||||
revision = "847319b7fc94cab682988f93da778204da164588"
|
||||
|
||||
[[projects]]
|
||||
branch = "v1"
|
||||
name = "gopkg.in/check.v1"
|
||||
packages = ["."]
|
||||
revision = "20d25e2804050c1cd24a7eea1e7a6447dd0e74ec"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "253ec289f823a19c6473233e4934b31f5e623b0fb3136183b129cb652a5685c2"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
||||
+13
-10
@@ -39,7 +39,10 @@ Let us assume we have a simple web application to protect. We'll modify this app
|
||||
```golang
|
||||
package main
|
||||
|
||||
import "net/http"
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func hello(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "Hello, World!")
|
||||
@@ -55,7 +58,7 @@ Each service provider must have an self-signed X.509 key pair established. You c
|
||||
|
||||
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](https://www.testshib.org/), an identity provider designed for testing.
|
||||
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 [samltest.id](https://samltest.id/), an identity provider designed for testing.
|
||||
|
||||
```golang
|
||||
package main
|
||||
@@ -85,7 +88,7 @@ func main() {
|
||||
panic(err) // TODO handle error
|
||||
}
|
||||
|
||||
idpMetadataURL, err := url.Parse("https://www.testshib.org/metadata/testshib-providers.xml")
|
||||
idpMetadataURL, err := url.Parse("https://samltest.id/saml/idp")
|
||||
if err != nil {
|
||||
panic(err) // TODO handle error
|
||||
}
|
||||
@@ -108,22 +111,22 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
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:
|
||||
Next we'll have to register our service provider with the identity provider to establish trust from the service provider to the IDP. For [samltest.id](https://samltest.id/), you can do something like:
|
||||
|
||||
mdpath=saml-test-$USER-$HOST.xml
|
||||
curl localhost:8000/saml/metadata > $mdpath
|
||||
|
||||
Navigate to https://www.testshib.org/register.html and upload the file you fetched.
|
||||
Navigate to https://samltest.id/upload.php 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`
|
||||
|
||||
1. The middleware redirects you to `https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO`
|
||||
1. The middleware redirects you to `https://samltest.id/idp/profile/SAML2/Redirect/SSO`
|
||||
|
||||
1. testshib.org prompts you for a username and password.
|
||||
1. samltest.id prompts you for a username and password.
|
||||
|
||||
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.
|
||||
1. samltest.id 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. The local service validates the response, issues a session cookie, and redirects you to the original URL, `localhost:8000/hello`.
|
||||
|
||||
@@ -131,7 +134,7 @@ Now you should be able to authenticate. The flow should look like this:
|
||||
|
||||
## Getting Started as an Identity Provider
|
||||
|
||||
Please see `examples/idp/` for a substantially complete example of how to use the library and helpers to be an identity provider.
|
||||
Please see `example/idp/` for a substantially complete example of how to use the library and helpers to be an identity provider.
|
||||
|
||||
## Support
|
||||
|
||||
@@ -159,7 +162,7 @@ The SAML specification is a collection of PDFs (sadly):
|
||||
|
||||
- [SAMLConformance](http://docs.oasis-open.org/security/saml/v2.0/saml-conformance-2.0-os.pdf) includes a support matrix for various parts of the protocol.
|
||||
|
||||
[TestShib](https://www.testshib.org/) is a testing ground for SAML service and identity providers.
|
||||
[SAMLtest](https://samltest.id/) is a testing ground for SAML service and identity providers.
|
||||
|
||||
## Security Issues
|
||||
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
module github.com/crewjam/saml
|
||||
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/beevik/etree v1.0.1
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/jonboulle/clockwork v0.1.0 // indirect
|
||||
github.com/kr/pretty v0.1.0
|
||||
github.com/russellhaering/goxmldsig v0.0.0-20180430223755-7acd5e4a6ef7
|
||||
github.com/stretchr/testify v1.4.0
|
||||
github.com/zenazn/goji v0.9.1-0.20160507202103-64eb34159fe5
|
||||
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||
gopkg.in/yaml.v2 v2.2.4 // indirect
|
||||
)
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
github.com/beevik/etree v1.0.1 h1:lWzdj5v/Pj1X360EV7bUudox5SRipy4qZLjY0rhb0ck=
|
||||
github.com/beevik/etree v1.0.1/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9 h1:74lLNRzvsdIlkTgfDSMuaPjBr4cf6k7pwQQANm/yLKU=
|
||||
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russellhaering/goxmldsig v0.0.0-20180430223755-7acd5e4a6ef7 h1:J4AOUcOh/t1XbQcJfkEqhzgvMJ2tDxdCVvmHxW5QXao=
|
||||
github.com/russellhaering/goxmldsig v0.0.0-20180430223755-7acd5e4a6ef7/go.mod h1:Oz4y6ImuOQZxynhbSXk7btjEfNBtGlj2dcaOvXl2FSM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/zenazn/goji v0.9.1-0.20160507202103-64eb34159fe5 h1:mXV20Aj/BdWrlVzIn1kXFa+Tq62INlUi0cFFlztTaK0=
|
||||
github.com/zenazn/goji v0.9.1-0.20160507202103-64eb34159fe5/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 h1:ACG4HJsFiNMf47Y4PeRoebLNy/2lXT9EtprMuTFWt1M=
|
||||
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69 h1:rOhMmluY6kLMhdnrivzec6lLgaVbMHMn2ISQXJeJ5EM=
|
||||
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
+27
-13
@@ -20,9 +20,10 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/beevik/etree"
|
||||
dsig "github.com/russellhaering/goxmldsig"
|
||||
|
||||
"github.com/crewjam/saml/logger"
|
||||
"github.com/crewjam/saml/xmlenc"
|
||||
dsig "github.com/russellhaering/goxmldsig"
|
||||
)
|
||||
|
||||
// Session represents a user session. It is returned by the
|
||||
@@ -34,13 +35,14 @@ type Session struct {
|
||||
ExpireTime time.Time
|
||||
Index string
|
||||
|
||||
NameID string
|
||||
Groups []string
|
||||
UserName string
|
||||
UserEmail string
|
||||
UserCommonName string
|
||||
UserSurname string
|
||||
UserGivenName string
|
||||
NameID string
|
||||
Groups []string
|
||||
UserName string
|
||||
UserEmail string
|
||||
UserCommonName string
|
||||
UserSurname string
|
||||
UserGivenName string
|
||||
UserScopedAffiliation string
|
||||
}
|
||||
|
||||
// SessionProvider is an interface used by IdentityProvider to determine the
|
||||
@@ -110,7 +112,7 @@ func (idp *IdentityProvider) Metadata() *EntityDescriptor {
|
||||
ValidUntil: TimeNow().Add(DefaultValidDuration),
|
||||
CacheDuration: DefaultValidDuration,
|
||||
IDPSSODescriptors: []IDPSSODescriptor{
|
||||
IDPSSODescriptor{
|
||||
{
|
||||
SSODescriptor: SSODescriptor{
|
||||
RoleDescriptor: RoleDescriptor{
|
||||
ProtocolSupportEnumeration: "urn:oasis:names:tc:SAML:2.0:protocol",
|
||||
@@ -620,6 +622,18 @@ func (DefaultAssertionMaker) MakeAssertion(req *IdpAuthnRequest, session *Sessio
|
||||
})
|
||||
}
|
||||
|
||||
if session.UserScopedAffiliation != "" {
|
||||
attributes = append(attributes, Attribute{
|
||||
FriendlyName: "uid",
|
||||
Name: "urn:oid:1.3.6.1.4.1.5923.1.1.1.9",
|
||||
NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
|
||||
Values: []AttributeValue{{
|
||||
Type: "xs:string",
|
||||
Value: session.UserScopedAffiliation,
|
||||
}},
|
||||
})
|
||||
}
|
||||
|
||||
if len(session.Groups) != 0 {
|
||||
groupMemberAttributeValues := []AttributeValue{}
|
||||
for _, group := range session.Groups {
|
||||
@@ -661,7 +675,7 @@ func (DefaultAssertionMaker) MakeAssertion(req *IdpAuthnRequest, session *Sessio
|
||||
Value: session.NameID,
|
||||
},
|
||||
SubjectConfirmations: []SubjectConfirmation{
|
||||
SubjectConfirmation{
|
||||
{
|
||||
Method: "urn:oasis:names:tc:SAML:2.0:cm:bearer",
|
||||
SubjectConfirmationData: &SubjectConfirmationData{
|
||||
Address: req.HTTPRequest.RemoteAddr,
|
||||
@@ -676,13 +690,13 @@ func (DefaultAssertionMaker) MakeAssertion(req *IdpAuthnRequest, session *Sessio
|
||||
NotBefore: notBefore,
|
||||
NotOnOrAfter: notOnOrAfterAfter,
|
||||
AudienceRestrictions: []AudienceRestriction{
|
||||
AudienceRestriction{
|
||||
{
|
||||
Audience: Audience{Value: req.ServiceProviderMetadata.EntityID},
|
||||
},
|
||||
},
|
||||
},
|
||||
AuthnStatements: []AuthnStatement{
|
||||
AuthnStatement{
|
||||
{
|
||||
AuthnInstant: session.CreateTime,
|
||||
SessionIndex: session.Index,
|
||||
SubjectLocality: &SubjectLocality{
|
||||
@@ -696,7 +710,7 @@ func (DefaultAssertionMaker) MakeAssertion(req *IdpAuthnRequest, session *Sessio
|
||||
},
|
||||
},
|
||||
AttributeStatements: []AttributeStatement{
|
||||
AttributeStatement{
|
||||
{
|
||||
Attributes: attributes,
|
||||
},
|
||||
},
|
||||
|
||||
+1
-1
@@ -133,7 +133,7 @@ type ContactPerson struct {
|
||||
// See http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf §2.4.1
|
||||
type RoleDescriptor struct {
|
||||
ID string `xml:",attr,omitempty"`
|
||||
ValidUntil time.Time `xml:"validUntil,attr,omitempty"`
|
||||
ValidUntil *time.Time `xml:"validUntil,attr,omitempty"`
|
||||
CacheDuration time.Duration `xml:"cacheDuration,attr,omitempty"`
|
||||
ProtocolSupportEnumeration string `xml:"protocolSupportEnumeration,attr"`
|
||||
ErrorURL string `xml:"errorURL,attr,omitempty"`
|
||||
|
||||
+62
-62
@@ -1,46 +1,46 @@
|
||||
//
|
||||
//
|
||||
// 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
|
||||
//
|
||||
//
|
||||
// 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
|
||||
// `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`.
|
||||
//
|
||||
//
|
||||
// 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
|
||||
//
|
||||
// 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.
|
||||
// ```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)
|
||||
@@ -48,14 +48,14 @@
|
||||
// }
|
||||
// ```
|
||||
// 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](https://www.testshib.org/), an identity provider designed for testing.
|
||||
//
|
||||
//
|
||||
// ```golang
|
||||
// package main
|
||||
//
|
||||
//
|
||||
// import (
|
||||
// "crypto/rsa"
|
||||
// "crypto/tls"
|
||||
@@ -63,15 +63,15 @@
|
||||
// "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 {
|
||||
@@ -81,17 +81,17 @@
|
||||
// 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),
|
||||
@@ -104,61 +104,61 @@
|
||||
// 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
|
||||
//
|
||||
//
|
||||
// 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`
|
||||
//
|
||||
//
|
||||
// 1. The middleware redirects you to `https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO`
|
||||
//
|
||||
//
|
||||
// 1. testshib.org prompts you for a username and password.
|
||||
//
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
//
|
||||
// 1. The local service validates the response, issues a session cookie, and redirects you to the original URL, `localhost:8000/hello`.
|
||||
//
|
||||
//
|
||||
// 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
|
||||
//
|
||||
//
|
||||
// Please see `examples/idp/` for a substantially complete example of how to use the library and helpers to be an identity provider.
|
||||
//
|
||||
//
|
||||
// Support
|
||||
//
|
||||
//
|
||||
// The SAML standard is huge and complex with many dark corners and strange, unused features. This package implements the most commonly used subset of these features required to provide a single sign on experience. The package supports at least the subset of SAML known as [interoperable SAML](http://saml2int.org).
|
||||
//
|
||||
//
|
||||
// This package supports the Web SSO profile. Message flows from the service provider to the IDP are supported using the HTTP Redirect binding and the HTTP POST binding. Message flows from the IDP to the service provider are supported via the HTTP POST binding.
|
||||
//
|
||||
//
|
||||
// The package supports signed and encrypted SAML assertions. It does not support signed or encrypted requests.
|
||||
//
|
||||
//
|
||||
// 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.)
|
||||
//
|
||||
//
|
||||
// References
|
||||
//
|
||||
//
|
||||
// The SAML specification is a collection of PDFs (sadly):
|
||||
//
|
||||
//
|
||||
// - [SAMLCore](http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf) defines data types.
|
||||
//
|
||||
//
|
||||
// - [SAMLBindings](http://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf) defines the details of the HTTP requests in play.
|
||||
//
|
||||
//
|
||||
// - [SAMLProfiles](http://docs.oasis-open.org/security/saml/v2.0/saml-profiles-2.0-os.pdf) describes data flows.
|
||||
//
|
||||
//
|
||||
// - [SAMLConformance](http://docs.oasis-open.org/security/saml/v2.0/saml-conformance-2.0-os.pdf) includes a support matrix for various parts of the protocol.
|
||||
//
|
||||
//
|
||||
// [TestShib](https://www.testshib.org/) is a testing ground for SAML service and identity providers.
|
||||
//
|
||||
//
|
||||
// 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)).
|
||||
package saml
|
||||
|
||||
+144
-2
@@ -39,6 +39,75 @@ type AuthnRequest struct {
|
||||
ProviderName string `xml:",attr"`
|
||||
}
|
||||
|
||||
type LogoutRequest struct {
|
||||
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol LogoutRequest"`
|
||||
|
||||
ID string `xml:",attr"`
|
||||
Version string `xml:",attr"`
|
||||
IssueInstant time.Time `xml:",attr"`
|
||||
Destination string `xml:",attr"`
|
||||
Issuer *Issuer `xml:"urn:oasis:names:tc:SAML:2.0:assertion Issuer"`
|
||||
NameID *NameID
|
||||
Signature *etree.Element
|
||||
|
||||
SessionIndex string `xml:",attr"`
|
||||
}
|
||||
|
||||
// Element returns an etree.Element representing the object in XML form.
|
||||
func (r *LogoutRequest) Element() *etree.Element {
|
||||
el := etree.NewElement("samlp:LogoutRequest")
|
||||
el.CreateAttr("xmlns:saml", "urn:oasis:names:tc:SAML:2.0:assertion")
|
||||
el.CreateAttr("xmlns:samlp", "urn:oasis:names:tc:SAML:2.0:protocol")
|
||||
el.CreateAttr("ID", r.ID)
|
||||
el.CreateAttr("Version", r.Version)
|
||||
el.CreateAttr("IssueInstant", r.IssueInstant.Format(timeFormat))
|
||||
if r.Destination != "" {
|
||||
el.CreateAttr("Destination", r.Destination)
|
||||
}
|
||||
if r.Issuer != nil {
|
||||
el.AddChild(r.Issuer.Element())
|
||||
}
|
||||
if r.NameID != nil {
|
||||
el.AddChild(r.NameID.Element())
|
||||
}
|
||||
if r.Signature != nil {
|
||||
el.AddChild(r.Signature)
|
||||
}
|
||||
if r.SessionIndex != "" {
|
||||
el.CreateAttr("SessionIndex", r.SessionIndex)
|
||||
}
|
||||
return el
|
||||
}
|
||||
|
||||
// MarshalXML implements xml.Marshaler
|
||||
func (r *LogoutRequest) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
type Alias LogoutRequest
|
||||
aux := &struct {
|
||||
IssueInstant RelaxedTime `xml:",attr"`
|
||||
*Alias
|
||||
}{
|
||||
IssueInstant: RelaxedTime(r.IssueInstant),
|
||||
Alias: (*Alias)(r),
|
||||
}
|
||||
return e.Encode(aux)
|
||||
}
|
||||
|
||||
// UnmarshalXML implements xml.Unmarshaler
|
||||
func (r *LogoutRequest) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
type Alias LogoutRequest
|
||||
aux := &struct {
|
||||
IssueInstant RelaxedTime `xml:",attr"`
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(r),
|
||||
}
|
||||
if err := d.DecodeElement(&aux, &start); err != nil {
|
||||
return err
|
||||
}
|
||||
r.IssueInstant = time.Time(aux.IssueInstant)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Element returns an etree.Element representing the object
|
||||
// Element returns an etree.Element representing the object in XML form.
|
||||
func (r *AuthnRequest) Element() *etree.Element {
|
||||
@@ -218,7 +287,7 @@ func (r *Response) Element() *etree.Element {
|
||||
// cannonicalizer. This could be avoided by providing a prefix list to the
|
||||
// cannonicalizer, but prefix lists do not appear to be implemented correctly
|
||||
// in some libraries, so the safest action is to always produce XML that is
|
||||
// (a) in cannonical form and (b) does not require prefix lists.
|
||||
// (a) in canonical form and (b) does not require prefix lists.
|
||||
el.CreateAttr("xmlns:xs", "http://www.w3.org/2001/XMLSchema")
|
||||
|
||||
el.CreateAttr("ID", r.ID)
|
||||
@@ -357,7 +426,7 @@ const (
|
||||
StatusNoAvailableIDP = "urn:oasis:names:tc:SAML:2.0:status:NoAvailableIDP"
|
||||
|
||||
// StatusNoPassive means Indicates the responding provider cannot authenticate the principal passively, as has been requested.
|
||||
StatusNoPassive = "urn:oasis:names:tc:SAML:2.0:status:NoPassive"
|
||||
StatusNoPassive = "urn:oasis:names:tc:SAML:2.0:status:NoPassive" //nolint:gosec
|
||||
|
||||
// StatusNoSupportedIDP is used by an intermediary to indicate that none of the identity providers in an <IDPList> are supported by the intermediary.
|
||||
StatusNoSupportedIDP = "urn:oasis:names:tc:SAML:2.0:status:NoSupportedIDP"
|
||||
@@ -929,3 +998,76 @@ func (a *AttributeValue) Element() *etree.Element {
|
||||
el.SetText(a.Value)
|
||||
return el
|
||||
}
|
||||
|
||||
// LogoutResponse represents the SAML object of the same name.
|
||||
//
|
||||
// See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
|
||||
type LogoutResponse struct {
|
||||
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol LogoutResponse"`
|
||||
ID string `xml:",attr"`
|
||||
InResponseTo string `xml:",attr"`
|
||||
Version string `xml:",attr"`
|
||||
IssueInstant time.Time `xml:",attr"`
|
||||
Destination string `xml:",attr"`
|
||||
Consent string `xml:",attr"`
|
||||
Issuer *Issuer `xml:"urn:oasis:names:tc:SAML:2.0:assertion Issuer"`
|
||||
Signature *etree.Element
|
||||
Status Status `xml:"urn:oasis:names:tc:SAML:2.0:protocol Status"`
|
||||
}
|
||||
|
||||
// Element returns an etree.Element representing the object in XML form.
|
||||
func (r *LogoutResponse) Element() *etree.Element {
|
||||
el := etree.NewElement("samlp:Response")
|
||||
el.CreateAttr("xmlns:saml", "urn:oasis:names:tc:SAML:2.0:assertion")
|
||||
el.CreateAttr("xmlns:samlp", "urn:oasis:names:tc:SAML:2.0:protocol")
|
||||
|
||||
el.CreateAttr("ID", r.ID)
|
||||
if r.InResponseTo != "" {
|
||||
el.CreateAttr("InResponseTo", r.InResponseTo)
|
||||
}
|
||||
el.CreateAttr("Version", r.Version)
|
||||
el.CreateAttr("IssueInstant", r.IssueInstant.Format(timeFormat))
|
||||
if r.Destination != "" {
|
||||
el.CreateAttr("Destination", r.Destination)
|
||||
}
|
||||
if r.Consent != "" {
|
||||
el.CreateAttr("Consent", r.Consent)
|
||||
}
|
||||
if r.Issuer != nil {
|
||||
el.AddChild(r.Issuer.Element())
|
||||
}
|
||||
if r.Signature != nil {
|
||||
el.AddChild(r.Signature)
|
||||
}
|
||||
el.AddChild(r.Status.Element())
|
||||
return el
|
||||
}
|
||||
|
||||
// MarshalXML implements xml.Marshaler
|
||||
func (r *LogoutResponse) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
type Alias LogoutResponse
|
||||
aux := &struct {
|
||||
IssueInstant RelaxedTime `xml:",attr"`
|
||||
*Alias
|
||||
}{
|
||||
IssueInstant: RelaxedTime(r.IssueInstant),
|
||||
Alias: (*Alias)(r),
|
||||
}
|
||||
return e.Encode(aux)
|
||||
}
|
||||
|
||||
// UnmarshalXML implements xml.Unmarshaler
|
||||
func (r *LogoutResponse) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
type Alias LogoutResponse
|
||||
aux := &struct {
|
||||
IssueInstant RelaxedTime `xml:",attr"`
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(r),
|
||||
}
|
||||
if err := d.DecodeElement(&aux, &start); err != nil {
|
||||
return err
|
||||
}
|
||||
r.IssueInstant = time.Time(aux.IssueInstant)
|
||||
return nil
|
||||
}
|
||||
|
||||
+183
-23
@@ -16,10 +16,11 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/beevik/etree"
|
||||
"github.com/crewjam/saml/logger"
|
||||
"github.com/crewjam/saml/xmlenc"
|
||||
dsig "github.com/russellhaering/goxmldsig"
|
||||
"github.com/russellhaering/goxmldsig/etreeutils"
|
||||
|
||||
"github.com/crewjam/saml/logger"
|
||||
"github.com/crewjam/saml/xmlenc"
|
||||
)
|
||||
|
||||
// NameIDFormat is the format of the id
|
||||
@@ -34,9 +35,9 @@ func (n NameIDFormat) Element() *etree.Element {
|
||||
|
||||
// Name ID formats
|
||||
const (
|
||||
UnspecifiedNameIDFormat NameIDFormat = "urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified"
|
||||
UnspecifiedNameIDFormat NameIDFormat = "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
|
||||
TransientNameIDFormat NameIDFormat = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
|
||||
EmailAddressNameIDFormat NameIDFormat = "urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress"
|
||||
EmailAddressNameIDFormat NameIDFormat = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
|
||||
PersistentNameIDFormat NameIDFormat = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"
|
||||
)
|
||||
|
||||
@@ -54,7 +55,8 @@ type ServiceProvider struct {
|
||||
Key *rsa.PrivateKey
|
||||
|
||||
// Certificate is the RSA public part of Key.
|
||||
Certificate *x509.Certificate
|
||||
Certificate *x509.Certificate
|
||||
Intermediates []*x509.Certificate
|
||||
|
||||
// MetadataURL is the full URL to the metadata endpoint on this host,
|
||||
// i.e. https://example.com/saml/metadata
|
||||
@@ -64,6 +66,10 @@ type ServiceProvider struct {
|
||||
// on this host, i.e. https://example.com/saml/acs
|
||||
AcsURL url.URL
|
||||
|
||||
// SloURL is the full URL to the SAML Single Logout endpoint on this host.
|
||||
// i.e. https://example.com/saml/slo
|
||||
SloURL url.URL
|
||||
|
||||
// IDPMetadata is the metadata from the identity provider.
|
||||
IDPMetadata *EntityDescriptor
|
||||
|
||||
@@ -81,6 +87,9 @@ type ServiceProvider struct {
|
||||
// ForceAuthn allows you to force re-authentication of users even if the user
|
||||
// has a SSO session at the IdP.
|
||||
ForceAuthn *bool
|
||||
|
||||
// AllowIdpInitiated
|
||||
AllowIDPInitiated bool
|
||||
}
|
||||
|
||||
// MaxIssueDelay is the longest allowed time between when a SAML assertion is
|
||||
@@ -109,6 +118,10 @@ func (sp *ServiceProvider) Metadata() *EntityDescriptor {
|
||||
authnRequestsSigned := false
|
||||
wantAssertionsSigned := true
|
||||
validUntil := TimeNow().Add(validDuration)
|
||||
certBytes := sp.Certificate.Raw
|
||||
for _, intermediate := range sp.Intermediates {
|
||||
certBytes = append(certBytes, intermediate.Raw...)
|
||||
}
|
||||
return &EntityDescriptor{
|
||||
EntityID: sp.MetadataURL.String(),
|
||||
ValidUntil: validUntil,
|
||||
@@ -122,13 +135,13 @@ func (sp *ServiceProvider) Metadata() *EntityDescriptor {
|
||||
{
|
||||
Use: "signing",
|
||||
KeyInfo: KeyInfo{
|
||||
Certificate: base64.StdEncoding.EncodeToString(sp.Certificate.Raw),
|
||||
Certificate: base64.StdEncoding.EncodeToString(certBytes),
|
||||
},
|
||||
},
|
||||
{
|
||||
Use: "encryption",
|
||||
KeyInfo: KeyInfo{
|
||||
Certificate: base64.StdEncoding.EncodeToString(sp.Certificate.Raw),
|
||||
Certificate: base64.StdEncoding.EncodeToString(certBytes),
|
||||
},
|
||||
EncryptionMethods: []EncryptionMethod{
|
||||
{Algorithm: "http://www.w3.org/2001/04/xmlenc#aes128-cbc"},
|
||||
@@ -138,7 +151,14 @@ func (sp *ServiceProvider) Metadata() *EntityDescriptor {
|
||||
},
|
||||
},
|
||||
},
|
||||
ValidUntil: validUntil,
|
||||
ValidUntil: &validUntil,
|
||||
},
|
||||
SingleLogoutServices: []Endpoint{
|
||||
{
|
||||
Binding: HTTPPostBinding,
|
||||
Location: sp.SloURL.String(),
|
||||
ResponseLocation: sp.SloURL.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
AuthnRequestsSigned: &authnRequestsSigned,
|
||||
@@ -205,6 +225,19 @@ func (sp *ServiceProvider) GetSSOBindingLocation(binding string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetSLOBindingLocation returns URL for the IDP's Single Log Out Service binding
|
||||
// of the specified type (HTTPRedirectBinding or HTTPPostBinding)
|
||||
func (sp *ServiceProvider) GetSLOBindingLocation(binding string) string {
|
||||
for _, idpSSODescriptor := range sp.IDPMetadata.IDPSSODescriptors {
|
||||
for _, singleLogoutService := range idpSSODescriptor.SingleLogoutServices {
|
||||
if singleLogoutService.Binding == binding {
|
||||
return singleLogoutService.Location
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// getIDPSigningCerts returns the certificates which we can use to verify things
|
||||
// signed by the IDP in PEM format, or nil if no such certificate is found.
|
||||
func (sp *ServiceProvider) getIDPSigningCerts() ([]*x509.Certificate, error) {
|
||||
@@ -257,18 +290,9 @@ func (sp *ServiceProvider) getIDPSigningCerts() ([]*x509.Certificate, error) {
|
||||
|
||||
// MakeAuthenticationRequest produces a new AuthnRequest object for idpURL.
|
||||
func (sp *ServiceProvider) MakeAuthenticationRequest(idpURL string) (*AuthnRequest, error) {
|
||||
var nameIDFormat string
|
||||
switch sp.AuthnNameIDFormat {
|
||||
case "":
|
||||
// To maintain library back-compat, use "transient" if unset.
|
||||
nameIDFormat = string(TransientNameIDFormat)
|
||||
case UnspecifiedNameIDFormat:
|
||||
// Spec defines an empty value as "unspecified" so don't set one.
|
||||
default:
|
||||
nameIDFormat = string(sp.AuthnNameIDFormat)
|
||||
}
|
||||
|
||||
allowCreate := true
|
||||
nameIDFormat := sp.nameIDFormat()
|
||||
req := AuthnRequest{
|
||||
AssertionConsumerServiceURL: sp.AcsURL.String(),
|
||||
Destination: idpURL,
|
||||
@@ -378,6 +402,39 @@ func (ivr *InvalidResponseError) Error() string {
|
||||
return fmt.Sprintf("Authentication failed")
|
||||
}
|
||||
|
||||
func responseIsSigned(response *etree.Document) (bool, error) {
|
||||
signatureElement, err := findChild(response.Root(), "http://www.w3.org/2000/09/xmldsig#", "Signature")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return signatureElement != nil, nil
|
||||
}
|
||||
|
||||
// validateDestination validates the Destination attribute.
|
||||
// If the response is signed, the Destination is required to be present.
|
||||
func (sp *ServiceProvider) validateDestination(response []byte, responseDom *Response) error {
|
||||
responseXML := etree.NewDocument()
|
||||
err := responseXML.ReadFromBytes(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
signed, err := responseIsSigned(responseXML)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Compare if the response is signed OR the Destination is provided.
|
||||
// (Even if the response is not signed, if the Destination is set it must match.)
|
||||
if signed || responseDom.Destination != "" {
|
||||
if responseDom.Destination != sp.AcsURL.String() {
|
||||
return fmt.Errorf("`Destination` does not match AcsURL (expected %q, actual %q)", sp.AcsURL.String(), responseDom.Destination)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseResponse extracts the SAML IDP response received in req, validates
|
||||
// it, and returns the verified attributes of the request.
|
||||
//
|
||||
@@ -402,18 +459,39 @@ func (sp *ServiceProvider) ParseResponse(req *http.Request, possibleRequestIDs [
|
||||
return nil, retErr
|
||||
}
|
||||
retErr.Response = string(rawResponseBuf)
|
||||
assertion, err := sp.ParseXMLResponse(rawResponseBuf, possibleRequestIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return assertion, nil
|
||||
|
||||
}
|
||||
|
||||
func (sp *ServiceProvider) ParseXMLResponse(decodedResponseXML []byte, possibleRequestIDs []string) (*Assertion, error) {
|
||||
now := TimeNow()
|
||||
var err error
|
||||
retErr := &InvalidResponseError{
|
||||
Now: now,
|
||||
Response: string(decodedResponseXML),
|
||||
}
|
||||
|
||||
// do some validation first before we decrypt
|
||||
resp := Response{}
|
||||
if err := xml.Unmarshal(rawResponseBuf, &resp); err != nil {
|
||||
if err := xml.Unmarshal([]byte(decodedResponseXML), &resp); err != nil {
|
||||
retErr.PrivateErr = fmt.Errorf("cannot unmarshal response: %s", err)
|
||||
return nil, retErr
|
||||
}
|
||||
if resp.Destination != sp.AcsURL.String() {
|
||||
retErr.PrivateErr = fmt.Errorf("`Destination` does not match AcsURL (expected %q)", sp.AcsURL.String())
|
||||
|
||||
if err := sp.validateDestination(decodedResponseXML, &resp); err != nil {
|
||||
retErr.PrivateErr = err
|
||||
return nil, retErr
|
||||
}
|
||||
|
||||
if sp.AllowIDPInitiated && len(possibleRequestIDs) == 0 {
|
||||
possibleRequestIDs = append([]string{""})
|
||||
}
|
||||
|
||||
requestIDvalid := false
|
||||
for _, possibleRequestID := range possibleRequestIDs {
|
||||
if resp.InResponseTo == possibleRequestID {
|
||||
@@ -442,7 +520,7 @@ func (sp *ServiceProvider) ParseResponse(req *http.Request, possibleRequestIDs [
|
||||
if resp.EncryptedAssertion == nil {
|
||||
|
||||
doc := etree.NewDocument()
|
||||
if err := doc.ReadFromBytes(rawResponseBuf); err != nil {
|
||||
if err := doc.ReadFromBytes(decodedResponseXML); err != nil {
|
||||
retErr.PrivateErr = err
|
||||
return nil, retErr
|
||||
}
|
||||
@@ -465,7 +543,7 @@ func (sp *ServiceProvider) ParseResponse(req *http.Request, possibleRequestIDs [
|
||||
// decrypt the response
|
||||
if resp.EncryptedAssertion != nil {
|
||||
doc := etree.NewDocument()
|
||||
if err := doc.ReadFromBytes(rawResponseBuf); err != nil {
|
||||
if err := doc.ReadFromBytes(decodedResponseXML); err != nil {
|
||||
retErr.PrivateErr = err
|
||||
return nil, retErr
|
||||
}
|
||||
@@ -681,3 +759,85 @@ func (sp *ServiceProvider) validateSignature(el *etree.Element) error {
|
||||
_, err = validationContext.Validate(el)
|
||||
return err
|
||||
}
|
||||
|
||||
// MakeLogoutRequest produces a new LogoutRequest object for idpURL.
|
||||
func (sp *ServiceProvider) MakeLogoutRequest(idpURL, nameID string) (*LogoutRequest, error) {
|
||||
|
||||
req := LogoutRequest{
|
||||
ID: fmt.Sprintf("id-%x", randomBytes(20)),
|
||||
IssueInstant: TimeNow(),
|
||||
Version: "2.0",
|
||||
Destination: idpURL,
|
||||
Issuer: &Issuer{
|
||||
Format: "urn:oasis:names:tc:SAML:2.0:nameid-format:entity",
|
||||
Value: sp.MetadataURL.String(),
|
||||
},
|
||||
NameID: &NameID{
|
||||
Format: sp.nameIDFormat(),
|
||||
Value: nameID,
|
||||
NameQualifier: sp.IDPMetadata.EntityID,
|
||||
SPNameQualifier: sp.Metadata().EntityID,
|
||||
},
|
||||
}
|
||||
return &req, nil
|
||||
}
|
||||
|
||||
// MakeRedirectLogoutRequest creates a SAML authentication request using
|
||||
// the HTTP-Redirect binding. It returns a URL that we will redirect the user to
|
||||
// in order to start the auth process.
|
||||
func (sp *ServiceProvider) MakeRedirectLogoutRequest(nameID string) (*LogoutRequest, error) {
|
||||
return sp.MakeLogoutRequest(sp.GetSLOBindingLocation(HTTPRedirectBinding), nameID)
|
||||
}
|
||||
|
||||
func (sp *ServiceProvider) nameIDFormat() string {
|
||||
var nameIDFormat string
|
||||
switch sp.AuthnNameIDFormat {
|
||||
case "":
|
||||
// To maintain library back-compat, use "transient" if unset.
|
||||
nameIDFormat = string(TransientNameIDFormat)
|
||||
case UnspecifiedNameIDFormat:
|
||||
// Spec defines an empty value as "unspecified" so don't set one.
|
||||
default:
|
||||
nameIDFormat = string(sp.AuthnNameIDFormat)
|
||||
}
|
||||
return nameIDFormat
|
||||
}
|
||||
|
||||
// ValidateLogoutResponse returns a nil error iff the logout request is valid.
|
||||
func (sp *ServiceProvider) ValidateLogoutResponse(r *http.Request) error {
|
||||
r.ParseForm()
|
||||
rawResponseBuf, err := base64.StdEncoding.DecodeString(r.PostForm.Get("SAMLResponse"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse base64: %s", err)
|
||||
}
|
||||
|
||||
resp := LogoutResponse{}
|
||||
if err := xml.Unmarshal(rawResponseBuf, &resp); err != nil {
|
||||
return fmt.Errorf("cannot unmarshal response: %s", err)
|
||||
}
|
||||
if resp.Destination != sp.SloURL.String() {
|
||||
return fmt.Errorf("`Destination` does not match SloURL (expected %q)", sp.SloURL.String())
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if resp.IssueInstant.Add(MaxIssueDelay).Before(now) {
|
||||
return fmt.Errorf("issueInstant expired at %s", resp.IssueInstant.Add(MaxIssueDelay))
|
||||
}
|
||||
if resp.Issuer.Value != sp.IDPMetadata.EntityID {
|
||||
return fmt.Errorf("issuer does not match the IDP metadata (expected %q)", sp.IDPMetadata.EntityID)
|
||||
}
|
||||
if resp.Status.StatusCode.Value != StatusSuccess {
|
||||
return fmt.Errorf("status code was not %s", StatusSuccess)
|
||||
}
|
||||
|
||||
doc := etree.NewDocument()
|
||||
if err := doc.ReadFromBytes(rawResponseBuf); err != nil {
|
||||
return err
|
||||
}
|
||||
responseEl := doc.Root()
|
||||
if err = sp.validateSigned(responseEl); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+1
-3
@@ -79,7 +79,7 @@ func getCiphertext(encryptedKey *etree.Element) ([]byte, error) {
|
||||
return ciphertext, nil
|
||||
}
|
||||
|
||||
func validateRSAKey(key interface{}, encryptedKey *etree.Element) (*rsa.PrivateKey, error) {
|
||||
func validateRSAKeyIfPresent(key interface{}, encryptedKey *etree.Element) (*rsa.PrivateKey, error) {
|
||||
rsaKey, ok := key.(*rsa.PrivateKey)
|
||||
if !ok {
|
||||
return nil, errors.New("expected key to be a *rsa.PrivateKey")
|
||||
@@ -110,8 +110,6 @@ func validateRSAKey(key interface{}, encryptedKey *etree.Element) (*rsa.PrivateK
|
||||
}
|
||||
} else if el = encryptedKey.FindElement("./KeyInfo/X509Data/X509IssuerSerial"); el != nil {
|
||||
// TODO: determine how to validate the issuer serial information
|
||||
} else {
|
||||
return nil, ErrCannotFindRequiredElement("X509Certificate or X509IssuerSerial")
|
||||
}
|
||||
return rsaKey, nil
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
package xmlenc
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"crypto/sha1" //nolint:gosec // required for protocol support
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"hash"
|
||||
|
||||
+1
-1
@@ -94,7 +94,7 @@ func (e RSA) Encrypt(certificate interface{}, plaintext []byte) (*etree.Element,
|
||||
|
||||
// Decrypt implements Decryptor. `key` must be an *rsa.PrivateKey.
|
||||
func (e RSA) Decrypt(key interface{}, ciphertextEl *etree.Element) ([]byte, error) {
|
||||
rsaKey, err := validateRSAKey(key, ciphertextEl)
|
||||
rsaKey, err := validateRSAKeyIfPresent(key, ciphertextEl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+1
-2
@@ -6,14 +6,13 @@ package xmlenc
|
||||
import (
|
||||
"crypto/rand"
|
||||
"hash"
|
||||
"io"
|
||||
|
||||
"github.com/beevik/etree"
|
||||
)
|
||||
|
||||
// RandReader is a thunk that allows test to replace the source of randomness used by
|
||||
// this package. By default it is Reader from crypto/rand.
|
||||
var RandReader io.Reader = rand.Reader
|
||||
var RandReader = rand.Reader
|
||||
|
||||
// Encrypter is an interface that encrypts things. Given a plaintext it returns an
|
||||
// XML EncryptedData or EncryptedKey element. The required type of `key` varies
|
||||
|
||||
Reference in New Issue
Block a user