pax_global_header 0000666 0000000 0000000 00000000064 14154673411 0014520 g ustar 00root root 0000000 0000000 52 comment=60a32b32095ab361c827116afd3f0041874c6c9c saml-0.4.6/ 0000775 0000000 0000000 00000000000 14154673411 0012463 5 ustar 00root root 0000000 0000000 saml-0.4.6/.github/ 0000775 0000000 0000000 00000000000 14154673411 0014023 5 ustar 00root root 0000000 0000000 saml-0.4.6/.github/dependabot.yml 0000664 0000000 0000000 00000000177 14154673411 0016660 0 ustar 00root root 0000000 0000000 version: 2 updates: - package-ecosystem: gomod directory: "/" schedule: interval: daily open-pull-requests-limit: 10 saml-0.4.6/.github/stale.yml 0000664 0000000 0000000 00000001253 14154673411 0015657 0 ustar 00root root 0000000 0000000 # Number of days of inactivity before an issue becomes stale daysUntilStale: 60 # Number of days of inactivity before a stale issue is closed daysUntilClose: 7 # Issues with these labels will never be considered stale exemptLabels: - pinned - security # Label to use when marking an issue as stale staleLabel: wontfix # Comment to post when marking an issue as stale. Set to `false` to disable markComment: > This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. # Comment to post when closing a stale issue. Set to `false` to disable closeComment: true saml-0.4.6/.github/workflows/ 0000775 0000000 0000000 00000000000 14154673411 0016060 5 ustar 00root root 0000000 0000000 saml-0.4.6/.github/workflows/lint.yml 0000664 0000000 0000000 00000000505 14154673411 0017551 0 ustar 00root root 0000000 0000000 name: lint on: push: branches: [ 'main' ] pull_request: branches: [ 'main' ] jobs: golangci: name: Run golangci-lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: golangci-lint uses: golangci/golangci-lint-action@v2 with: version: v1.43.0 saml-0.4.6/.github/workflows/maint.yml 0000664 0000000 0000000 00000001625 14154673411 0017717 0 ustar 00root root 0000000 0000000 name: Maintainer on: workflow_dispatch: schedule: - cron: "0 12 * * 0" jobs: upgrade_go: name: Upgrade go.mod runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-go@v2 with: go-version: "^1.15.6" - name: Install goupdate run: | ( cd $(mktemp -d) go get github.com/crewjam/goupdate ) git config --global user.email noreply@github.com git config --global user.name "Github Actions" - name: Update go.mod run: | go version go env $(go env GOPATH)/bin/goupdate -test 'go test ./...' --commit -v - name: Create Pull Request uses: peter-evans/create-pull-request@v3 with: commit-message: "Update go.mod" branch: auto/update-go title: "Update go.mod" body: "" saml-0.4.6/.github/workflows/test.yml 0000664 0000000 0000000 00000001026 14154673411 0017561 0 ustar 00root root 0000000 0000000 name: test on: push: branches: [ 'main' ] pull_request: branches: [ 'main' ] jobs: tests: name: Run tests runs-on: ubuntu-latest strategy: matrix: go: [ '1.13.x', '1.14.x', '1.15.x', '1.16.x', '1.17.x' ] steps: - name: Set up Go ${{ matrix.go }} uses: actions/setup-go@v2 - name: Check out code into the Go module directory uses: actions/checkout@v2 with: go-version: ${{ matrix.go }} - name: Run Go tests run: go test -v ./... saml-0.4.6/.gitignore 0000664 0000000 0000000 00000000111 14154673411 0014444 0 ustar 00root root 0000000 0000000 coverage.out coverage.html vendor/ # IDE-specific settings .idea .vscode saml-0.4.6/.golangci.yml 0000664 0000000 0000000 00000010370 14154673411 0015050 0 ustar 00root root 0000000 0000000 # Configuration file for golangci-lint # # https://github.com/golangci/golangci-lint # # fighting with false positives? # https://github.com/golangci/golangci-lint#nolint linters: enable: - gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification [fast: true, auto-fix: true] - goimports # Goimports does everything that gofmt does. Additionally it checks unused imports [fast: true, auto-fix: true] - gosec # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases [fast: true, auto-fix: false] - misspell # Finds commonly misspelled English words in comments [fast: true, auto-fix: true] - deadcode # Finds unused code [fast: true, auto-fix: false] - revive # Golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes [fast: true, auto-fix: false] - unconvert # Remove unnecessary type conversions [fast: true, auto-fix: false] disable: # TODO(ross): fix errors reported by these checkers and enable them - bodyclose # checks whether HTTP response body is closed successfully [fast: false, auto-fix: false] - depguard # Go linter that checks if package imports are in a list of acceptable packages [fast: true, auto-fix: false] - dupl # Tool for code clone detection [fast: true, auto-fix: false] - errcheck # Inspects source code for security problems [fast: true, auto-fix: false] - gochecknoglobals # Checks that no globals are present in Go code [fast: true, auto-fix: false] - gochecknoinits # Checks that no init functions are present in Go code [fast: true, auto-fix: false] - goconst # Finds repeated strings that could be replaced by a constant [fast: true, auto-fix: false] - gocritic # The most opinionated Go source code linter [fast: true, auto-fix: false] - gocyclo # Computes and checks the cyclomatic complexity of functions [fast: true, auto-fix: false] - gosimple # Linter for Go source code that specializes in simplifying a code [fast: false, auto-fix: false] - govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string [fast: false, auto-fix: false] - ineffassign # Detects when assignments to existing variables are not used [fast: true, auto-fix: false] - interfacer # Linter that suggests narrower interface types [fast: false, auto-fix: false] - lll # Reports long lines [fast: true, auto-fix: false] - maligned # Tool to detect Go structs that would take less memory if their fields were sorted [fast: true, auto-fix: false] - nakedret # Finds naked returns in functions greater than a specified function length [fast: true, auto-fix: false] - prealloc # Finds slice declarations that could potentially be preallocated [fast: true, auto-fix: false] - scopelint # Scopelint checks for unpinned variables in go programs [fast: true, auto-fix: false] - staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks [fast: false, auto-fix: false] - structcheck # Finds unused struct fields [fast: true, auto-fix: false] - stylecheck # Stylecheck is a replacement for golint [fast: false, auto-fix: false] - typecheck # Like the front-end of a Go compiler, parses and type-checks Go code [fast: true, auto-fix: false] - unparam # Reports unused function parameters [fast: false, auto-fix: false] - unused # Checks Go code for unused constants, variables, functions and types [fast: false, auto-fix: false] - varcheck # Finds unused global variables and constants [fast: true, auto-fix: false] linters-settings: goimports: local-prefixes: github.com/crewjam/saml govet: disable: - shadow enable: - asmdecl - assign - atomic - bools - buildtag - cgocall - composites - copylocks - errorsas - httpresponse - loopclosure - lostcancel - nilfunc - printf - shift - stdmethods - structtag - tests - unmarshal - unreachable - unsafeptr - unusedresult issues: exclude-use-default: false exclude: - G104 # 'Errors unhandled. (gosec) saml-0.4.6/LICENSE 0000664 0000000 0000000 00000002412 14154673411 0013467 0 ustar 00root root 0000000 0000000 Copyright (c) 2015, Ross Kinder All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. saml-0.4.6/README.md 0000664 0000000 0000000 00000015766 14154673411 0013761 0 ustar 00root root 0000000 0000000 # SAML [](http://godoc.org/github.com/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. ## Getting Started as a Service Provider 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 import ( "fmt" "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 [samltest.id](https://samltest.id/), an identity provider designed for testing. ```golang package main import ( "context" "crypto/rsa" "crypto/tls" "crypto/x509" "fmt" "net/http" "net/url" "github.com/crewjam/saml/samlsp" ) func hello(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello, %s!", samlsp.AttributeFromContext(r.Context(), "cn")) } 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://samltest.id/saml/idp") if err != nil { panic(err) // TODO handle error } idpMetadata, err := samlsp.FetchMetadata(context.Background(), http.DefaultClient, *idpMetadataURL) 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, IDPMetadata: idpMetadata, }) 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 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://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://samltest.id/idp/profile/SAML2/Redirect/SSO` 1. samltest.id prompts you for a username and password. 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`. 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 `example/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](https://kantarainitiative.github.io/SAMLprofiles/saml2int.html). 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 can produce signed SAML assertions, and can validate both 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 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. [SAMLtest](https://samltest.id/) 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 `78B6038B3B9DFB88`](https://keybase.io/crewjam)). If your issue is *not* a security issue, please use the issue tracker so other contributors can help. saml-0.4.6/duration.go 0000664 0000000 0000000 00000005616 14154673411 0014647 0 ustar 00root root 0000000 0000000 package saml import ( "fmt" "regexp" "strconv" "strings" "time" ) // Duration is a time.Duration that uses the xsd:duration format for text // marshalling and unmarshalling. type Duration time.Duration // MarshalText implements the encoding.TextMarshaler interface. func (d Duration) MarshalText() ([]byte, error) { if d == 0 { return nil, nil } out := "PT" if d < 0 { d *= -1 out = "-" + out } h := time.Duration(d) / time.Hour m := time.Duration(d) % time.Hour / time.Minute s := time.Duration(d) % time.Minute / time.Second ns := time.Duration(d) % time.Second if h > 0 { out += fmt.Sprintf("%dH", h) } if m > 0 { out += fmt.Sprintf("%dM", m) } if s > 0 || ns > 0 { out += fmt.Sprintf("%d", s) if ns > 0 { out += strings.TrimRight(fmt.Sprintf(".%09d", ns), "0") } out += "S" } return []byte(out), nil } const ( day = 24 * time.Hour month = 30 * day // Assumed to be 30 days. year = 365 * day // Assumed to be non-leap year. ) var ( durationRegexp = regexp.MustCompile(`^(-?)P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?(?:T(.+))?$`) durationTimeRegexp = regexp.MustCompile(`^(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?$`) ) // UnmarshalText implements the encoding.TextUnmarshaler interface. func (d *Duration) UnmarshalText(text []byte) error { if text == nil { *d = 0 return nil } var ( out time.Duration sign time.Duration = 1 ) match := durationRegexp.FindStringSubmatch(string(text)) if match == nil || strings.Join(match[2:6], "") == "" { return fmt.Errorf("invalid duration (%s)", text) } if match[1] == "-" { sign = -1 } if match[2] != "" { y, err := strconv.Atoi(match[2]) if err != nil { return fmt.Errorf("invalid duration years (%s): %s", text, err) } out += time.Duration(y) * year } if match[3] != "" { m, err := strconv.Atoi(match[3]) if err != nil { return fmt.Errorf("invalid duration months (%s): %s", text, err) } out += time.Duration(m) * month } if match[4] != "" { d, err := strconv.Atoi(match[4]) if err != nil { return fmt.Errorf("invalid duration days (%s): %s", text, err) } out += time.Duration(d) * day } if match[5] != "" { match := durationTimeRegexp.FindStringSubmatch(match[5]) if match == nil { return fmt.Errorf("invalid duration (%s)", text) } if match[1] != "" { h, err := strconv.Atoi(match[1]) if err != nil { return fmt.Errorf("invalid duration hours (%s): %s", text, err) } out += time.Duration(h) * time.Hour } if match[2] != "" { m, err := strconv.Atoi(match[2]) if err != nil { return fmt.Errorf("invalid duration minutes (%s): %s", text, err) } out += time.Duration(m) * time.Minute } if match[3] != "" { s, err := strconv.ParseFloat(match[3], 64) if err != nil { return fmt.Errorf("invalid duration seconds (%s): %s", text, err) } out += time.Duration(s * float64(time.Second)) } } *d = Duration(sign * out) return nil } saml-0.4.6/duration_test.go 0000664 0000000 0000000 00000005350 14154673411 0015701 0 ustar 00root root 0000000 0000000 package saml import ( "errors" "strconv" "testing" "time" "gotest.tools/assert" is "gotest.tools/assert/cmp" ) var durationMarshalTests = []struct { in time.Duration expected []byte }{ {0, nil}, {time.Nanosecond, []byte("PT0.000000001S")}, {time.Millisecond, []byte("PT0.001S")}, {time.Second, []byte("PT1S")}, {time.Minute, []byte("PT1M")}, {time.Hour, []byte("PT1H")}, {-time.Hour, []byte("-PT1H")}, {2*time.Hour + 3*time.Minute + 4*time.Second + 5*time.Nanosecond, []byte("PT2H3M4.000000005S")}, } func TestDuration(t *testing.T) { for i, testCase := range durationMarshalTests { t.Run(strconv.Itoa(i), func(t *testing.T) { actual, err := Duration(testCase.in).MarshalText() assert.Check(t, err) assert.Check(t, is.DeepEqual(testCase.expected, actual)) }) } } var durationUnmarshalTests = []struct { in []byte expected time.Duration err error }{ {nil, 0, nil}, {[]byte("PT0.0000000001S"), 0, nil}, {[]byte("PT0.000000001S"), time.Nanosecond, nil}, {[]byte("PT0.001S"), time.Millisecond, nil}, {[]byte("PT1S"), time.Second, nil}, {[]byte("PT1M"), time.Minute, nil}, {[]byte("PT1H"), time.Hour, nil}, {[]byte("-PT1H"), -time.Hour, nil}, {[]byte("P1D"), 24 * time.Hour, nil}, {[]byte("P1M"), 720 * time.Hour, nil}, {[]byte("P1Y"), 8760 * time.Hour, nil}, {[]byte("P2Y3M4DT5H6M7.000000008S"), 19781*time.Hour + 6*time.Minute + 7*time.Second + 8*time.Nanosecond, nil}, {[]byte("P0Y0M0DT0H0M0S"), 0, nil}, {[]byte("PT0001.0000S"), time.Second, nil}, {[]byte(""), 0, errors.New("invalid duration ()")}, {[]byte("12345"), 0, errors.New("invalid duration (12345)")}, {[]byte("P1D1M1Y"), 0, errors.New("invalid duration (P1D1M1Y)")}, {[]byte("P1H1M1S"), 0, errors.New("invalid duration (P1H1M1S)")}, {[]byte("PT1S1M1H"), 0, errors.New("invalid duration (PT1S1M1H)")}, {[]byte(" P1Y "), 0, errors.New("invalid duration ( P1Y )")}, {[]byte("P"), 0, errors.New("invalid duration (P)")}, {[]byte("-P"), 0, errors.New("invalid duration (-P)")}, {[]byte("PT"), 0, errors.New("invalid duration (PT)")}, {[]byte("P1YMD"), 0, errors.New("invalid duration (P1YMD)")}, {[]byte("P1YT"), 0, errors.New("invalid duration (P1YT)")}, {[]byte("P-1Y"), 0, errors.New("invalid duration (P-1Y)")}, {[]byte("P1.5Y"), 0, errors.New("invalid duration (P1.5Y)")}, {[]byte("PT1.S"), 0, errors.New("invalid duration (PT1.S)")}, } func TestDurationUnmarshal(t *testing.T) { for i, testCase := range durationUnmarshalTests { t.Run(strconv.Itoa(i), func(t *testing.T) { var actual Duration err := actual.UnmarshalText(testCase.in) if testCase.err == nil { assert.Check(t, err) } else { assert.Check(t, is.Error(err, testCase.err.Error())) } assert.Check(t, is.Equal(Duration(testCase.expected), actual)) }) } } saml-0.4.6/example/ 0000775 0000000 0000000 00000000000 14154673411 0014116 5 ustar 00root root 0000000 0000000 saml-0.4.6/example/idp/ 0000775 0000000 0000000 00000000000 14154673411 0014672 5 ustar 00root root 0000000 0000000 saml-0.4.6/example/idp/idp.go 0000664 0000000 0000000 00000010622 14154673411 0015776 0 ustar 00root root 0000000 0000000 package main import ( "crypto" "crypto/x509" "encoding/pem" "flag" "net/url" "github.com/zenazn/goji" "golang.org/x/crypto/bcrypt" "github.com/crewjam/saml/logger" "github.com/crewjam/saml/samlidp" ) var key = func() crypto.PrivateKey { b, _ := pem.Decode([]byte(`-----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEA0OhbMuizgtbFOfwbK7aURuXhZx6VRuAs3nNibiuifwCGz6u9 yy7bOR0P+zqN0YkjxaokqFgra7rXKCdeABmoLqCC0U+cGmLNwPOOA0PaD5q5xKhQ 4Me3rt/R9C4Ca6k3/OnkxnKwnogcsmdgs2l8liT3qVHP04Oc7Uymq2v09bGb6nPu fOrkXS9F6mSClxHG/q59AGOWsXK1xzIRV1eu8W2SNdyeFVU1JHiQe444xLoPul5t InWasKayFsPlJfWNc8EoU8COjNhfo/GovFTHVjh9oUR/gwEFVwifIHihRE0Hazn2 EQSLaOr2LM0TsRsQroFjmwSGgI+X2bfbMTqWOQIDAQABAoIBAFWZwDTeESBdrLcT zHZe++cJLxE4AObn2LrWANEv5AeySYsyzjRBYObIN9IzrgTb8uJ900N/zVr5VkxH xUa5PKbOcowd2NMfBTw5EEnaNbILLm+coHdanrNzVu59I9TFpAFoPavrNt/e2hNo NMGPSdOkFi81LLl4xoadz/WR6O/7N2famM+0u7C2uBe+TrVwHyuqboYoidJDhO8M w4WlY9QgAUhkPyzZqrl+VfF1aDTGVf4LJgaVevfFCas8Ws6DQX5q4QdIoV6/0vXi B1M+aTnWjHuiIzjBMWhcYW2+I5zfwNWRXaxdlrYXRukGSdnyO+DH/FhHePJgmlkj NInADDkCgYEA6MEQFOFSCc/ELXYWgStsrtIlJUcsLdLBsy1ocyQa2lkVUw58TouW RciE6TjW9rp31pfQUnO2l6zOUC6LT9Jvlb9PSsyW+rvjtKB5PjJI6W0hjX41wEO6 fshFELMJd9W+Ezao2AsP2hZJ8McCF8no9e00+G4xTAyxHsNI2AFTCQcCgYEA5cWZ JwNb4t7YeEajPt9xuYNUOQpjvQn1aGOV7KcwTx5ELP/Hzi723BxHs7GSdrLkkDmi Gpb+mfL4wxCt0fK0i8GFQsRn5eusyq9hLqP/bmjpHoXe/1uajFbE1fZQR+2LX05N 3ATlKaH2hdfCJedFa4wf43+cl6Yhp6ZA0Yet1r8CgYEAwiu1j8W9G+RRA5/8/DtO yrUTOfsbFws4fpLGDTA0mq0whf6Soy/96C90+d9qLaC3srUpnG9eB0CpSOjbXXbv kdxseLkexwOR3bD2FHX8r4dUM2bzznZyEaxfOaQypN8SV5ME3l60Fbr8ajqLO288 wlTmGM5Mn+YCqOg/T7wjGmcCgYBpzNfdl/VafOROVbBbhgXWtzsz3K3aYNiIjbp+ MunStIwN8GUvcn6nEbqOaoiXcX4/TtpuxfJMLw4OvAJdtxUdeSmEee2heCijV6g3 ErrOOy6EqH3rNWHvlxChuP50cFQJuYOueO6QggyCyruSOnDDuc0BM0SGq6+5g5s7 H++S/wKBgQDIkqBtFr9UEf8d6JpkxS0RXDlhSMjkXmkQeKGFzdoJcYVFIwq8jTNB nJrVIGs3GcBkqGic+i7rTO1YPkquv4dUuiIn+vKZVoO6b54f+oPBXd4S0BnuEqFE rdKNuCZhiaE2XD9L/O9KP1fh5bfEcKwazQ23EvpJHBMm8BGC+/YZNw== -----END RSA PRIVATE KEY-----`)) k, _ := x509.ParsePKCS1PrivateKey(b.Bytes) return k }() var cert = func() *x509.Certificate { b, _ := pem.Decode([]byte(`-----BEGIN CERTIFICATE----- MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNV BAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5 NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEB BQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8A hs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+a ucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWx m+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6 D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURN B2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0O BBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56 zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5 pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uv NONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEf y/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL /RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsb GFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTL UzreO96WzlBBMtY= -----END CERTIFICATE-----`)) c, _ := x509.ParseCertificate(b.Bytes) return c }() func main() { logr := logger.DefaultLogger baseURLstr := flag.String("idp", "", "The URL to the IDP") flag.Parse() baseURL, err := url.Parse(*baseURLstr) if err != nil { logr.Fatalf("cannot parse base URL: %v", err) } idpServer, err := samlidp.New(samlidp.Options{ URL: *baseURL, Key: key, Logger: logr, Certificate: cert, Store: &samlidp.MemoryStore{}, }) if err != nil { logr.Fatalf("%s", err) } hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("hunter2"), bcrypt.DefaultCost) err = idpServer.Store.Put("/users/alice", samlidp.User{Name: "alice", HashedPassword: hashedPassword, Groups: []string{"Administrators", "Users"}, Email: "alice@example.com", CommonName: "Alice Smith", Surname: "Smith", GivenName: "Alice", }) if err != nil { logr.Fatalf("%s", err) } err = idpServer.Store.Put("/users/bob", samlidp.User{ Name: "bob", HashedPassword: hashedPassword, Groups: []string{"Users"}, Email: "bob@example.com", CommonName: "Bob Smith", Surname: "Smith", GivenName: "Bob", }) if err != nil { logr.Fatalf("%s", err) } goji.Handle("/*", idpServer) goji.Serve() } saml-0.4.6/example/service.go 0000664 0000000 0000000 00000011263 14154673411 0016110 0 ustar 00root root 0000000 0000000 // This is an example that implements a bitly-esque short link service. package main import ( "bytes" "context" "crypto/rsa" "crypto/tls" "crypto/x509" "encoding/xml" "flag" "fmt" "net/http" "net/url" "strings" "github.com/dchest/uniuri" "github.com/kr/pretty" "github.com/zenazn/goji" "github.com/zenazn/goji/web" "github.com/crewjam/saml/samlsp" ) var links = map[string]Link{} // Link represents a short link type Link struct { ShortLink string Target string Owner string } // CreateLink handles requests to create links func CreateLink(c web.C, w http.ResponseWriter, r *http.Request) { account := r.Header.Get("X-Remote-User") l := Link{ ShortLink: uniuri.New(), Target: r.FormValue("t"), Owner: account, } links[l.ShortLink] = l fmt.Fprintf(w, "%s\n", l.ShortLink) return } // ServeLink handles requests to redirect to a link func ServeLink(c web.C, w http.ResponseWriter, r *http.Request) { l, ok := links[strings.TrimPrefix(r.URL.Path, "/")] if !ok { http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return } http.Redirect(w, r, l.Target, http.StatusFound) return } // ListLinks returns a list of the current user's links func ListLinks(c web.C, w http.ResponseWriter, r *http.Request) { account := r.Header.Get("X-Remote-User") for _, l := range links { if l.Owner == account { fmt.Fprintf(w, "%s\n", l.ShortLink) } } } var ( key = []byte(`-----BEGIN RSA PRIVATE KEY----- MIICXgIBAAKBgQDU8wdiaFmPfTyRYuFlVPi866WrH/2JubkHzp89bBQopDaLXYxi 3PTu3O6Q/KaKxMOFBqrInwqpv/omOGZ4ycQ51O9I+Yc7ybVlW94lTo2gpGf+Y/8E PsVbnZaFutRctJ4dVIp9aQ2TpLiGT0xX1OzBO/JEgq9GzDRf+B+eqSuglwIDAQAB AoGBAMuy1eN6cgFiCOgBsB3gVDdTKpww87Qk5ivjqEt28SmXO13A1KNVPS6oQ8SJ CT5Azc6X/BIAoJCURVL+LHdqebogKljhH/3yIel1kH19vr4E2kTM/tYH+qj8afUS JEmArUzsmmK8ccuNqBcllqdwCZjxL4CHDUmyRudFcHVX9oyhAkEA/OV1OkjM3CLU N3sqELdMmHq5QZCUihBmk3/N5OvGdqAFGBlEeewlepEVxkh7JnaNXAXrKHRVu/f/ fbCQxH+qrwJBANeQERF97b9Sibp9xgolb749UWNlAdqmEpmlvmS202TdcaaT1msU 4rRLiQN3X9O9mq4LZMSVethrQAdX1whawpkCQQDk1yGf7xZpMJ8F4U5sN+F4rLyM Rq8Sy8p2OBTwzCUXXK+fYeXjybsUUMr6VMYTRP2fQr/LKJIX+E5ZxvcIyFmDAkEA yfjNVUNVaIbQTzEbRlRvT6MqR+PTCefC072NF9aJWR93JimspGZMR7viY6IM4lrr vBkm0F5yXKaYtoiiDMzlOQJADqmEwXl0D72ZG/2KDg8b4QZEmC9i5gidpQwJXUc6 hU+IVQoLxRq0fBib/36K9tcrrO5Ba4iEvDcNY+D8yGbUtA== -----END RSA PRIVATE KEY----- `) cert = []byte(`-----BEGIN CERTIFICATE----- MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJV UzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0 MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMx CzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCB nzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9 ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmH O8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKv Rsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgk akpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeT QLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvn OwJlNCASPZRH/JmF8tX0hoHuAQ== -----END CERTIFICATE----- `) ) func main() { rootURLstr := flag.String("url", "https://962766ce.ngrok.io", "The base URL of this service") idpMetadataURLstr := flag.String("idp", "https://516becc2.ngrok.io/metadata", "The metadata URL for the IDP") flag.Parse() keyPair, err := tls.X509KeyPair(cert, 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(*idpMetadataURLstr) if err != nil { panic(err) // TODO handle error } idpMetadata, err := samlsp.FetchMetadata(context.Background(), http.DefaultClient, *idpMetadataURL) if err != nil { panic(err) // TODO handle error } rootURL, err := url.Parse(*rootURLstr) if err != nil { panic(err) // TODO handle error } samlSP, err := samlsp.New(samlsp.Options{ URL: *rootURL, Key: keyPair.PrivateKey.(*rsa.PrivateKey), Certificate: keyPair.Leaf, AllowIDPInitiated: true, IDPMetadata: idpMetadata, }) if err != nil { panic(err) // TODO handle error } // register with the service provider spMetadataBuf, _ := xml.MarshalIndent(samlSP.ServiceProvider.Metadata(), "", " ") spURL := *idpMetadataURL spURL.Path = "/services/sp" http.Post(spURL.String(), "text/xml", bytes.NewReader(spMetadataBuf)) goji.Handle("/saml/*", samlSP) authMux := web.New() authMux.Use(samlSP.RequireAccount) authMux.Get("/whoami", func(w http.ResponseWriter, r *http.Request) { pretty.Fprintf(w, "%# v", r) }) authMux.Post("/", CreateLink) authMux.Get("/", ListLinks) goji.Handle("/*", authMux) goji.Get("/:link", ServeLink) goji.Serve() } saml-0.4.6/example/trivial/ 0000775 0000000 0000000 00000000000 14154673411 0015570 5 ustar 00root root 0000000 0000000 saml-0.4.6/example/trivial/trivial.go 0000664 0000000 0000000 00000003616 14154673411 0017577 0 ustar 00root root 0000000 0000000 package main import ( "context" "crypto/rsa" "crypto/tls" "crypto/x509" "fmt" "net/http" "net/url" "github.com/crewjam/saml/samlsp" ) var samlMiddleware *samlsp.Middleware func hello(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello, %s!", samlsp.AttributeFromContext(r.Context(), "displayName")) } func logout(w http.ResponseWriter, r *http.Request) { nameID := samlsp.AttributeFromContext(r.Context(), "urn:oasis:names:tc:SAML:attribute:subject-id") url, err := samlMiddleware.ServiceProvider.MakeRedirectLogoutRequest(nameID, "") if err != nil { panic(err) // TODO handle error } err = samlMiddleware.Session.DeleteSession(w, r) if err != nil { panic(err) // TODO handle error } w.Header().Add("Location", url.String()) w.WriteHeader(http.StatusFound) } 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://samltest.id/saml/idp") if err != nil { panic(err) // TODO handle error } idpMetadata, err := samlsp.FetchMetadata(context.Background(), http.DefaultClient, *idpMetadataURL) if err != nil { panic(err) // TODO handle error } rootURL, err := url.Parse("http://localhost:8000") if err != nil { panic(err) // TODO handle error } samlMiddleware, _ = samlsp.New(samlsp.Options{ URL: *rootURL, Key: keyPair.PrivateKey.(*rsa.PrivateKey), Certificate: keyPair.Leaf, IDPMetadata: idpMetadata, SignRequest: true, // some IdP require the SLO request to be signed }) app := http.HandlerFunc(hello) slo := http.HandlerFunc(logout) http.Handle("/hello", samlMiddleware.RequireAccount(app)) http.Handle("/saml/", samlMiddleware) http.Handle("/logout", slo) http.ListenAndServe(":8000", nil) } saml-0.4.6/go.mod 0000664 0000000 0000000 00000001221 14154673411 0013565 0 ustar 00root root 0000000 0000000 module github.com/crewjam/saml go 1.13 require ( github.com/beevik/etree v1.1.0 github.com/crewjam/httperr v0.2.0 github.com/davecgh/go-spew v1.1.1 // indirect github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 github.com/golang-jwt/jwt/v4 v4.1.0 github.com/google/go-cmp v0.5.6 github.com/kr/pretty v0.3.0 github.com/mattermost/xml-roundtrip-validator v0.1.0 github.com/pkg/errors v0.9.1 // indirect github.com/russellhaering/goxmldsig v1.1.1 github.com/zenazn/goji v1.0.1 golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect gotest.tools v2.2.0+incompatible ) saml-0.4.6/go.sum 0000664 0000000 0000000 00000013414 14154673411 0013621 0 ustar 00root root 0000000 0000000 github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/crewjam/httperr v0.2.0 h1:b2BfXR8U3AlIHwNeFFvZ+BV1LFvKLlzMjzaTnZMybNo= github.com/crewjam/httperr v0.2.0/go.mod h1:Jlz+Sg/XqBQhyMjdDiC+GNNRzZTD7x39Gu3pglZ5oH4= 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-20200228104902-7aecb25e1fe5 h1:RAV05c0xOkJ3dZGS0JFybxFKZ2WMLabgx3uXnd7rpGs= github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4= github.com/golang-jwt/jwt/v4 v4.1.0 h1:XUgk2Ex5veyVFVeLm0xhusUTQybEbexJXrvPNOKkSY0= github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU= github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/russellhaering/goxmldsig v1.1.1 h1:vI0r2osGF1A9PLvsGdPUAGwEIrKa4Pj5sesSBsebIxM= github.com/russellhaering/goxmldsig v1.1.1/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/zenazn/goji v1.0.1 h1:4lbD8Mx2h7IvloP7r2C0D6ltZP6Ufip8Hn0wmSK5LR8= github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= saml-0.4.6/identity_provider.go 0000664 0000000 0000000 00000104762 14154673411 0016567 0 ustar 00root root 0000000 0000000 package saml import ( "bytes" "compress/flate" "crypto" "crypto/tls" "crypto/x509" "encoding/base64" "encoding/xml" "fmt" "io" "io/ioutil" "net/http" "net/url" "os" "regexp" "strconv" "text/template" "time" "github.com/beevik/etree" xrv "github.com/mattermost/xml-roundtrip-validator" dsig "github.com/russellhaering/goxmldsig" "github.com/crewjam/saml/logger" "github.com/crewjam/saml/xmlenc" ) // Session represents a user session. It is returned by the // SessionProvider implementation's GetSession method. Fields here // are used to set fields in the SAML assertion. type Session struct { ID string CreateTime time.Time ExpireTime time.Time Index string NameID string Groups []string UserName string UserEmail string UserCommonName string UserSurname string UserGivenName string UserScopedAffiliation string CustomAttributes []Attribute } // SessionProvider is an interface used by IdentityProvider to determine the // Session associated with a request. For an example implementation, see // GetSession in the samlidp package. type SessionProvider interface { // GetSession returns the remote user session associated with the http.Request. // // If (and only if) the request is not associated with a session then GetSession // must complete the HTTP request and return nil. 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) (*EntityDescriptor, error) } // AssertionMaker is an interface used by IdentityProvider to construct the // assertion for a request. The default implementation is DefaultAssertionMaker, // which is used if not AssertionMaker is specified. type AssertionMaker interface { // MakeAssertion constructs an assertion from session and the request and // assigns it to req.Assertion. MakeAssertion(req *IdpAuthnRequest, session *Session) error } // IdentityProvider implements the SAML Identity Provider role (IDP). // // An identity provider receives SAML assertion requests and responds // with SAML Assertions. // // You must provide a keypair that is used to // sign assertions. // // 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 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 ValidDuration *time.Duration } // Metadata returns the metadata structure for this identity provider. func (idp *IdentityProvider) Metadata() *EntityDescriptor { certStr := base64.StdEncoding.EncodeToString(idp.Certificate.Raw) var validDuration time.Duration if idp.ValidDuration != nil { validDuration = *idp.ValidDuration } else { validDuration = DefaultValidDuration } ed := &EntityDescriptor{ EntityID: idp.MetadataURL.String(), ValidUntil: TimeNow().Add(validDuration), CacheDuration: validDuration, IDPSSODescriptors: []IDPSSODescriptor{ { SSODescriptor: SSODescriptor{ RoleDescriptor: RoleDescriptor{ ProtocolSupportEnumeration: "urn:oasis:names:tc:SAML:2.0:protocol", KeyDescriptors: []KeyDescriptor{ { Use: "signing", KeyInfo: KeyInfo{ X509Data: X509Data{ X509Certificates: []X509Certificate{ {Data: certStr}, }, }, }, }, { Use: "encryption", KeyInfo: KeyInfo{ X509Data: X509Data{ X509Certificates: []X509Certificate{ {Data: certStr}, }, }, }, EncryptionMethods: []EncryptionMethod{ {Algorithm: "http://www.w3.org/2001/04/xmlenc#aes128-cbc"}, {Algorithm: "http://www.w3.org/2001/04/xmlenc#aes192-cbc"}, {Algorithm: "http://www.w3.org/2001/04/xmlenc#aes256-cbc"}, {Algorithm: "http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p"}, }, }, }, }, NameIDFormats: []NameIDFormat{NameIDFormat("urn:oasis:names:tc:SAML:2.0:nameid-format:transient")}, }, SingleSignOnServices: []Endpoint{ { Binding: HTTPRedirectBinding, Location: idp.SSOURL.String(), }, { Binding: HTTPPostBinding, Location: idp.SSOURL.String(), }, }, }, }, } 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 // URLs func (idp *IdentityProvider) Handler() http.Handler { mux := http.NewServeMux() mux.HandleFunc(idp.MetadataURL.Path, idp.ServeMetadata) mux.HandleFunc(idp.SSOURL.Path, idp.ServeSSO) return mux } // ServeMetadata is an http.HandlerFunc that serves the IDP metadata func (idp *IdentityProvider) ServeMetadata(w http.ResponseWriter, r *http.Request) { buf, _ := xml.MarshalIndent(idp.Metadata(), "", " ") w.Header().Set("Content-Type", "application/samlmetadata+xml") w.Write(buf) } // ServeSSO handles SAML auth requests. // // When it gets a request for a user that does not have a valid session, // then it prompts the user via XXX. // // If the session already exists, then it produces a SAML assertion and // returns an HTTP response according to the specified binding. The // only supported binding right now is the HTTP-POST binding which returns // an HTML form in the appropriate format with Javascript to automatically // submit that form the to service provider's Assertion Customer Service // endpoint. // // If the SAML request is invalid or cannot be verified a simple StatusBadRequest // response is sent. // // If the assertion cannot be created or returned, a StatusInternalServerError // response is sent. func (idp *IdentityProvider) ServeSSO(w http.ResponseWriter, r *http.Request) { req, err := NewIdpAuthnRequest(idp, r) if err != nil { idp.Logger.Printf("failed to parse request: %s", err) http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } if err := req.Validate(); err != nil { idp.Logger.Printf("failed to validate request: %s", err) http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } // TODO(ross): we must check that the request ID has not been previously // issued. session := idp.SessionProvider.GetSession(w, r, req) if session == nil { return } assertionMaker := idp.AssertionMaker if assertionMaker == nil { assertionMaker = DefaultAssertionMaker{} } if err := assertionMaker.MakeAssertion(req, session); err != nil { idp.Logger.Printf("failed to make assertion: %s", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } if err := req.WriteResponse(w); err != nil { idp.Logger.Printf("failed to write response: %s", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } } // ServeIDPInitiated handes an IDP-initiated authorization request. Requests of this // type require us to know a registered service provider and (optionally) the RelayState // that will be passed to the application. func (idp *IdentityProvider) ServeIDPInitiated(w http.ResponseWriter, r *http.Request, serviceProviderID string, relayState string) { req := &IdpAuthnRequest{ IDP: idp, HTTPRequest: r, RelayState: relayState, Now: TimeNow(), } session := idp.SessionProvider.GetSession(w, r, req) if session == nil { // If GetSession returns nil, it must have written an HTTP response, per the interface // (this is probably because it drew a login form or something) return } var err error req.ServiceProviderMetadata, err = idp.ServiceProviderProvider.GetServiceProvider(r, serviceProviderID) if err == os.ErrNotExist { idp.Logger.Printf("cannot find service provider: %s", serviceProviderID) http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return } else if err != nil { idp.Logger.Printf("cannot find service provider %s: %v", serviceProviderID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } // find an ACS endpoint that we can use for _, spssoDescriptor := range req.ServiceProviderMetadata.SPSSODescriptors { for _, endpoint := range spssoDescriptor.AssertionConsumerServices { if endpoint.Binding == HTTPPostBinding { // explicitly copy loop iterator variables // // c.f. https://github.com/golang/go/wiki/CommonMistakes#using-reference-to-loop-iterator-variable // // (note that I'm pretty sure this isn't strictly necessary because we break out of the loop immediately, // but it certainly doesn't hurt anything and may prevent bugs in the future.) endpoint, spssoDescriptor := endpoint, spssoDescriptor req.ACSEndpoint = &endpoint req.SPSSODescriptor = &spssoDescriptor break } } if req.ACSEndpoint != nil { break } } if req.ACSEndpoint == nil { idp.Logger.Printf("saml metadata does not contain an Assertion Customer Service url") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } assertionMaker := idp.AssertionMaker if assertionMaker == nil { assertionMaker = DefaultAssertionMaker{} } if err := assertionMaker.MakeAssertion(req, session); err != nil { idp.Logger.Printf("failed to make assertion: %s", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } if err := req.WriteResponse(w); err != nil { idp.Logger.Printf("failed to write response: %s", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } } // IdpAuthnRequest is used by IdentityProvider to handle a single authentication request. type IdpAuthnRequest struct { IDP *IdentityProvider HTTPRequest *http.Request RelayState string RequestBuffer []byte Request AuthnRequest ServiceProviderMetadata *EntityDescriptor SPSSODescriptor *SPSSODescriptor ACSEndpoint *IndexedEndpoint Assertion *Assertion AssertionEl *etree.Element ResponseEl *etree.Element Now time.Time } // NewIdpAuthnRequest returns a new IdpAuthnRequest for the given HTTP request to the authorization // service. func NewIdpAuthnRequest(idp *IdentityProvider, r *http.Request) (*IdpAuthnRequest, error) { req := &IdpAuthnRequest{ IDP: idp, HTTPRequest: r, Now: TimeNow(), } switch r.Method { case "GET": compressedRequest, err := base64.StdEncoding.DecodeString(r.URL.Query().Get("SAMLRequest")) if err != nil { return nil, fmt.Errorf("cannot decode request: %s", err) } req.RequestBuffer, err = ioutil.ReadAll(flate.NewReader(bytes.NewReader(compressedRequest))) if err != nil { return nil, fmt.Errorf("cannot decompress request: %s", err) } req.RelayState = r.URL.Query().Get("RelayState") case "POST": if err := r.ParseForm(); err != nil { return nil, err } var err error req.RequestBuffer, err = base64.StdEncoding.DecodeString(r.PostForm.Get("SAMLRequest")) if err != nil { return nil, err } req.RelayState = r.PostForm.Get("RelayState") default: return nil, fmt.Errorf("method not allowed") } return req, nil } // Validate checks that the authentication request is valid and assigns // the AuthnRequest and Metadata properties. Returns a non-nil error if the // request is not valid. func (req *IdpAuthnRequest) Validate() error { if err := xrv.Validate(bytes.NewReader(req.RequestBuffer)); err != nil { return err } if err := xml.Unmarshal(req.RequestBuffer, &req.Request); err != nil { return err } // We always have exactly one IDP SSO descriptor if len(req.IDP.Metadata().IDPSSODescriptors) != 1 { panic("expected exactly one IDP SSO descriptor in IDP metadata") } idpSsoDescriptor := req.IDP.Metadata().IDPSSODescriptors[0] // TODO(ross): support signed authn requests // For now we do the safe thing and fail in the case where we think // requests might be signed. if idpSsoDescriptor.WantAuthnRequestsSigned != nil && *idpSsoDescriptor.WantAuthnRequestsSigned { return fmt.Errorf("authn request signature checking is not currently supported") } // In http://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf §3.4.5.2 // we get a description of the Destination attribute: // // If the message is signed, the Destination XML attribute in the root SAML // element of the protocol message MUST contain the URL to which the sender // has instructed the user agent to deliver the message. The recipient MUST // then verify that the value matches the location at which the message has // been received. // // We require the destination be correct either (a) if signing is enabled or // (b) if it was provided. mustHaveDestination := idpSsoDescriptor.WantAuthnRequestsSigned != nil && *idpSsoDescriptor.WantAuthnRequestsSigned mustHaveDestination = mustHaveDestination || req.Request.Destination != "" if mustHaveDestination { if req.Request.Destination != req.IDP.SSOURL.String() { return fmt.Errorf("expected destination to be %q, not %q", req.IDP.SSOURL.String(), req.Request.Destination) } } if req.Request.IssueInstant.Add(MaxIssueDelay).Before(req.Now) { return fmt.Errorf("request expired at %s", req.Request.IssueInstant.Add(MaxIssueDelay)) } if req.Request.Version != "2.0" { return fmt.Errorf("expected SAML request version 2.0 got %v", req.Request.Version) } // find the service provider 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 // Check that the ACS URL matches an ACS endpoint in the SP metadata. if err := req.getACSEndpoint(); err != nil { return fmt.Errorf("cannot find assertion consumer service: %v", err) } return nil } func (req *IdpAuthnRequest) getACSEndpoint() error { if req.Request.AssertionConsumerServiceIndex != "" { for _, spssoDescriptor := range req.ServiceProviderMetadata.SPSSODescriptors { for _, spAssertionConsumerService := range spssoDescriptor.AssertionConsumerServices { if strconv.Itoa(spAssertionConsumerService.Index) == req.Request.AssertionConsumerServiceIndex { // explicitly copy loop iterator variables // // c.f. https://github.com/golang/go/wiki/CommonMistakes#using-reference-to-loop-iterator-variable // // (note that I'm pretty sure this isn't strictly necessary because we break out of the loop immediately, // but it certainly doesn't hurt anything and may prevent bugs in the future.) spssoDescriptor, spAssertionConsumerService := spssoDescriptor, spAssertionConsumerService req.SPSSODescriptor = &spssoDescriptor req.ACSEndpoint = &spAssertionConsumerService return nil } } } } if req.Request.AssertionConsumerServiceURL != "" { for _, spssoDescriptor := range req.ServiceProviderMetadata.SPSSODescriptors { for _, spAssertionConsumerService := range spssoDescriptor.AssertionConsumerServices { if spAssertionConsumerService.Location == req.Request.AssertionConsumerServiceURL { // explicitly copy loop iterator variables // // c.f. https://github.com/golang/go/wiki/CommonMistakes#using-reference-to-loop-iterator-variable // // (note that I'm pretty sure this isn't strictly necessary because we break out of the loop immediately, // but it certainly doesn't hurt anything and may prevent bugs in the future.) spssoDescriptor, spAssertionConsumerService := spssoDescriptor, spAssertionConsumerService req.SPSSODescriptor = &spssoDescriptor req.ACSEndpoint = &spAssertionConsumerService return nil } } } } // 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: // explicitly copy loop iterator variables // // c.f. https://github.com/golang/go/wiki/CommonMistakes#using-reference-to-loop-iterator-variable // // (note that I'm pretty sure this isn't strictly necessary because we break out of the loop immediately, // but it certainly doesn't hurt anything and may prevent bugs in the future.) spssoDescriptor, spAssertionConsumerService := spssoDescriptor, spAssertionConsumerService 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: // explicitly copy loop iterator variables // // c.f. https://github.com/golang/go/wiki/CommonMistakes#using-reference-to-loop-iterator-variable // // (note that I'm pretty sure this isn't strictly necessary because we break out of the loop immediately, // but it certainly doesn't hurt anything and may prevent bugs in the future.) spssoDescriptor, spAssertionConsumerService := spssoDescriptor, spAssertionConsumerService req.SPSSODescriptor = &spssoDescriptor req.ACSEndpoint = &spAssertionConsumerService return nil } } } } return os.ErrNotExist // no ACS url found or specified } // DefaultAssertionMaker produces a SAML assertion for the // given request and assigns it to req.Assertion. type DefaultAssertionMaker struct { } // MakeAssertion implements AssertionMaker. It produces a SAML assertion from the // given request and assigns it to req.Assertion. func (DefaultAssertionMaker) MakeAssertion(req *IdpAuthnRequest, session *Session) error { attributes := []Attribute{} var attributeConsumingService *AttributeConsumingService for _, acs := range req.SPSSODescriptor.AttributeConsumingServices { if acs.IsDefault != nil && *acs.IsDefault { // explicitly copy loop iterator variables // // c.f. https://github.com/golang/go/wiki/CommonMistakes#using-reference-to-loop-iterator-variable // // (note that I'm pretty sure this isn't strictly necessary because we break out of the loop immediately, // but it certainly doesn't hurt anything and may prevent bugs in the future.) acs := acs attributeConsumingService = &acs break } } if attributeConsumingService == nil { for _, acs := range req.SPSSODescriptor.AttributeConsumingServices { // explicitly copy loop iterator variables // // c.f. https://github.com/golang/go/wiki/CommonMistakes#using-reference-to-loop-iterator-variable // // (note that I'm pretty sure this isn't strictly necessary because we break out of the loop immediately, // but it certainly doesn't hurt anything and may prevent bugs in the future.) acs := acs attributeConsumingService = &acs break } } if attributeConsumingService == nil { attributeConsumingService = &AttributeConsumingService{} } for _, requestedAttribute := range attributeConsumingService.RequestedAttributes { if requestedAttribute.NameFormat == "urn:oasis:names:tc:SAML:2.0:attrname-format:basic" || requestedAttribute.NameFormat == "urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified" { attrName := requestedAttribute.Name attrName = regexp.MustCompile("[^A-Za-z0-9]+").ReplaceAllString(attrName, "") switch attrName { case "email", "emailaddress": attributes = append(attributes, Attribute{ FriendlyName: requestedAttribute.FriendlyName, Name: requestedAttribute.Name, NameFormat: requestedAttribute.NameFormat, Values: []AttributeValue{{ Type: "xs:string", Value: session.UserEmail, }}, }) case "name", "fullname", "cn", "commonname": attributes = append(attributes, Attribute{ FriendlyName: requestedAttribute.FriendlyName, Name: requestedAttribute.Name, NameFormat: requestedAttribute.NameFormat, Values: []AttributeValue{{ Type: "xs:string", Value: session.UserCommonName, }}, }) case "givenname", "firstname": attributes = append(attributes, Attribute{ FriendlyName: requestedAttribute.FriendlyName, Name: requestedAttribute.Name, NameFormat: requestedAttribute.NameFormat, Values: []AttributeValue{{ Type: "xs:string", Value: session.UserGivenName, }}, }) case "surname", "lastname", "familyname": attributes = append(attributes, Attribute{ FriendlyName: requestedAttribute.FriendlyName, Name: requestedAttribute.Name, NameFormat: requestedAttribute.NameFormat, Values: []AttributeValue{{ Type: "xs:string", Value: session.UserSurname, }}, }) case "uid", "user", "userid": attributes = append(attributes, Attribute{ FriendlyName: requestedAttribute.FriendlyName, Name: requestedAttribute.Name, NameFormat: requestedAttribute.NameFormat, Values: []AttributeValue{{ Type: "xs:string", Value: session.UserName, }}, }) } } } if session.UserName != "" { attributes = append(attributes, Attribute{ FriendlyName: "uid", Name: "urn:oid:0.9.2342.19200300.100.1.1", NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", Values: []AttributeValue{{ Type: "xs:string", Value: session.UserName, }}, }) } if session.UserEmail != "" { attributes = append(attributes, Attribute{ FriendlyName: "eduPersonPrincipalName", Name: "urn:oid:1.3.6.1.4.1.5923.1.1.1.6", NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", Values: []AttributeValue{{ Type: "xs:string", Value: session.UserEmail, }}, }) } if session.UserSurname != "" { attributes = append(attributes, Attribute{ FriendlyName: "sn", Name: "urn:oid:2.5.4.4", NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", Values: []AttributeValue{{ Type: "xs:string", Value: session.UserSurname, }}, }) } if session.UserGivenName != "" { attributes = append(attributes, Attribute{ FriendlyName: "givenName", Name: "urn:oid:2.5.4.42", NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", Values: []AttributeValue{{ Type: "xs:string", Value: session.UserGivenName, }}, }) } if session.UserCommonName != "" { attributes = append(attributes, Attribute{ FriendlyName: "cn", Name: "urn:oid:2.5.4.3", NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", Values: []AttributeValue{{ Type: "xs:string", Value: session.UserCommonName, }}, }) } 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, }}, }) } for _, ca := range session.CustomAttributes { attributes = append(attributes, ca) } if len(session.Groups) != 0 { groupMemberAttributeValues := []AttributeValue{} for _, group := range session.Groups { groupMemberAttributeValues = append(groupMemberAttributeValues, AttributeValue{ Type: "xs:string", Value: group, }) } attributes = append(attributes, Attribute{ FriendlyName: "eduPersonAffiliation", Name: "urn:oid:1.3.6.1.4.1.5923.1.1.1.1", NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", Values: groupMemberAttributeValues, }) } // allow for some clock skew in the validity period using the // issuer's apparent clock. 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) } req.Assertion = &Assertion{ ID: fmt.Sprintf("id-%x", randomBytes(20)), IssueInstant: TimeNow(), Version: "2.0", Issuer: Issuer{ Format: "urn:oasis:names:tc:SAML:2.0:nameid-format:entity", Value: req.IDP.Metadata().EntityID, }, Subject: &Subject{ NameID: &NameID{ Format: "urn:oasis:names:tc:SAML:2.0:nameid-format:transient", NameQualifier: req.IDP.Metadata().EntityID, SPNameQualifier: req.ServiceProviderMetadata.EntityID, Value: session.NameID, }, SubjectConfirmations: []SubjectConfirmation{ { Method: "urn:oasis:names:tc:SAML:2.0:cm:bearer", SubjectConfirmationData: &SubjectConfirmationData{ Address: req.HTTPRequest.RemoteAddr, InResponseTo: req.Request.ID, NotOnOrAfter: req.Now.Add(MaxIssueDelay), Recipient: req.ACSEndpoint.Location, }, }, }, }, Conditions: &Conditions{ NotBefore: notBefore, NotOnOrAfter: notOnOrAfterAfter, AudienceRestrictions: []AudienceRestriction{ { Audience: Audience{Value: req.ServiceProviderMetadata.EntityID}, }, }, }, AuthnStatements: []AuthnStatement{ { AuthnInstant: session.CreateTime, SessionIndex: session.Index, SubjectLocality: &SubjectLocality{ Address: req.HTTPRequest.RemoteAddr, }, AuthnContext: AuthnContext{ AuthnContextClassRef: &AuthnContextClassRef{ Value: "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport", }, }, }, }, AttributeStatements: []AttributeStatement{ { Attributes: attributes, }, }, } return nil } // The Canonicalizer prefix list MUST be empty. Various implementations // (maybe ours?) do not appear to support non-empty prefix lists in XML C14N. const canonicalizerPrefixList = "" // MakeAssertionEl sets `AssertionEl` to a signed, possibly encrypted, version of `Assertion`. func (req *IdpAuthnRequest) MakeAssertionEl() error { keyPair := tls.Certificate{ Certificate: [][]byte{req.IDP.Certificate.Raw}, 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(signatureMethod); err != nil { return err } assertionEl := req.Assertion.Element() signedAssertionEl, err := signingContext.SignEnveloped(assertionEl) if err != nil { return err } sigEl := signedAssertionEl.Child[len(signedAssertionEl.Child)-1] req.Assertion.Signature = sigEl.(*etree.Element) signedAssertionEl = req.Assertion.Element() certBuf, err := req.getSPEncryptionCert() if err == os.ErrNotExist { req.AssertionEl = signedAssertionEl return nil } else if err != nil { return err } var signedAssertionBuf []byte { doc := etree.NewDocument() doc.SetRoot(signedAssertionEl) signedAssertionBuf, err = doc.WriteToBytes() if err != nil { return err } } encryptor := xmlenc.OAEP() encryptor.BlockCipher = xmlenc.AES128CBC encryptor.DigestMethod = &xmlenc.SHA1 encryptedDataEl, err := encryptor.Encrypt(certBuf, signedAssertionBuf, nil) if err != nil { return err } encryptedDataEl.CreateAttr("Type", "http://www.w3.org/2001/04/xmlenc#Element") encryptedAssertionEl := etree.NewElement("saml:EncryptedAssertion") encryptedAssertionEl.AddChild(encryptedDataEl) req.AssertionEl = encryptedAssertionEl return nil } // WriteResponse writes the `Response` to the http.ResponseWriter. If // `Response` is not already set, it calls MakeResponse to produce it. func (req *IdpAuthnRequest) WriteResponse(w http.ResponseWriter) error { if req.ResponseEl == nil { if err := req.MakeResponse(); err != nil { return err } } doc := etree.NewDocument() doc.SetRoot(req.ResponseEl) responseBuf, err := doc.WriteToBytes() if err != nil { return err } // the only supported binding is the HTTP-POST binding switch req.ACSEndpoint.Binding { case HTTPPostBinding: tmpl := template.Must(template.New("saml-post-form").Parse(`` + `
` + `` + `` + ``)) data := struct { URL string SAMLResponse string RelayState string }{ URL: req.ACSEndpoint.Location, SAMLResponse: base64.StdEncoding.EncodeToString(responseBuf), RelayState: req.RelayState, } buf := bytes.NewBuffer(nil) if err := tmpl.Execute(buf, data); err != nil { return err } if _, err := io.Copy(w, buf); err != nil { return err } return nil default: return fmt.Errorf("%s: unsupported binding %s", req.ServiceProviderMetadata.EntityID, req.ACSEndpoint.Binding) } } // getSPEncryptionCert returns the certificate which we can use to encrypt things // to the SP in PEM format, or nil if no such certificate is found. func (req *IdpAuthnRequest) getSPEncryptionCert() (*x509.Certificate, error) { certStr := "" for _, keyDescriptor := range req.SPSSODescriptor.KeyDescriptors { if keyDescriptor.Use == "encryption" { certStr = keyDescriptor.KeyInfo.X509Data.X509Certificates[0].Data break } } // If there are no certs explicitly labeled for encryption, return the first // non-empty cert we find. if certStr == "" { for _, keyDescriptor := range req.SPSSODescriptor.KeyDescriptors { if keyDescriptor.Use == "" && len(keyDescriptor.KeyInfo.X509Data.X509Certificates) != 0 && keyDescriptor.KeyInfo.X509Data.X509Certificates[0].Data != "" { certStr = keyDescriptor.KeyInfo.X509Data.X509Certificates[0].Data break } } } if certStr == "" { return nil, os.ErrNotExist } // cleanup whitespace and re-encode a PEM certStr = regexp.MustCompile(`\s+`).ReplaceAllString(certStr, "") certBytes, err := base64.StdEncoding.DecodeString(certStr) if err != nil { return nil, fmt.Errorf("cannot decode certificate base64: %v", err) } cert, err := x509.ParseCertificate(certBytes) if err != nil { return nil, fmt.Errorf("cannot parse certificate: %v", err) } return cert, nil } // unmarshalEtreeHack parses `el` and sets values in the structure `v`. // // This is a hack -- it first serializes the element, then uses xml.Unmarshal. func unmarshalEtreeHack(el *etree.Element, v interface{}) error { doc := etree.NewDocument() doc.SetRoot(el) buf, err := doc.WriteToBytes() if err != nil { return err } return xml.Unmarshal(buf, v) } // MakeResponse creates and assigns a new SAML response in ResponseEl. `Assertion` must // be non-nil. If MakeAssertionEl() has not been called, this function calls it for // you. func (req *IdpAuthnRequest) MakeResponse() error { if req.AssertionEl == nil { if err := req.MakeAssertionEl(); err != nil { return err } } response := &Response{ Destination: req.ACSEndpoint.Location, ID: fmt.Sprintf("id-%x", randomBytes(20)), InResponseTo: req.Request.ID, IssueInstant: req.Now, Version: "2.0", Issuer: &Issuer{ Format: "urn:oasis:names:tc:SAML:2.0:nameid-format:entity", Value: req.IDP.MetadataURL.String(), }, Status: Status{ StatusCode: StatusCode{ Value: StatusSuccess, }, }, } responseEl := response.Element() responseEl.AddChild(req.AssertionEl) // AssertionEl either an EncryptedAssertion or Assertion element // Sign the response element (we've already signed the Assertion element) { keyPair := tls.Certificate{ Certificate: [][]byte{req.IDP.Certificate.Raw}, 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(signatureMethod); err != nil { return err } signedResponseEl, err := signingContext.SignEnveloped(responseEl) if err != nil { return err } sigEl := signedResponseEl.ChildElements()[len(signedResponseEl.ChildElements())-1] response.Signature = sigEl responseEl = response.Element() responseEl.AddChild(req.AssertionEl) } req.ResponseEl = responseEl return nil } saml-0.4.6/identity_provider_go116_test.go 0000664 0000000 0000000 00000004067 14154673411 0020540 0 ustar 00root root 0000000 0000000 //go:build !go1.17 // +build !go1.17 package saml import ( "bytes" "compress/flate" "encoding/base64" "io/ioutil" "net/http" "net/http/httptest" "net/url" "testing" "gotest.tools/assert" is "gotest.tools/assert/cmp" ) func TestIDPHTTPCanHandleSSORequest(t *testing.T) { test := NewIdentifyProviderTest(t) w := httptest.NewRecorder() const validRequest = `lJJBayoxFIX%2FypC9JhnU5wszAz7lgWCLaNtFd5fMbQ1MkmnunVb%2FfUfbUqEgdhs%2BTr5zkmLW8S5s8KVD4mzvm0Cl6FIwEciRCeCRDFuznd2sTD5Upk2Ro42NyGZEmNjFMI%2BBOo9pi%2BnVWbzfrEqxY27JSEntEPfg2waHNnpJ4JtcgiWRLfoLXYBjwDfu6p%2B8JIoiWy5K4eqBUipXIzVRUwXKKtRK53qkJ3qqQVuNPUjU4TIQQ%2BBS5EqPBzofKH2ntBn%2FMervo8jWnyX%2BuVC78FwKkT1gopNKX1JUxSklXTMIfM0gsv8xeeDL%2BPGk7%2FF0Qg0GdnwQ1cW5PDLUwFDID6uquO1Dlot1bJw9%2FPLRmia%2BzRMCYyk4dSiq6205QSDXOxfy3KAq5Pkvqt4DAAD%2F%2Fw%3D%3D` r, _ := http.NewRequest("GET", "https://idp.example.com/saml/sso?RelayState=ThisIsTheRelayState&"+ "SAMLRequest="+validRequest, nil) test.IDP.Handler().ServeHTTP(w, r) assert.Check(t, is.Equal(http.StatusOK, w.Code)) // rejects requests that are invalid w = httptest.NewRecorder() r, _ = http.NewRequest("GET", "https://idp.example.com/saml/sso?RelayState=ThisIsTheRelayState&"+ "SAMLRequest=PEF1dGhuUmVxdWVzdA%3D%3D", nil) test.IDP.Handler().ServeHTTP(w, r) assert.Check(t, is.Equal(http.StatusBadRequest, w.Code)) // rejects requests that contain malformed XML { a, _ := url.QueryUnescape(validRequest) b, _ := base64.StdEncoding.DecodeString(a) c, _ := ioutil.ReadAll(flate.NewReader(bytes.NewReader(b))) d := bytes.Replace(c, []byte("